Removing the Square Bracket Decorations with SQL Prompt
If you avoid illegal characters and reserved words in your identifiers, you'll rarely need delimiters. Sadly, SSMS applies square bracket delimiters indiscriminately, as a precaution, when generating build scripts. Phil Factor provides a handy function that adds quoted delimiters only where they are really needed and then sits back and lets SQL Prompt strip out any extraneous square brackets, in a flash.
For some, one of the more puzzling options in the SQL Prompt menu is the item ‘Remove Square Brackets‘. What square brackets and why remove them? What is their purpose and why are they square? These are all good questions.
Why use delimiters?
Identifiers, meaning the names of SQL Server metadata such as objects, aliases, columns and indexes, must conform to certain rules. If they don’t then you must delimit them.
The SQL standard specifies use of double quotation marks (“”) as delimiters. SQL Server inherits from Sybase a liking for the use of square brackets ([]) for the same purpose, though the double-quotes work fine too. The delimiters don’t become part of the identifier; they serve only to mark its beginning and end and are used only to tell the parsers what you intend.
You only rarely need to delimit or ‘quote’ identifiers. SQL requires you to delimit any identifier that is a reserved word, contains a character that isn’t allowed, or starts with a symbol reserved for a special object.
SQL allows only certain characters in an identifier. These characters include letters, decimal numbers, and a few symbols. An ordinary identifier must begin with a letter and contain only letters, underscore characters (_), and digits.
- A letter is defined by the Unicode Standard 3.2, and includes the Latin characters from a through z, from A through Z, and letter characters from the current collation.
- A digit means a decimal number from the current collation
- Allowable symbols include the @ (at) sign, $ (dollar) sign, # (number) sign or _ (underscore).
Why is there a problem with delimiters?
No experienced SQL Server developer would want to delimit identifiers unnecessarily and will avoid using both illegal characters and reserved words in their names. However, SQL Server choses to delimit all identifiers when you get a script via SSMS, regardless of whether it really needs it. It adopts this cautious approach just in case you are using an identifier that could, in future, become a reserved word. Almost all developers want to remove them, where possible, because they make SQL Code harder to read.
SQL Prompt provides the ‘Remove Square Brackets’ action to put right this quirk that SQL Server has with scripting.
Misuse of delimited identifiers
If you stick to the rules for naming objects, you’ll have little need for delimiters and your code is likely to be clearer. In the following example, I have allowed myself to use them, just to point out how quickly you can hit trouble or mischief. This code compiles and runs without complaint:
1 2 3 4 5 6 7 |
CREATE SCHEMA [select] go CREATE TYPE [select].[select] FROM varchar(200) NOT NULL go CREATE TABLE [select].[select] ([select] [select].[SELECT]) INSERT INTO [select].[select]([select].[select]) SELECT 'select' SELECT [select].[select] FROM [select].[select] |
Having convinced yourself that it runs, but isn’t the sort of code you’d want to inherit, try using Prompt’s Remove Square Brackets action (either from the SQL Prompt menu, or Action list, or by using the keyboard shortcut, Ctrl+B, Crtl.+B). It can’t remove those pesky square brackets can it? If it did, the code wouldn’t run.
However, in SSMS, script out the Person.Person
table from AdventureWorks
, and you’ll quickly get a build script full of unnecessary square brackets:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
CREATE TABLE [Person].[Person]( [BusinessEntityID] [int] NOT NULL, [PersonType] [nchar](2) NOT NULL, [NameStyle] [dbo].[NameStyle] NOT NULL, [Title] [nvarchar](8) NULL, [FirstName] [dbo].[Name] NOT NULL, [MiddleName] [dbo].[Name] NULL, [LastName] [dbo].[Name] NOT NULL, [Suffix] [nvarchar](10) NULL, [EmailPromotion] [int] NOT NULL, [AdditionalContactInfo] [xml](CONTENT [Person].[AdditionalContactInfoSchemaCollection]) NULL, [Demographics] [xml](CONTENT [Person].[IndividualSurveySchemaCollection]) NULL, [rowguid] [uniqueidentifier] ROWGUIDCOL NOT NULL, [ModifiedDate] [datetime] NOT NULL, CONSTRAINT [PK_Person_BusinessEntityID] PRIMARY KEY CLUSTERED ( [BusinessEntityID] ASC )WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY] ) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY] GO |
This time when you run the Remove square brackets actions, all these delimiters will disappear, making it much neater and easier to read.
What about a quoted version?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
CREATE TABLE "Person"."Person2"( "BusinessEntityID" "int" NOT NULL, "PersonType" "nchar"(2) NOT NULL, "NameStyle" "dbo"."NameStyle" NOT NULL, "Title" "nvarchar"(8) NULL, "FirstName" "dbo"."Name" NOT NULL, "MiddleName" "dbo"."Name" NULL, "LastName" "dbo"."Name" NOT NULL, "Suffix" "nvarchar"(10) NULL, "EmailPromotion" "int" NOT NULL, "AdditionalContactInfo" "xml"(CONTENT "Person"."AdditionalContactInfoSchemaCollection") NULL, "Demographics" "xml"(CONTENT "Person"."IndividualSurveySchemaCollection") NULL, "rowguid" "uniqueidentifier" ROWGUIDCOL NOT NULL, "ModifiedDate" "datetime" NOT NULL, CONSTRAINT "PK_Person_BusinessEntityID" PRIMARY KEY CLUSTERED ( "BusinessEntityID" ASC )WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON "PRIMARY" ) ON "PRIMARY" TEXTIMAGE_ON "PRIMARY" GO |
Nope, nothing happens. SQL Prompt is only interested in square brackets, not delimited identifiers in general. This is useful because it means that you can use the standard quoted identifiers where they are really required, and then use Prompt to vacuum up all the square-bracketed identifiers produced by SSMS.
Why worry about quoted identifiers?
Any time that you produce SQL routines that require you to get metadata, you need to be prepared to delimit the names of the tables, columns, types, indexes and objects. I do a lot of this and I’m forced to use a couple of system routines that are either messy or faulty. The result is the same as with SSMS; you end up having to delimit everything.
The system procedure sp_validname
is provided to check whether an identifier is valid. Unfortunately, it has a definition of ‘valid’ that is of no use for our purposes because it only considers an identifier to be invalid if it is wrong, even when delimited. This will only be the case if the identifier is blank, NULL
or the Unicode replacement character, U+FFFF
. It even fails to check for U+FFFF
.
The QuoteName()
function simply adds square bracket delimiters, and if the string already has a closing square bracket character in the string, it ‘escapes’ it. This is simply demonstrated, as follows:
1 2 |
SELECT QuoteName('LegalName') -- produces [LegalName] SELECT QuoteName('[Quoted Name]') -- produces [[Quoted Name]]] |
To prove that the extra closing square bracket is merely ‘escaping’ the square bracket within the string, we can try using this bracketed expression to create a table:
1 2 3 4 5 6 |
CREATE TABLE [[Quoted Name]]] ([[My ID]]] int IDENTITY, [[My String]]] varchar(30)) SELECT tables.name AS Table_name, columns.name AS Column_name FROM sys.tables INNER JOIN sys.columns ON COLUMNS.object_id = TABLES.object_id WHERE tables.create_date > DateAdd(MINUTE, -10, GetDate()); |
This works fine:
1 2 3 4 5 |
Table_name Column_name ----------------- -------------- [Quoted Name] [My ID] [Quoted Name] [My String] (2 rows affected) |
We can even specify the table name to delete it:
1 |
DROP TABLE [[Quoted Name]]] |
If you ask SQL Prompt to remove square brackets, it sensibly leaves them alone in the above code.
A better ‘QuoteName’ for delimiting identifiers only when necessary
The answer to delimiting is, of course, to devise a way of checking an identifier, and only delimiting it when necessary. We ought, I suppose, to have the functionality of the poor shriveled sp_validname
. I’ve done this as a function though it can easily be done with a procedure. It accepts two parameters, @TheIdeintifier
and @TheDelimiter
, with the latter being a double quote, by default, although you can set it to any valid delimiter.
I use this when I’m creating scripts directly from metadata, which can then be executed. You can never be sure that the names that you get from the metadata views will be valid, and so you need to check and compensate accordingly. Where you never see the code, then it is worth just ‘quotenaming’ everything with square brackets. After all, if it is never seen it does no harm. However, for anything that is ever seen, it is worth taking the extra trouble.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 |
CREATE OR alter FUNCTION DelimitedIfNecessary ( @TheIdentifier sysname, @TheDelimiter NCHAR='"') /* usually either a '[' or '"' (I recommend ") Can be a single quotation mark ( ' ), a left or right bracket ( [] ) , a double quotation mark ( " ), a left or right parenthesis ( () ), a greater than or less than sign ( >< ), a left or right brace ( {} ) or a backtick ( ` ). */ RETURNS sysname --unicode character date AS BEGIN IF @TheDelimiter NOT IN ('<','>','[',']','"','''','(',')','{','}','`') SET @TheDelimiter='[' IF Coalesce(@TheIdentifier, '') = '' OR CharIndex(NChar(0xFFFF) COLLATE Latin1_General_100_BIN2, @TheIdentifier) <> 0 RETURN 'null'; IF PatIndex('%[^a-zA-Z0-9@$#_]%', @TheIdentifier) > 0 RETURN QuoteName(@TheIdentifier,@TheDelimiter); IF PatIndex('[@#_]%', @TheIdentifier) > 0 RETURN QuoteName(@TheIdentifier,@TheDelimiter); IF EXISTS ( SELECT * FROM ( VALUES ('ADD'), ('EXTERNAL'), ('PROCEDURE'), ('ALL'), ('FETCH'), ('PUBLIC'), ('ALTER'), ('FILE'), ('RAISERROR'), ('AND'), ('FILLFACTOR'), ('READ'), ('ANY'), ('FOR'), ('READTEXT'), ('AS'), ('FOREIGN'), ('RECONFIGURE'), ('ASC'), ('FREETEXT'), ('REFERENCES'), ('AUTHORIZATION'), ('FREETEXTTABLE'), ('REPLICATION'), ('BACKUP'), ('FROM'), ('RESTORE'), ('BEGIN'), ('FULL'), ('RESTRICT'), ('BETWEEN'), ('FUNCTION'), ('RETURN'), ('BREAK'), ('GOTO'), ('REVERT'), ('BROWSE'), ('GRANT'), ('REVOKE'), ('BULK'), ('GROUP'), ('RIGHT'), ('BY'), ('HAVING'), ('ROLLBACK'), ('CASCADE'), ('HOLDLOCK'), ('ROWCOUNT'), ('CASE'), ('IDENTITY'), ('ROWGUIDCOL'), ('CHECK'), ('IDENTITY_INSERT'), ('RULE'), ('CHECKPOINT'), ('IDENTITYCOL'), ('SAVE'), ('CLOSE'), ('IF'), ('SCHEMA'), ('CLUSTERED'), ('IN'), ('SECURITYAUDIT'), ('COALESCE'), ('INDEX'), ('SELECT'), ('COLLATE'), ('INNER'), ('SEMANTICKEYPHRASETABLE'), ('COLUMN'), ('INSERT'), ('SEMANTICSIMILARITYDETAILSTABLE'), ('COMMIT'), ('INTERSECT'), ('SEMANTICSIMILARITYTABLE'), ('COMPUTE'), ('INTO'), ('SESSION_USER'), ('CONSTRAINT'), ('IS'), ('SET'), ('CONTAINS'), ('JOIN'), ('SETUSER'), ('CONTAINSTABLE'), ('KEY'), ('SHUTDOWN'), ('CONTINUE'), ('KILL'), ('SOME'), ('CONVERT'), ('LEFT'), ('STATISTICS'), ('CREATE'), ('LIKE'), ('SYSTEM_USER'), ('CROSS'), ('LINENO'), ('TABLE'), ('CURRENT'), ('LOAD'), ('TABLESAMPLE'), ('CURRENT_DATE'), ('MERGE'), ('TEXTSIZE'), ('CURRENT_TIME'), ('NATIONAL'), ('THEN'), ('CURRENT_TIMESTAMP'), ('NOCHECK'), ('TO'), ('CURRENT_USER'), ('NONCLUSTERED'), ('TOP'), ('CURSOR'), ('NOT'), ('TRAN'), ('DATABASE'), ('NULL'), ('TRANSACTION'), ('DBCC'), ('NULLIF'), ('TRIGGER'), ('DEALLOCATE'), ('OF'), ('TRUNCATE'), ('DECLARE'), ('OFF'), ('TRY_CONVERT'), ('DEFAULT'), ('OFFSETS'), ('TSEQUAL'), ('DELETE'), ('ON'), ('UNION'), ('DENY'), ('OPEN'), ('UNIQUE'), ('DESC'), ('OPENDATASOURCE'), ('UNPIVOT'), ('DISK'), ('OPENQUERY'), ('UPDATE'), ('DISTINCT'), ('OPENROWSET'), ('UPDATETEXT'), ('DISTRIBUTED'), ('OPENXML'), ('USE'), ('DOUBLE'), ('OPTION'), ('USER'), ('DROP'), ('OR'), ('VALUES'), ('DUMP'), ('ORDER'), ('VARYING'), ('ELSE'), ('OUTER'), ('VIEW'), ('END'), ('OVER'), ('WAITFOR'), ('ERRLVL'), ('PERCENT'), ('WHEN'), ('ESCAPE'), ('PIVOT'), ('WHERE'), ('EXCEPT'), ('PLAN'), ('WHILE'), ('EXEC'), ('PRECISION'), ('WITH'), ('EXECUTE'), ('PRIMARY'), ('WITHIN GROUP'), ('EXISTS'), ('PRINT'), ('WRITETEXT'), ('EXIT'), ('PROC'), ('LABEL') ) AS Reserved_Words (word) WHERE word = @TheIdentifier ) RETURN QuoteName(@TheIdentifier,@TheDelimiter); RETURN @TheIdentifier; END; GO /*Quick test that the default for @TheDelimiter is working*/ DECLARE @TheIdentifier sysname EXECUTE @TheIdentifier = dbo.DelimitedIfNecessary '@invalid_name' if @theidentifier<>'"@invalid_name"' RAISERROR ('the default value isn''t working',16,1) /* Now test it with a range of inputs */ SELECT produced, ShouldBe FROM ( VALUES (dbo.DelimitedIfNecessary('valid_name@','['), 'valid_name@'), (dbo.DelimitedIfNecessary('@invalid_name','['), '[@invalid_name]'), (dbo.DelimitedIfNecessary('v#alid$_n@me','['), 'v#alid$_n@me'), (dbo.DelimitedIfNecessary('valid###_name','['), 'valid###_name'), (dbo.DelimitedIfNecessary('invalid-name','['), '[invalid-name]'), (dbo.DelimitedIfNecessary('[invalid_name]','['), '[[invalid_name]]]'), (dbo.DelimitedIfNecessary('#invalid-name','['), '[#invalid-name]'), (dbo.DelimitedIfNecessary(NULL,'['), 'null'), (dbo.DelimitedIfNecessary('Invalid(name)','['), '[Invalid(name)]'), (dbo.DelimitedIfNecessary('invalid name','['), '[invalid name]'), (dbo.DelimitedIfNecessary('','['), 'null'), (dbo.DelimitedIfNecessary('valid_name@','"'), 'valid_name@'), (dbo.DelimitedIfNecessary('@invalid_name','"'), '"@invalid_name"'), (dbo.DelimitedIfNecessary('v#alid$_n@me','"'), 'v#alid$_n@me'), (dbo.DelimitedIfNecessary('valid###_name','"'), 'valid###_name'), (dbo.DelimitedIfNecessary('invalid-name','"'), '"invalid-name"'), (dbo.DelimitedIfNecessary('"invalid_name"','"'), '"""invalid_name"""'), (dbo.DelimitedIfNecessary('#invalid-name','"'), '"#invalid-name"'), (dbo.DelimitedIfNecessary(NULL,'"'), 'null'), (dbo.DelimitedIfNecessary('Invalid(name)','"'), '"Invalid(name)"'), (dbo.DelimitedIfNecessary('invalid name','"'), '"invalid name"'), (dbo.DelimitedIfNecessary('','['), 'null') ) AS f (produced, ShouldBe) WHERE produced <> ShouldBe; /* comment this out to see the successful tests as well */ |
Conclusions
Whenever possible, you should avoid the need for having identifiers that aren’t valid SQL. That way, you avoid any misunderstandings or ambiguity. In some rare circumstances, you may need to introduce some invalid characters into identifiers, usually aliases. You may need to do it in column names, or preferably your aliases for them, to ensure that they are understandable in reports. Then you will need to delimit them.
Whenever we need to be able to see and understand SQL easily, we need to remove all surplus unnecessary delimiters. SQL Prompt has an excellent way of doing this. You’ll quickly convince yourself of this by generating scripts for tables in SSMS, and then running the Remove Square Brackets action. It is very satisfying to see the structure of a table appear from behind its decorations of square brackets.