diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/SpannerStatementParser.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/SpannerStatementParser.java index 2689145b7ac..3e70170389c 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/SpannerStatementParser.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/SpannerStatementParser.java @@ -145,7 +145,7 @@ String removeCommentsAndTrimInternal(String sql) { startQuote = 0; } } else if (c == '\\') { - lastCharWasEscapeChar = true; + lastCharWasEscapeChar = !lastCharWasEscapeChar; } else { lastCharWasEscapeChar = false; } @@ -294,7 +294,7 @@ protected boolean checkReturningClauseInternal(String rawSql) { startQuote = 0; } } else if (c == '\\') { - lastCharWasEscapeChar = true; + lastCharWasEscapeChar = !lastCharWasEscapeChar; } else { lastCharWasEscapeChar = false; } diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/SpannerStatementParserTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/SpannerStatementParserTest.java index 10f01df937a..048f95ed778 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/SpannerStatementParserTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/SpannerStatementParserTest.java @@ -16,10 +16,16 @@ package com.google.cloud.spanner.connection; +import static com.google.cloud.spanner.ErrorCode.INVALID_ARGUMENT; import static com.google.cloud.spanner.connection.StatementParserTest.assertUnclosedLiteral; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; import com.google.cloud.spanner.Dialect; +import com.google.cloud.spanner.SpannerException; +import com.google.cloud.spanner.Statement; import com.google.cloud.spanner.connection.StatementParserTest.CommentInjector; import org.junit.Test; import org.junit.runner.RunWith; @@ -39,6 +45,55 @@ static String skip(String sql, int currentIndex) { return sql.substring(currentIndex, position); } + @Test + public void testRemoveCommentsAndTrim() { + AbstractStatementParser parser = + AbstractStatementParser.getInstance(Dialect.GOOGLE_STANDARD_SQL); + + // Statements that should parse correctly + String[] validStatements = + new String[] { + "SELECT '\\\\'", // SELECT '\\' (escaped backslash, followed by quote) + "SELECT '\\''", // SELECT '\'' (escaped quote, followed by an actual closing quote) + "SELECT '\\\\\\\\'" // SELECT '\\\\' (two escaped backslashes) + }; + for (String sql : validStatements) { + assertEquals(sql, parser.removeCommentsAndTrim(sql)); + } + + // Statements that contain an unclosed literal because the final quote is + // escaped + String[] invalidStatements = + new String[] { + "SELECT '\\'" // SELECT '\' (escaped closing quote) + }; + + for (String sql : invalidStatements) { + try { + parser.removeCommentsAndTrim(sql); + fail("Expected SpannerException for unclosed literal: " + sql); + } catch (SpannerException e) { + assertEquals(INVALID_ARGUMENT, e.getErrorCode()); + } + } + } + + @Test + public void testReturningClauseWithBackslashes() { + AbstractStatementParser parser = + AbstractStatementParser.getInstance(Dialect.GOOGLE_STANDARD_SQL); + + // Valid returning clause, double backslash in string literal should be handled + // correctly. + String sqlWithReturning = "INSERT INTO my_table (value) VALUES ('foo \\\\ bar') THEN RETURN id"; + assertTrue(parser.parse(Statement.of(sqlWithReturning)).hasReturningClause()); + + // No returning clause, `then return` is inside a string literal with a double + // backslash. + String sqlWithoutReturning = "INSERT INTO my_table (value) VALUES ('then \\\\ return')"; + assertFalse(parser.parse(Statement.of(sqlWithoutReturning)).hasReturningClause()); + } + @Test public void testSkip() { assertEquals("", skip(""));