diff --git a/normalizer.go b/normalizer.go index 7563e3d..a893c43 100644 --- a/normalizer.go +++ b/normalizer.go @@ -408,6 +408,11 @@ func (n *Normalizer) shouldStripIdentifierQuotes(token *Token, lastValueToken *L if isAliasContext(lastValueToken) && !token.isSimpleIdentifier { return false } + // Bracket-quoted identifiers whose content contains whitespace cannot be + // safely unquoted: the stripped form would produce multiple SQL tokens. + if strings.ContainsAny(token.Value, " \t\n\r") { + return false + } return true } @@ -470,6 +475,12 @@ func (n *Normalizer) appendSpace(token *Token, lastValueToken *LastValueToken, n return } + // do not add a space between a dot-suffixed identifier and the following identifier, + // because the lexer splits e.g. t.[Col] into "t." and "[Col]" as separate tokens + if lastValueToken != nil && len(lastValueToken.Value) > 1 && lastValueToken.Value[len(lastValueToken.Value)-1] == '.' && (token.Type == IDENT || token.Type == QUOTED_IDENT) { + return + } + // do not add a space after a colon when followed by an identifier or quoted identifier, // BUT only if the token before the colon was NOT an identifier (to preserve MySQL labels and Snowflake semi-structured access) // This handles Oracle bind variables like :param or :"param" diff --git a/normalizer_test.go b/normalizer_test.go index 79fbda1..d6d842a 100644 --- a/normalizer_test.go +++ b/normalizer_test.go @@ -1041,6 +1041,85 @@ func TestNormalizeDeobfuscatedSQL(t *testing.T) { WithDBMS(DBMSSQLServer), }, }, + { + // simple bracket-quoted column after dot — no space, brackets stripped, metadata on + input: `SELECT t.[SimpleCol] FROM dbo.SomeTable AS t`, + expected: `SELECT t.SimpleCol FROM dbo.SomeTable AS t`, + statementMetadata: StatementMetadata{ + Tables: []string{`dbo.SomeTable`}, + Comments: []string{}, + Commands: []string{"SELECT"}, + Procedures: []string{}, + Size: 19, + }, + normalizationConfig: &normalizerConfig{ + CollectComments: true, + CollectCommands: true, + CollectTables: true, + KeepSQLAlias: true, + }, + lexerOptions: []lexerOption{ + WithDBMS(DBMSSQLServer), + }, + }, + { + // same as above with all metadata collection disabled — output must be identical + input: `SELECT t.[SimpleCol] FROM dbo.SomeTable AS t`, + expected: `SELECT t.SimpleCol FROM dbo.SomeTable AS t`, + statementMetadata: StatementMetadata{ + Tables: []string{}, + Comments: []string{}, + Commands: []string{}, + Procedures: []string{}, + Size: 0, + }, + normalizationConfig: &normalizerConfig{ + KeepSQLAlias: true, + }, + lexerOptions: []lexerOption{ + WithDBMS(DBMSSQLServer), + }, + }, + { + input: `SELECT t.[Column With Spaces] FROM dbo.SomeTable AS t`, + expected: `SELECT t.[Column With Spaces] FROM dbo.SomeTable AS t`, + statementMetadata: StatementMetadata{ + Tables: []string{`dbo.SomeTable`}, + Comments: []string{}, + Commands: []string{"SELECT"}, + Procedures: []string{}, + Size: 19, + }, + normalizationConfig: &normalizerConfig{ + CollectComments: true, + CollectCommands: true, + CollectTables: true, + KeepSQLAlias: true, + }, + lexerOptions: []lexerOption{ + WithDBMS(DBMSSQLServer), + }, + }, + { + input: `SELECT [First Name], [Last Name] FROM [My Table]`, + expected: `SELECT [First Name], [Last Name] FROM [My Table]`, + statementMetadata: StatementMetadata{ + Tables: []string{`My Table`}, + Comments: []string{}, + Commands: []string{"SELECT"}, + Procedures: []string{}, + Size: 14, + }, + normalizationConfig: &normalizerConfig{ + CollectComments: true, + CollectCommands: true, + CollectTables: true, + KeepSQLAlias: true, + }, + lexerOptions: []lexerOption{ + WithDBMS(DBMSSQLServer), + }, + }, { input: `( @p1 bigint ) SELECT * from dbm_user as prepared_user WHERE id = @p1`, expected: `SELECT * from dbm_user as prepared_user WHERE id = @p1`,