From 3247a05b5cc94fa94f7945139bb2a35aa6c562a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Knut=20Olav=20L=C3=B8ite?= Date: Tue, 19 Mar 2024 09:16:27 +0100 Subject: [PATCH] refactor: move skip methods to abstract parser (#2948) 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)