From 4ab6017ec3e2b77b311700ed2b79877af77efea4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Knut=20Olav=20L=C3=B8ite?= Date: Wed, 13 Mar 2024 14:41:59 +0100 Subject: [PATCH 1/2] refactor: move skip methods to abstract parser Move the PostgreSQL skip methods from the PostgreSQL parser to the abstract parser. This is step 1 in refactoring the GoogleSQL and PostgreSQL parser so they can share more code. The eventual goal is to allow the GoogleSQL parser to be able to handle SQL string without having to remove the comments from the string first. --- .../connection/AbstractStatementParser.java | 166 ++++++++++++++++++ .../connection/PostgreSQLStatementParser.java | 147 ---------------- 2 files changed, 166 insertions(+), 147 deletions(-) diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/AbstractStatementParser.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/AbstractStatementParser.java index 8fc3043791e..ac984a0f864 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/AbstractStatementParser.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/AbstractStatementParser.java @@ -40,6 +40,7 @@ import java.util.concurrent.Callable; import java.util.logging.Level; import java.util.logging.Logger; +import javax.annotation.Nullable; /** * Internal class for the Spanner Connection API. @@ -696,4 +697,169 @@ static int countOccurrencesOf(char c, String string) { public boolean checkReturningClause(String sql) { return checkReturningClauseInternal(sql); } + + /** + * Returns true for characters that can be used as the first character in unquoted identifiers. + */ + boolean isValidIdentifierFirstChar(char c) { + return Character.isLetter(c) || c == UNDERSCORE; + } + + /** Returns true for characters that can be used in unquoted identifiers. */ + boolean isValidIdentifierChar(char c) { + return isValidIdentifierFirstChar(c) || Character.isDigit(c) || c == DOLLAR; + } + + /** Reads a dollar-quoted string literal from position index in the given sql string. */ + String parseDollarQuotedString(String sql, int index) { + // Look ahead to the next dollar sign (if any). Everything in between is the quote tag. + StringBuilder tag = new StringBuilder(); + while (index < sql.length()) { + char c = sql.charAt(index); + if (c == DOLLAR) { + return tag.toString(); + } + if (!isValidIdentifierChar(c)) { + break; + } + tag.append(c); + index++; + } + return null; + } + + /** + * Skips the next character, literal, identifier, or comment in the given sql string from the + * given index. The skipped characters are added to result if it is not null. + */ + int skip(String sql, int currentIndex, @Nullable StringBuilder result) { + char currentChar = sql.charAt(currentIndex); + if (currentChar == SINGLE_QUOTE || currentChar == DOUBLE_QUOTE) { + appendIfNotNull(result, currentChar); + return skipQuoted(sql, currentIndex, currentChar, result); + } else if (currentChar == DOLLAR) { + String dollarTag = parseDollarQuotedString(sql, currentIndex + 1); + if (dollarTag != null) { + appendIfNotNull(result, currentChar, dollarTag, currentChar); + return skipQuoted( + sql, currentIndex + dollarTag.length() + 1, currentChar, dollarTag, result); + } + } else if (currentChar == HYPHEN + && sql.length() > (currentIndex + 1) + && sql.charAt(currentIndex + 1) == HYPHEN) { + return skipSingleLineComment(sql, currentIndex, result); + } else if (currentChar == SLASH + && sql.length() > (currentIndex + 1) + && sql.charAt(currentIndex + 1) == ASTERISK) { + return skipMultiLineComment(sql, currentIndex, result); + } + + appendIfNotNull(result, currentChar); + return currentIndex + 1; + } + + /** Skips a single-line comment from startIndex and adds it to result if result is not null. */ + static int skipSingleLineComment(String sql, int startIndex, @Nullable StringBuilder result) { + int endIndex = sql.indexOf('\n', startIndex + 2); + if (endIndex == -1) { + endIndex = sql.length(); + } else { + // Include the newline character. + endIndex++; + } + appendIfNotNull(result, sql.substring(startIndex, endIndex)); + return endIndex; + } + + /** Skips a multi-line comment from startIndex and adds it to result if result is not null. */ + static int skipMultiLineComment(String sql, int startIndex, @Nullable StringBuilder result) { + // Current position is start + '/*'.length(). + int pos = startIndex + 2; + // PostgreSQL allows comments to be nested. That is, the following is allowed: + // '/* test /* inner comment */ still a comment */' + int level = 1; + while (pos < sql.length()) { + if (sql.charAt(pos) == SLASH && sql.length() > (pos + 1) && sql.charAt(pos + 1) == ASTERISK) { + level++; + } + if (sql.charAt(pos) == ASTERISK && sql.length() > (pos + 1) && sql.charAt(pos + 1) == SLASH) { + level--; + if (level == 0) { + pos += 2; + appendIfNotNull(result, sql.substring(startIndex, pos)); + return pos; + } + } + pos++; + } + appendIfNotNull(result, sql.substring(startIndex)); + return sql.length(); + } + + /** Skips a quoted string from startIndex. */ + private int skipQuoted( + String sql, int startIndex, char startQuote, @Nullable StringBuilder result) { + return skipQuoted(sql, startIndex, startQuote, null, result); + } + + /** + * Skips a quoted string from startIndex. The quote character is assumed to be $ if dollarTag is + * not null. + */ + private int skipQuoted( + String sql, + int startIndex, + char startQuote, + String dollarTag, + @Nullable StringBuilder result) { + int currentIndex = startIndex + 1; + while (currentIndex < sql.length()) { + char currentChar = sql.charAt(currentIndex); + if (currentChar == startQuote) { + if (currentChar == DOLLAR) { + // Check if this is the end of the current dollar quoted string. + String tag = parseDollarQuotedString(sql, currentIndex + 1); + if (tag != null && tag.equals(dollarTag)) { + appendIfNotNull(result, currentChar, dollarTag, currentChar); + return currentIndex + tag.length() + 2; + } + } else if (sql.length() > currentIndex + 1 && sql.charAt(currentIndex + 1) == startQuote) { + // This is an escaped quote (e.g. 'foo''bar') + appendIfNotNull(result, currentChar); + appendIfNotNull(result, currentChar); + currentIndex += 2; + continue; + } else { + appendIfNotNull(result, currentChar); + return currentIndex + 1; + } + } + currentIndex++; + appendIfNotNull(result, currentChar); + } + throw SpannerExceptionFactory.newSpannerException( + ErrorCode.INVALID_ARGUMENT, "SQL statement contains an unclosed literal: " + sql); + } + + /** Appends the given character to result if result is not null. */ + private void appendIfNotNull(@Nullable StringBuilder result, char currentChar) { + if (result != null) { + result.append(currentChar); + } + } + + /** Appends the given suffix to result if result is not null. */ + private static void appendIfNotNull(@Nullable StringBuilder result, String suffix) { + if (result != null) { + result.append(suffix); + } + } + + /** Appends the given prefix, tag, and suffix to result if result is not null. */ + private static void appendIfNotNull( + @Nullable StringBuilder result, char prefix, String tag, char suffix) { + if (result != null) { + result.append(prefix).append(tag).append(suffix); + } + } } diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/PostgreSQLStatementParser.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/PostgreSQLStatementParser.java index 8cb2b7e464a..572ea056546 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/PostgreSQLStatementParser.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/PostgreSQLStatementParser.java @@ -26,7 +26,6 @@ import java.util.HashSet; import java.util.Set; import java.util.regex.Pattern; -import javax.annotation.Nullable; @InternalApi public class PostgreSQLStatementParser extends AbstractStatementParser { @@ -136,23 +135,6 @@ String removeCommentsAndTrimInternal(String sql) { return res.toString().trim(); } - String parseDollarQuotedString(String sql, int index) { - // Look ahead to the next dollar sign (if any). Everything in between is the quote tag. - StringBuilder tag = new StringBuilder(); - while (index < sql.length()) { - char c = sql.charAt(index); - if (c == DOLLAR) { - return tag.toString(); - } - if (!isValidIdentifierChar(c)) { - break; - } - tag.append(c); - index++; - } - return null; - } - /** PostgreSQL does not support statement hints. */ @Override String removeStatementHint(String sql) { @@ -220,135 +202,6 @@ public Set getQueryParameters(String sql) { return parameters; } - private int skip(String sql, int currentIndex, @Nullable StringBuilder result) { - char currentChar = sql.charAt(currentIndex); - if (currentChar == SINGLE_QUOTE || currentChar == DOUBLE_QUOTE) { - appendIfNotNull(result, currentChar); - return skipQuoted(sql, currentIndex, currentChar, result); - } else if (currentChar == DOLLAR) { - String dollarTag = parseDollarQuotedString(sql, currentIndex + 1); - if (dollarTag != null) { - appendIfNotNull(result, currentChar, dollarTag, currentChar); - return skipQuoted( - sql, currentIndex + dollarTag.length() + 1, currentChar, dollarTag, result); - } - } else if (currentChar == HYPHEN - && sql.length() > (currentIndex + 1) - && sql.charAt(currentIndex + 1) == HYPHEN) { - return skipSingleLineComment(sql, currentIndex, result); - } else if (currentChar == SLASH - && sql.length() > (currentIndex + 1) - && sql.charAt(currentIndex + 1) == ASTERISK) { - return skipMultiLineComment(sql, currentIndex, result); - } - - appendIfNotNull(result, currentChar); - return currentIndex + 1; - } - - static int skipSingleLineComment(String sql, int currentIndex, @Nullable StringBuilder result) { - int endIndex = sql.indexOf('\n', currentIndex + 2); - if (endIndex == -1) { - endIndex = sql.length(); - } else { - // Include the newline character. - endIndex++; - } - appendIfNotNull(result, sql.substring(currentIndex, endIndex)); - return endIndex; - } - - static int skipMultiLineComment(String sql, int startIndex, @Nullable StringBuilder result) { - // Current position is start + '/*'.length(). - int pos = startIndex + 2; - // PostgreSQL allows comments to be nested. That is, the following is allowed: - // '/* test /* inner comment */ still a comment */' - int level = 1; - while (pos < sql.length()) { - if (sql.charAt(pos) == SLASH && sql.length() > (pos + 1) && sql.charAt(pos + 1) == ASTERISK) { - level++; - } - if (sql.charAt(pos) == ASTERISK && sql.length() > (pos + 1) && sql.charAt(pos + 1) == SLASH) { - level--; - if (level == 0) { - pos += 2; - appendIfNotNull(result, sql.substring(startIndex, pos)); - return pos; - } - } - pos++; - } - appendIfNotNull(result, sql.substring(startIndex)); - return sql.length(); - } - - private int skipQuoted( - String sql, int startIndex, char startQuote, @Nullable StringBuilder result) { - return skipQuoted(sql, startIndex, startQuote, null, result); - } - - private int skipQuoted( - String sql, - int startIndex, - char startQuote, - String dollarTag, - @Nullable StringBuilder result) { - int currentIndex = startIndex + 1; - while (currentIndex < sql.length()) { - char currentChar = sql.charAt(currentIndex); - if (currentChar == startQuote) { - if (currentChar == DOLLAR) { - // Check if this is the end of the current dollar quoted string. - String tag = parseDollarQuotedString(sql, currentIndex + 1); - if (tag != null && tag.equals(dollarTag)) { - appendIfNotNull(result, currentChar, dollarTag, currentChar); - return currentIndex + tag.length() + 2; - } - } else if (sql.length() > currentIndex + 1 && sql.charAt(currentIndex + 1) == startQuote) { - // This is an escaped quote (e.g. 'foo''bar') - appendIfNotNull(result, currentChar); - appendIfNotNull(result, currentChar); - currentIndex += 2; - continue; - } else { - appendIfNotNull(result, currentChar); - return currentIndex + 1; - } - } - currentIndex++; - appendIfNotNull(result, currentChar); - } - throw SpannerExceptionFactory.newSpannerException( - ErrorCode.INVALID_ARGUMENT, "SQL statement contains an unclosed literal: " + sql); - } - - private void appendIfNotNull(@Nullable StringBuilder result, char currentChar) { - if (result != null) { - result.append(currentChar); - } - } - - private static void appendIfNotNull(@Nullable StringBuilder result, String suffix) { - if (result != null) { - result.append(suffix); - } - } - - private void appendIfNotNull( - @Nullable StringBuilder result, char prefix, String tag, char suffix) { - if (result != null) { - result.append(prefix).append(tag).append(suffix); - } - } - - private boolean isValidIdentifierFirstChar(char c) { - return Character.isLetter(c) || c == UNDERSCORE; - } - - private boolean isValidIdentifierChar(char c) { - return isValidIdentifierFirstChar(c) || Character.isDigit(c) || c == DOLLAR; - } - private boolean checkCharPrecedingReturning(char ch) { return (ch == SPACE) || (ch == SINGLE_QUOTE) From 2d30e032163e7f967c2af0f239c73122649cdf31 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Knut=20Olav=20L=C3=B8ite?= Date: Wed, 13 Mar 2024 15:18:52 +0100 Subject: [PATCH 2/2] refactor: generalize skip methods Generalize the various skip methods so these can be used for both dialects. Each dialect implements a number of abstract methods to indicate what type of statements and constructs they support. These methods are used by the generalized skip methods to determine the start and end of literals, identifiers, and comments. --- .../connection/AbstractStatementParser.java | 120 ++++++++++++++++-- .../connection/PostgreSQLStatementParser.java | 40 ++++++ .../connection/SpannerStatementParser.java | 40 ++++++ .../SpannerStatementParserTest.java | 83 ++++++++++++ .../connection/StatementParserTest.java | 4 +- 5 files changed, 276 insertions(+), 11 deletions(-) create mode 100644 google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/SpannerStatementParserTest.java diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/AbstractStatementParser.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/AbstractStatementParser.java index ac984a0f864..0382757a80a 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/AbstractStatementParser.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/AbstractStatementParser.java @@ -595,6 +595,7 @@ private boolean statementStartsWith(String sql, Iterable checkStatements static final char CLOSE_PARENTHESIS = ')'; static final char COMMA = ','; static final char UNDERSCORE = '_'; + static final char BACKSLASH = '\\'; /** * Removes comments from and trims the given sql statement using the dialect of this parser. @@ -698,6 +699,62 @@ public boolean checkReturningClause(String sql) { return checkReturningClauseInternal(sql); } + /** + * Returns true if this dialect supports nested comments. + * + *
    + *
  • This method should return false for dialects that consider this to be a valid comment: + * /* A comment /* still a comment */. + *
  • This method should return true for dialects that require all comment start sequences to + * be balanced with a comment end sequence: + * /* A comment /* still a comment */ Also still a comment */. + *
+ */ + abstract boolean supportsNestedComments(); + + /** + * Returns true for dialects that support dollar-quoted string literals. + * + *

Example: $tag$This is a string$tag$. + */ + abstract boolean supportsDollarQuotedStrings(); + + /** + * Returns true for dialects that support backticks as a quoting character, either for string + * literals or identifiers. + */ + abstract boolean supportsBacktickQuote(); + + /** + * Returns true for dialects that support triple-quoted string literals and identifiers. + * + *

Example: ```This is a triple-quoted string``` + */ + abstract boolean supportsTripleQuotedStrings(); + + /** + * Returns true if the dialect supports escaping a quote character within a literal with the same + * quote as the literal is using. That is: 'foo''bar' means "foo'bar". + */ + abstract boolean supportsEscapeQuoteWithQuote(); + + /** Returns true if the dialect supports starting an escape sequence with a backslash. */ + abstract boolean supportsBackslashEscape(); + + /** + * Returns true if the dialect supports single-line comments that start with a dash. + * + *

Example: # This is a comment + */ + abstract boolean supportsHashSingleLineComments(); + + /** + * Returns true for dialects that allow line-feeds in quoted strings. Note that the return value + * of this is not used for triple-quoted strings. Triple-quoted strings are assumed to always + * support line-feeds. + */ + abstract boolean supportsLineFeedInQuotedString(); + /** * Returns true for characters that can be used as the first character in unquoted identifiers. */ @@ -733,11 +790,17 @@ String parseDollarQuotedString(String sql, int index) { * given index. The skipped characters are added to result if it is not null. */ int skip(String sql, int currentIndex, @Nullable StringBuilder result) { + if (currentIndex >= sql.length()) { + return currentIndex; + } char currentChar = sql.charAt(currentIndex); - if (currentChar == SINGLE_QUOTE || currentChar == DOUBLE_QUOTE) { + + if (currentChar == SINGLE_QUOTE + || currentChar == DOUBLE_QUOTE + || (supportsBacktickQuote() && currentChar == BACKTICK_QUOTE)) { appendIfNotNull(result, currentChar); return skipQuoted(sql, currentIndex, currentChar, result); - } else if (currentChar == DOLLAR) { + } else if (supportsDollarQuotedStrings() && currentChar == DOLLAR) { String dollarTag = parseDollarQuotedString(sql, currentIndex + 1); if (dollarTag != null) { appendIfNotNull(result, currentChar, dollarTag, currentChar); @@ -748,6 +811,8 @@ int skip(String sql, int currentIndex, @Nullable StringBuilder result) { && sql.length() > (currentIndex + 1) && sql.charAt(currentIndex + 1) == HYPHEN) { return skipSingleLineComment(sql, currentIndex, result); + } else if (currentChar == DASH && supportsHashSingleLineComments()) { + return skipSingleLineComment(sql, currentIndex, result); } else if (currentChar == SLASH && sql.length() > (currentIndex + 1) && sql.charAt(currentIndex + 1) == ASTERISK) { @@ -772,14 +837,17 @@ static int skipSingleLineComment(String sql, int startIndex, @Nullable StringBui } /** Skips a multi-line comment from startIndex and adds it to result if result is not null. */ - static int skipMultiLineComment(String sql, int startIndex, @Nullable StringBuilder result) { + int skipMultiLineComment(String sql, int startIndex, @Nullable StringBuilder result) { // Current position is start + '/*'.length(). int pos = startIndex + 2; // PostgreSQL allows comments to be nested. That is, the following is allowed: // '/* test /* inner comment */ still a comment */' int level = 1; while (pos < sql.length()) { - if (sql.charAt(pos) == SLASH && sql.length() > (pos + 1) && sql.charAt(pos + 1) == ASTERISK) { + if (supportsNestedComments() + && sql.charAt(pos) == SLASH + && sql.length() > (pos + 1) + && sql.charAt(pos + 1) == ASTERISK) { level++; } if (sql.charAt(pos) == ASTERISK && sql.length() > (pos + 1) && sql.charAt(pos + 1) == SLASH) { @@ -806,33 +874,67 @@ private int skipQuoted( * Skips a quoted string from startIndex. The quote character is assumed to be $ if dollarTag is * not null. */ - private int skipQuoted( + int skipQuoted( String sql, int startIndex, char startQuote, - String dollarTag, + @Nullable String dollarTag, @Nullable StringBuilder result) { - int currentIndex = startIndex + 1; + boolean isTripleQuoted = + supportsTripleQuotedStrings() + && sql.length() > startIndex + 2 + && sql.charAt(startIndex + 1) == startQuote + && sql.charAt(startIndex + 2) == startQuote; + int currentIndex = startIndex + (isTripleQuoted ? 3 : 1); + if (isTripleQuoted) { + appendIfNotNull(result, startQuote); + appendIfNotNull(result, startQuote); + } while (currentIndex < sql.length()) { char currentChar = sql.charAt(currentIndex); if (currentChar == startQuote) { - if (currentChar == DOLLAR) { + if (supportsDollarQuotedStrings() && currentChar == DOLLAR) { // Check if this is the end of the current dollar quoted string. String tag = parseDollarQuotedString(sql, currentIndex + 1); if (tag != null && tag.equals(dollarTag)) { appendIfNotNull(result, currentChar, dollarTag, currentChar); return currentIndex + tag.length() + 2; } - } else if (sql.length() > currentIndex + 1 && sql.charAt(currentIndex + 1) == startQuote) { + } else if (supportsEscapeQuoteWithQuote() + && sql.length() > currentIndex + 1 + && sql.charAt(currentIndex + 1) == startQuote) { // This is an escaped quote (e.g. 'foo''bar') appendIfNotNull(result, currentChar); appendIfNotNull(result, currentChar); currentIndex += 2; continue; + } else if (isTripleQuoted) { + // Check if this is the end of the triple-quoted string. + if (sql.length() > currentIndex + 2 + && sql.charAt(currentIndex + 1) == startQuote + && sql.charAt(currentIndex + 2) == startQuote) { + appendIfNotNull(result, currentChar); + appendIfNotNull(result, currentChar); + appendIfNotNull(result, currentChar); + return currentIndex + 3; + } } else { appendIfNotNull(result, currentChar); return currentIndex + 1; } + } else if (supportsBackslashEscape() + && currentChar == BACKSLASH + && sql.length() > currentIndex + 1 + && sql.charAt(currentIndex + 1) == startQuote) { + // This is an escaped quote (e.g. 'foo\'bar'). + // 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'. + appendIfNotNull(result, currentChar); + appendIfNotNull(result, sql.charAt(currentIndex + 1)); + currentIndex += 2; + continue; + } else if (currentChar == '\n' && !isTripleQuoted && !supportsLineFeedInQuotedString()) { + break; } currentIndex++; appendIfNotNull(result, currentChar); diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/PostgreSQLStatementParser.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/PostgreSQLStatementParser.java index 572ea056546..6b0c69d40a9 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/PostgreSQLStatementParser.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/PostgreSQLStatementParser.java @@ -48,6 +48,46 @@ protected boolean supportsExplain() { return false; } + @Override + boolean supportsNestedComments() { + return true; + } + + @Override + boolean supportsDollarQuotedStrings() { + return true; + } + + @Override + boolean supportsBacktickQuote() { + return false; + } + + @Override + boolean supportsTripleQuotedStrings() { + return false; + } + + @Override + boolean supportsEscapeQuoteWithQuote() { + return true; + } + + @Override + boolean supportsBackslashEscape() { + return false; + } + + @Override + boolean supportsHashSingleLineComments() { + return false; + } + + @Override + boolean supportsLineFeedInQuotedString() { + return true; + } + /** * Removes comments from and trims the given sql statement. PostgreSQL supports two types of * comments: 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 251c5a2e6ec..1c5cdda7b01 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 @@ -50,6 +50,46 @@ protected boolean supportsExplain() { return true; } + @Override + boolean supportsNestedComments() { + return false; + } + + @Override + boolean supportsDollarQuotedStrings() { + return false; + } + + @Override + boolean supportsBacktickQuote() { + return true; + } + + @Override + boolean supportsTripleQuotedStrings() { + return true; + } + + @Override + boolean supportsEscapeQuoteWithQuote() { + return false; + } + + @Override + boolean supportsBackslashEscape() { + return true; + } + + @Override + boolean supportsHashSingleLineComments() { + return true; + } + + @Override + boolean supportsLineFeedInQuotedString() { + return false; + } + /** * Removes comments from and trims the given sql statement. Spanner supports three types of * comments: 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 new file mode 100644 index 00000000000..d4dc76d48bb --- /dev/null +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/SpannerStatementParserTest.java @@ -0,0 +1,83 @@ +/* + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.cloud.spanner.connection; + +import static org.junit.Assert.assertEquals; + +import com.google.cloud.spanner.Dialect; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public class SpannerStatementParserTest { + + static String skip(String sql) { + return skip(sql, 0); + } + + static String skip(String sql, int currentIndex) { + int position = + AbstractStatementParser.getInstance(Dialect.GOOGLE_STANDARD_SQL) + .skip(sql, currentIndex, null); + return sql.substring(currentIndex, position); + } + + @Test + public void testSkip() { + assertEquals("", skip("")); + assertEquals("1", skip("1 ")); + assertEquals("1", skip("12 ")); + assertEquals("2", skip("12 ", 1)); + assertEquals("", skip("12", 2)); + + assertEquals("'foo'", skip("'foo' ", 0)); + assertEquals("'foo'", skip("'foo''bar' ", 0)); + assertEquals("'foo'", skip("'foo' 'bar' ", 0)); + assertEquals("'bar'", skip("'foo''bar' ", 5)); + assertEquals("'foo\"bar\"'", skip("'foo\"bar\"' ", 0)); + assertEquals("\"foo'bar'\"", skip("\"foo'bar'\" ", 0)); + assertEquals("`foo'bar'`", skip("`foo'bar'` ", 0)); + + assertEquals("'''foo'bar'''", skip("'''foo'bar''' ", 0)); + assertEquals("'''foo\\'bar'''", skip("'''foo\\'bar''' ", 0)); + assertEquals("'''foo\\'\\'bar'''", skip("'''foo\\'\\'bar''' ", 0)); + assertEquals("'''foo\\'\\'\\'bar'''", skip("'''foo\\'\\'\\'bar''' ", 0)); + assertEquals("\"\"\"foo'bar\"\"\"", skip("\"\"\"foo'bar\"\"\"", 0)); + assertEquals("```foo'bar```", skip("```foo'bar```", 0)); + + assertEquals("-- comment\n", skip("-- comment\nselect * from foo", 0)); + assertEquals("# comment\n", skip("# comment\nselect * from foo", 0)); + assertEquals("/* comment */", skip("/* comment */ select * from foo", 0)); + assertEquals( + "/* comment /* GoogleSQL does not support nested comments */", + skip("/* comment /* GoogleSQL does not support nested comments */ select * from foo", 0)); + // GoogleSQL does not support dollar-quoted strings. + assertEquals("$", skip("$tag$not a string$tag$ select * from foo", 0)); + + assertEquals("/* 'test' */", skip("/* 'test' */ foo")); + assertEquals("-- 'test' \n", skip("-- 'test' \n foo")); + assertEquals("'/* test */'", skip("'/* test */' foo")); + + // Raw strings do not consider '\' as something that starts an escape sequence, but any + // quote character following it is still preserved within the string, as the definition of a + // raw string says that 'both characters are preserved'. + assertEquals("'foo\\''", skip("'foo\\'' ", 0)); + assertEquals("'foo\\''", skip("r'foo\\'' ", 1)); + assertEquals("'''foo\\'\\'\\'bar'''", skip("'''foo\\'\\'\\'bar''' ", 0)); + } +} diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/StatementParserTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/StatementParserTest.java index d3438b2b661..c60550c3ba6 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/StatementParserTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/StatementParserTest.java @@ -1600,11 +1600,11 @@ public void testPostgreSQLReturningClause() { } int skipSingleLineComment(String sql, int startIndex) { - return PostgreSQLStatementParser.skipSingleLineComment(sql, startIndex, null); + return AbstractStatementParser.skipSingleLineComment(sql, startIndex, null); } int skipMultiLineComment(String sql, int startIndex) { - return PostgreSQLStatementParser.skipMultiLineComment(sql, startIndex, null); + return parser.skipMultiLineComment(sql, startIndex, null); } @Test