diff --git a/obfuscator_test.go b/obfuscator_test.go index df3dbf0..7dd2d2a 100644 --- a/obfuscator_test.go +++ b/obfuscator_test.go @@ -45,6 +45,31 @@ func TestObfuscator(t *testing.T) { expected: "SELECT * FROM users where id = ?", replaceDigits: false, }, + { + // SQL Server does not treat backslash as a string escape, so + // ESCAPE N'\' is a complete literal (a single backslash). + input: `DECLARE @p1 NVARCHAR(50)=N'%foo%', @p2 NVARCHAR(50)=N'%bar%'; SELECT col FROM tbl WHERE col LIKE @p1 ESCAPE N'\' AND col LIKE @p2 ESCAPE N'\';`, + expected: `DECLARE @p1 NVARCHAR(?)=N?, @p2 NVARCHAR(?)=N?; SELECT col FROM tbl WHERE col LIKE @p1 ESCAPE N? AND col LIKE @p2 ESCAPE N?;`, + dbms: DBMSSQLServer, + }, + { + // Oracle does not treat backslash as a string escape either. + input: `SELECT col FROM tbl WHERE col LIKE '%foo\_%' ESCAPE '\' AND col LIKE '%bar\_%' ESCAPE '\' AND flag = 'x'`, + expected: `SELECT col FROM tbl WHERE col LIKE ? ESCAPE ? AND col LIKE ? ESCAPE ? AND flag = ?`, + dbms: DBMSOracle, + }, + { + // MySQL genuinely uses backslash as a string escape + input: `SELECT col FROM tbl WHERE col LIKE '%foo%' ESCAPE '\' AND flag = 1`, + expected: `SELECT col FROM tbl WHERE col LIKE ? ESCAPE ?`, + dbms: DBMSMySQL, + }, + { + // Snowflake also supports backslash escape sequences + input: `SELECT col FROM tbl WHERE col LIKE '%foo%' ESCAPE '\' AND flag = 1`, + expected: `SELECT col FROM tbl WHERE col LIKE ? ESCAPE ?`, + dbms: DBMSSnowflake, + }, { input: "SELECT * FROM \"users table\" where id = 1", expected: "SELECT * FROM \"users table\" where id = ?", diff --git a/sqllexer.go b/sqllexer.go index 9ee985b..f4bd298 100644 --- a/sqllexer.go +++ b/sqllexer.go @@ -318,6 +318,10 @@ func (s *Lexer) scanStringWithDelimiter(delimiter rune) *Token { escaped := false escapedQuote := false + // SQL Server (T-SQL) and Oracle do not use backslash as a string escape + // character; a quote inside a literal is escaped by doubling it (''). + backslashEscapes := s.config.DBMS != DBMSSQLServer && s.config.DBMS != DBMSOracle + ch := s.next() // consume opening quote for ; !isEOF(ch); ch = s.next() { @@ -327,7 +331,7 @@ func (s *Lexer) scanStringWithDelimiter(delimiter rune) *Token { continue } - if ch == '\\' { + if backslashEscapes && ch == '\\' { escaped = true continue } diff --git a/sqllexer_test.go b/sqllexer_test.go index 1a127f4..b950a01 100644 --- a/sqllexer_test.go +++ b/sqllexer_test.go @@ -1027,6 +1027,79 @@ here */`, {STRING, `'\'`}, }, }, + { + // In SQL Server, backslash is not a string escape character + // (quotes are escaped by doubling: ''). A string literal containing a + // lone backslash, e.g. ESCAPE '\', must terminate at its closing quote + // even when more SQL follows. + name: "sqlserver escape backslash mid-query", + input: `SELECT * FROM t WHERE x LIKE '%a%' ESCAPE '\' AND y = 1`, + expected: []TokenSpec{ + {COMMAND, "SELECT"}, + {SPACE, " "}, + {WILDCARD, "*"}, + {SPACE, " "}, + {KEYWORD, "FROM"}, + {SPACE, " "}, + {IDENT, "t"}, + {SPACE, " "}, + {KEYWORD, "WHERE"}, + {SPACE, " "}, + {IDENT, "x"}, + {SPACE, " "}, + {KEYWORD, "LIKE"}, + {SPACE, " "}, + {STRING, `'%a%'`}, + {SPACE, " "}, + {KEYWORD, "ESCAPE"}, + {SPACE, " "}, + {STRING, `'\'`}, + {SPACE, " "}, + {KEYWORD, "AND"}, + {SPACE, " "}, + {IDENT, "y"}, + {SPACE, " "}, + {OPERATOR, "="}, + {SPACE, " "}, + {NUMBER, "1"}, + }, + lexerOpts: []lexerOption{WithDBMS(DBMSSQLServer)}, + }, + { + // Oracle also does not treat backslash as a string escape character. + name: "oracle escape backslash mid-query", + input: `SELECT * FROM t WHERE x LIKE '%a%' ESCAPE '\' AND y = 1`, + expected: []TokenSpec{ + {COMMAND, "SELECT"}, + {SPACE, " "}, + {WILDCARD, "*"}, + {SPACE, " "}, + {KEYWORD, "FROM"}, + {SPACE, " "}, + {IDENT, "t"}, + {SPACE, " "}, + {KEYWORD, "WHERE"}, + {SPACE, " "}, + {IDENT, "x"}, + {SPACE, " "}, + {KEYWORD, "LIKE"}, + {SPACE, " "}, + {STRING, `'%a%'`}, + {SPACE, " "}, + {KEYWORD, "ESCAPE"}, + {SPACE, " "}, + {STRING, `'\'`}, + {SPACE, " "}, + {KEYWORD, "AND"}, + {SPACE, " "}, + {IDENT, "y"}, + {SPACE, " "}, + {OPERATOR, "="}, + {SPACE, " "}, + {NUMBER, "1"}, + }, + lexerOpts: []lexerOption{WithDBMS(DBMSOracle)}, + }, { name: "simple select with multiline comments as separators", input: `SELECT/**/*/**/FROM/**/test`,