diff --git a/google-cloud-spanner/clirr-ignored-differences.xml b/google-cloud-spanner/clirr-ignored-differences.xml index 14ccf81c057..bcea799a409 100644 --- a/google-cloud-spanner/clirr-ignored-differences.xml +++ b/google-cloud-spanner/clirr-ignored-differences.xml @@ -927,5 +927,23 @@ com/google/cloud/spanner/connection/ConnectionOptions VALID_PROPERTIES + + + + 7002 + com/google/cloud/spanner/connection/AbstractStatementParser + boolean supportsExplain() + + + 7002 + com/google/cloud/spanner/connection/PostgreSQLStatementParser + boolean supportsExplain() + + + 7002 + com/google/cloud/spanner/connection/SpannerStatementParser + boolean supportsExplain() + + diff --git a/google-cloud-spanner/pom.xml b/google-cloud-spanner/pom.xml index 6bdc111ed87..eed3735b857 100644 --- a/google-cloud-spanner/pom.xml +++ b/google-cloud-spanner/pom.xml @@ -517,6 +517,19 @@ + + org.apache.maven.plugins + maven-compiler-plugin + + + + org.openjdk.jmh + jmh-generator-annprocess + 1.37 + + + + diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/Statement.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/Statement.java index a89c7c048fc..a9cf3e7dec4 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/Statement.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/Statement.java @@ -21,6 +21,7 @@ import com.google.cloud.spanner.ReadContext.QueryAnalyzeMode; import com.google.common.base.Preconditions; +import com.google.common.collect.ImmutableMap; import com.google.spanner.v1.ExecuteSqlRequest.QueryOptions; import java.io.Serializable; import java.util.Collections; @@ -140,7 +141,7 @@ Builder handle(Value value) { /** Creates a {@code Statement} with the given SQL text {@code sql}. */ public static Statement of(String sql) { - return newBuilder(sql).build(); + return new Statement(sql, ImmutableMap.of(), /*queryOptions=*/ null); } /** Creates a new statement builder with the SQL text {@code sql}. */ 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 406622586d8..30bf861d380 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 @@ -31,6 +31,7 @@ import com.google.cloud.spanner.connection.UnitOfWork.CallType; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Preconditions; +import com.google.common.base.Splitter; import com.google.common.cache.Cache; import com.google.common.cache.CacheBuilder; import com.google.common.cache.CacheStats; @@ -41,6 +42,7 @@ import java.util.Collection; import java.util.Collections; import java.util.HashMap; +import java.util.Iterator; import java.util.Map; import java.util.Objects; import java.util.Set; @@ -511,7 +513,7 @@ ParsedStatement parse(Statement statement, QueryOptions defaultQueryOptions) { return parsedStatement.copy(statement, defaultQueryOptions); } - private ParsedStatement internalParse(Statement statement, QueryOptions defaultQueryOptions) { + ParsedStatement internalParse(Statement statement, QueryOptions defaultQueryOptions) { StatementHintParser statementHintParser = new StatementHintParser(getDialect(), statement.getSql()); ReadQueryUpdateTransactionOption[] optionsFromHints = EMPTY_OPTIONS; @@ -521,16 +523,21 @@ private ParsedStatement internalParse(Statement statement, QueryOptions defaultQ statement.toBuilder().replace(statementHintParser.getSqlWithoutClientSideHints()).build(); optionsFromHints = convertHintsToOptions(statementHintParser.getClientSideStatementHints()); } + // TODO: Qualify statements without removing comments first. String sql = removeCommentsAndTrim(statement.getSql()); ClientSideStatementImpl client = parseClientSideStatement(sql); if (client != null) { return ParsedStatement.clientSideStatement(client, statement, sql); - } else if (isQuery(sql)) { - return ParsedStatement.query(statement, sql, defaultQueryOptions, optionsFromHints); - } else if (isUpdateStatement(sql)) { - return ParsedStatement.update(statement, sql, checkReturningClause(sql), optionsFromHints); - } else if (isDdlStatement(sql)) { - return ParsedStatement.ddl(statement, sql); + } else { + String sqlWithoutHints = + !sql.isEmpty() && sql.charAt(0) == '@' ? removeStatementHint(sql) : sql; + if (isQuery(sqlWithoutHints)) { + return ParsedStatement.query(statement, sql, defaultQueryOptions, optionsFromHints); + } else if (isUpdateStatement(sqlWithoutHints)) { + return ParsedStatement.update(statement, sql, checkReturningClause(sql), optionsFromHints); + } else if (isDdlStatement(sqlWithoutHints)) { + return ParsedStatement.ddl(statement, sql); + } } return ParsedStatement.unknown(statement, sql); } @@ -610,20 +617,16 @@ public boolean isUpdateStatement(String sql) { return statementStartsWith(sql, dmlStatements); } - protected abstract boolean supportsExplain(); - private boolean statementStartsWith(String sql, Iterable checkStatements) { Preconditions.checkNotNull(sql); - String[] tokens = sql.split("\\s+", 2); - int checkIndex = 0; - if (supportsExplain() && tokens[0].equalsIgnoreCase("EXPLAIN")) { - checkIndex = 1; - } - if (tokens.length > checkIndex) { - for (String check : checkStatements) { - if (tokens[checkIndex].equalsIgnoreCase(check)) { - return true; - } + Iterator tokens = Splitter.onPattern("\\s+").split(sql).iterator(); + if (!tokens.hasNext()) { + return false; + } + String token = tokens.next(); + for (String check : checkStatements) { + if (token.equalsIgnoreCase(check)) { + return true; } } return false; @@ -929,7 +932,8 @@ int skipQuoted( appendIfNotNull(result, startQuote); appendIfNotNull(result, startQuote); } - while (currentIndex < sql.length()) { + int length = sql.length(); + while (currentIndex < length) { char currentChar = sql.charAt(currentIndex); if (currentChar == startQuote) { if (supportsDollarQuotedStrings() && currentChar == DOLLAR) { @@ -940,7 +944,7 @@ int skipQuoted( return currentIndex + tag.length() + 2; } } else if (supportsEscapeQuoteWithQuote() - && sql.length() > currentIndex + 1 + && length > currentIndex + 1 && sql.charAt(currentIndex + 1) == startQuote) { // This is an escaped quote (e.g. 'foo''bar') appendIfNotNull(result, currentChar); @@ -949,7 +953,7 @@ int skipQuoted( continue; } else if (isTripleQuoted) { // Check if this is the end of the triple-quoted string. - if (sql.length() > currentIndex + 2 + if (length > currentIndex + 2 && sql.charAt(currentIndex + 1) == startQuote && sql.charAt(currentIndex + 2) == startQuote) { appendIfNotNull(result, currentChar); @@ -963,7 +967,7 @@ int skipQuoted( } } else if (supportsBackslashEscape() && currentChar == BACKSLASH - && sql.length() > currentIndex + 1 + && 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 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 4f39c549de9..60b64b0cd4f 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 @@ -46,15 +46,6 @@ Dialect getDialect() { return Dialect.POSTGRESQL; } - /** - * Indicates whether the parser supports the {@code EXPLAIN} clause. The PostgreSQL parser does - * not support it. - */ - @Override - protected boolean supportsExplain() { - return false; - } - @Override boolean supportsNestedComments() { return true; @@ -125,7 +116,8 @@ String removeCommentsAndTrimInternal(String sql) { int multiLineCommentStartIdx = -1; StringBuilder res = new StringBuilder(sql.length()); int index = 0; - while (index < sql.length()) { + int length = sql.length(); + while (index < length) { char c = sql.charAt(index); if (isInSingleLineComment) { if (c == '\n') { @@ -134,10 +126,10 @@ String removeCommentsAndTrimInternal(String sql) { res.append(c); } } else if (multiLineCommentLevel > 0) { - if (sql.length() > index + 1 && c == ASTERISK && sql.charAt(index + 1) == SLASH) { + if (length > index + 1 && c == ASTERISK && sql.charAt(index + 1) == SLASH) { multiLineCommentLevel--; if (multiLineCommentLevel == 0) { - if (!whitespaceBeforeOrAfterMultiLineComment && (sql.length() > index + 2)) { + if (!whitespaceBeforeOrAfterMultiLineComment && (length > index + 2)) { whitespaceBeforeOrAfterMultiLineComment = Character.isWhitespace(sql.charAt(index + 2)); } @@ -145,23 +137,23 @@ String removeCommentsAndTrimInternal(String sql) { // neither at the start nor at the end of SQL string, append an extra space. if (!whitespaceBeforeOrAfterMultiLineComment && (multiLineCommentStartIdx != 0) - && (index != sql.length() - 2)) { + && (index != length - 2)) { res.append(' '); } } index++; - } else if (sql.length() > index + 1 && c == SLASH && sql.charAt(index + 1) == ASTERISK) { + } else if (length > index + 1 && c == SLASH && sql.charAt(index + 1) == ASTERISK) { multiLineCommentLevel++; index++; } } else { // Check for -- which indicates the start of a single-line comment. - if (sql.length() > index + 1 && c == HYPHEN && sql.charAt(index + 1) == HYPHEN) { + if (length > index + 1 && c == HYPHEN && sql.charAt(index + 1) == HYPHEN) { // This is a single line comment. isInSingleLineComment = true; index += 2; continue; - } else if (sql.length() > index + 1 && c == SLASH && sql.charAt(index + 1) == ASTERISK) { + } else if (length > index + 1 && c == SLASH && sql.charAt(index + 1) == ASTERISK) { multiLineCommentLevel++; if (index >= 1) { whitespaceBeforeOrAfterMultiLineComment = Character.isWhitespace(sql.charAt(index - 1)); 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 fdd10bbf5ae..2689145b7ac 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 @@ -46,15 +46,6 @@ Dialect getDialect() { return Dialect.GOOGLE_STANDARD_SQL; } - /** - * Indicates whether the parser supports the {@code EXPLAIN} clause. The Spanner parser does - * support it. - */ - @Override - protected boolean supportsExplain() { - return true; - } - @Override boolean supportsNestedComments() { return false; diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/StatementParserBenchmark.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/StatementParserBenchmark.java new file mode 100644 index 00000000000..e028f8027cf --- /dev/null +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/StatementParserBenchmark.java @@ -0,0 +1,81 @@ +/* + * Copyright 2025 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 com.google.cloud.spanner.Dialect; +import com.google.cloud.spanner.Statement; +import com.google.cloud.spanner.connection.AbstractStatementParser.ParsedStatement; +import com.google.spanner.v1.ExecuteSqlRequest.QueryOptions; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.Fork; +import org.openjdk.jmh.annotations.Measurement; +import org.openjdk.jmh.annotations.Warmup; + +@Fork(value = 1, warmups = 0) +@Warmup(iterations = 1, time = 5) +@Measurement(iterations = 5, time = 5) +public class StatementParserBenchmark { + private static final Dialect dialect = Dialect.POSTGRESQL; + private static final AbstractStatementParser PARSER = + AbstractStatementParser.getInstance(dialect); + + private static final String LONG_QUERY_TEXT = + generateLongStatement("SELECT * FROM foo WHERE 1", 100 * 1024); // 100kb + + private static final String LONG_DML_TEXT = + generateLongStatement("update foo set bar=1 WHERE 1", 100 * 1024); // 100kb + + /** Generates a long SQL-looking string. */ + private static String generateLongStatement(String prefix, int length) { + StringBuilder sb = new StringBuilder(length + 50); + sb.append(prefix); + while (sb.length() < length) { + sb.append(" OR abcdefghijklmnopqrstuvwxyz='abcdefghijklmnopqrstuvwxyz'"); + } + return sb.toString(); + } + + @Benchmark + public ParsedStatement isQueryTest() { + return PARSER.internalParse( + Statement.of("CREATE TABLE FOO (ID INT64, NAME STRING(100)) PRIMARY KEY (ID)"), + QueryOptions.getDefaultInstance()); + } + + @Benchmark + public ParsedStatement longQueryTest() { + return PARSER.internalParse(Statement.of(LONG_QUERY_TEXT), QueryOptions.getDefaultInstance()); + } + + @Benchmark + public ParsedStatement longDmlTest() { + return PARSER.internalParse(Statement.of(LONG_DML_TEXT), QueryOptions.getDefaultInstance()); + } + + public static void main(String[] args) throws Exception { + for (int i = 0; i < 100000; i++) { + if (PARSER.internalParse(Statement.of(LONG_QUERY_TEXT), QueryOptions.getDefaultInstance()) + == null) { + throw new AssertionError(); + } + if (PARSER.internalParse(Statement.of(LONG_DML_TEXT), QueryOptions.getDefaultInstance()) + == null) { + throw new AssertionError(); + } + } + } +} pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy