From 1f91865b38d44021916476a02b0a4e69e6c0c3b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Knut=20Olav=20L=C3=B8ite?= Date: Fri, 28 Nov 2025 11:07:57 +0100 Subject: [PATCH 1/2] fix: backslash at end of string was misinterpreted A SQL string with a string literal that ended with an escaped backslash was misinterpreted by the statement parser as an unclosed literal. E.g. the string `'test\\'` would be seen as an invalid literal. --- parser/statement_parser.go | 4 ++-- parser/statement_parser_test.go | 38 +++++++++++++++++++++++++++++++++ 2 files changed, 40 insertions(+), 2 deletions(-) diff --git a/parser/statement_parser.go b/parser/statement_parser.go index 1679a741..4312865e 100644 --- a/parser/statement_parser.go +++ b/parser/statement_parser.go @@ -458,8 +458,8 @@ func (p *StatementParser) skipQuoted(sql []byte, pos int, quote byte) (int, int, // This was the end quote. return pos + 1, quoteLength, nil } - } else if (p.supportsBackslashEscape() || isEscapeString) && len(sql) > pos+1 && c == '\\' && sql[pos+1] == quote { - // This is an escaped quote (e.g. 'foo\'bar'). + } else if (p.supportsBackslashEscape() || isEscapeString) && len(sql) > pos+1 && c == '\\' && (sql[pos+1] == quote || sql[pos+1] == '\\') { + // This is an escaped quote (e.g. 'foo\'bar') or an escaped backslash (e.g 'test\\'). // Note that in raw strings, the \ officially does not start an // escape sequence, but the result is still the same, as in a raw // string 'both characters are preserved'. diff --git a/parser/statement_parser_test.go b/parser/statement_parser_test.go index 80f7e524..89161bfe 100644 --- a/parser/statement_parser_test.go +++ b/parser/statement_parser_test.go @@ -740,6 +740,17 @@ SELECT * FROM PersonsTable WHERE id=@id`, databasepb.DatabaseDialect_POSTGRESQL: spanner.ToSpannerError(status.Errorf(codes.InvalidArgument, "SQL statement contains an unclosed literal: %s", `?"?it\"?s"?`)), }, }, + "backslash at end of string": { + input: `?'test\\'?`, + wantSQL: map[databasepb.DatabaseDialect]string{ + databasepb.DatabaseDialect_GOOGLE_STANDARD_SQL: `@p1'test\\'@p2`, + databasepb.DatabaseDialect_POSTGRESQL: `$1'test\\'$2`, + }, + want: map[databasepb.DatabaseDialect][]string{ + databasepb.DatabaseDialect_GOOGLE_STANDARD_SQL: {"p1", "p2"}, + databasepb.DatabaseDialect_POSTGRESQL: {"p1", "p2"}, + }, + }, "triple-quoted string": { input: `?'''?it\'?s'''?`, wantSQL: map[databasepb.DatabaseDialect]string{ @@ -1092,6 +1103,16 @@ SELECT * FROM PersonsTable WHERE id=$1`, input: `?"?it\"?s"?`, wantErr: spanner.ToSpannerError(status.Errorf(codes.InvalidArgument, "SQL statement contains an unclosed literal: %s", `?"?it\"?s"?`)), }, + "backslash at end of string": { + input: `?'test\\'?`, + wantSQL: `$1'test\\'$2`, + want: []string{"p1", "p2"}, + }, + "backslash at end of double-quoted string": { + input: `?"test\\"?`, + wantSQL: `$1"test\\"$2`, + want: []string{"p1", "p2"}, + }, "triple-quoted string": { input: `?'''?it\'?s'''?`, wantErr: spanner.ToSpannerError(status.Errorf(codes.InvalidArgument, "SQL statement contains an unclosed literal: %s", `?'''?it\'?s'''?`)), @@ -1263,6 +1284,16 @@ func TestFindParamsWithCommentsPostgreSQL(t *testing.T) { wantSQL: `$1\"?it\\"\"?s\"%s$2`, want: []string{"p1", "p2"}, }, + "backslash at end of string": { + input: `?'test\\'?`, + wantSQL: `$1'test\\'$2`, + want: []string{"p1", "p2"}, + }, + "backslash at end of double-quoted string": { + input: `?"test\\"?`, + wantSQL: `$1"test\\"$2`, + want: []string{"p1", "p2"}, + }, "triple-quotes": { input: `?%s'''?it\''?s'''?`, wantSQL: `$1%s'''?it\''?s'''$2`, @@ -2028,6 +2059,13 @@ func TestSkip(t *testing.T) { databasepb.DatabaseDialect_POSTGRESQL: "'''foo\\'", }, }, + "escaped backslash at end of string literal": { + input: "'test\\\\' as foo", + skipped: map[databasepb.DatabaseDialect]string{ + databasepb.DatabaseDialect_GOOGLE_STANDARD_SQL: "'test\\\\'", + databasepb.DatabaseDialect_POSTGRESQL: "'test\\\\'", + }, + }, "string with linefeed": { input: "'foo\n' ", skipped: map[databasepb.DatabaseDialect]string{ From ba93813844a58eac61bcaf4858bfdb956d54a3b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Knut=20Olav=20L=C3=B8ite?= Date: Fri, 28 Nov 2025 11:27:58 +0100 Subject: [PATCH 2/2] chore: address review comments --- parser/statement_parser_test.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/parser/statement_parser_test.go b/parser/statement_parser_test.go index 89161bfe..a3653655 100644 --- a/parser/statement_parser_test.go +++ b/parser/statement_parser_test.go @@ -1285,13 +1285,13 @@ func TestFindParamsWithCommentsPostgreSQL(t *testing.T) { want: []string{"p1", "p2"}, }, "backslash at end of string": { - input: `?'test\\'?`, - wantSQL: `$1'test\\'$2`, + input: `?'test\\'%s?`, + wantSQL: `$1'test\\'%s$2`, want: []string{"p1", "p2"}, }, "backslash at end of double-quoted string": { - input: `?"test\\"?`, - wantSQL: `$1"test\\"$2`, + input: `?"test\\"%s?`, + wantSQL: `$1"test\\"%s$2`, want: []string{"p1", "p2"}, }, "triple-quotes": {