updates) {
+ parsedStatement.getSqlWithoutComments());
}
}
- return internalExecuteBatchUpdateAsync(CallType.ASYNC, parsedStatements);
+ return parsedStatements;
}
private QueryOption[] mergeDataBoost(QueryOption... options) {
diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ConnectionOptions.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ConnectionOptions.java
index e7209eeab91..a5753eca8ca 100644
--- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ConnectionOptions.java
+++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ConnectionOptions.java
@@ -26,6 +26,7 @@
import com.google.cloud.NoCredentials;
import com.google.cloud.ServiceOptions;
import com.google.cloud.spanner.DatabaseId;
+import com.google.cloud.spanner.Dialect;
import com.google.cloud.spanner.ErrorCode;
import com.google.cloud.spanner.Options.RpcPriority;
import com.google.cloud.spanner.SessionPoolOptions;
@@ -307,7 +308,9 @@ public String[] getValidValues() {
ConnectionProperty.createBooleanProperty("returnCommitStats", "", false),
ConnectionProperty.createBooleanProperty(
"autoConfigEmulator",
- "Automatically configure the connection to try to connect to the Cloud Spanner emulator (true/false). The instance and database in the connection string will automatically be created if these do not yet exist on the emulator.",
+ "Automatically configure the connection to try to connect to the Cloud Spanner emulator (true/false). "
+ + "The instance and database in the connection string will automatically be created if these do not yet exist on the emulator. "
+ + "Add dialect=postgresql to the connection string to make sure that the database that is created uses the PostgreSQL dialect.",
false),
ConnectionProperty.createBooleanProperty(
LENIENT_PROPERTY_NAME,
@@ -317,7 +320,8 @@ public String[] getValidValues() {
RPC_PRIORITY_NAME,
"Sets the priority for all RPC invocations from this connection (HIGH/MEDIUM/LOW). The default is HIGH."),
ConnectionProperty.createStringProperty(
- DIALECT_PROPERTY_NAME, "Sets the dialect to use for this connection."),
+ DIALECT_PROPERTY_NAME,
+ "Sets the dialect to use for new databases that are created by this connection."),
ConnectionProperty.createStringProperty(
DATABASE_ROLE_PROPERTY_NAME,
"Sets the database role to use for this connection. The default is privileges assigned to IAM role"),
@@ -626,6 +630,7 @@ public static Builder newBuilder() {
private final QueryOptions queryOptions;
private final boolean returnCommitStats;
private final boolean autoConfigEmulator;
+ private final Dialect dialect;
private final RpcPriority rpcPriority;
private final boolean delayTransactionStartUntilFirstWrite;
private final boolean trackSessionLeaks;
@@ -677,6 +682,7 @@ private ConnectionOptions(Builder builder) {
this.queryOptions = queryOptionsBuilder.build();
this.returnCommitStats = parseReturnCommitStats(this.uri);
this.autoConfigEmulator = parseAutoConfigEmulator(this.uri);
+ this.dialect = parseDialect(this.uri);
this.usePlainText = this.autoConfigEmulator || parseUsePlainText(this.uri);
this.host = determineHost(matcher, autoConfigEmulator, usePlainText);
this.rpcPriority = parseRPCPriority(this.uri);
@@ -939,6 +945,12 @@ static boolean parseAutoConfigEmulator(String uri) {
return Boolean.parseBoolean(value);
}
+ @VisibleForTesting
+ static Dialect parseDialect(String uri) {
+ String value = parseUriProperty(uri, DIALECT_PROPERTY_NAME);
+ return value != null ? Dialect.valueOf(value.toUpperCase()) : Dialect.GOOGLE_STANDARD_SQL;
+ }
+
@VisibleForTesting
static boolean parseLenient(String uri) {
String value = parseUriProperty(uri, LENIENT_PROPERTY_NAME);
@@ -1259,6 +1271,10 @@ public boolean isAutoConfigEmulator() {
return autoConfigEmulator;
}
+ public Dialect getDialect() {
+ return dialect;
+ }
+
/** The {@link RpcPriority} to use for the connection. */
RpcPriority getRPCPriority() {
return rpcPriority;
diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/EmulatorUtil.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/EmulatorUtil.java
index ee291642031..d850c3d3839 100644
--- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/EmulatorUtil.java
+++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/EmulatorUtil.java
@@ -18,6 +18,7 @@
import com.google.cloud.NoCredentials;
import com.google.cloud.spanner.DatabaseId;
+import com.google.cloud.spanner.Dialect;
import com.google.cloud.spanner.ErrorCode;
import com.google.cloud.spanner.InstanceConfigId;
import com.google.cloud.spanner.InstanceInfo;
@@ -41,8 +42,10 @@ class EmulatorUtil {
*
* @param spanner a {@link Spanner} instance that connects to an emulator instance
* @param databaseId the id of the instance and the database to create
+ * @param dialect the {@link Dialect} to use for the database to create
*/
- static void maybeCreateInstanceAndDatabase(Spanner spanner, DatabaseId databaseId) {
+ static void maybeCreateInstanceAndDatabase(
+ Spanner spanner, DatabaseId databaseId, Dialect dialect) {
Preconditions.checkArgument(
NoCredentials.getInstance().equals(spanner.getOptions().getCredentials()));
try {
@@ -70,7 +73,8 @@ static void maybeCreateInstanceAndDatabase(Spanner spanner, DatabaseId databaseI
.getDatabaseAdminClient()
.createDatabase(
databaseId.getInstanceId().getInstance(),
- databaseId.getDatabase(),
+ dialect.createDatabaseStatementFor(databaseId.getDatabase()),
+ dialect,
ImmutableList.of())
.get();
} catch (ExecutionException executionException) {
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 012bfbba875..8cb2b7e464a 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
@@ -184,9 +184,8 @@ ParametersInfo convertPositionalParametersToNamedParametersInternal(char paramCh
* Note: This is an internal API and breaking changes can be made without prior notice.
*
* Returns the PostgreSQL-style query parameters ($1, $2, ...) in the given SQL string. The
- * SQL-string is assumed to not contain any comments. Use {@link #removeCommentsAndTrim(String)}
- * to remove all comments before calling this method. Occurrences of query-parameter like strings
- * inside quoted identifiers or string literals are ignored.
+ * SQL-string is allowed to contain comments. Occurrences of query-parameter like strings inside
+ * quoted identifiers or string literals are ignored.
*
*
The following example will return a set containing ("$1", "$2").
* select col1, col2, "col$4"
@@ -195,7 +194,7 @@ ParametersInfo convertPositionalParametersToNamedParametersInternal(char paramCh
* and not col3=$1 and col4='$3'
*
*
- * @param sql the SQL-string to check for parameters. Must not contain comments.
+ * @param sql the SQL-string to check for parameters.
* @return A set containing all the parameters in the SQL-string.
*/
@InternalApi
@@ -233,12 +232,56 @@ private int skip(String sql, int currentIndex, @Nullable StringBuilder result) {
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);
@@ -285,6 +328,12 @@ private void appendIfNotNull(@Nullable StringBuilder result, char 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) {
diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/InlineBeginTransactionTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/InlineBeginTransactionTest.java
index eb3f65176b7..c7f5e396069 100644
--- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/InlineBeginTransactionTest.java
+++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/InlineBeginTransactionTest.java
@@ -69,6 +69,7 @@
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicInteger;
import org.junit.After;
import org.junit.AfterClass;
import org.junit.Before;
@@ -1359,6 +1360,55 @@ public void testInlinedBeginTxWithOnlyMutations() {
assertThat(countTransactionsStarted()).isEqualTo(1);
}
+ @Test
+ public void testInlinedBeginTxWithMutationsBeforeFailedSqlStatement() {
+ Statement insert = Statement.of("insert into foo (id) values (1)");
+ Statement update = Statement.of("update foo set value='Two' where id=2");
+ mockSpanner.putStatementResult(StatementResult.update(insert, 1L));
+ mockSpanner.putStatementResult(StatementResult.update(update, 1L));
+ // This error will be returned the first time the ExecuteSql method is called. The error is
+ // cleared after the first call, meaning that the second attempt will succeed.
+ mockSpanner.setExecuteSqlExecutionTime(
+ SimulatedExecutionTime.ofException(Status.ALREADY_EXISTS.asRuntimeException()));
+
+ DatabaseClient client =
+ spanner.getDatabaseClient(DatabaseId.of("[PROJECT]", "[INSTANCE]", "[DATABASE]"));
+ AtomicInteger attempts = new AtomicInteger();
+ client
+ .readWriteTransaction()
+ .run(
+ transaction -> {
+ attempts.incrementAndGet();
+ // Buffer a blind write before executing a SQL statement.
+ transaction.buffer(
+ Collections.singletonList(
+ Mutation.newInsertBuilder("FOO").set("ID").to(1L).build()));
+ try {
+ transaction.executeUpdate(insert);
+ } catch (SpannerException exception) {
+ assertEquals(ErrorCode.ALREADY_EXISTS, exception.getErrorCode());
+ // The error should only occur during the initial attempt.
+ assertEquals(1, attempts.get());
+ }
+ // We need to execute one more statement in the transaction in order to force a
+ // retry.
+ assertEquals(1L, transaction.executeUpdate(update));
+ return null;
+ });
+ // The transaction should be retried once.
+ assertEquals(2, attempts.get());
+ assertEquals(1, mockSpanner.countRequestsOfType(BeginTransactionRequest.class));
+ // We get 3 ExecuteSql requests:
+ // 1. The initial attempt that carries a BeginTransaction option.
+ // 2. The retry attempt that does not use a BeginTransaction option.
+ // 3. The second UPDATE statement in the transaction that is only executed during the retry.
+ assertEquals(3, mockSpanner.countRequestsOfType(ExecuteSqlRequest.class));
+ assertEquals(1, mockSpanner.countRequestsOfType(CommitRequest.class));
+ CommitRequest commit = mockSpanner.getRequestsOfType(CommitRequest.class).get(0);
+ // The mutations should only be applied once.
+ assertEquals(1, commit.getMutationsCount());
+ }
+
@SuppressWarnings("resource")
@Test
public void testTransactionManagerInlinedBeginTx() {
diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/ConnectionImplTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/ConnectionImplTest.java
index 09d2f92a6dc..7c5ba508e8f 100644
--- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/ConnectionImplTest.java
+++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/ConnectionImplTest.java
@@ -20,6 +20,7 @@
import static com.google.cloud.spanner.connection.AbstractConnectionImplTest.SELECT;
import static com.google.cloud.spanner.connection.AbstractConnectionImplTest.UPDATE;
import static com.google.cloud.spanner.connection.AbstractConnectionImplTest.expectSpannerException;
+import static com.google.cloud.spanner.connection.ConnectionImpl.checkResultTypeAllowed;
import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.CoreMatchers.notNullValue;
@@ -28,6 +29,7 @@
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import static org.mockito.Mockito.any;
@@ -73,6 +75,7 @@
import com.google.cloud.spanner.connection.StatementResult.ResultType;
import com.google.cloud.spanner.connection.UnitOfWork.CallType;
import com.google.cloud.spanner.connection.UnitOfWork.UnitOfWorkState;
+import com.google.common.collect.ImmutableSet;
import com.google.spanner.admin.database.v1.UpdateDatabaseDdlMetadata;
import com.google.spanner.v1.ExecuteSqlRequest.QueryOptions;
import com.google.spanner.v1.ResultSetStats;
@@ -1624,4 +1627,115 @@ UnitOfWork createNewUnitOfWork(boolean isInternalMetadataQuery) {
assertNull(connection.getTransactionTag());
}
}
+
+ @Test
+ public void testCheckResultTypeAllowed() {
+ AbstractStatementParser parser =
+ AbstractStatementParser.getInstance(Dialect.GOOGLE_STANDARD_SQL);
+ String query = "select * from foo";
+ String dml = "update foo set bar=1 where true";
+ String dmlReturning = "insert into foo (id, value) values (1, 'One') then return id";
+ String ddl = "create table foo";
+ String set = "set readonly=true";
+ String show = "show variable readonly";
+ String start = "start batch dml";
+
+ // null means all statements should be allowed.
+ ImmutableSet allowedResultTypes = null;
+ checkResultTypeAllowed(parser.parse(Statement.of(query)), allowedResultTypes);
+ checkResultTypeAllowed(parser.parse(Statement.of(dml)), allowedResultTypes);
+ checkResultTypeAllowed(parser.parse(Statement.of(dmlReturning)), allowedResultTypes);
+ checkResultTypeAllowed(parser.parse(Statement.of(ddl)), allowedResultTypes);
+ checkResultTypeAllowed(parser.parse(Statement.of(set)), allowedResultTypes);
+ checkResultTypeAllowed(parser.parse(Statement.of(show)), allowedResultTypes);
+ checkResultTypeAllowed(parser.parse(Statement.of(start)), allowedResultTypes);
+
+ allowedResultTypes = ImmutableSet.of();
+ assertThrowResultNotAllowed(parser, query, allowedResultTypes);
+ assertThrowResultNotAllowed(parser, dml, allowedResultTypes);
+ assertThrowResultNotAllowed(parser, dmlReturning, allowedResultTypes);
+ assertThrowResultNotAllowed(parser, ddl, allowedResultTypes);
+ assertThrowResultNotAllowed(parser, set, allowedResultTypes);
+ assertThrowResultNotAllowed(parser, show, allowedResultTypes);
+ assertThrowResultNotAllowed(parser, start, allowedResultTypes);
+
+ allowedResultTypes = ImmutableSet.of(ResultType.RESULT_SET);
+ checkResultTypeAllowed(parser.parse(Statement.of(query)), allowedResultTypes);
+ assertThrowResultNotAllowed(parser, dml, allowedResultTypes);
+ checkResultTypeAllowed(parser.parse(Statement.of(dmlReturning)), allowedResultTypes);
+ assertThrowResultNotAllowed(parser, ddl, allowedResultTypes);
+ assertThrowResultNotAllowed(parser, set, allowedResultTypes);
+ checkResultTypeAllowed(parser.parse(Statement.of(show)), allowedResultTypes);
+ assertThrowResultNotAllowed(parser, start, allowedResultTypes);
+
+ allowedResultTypes = ImmutableSet.of(ResultType.UPDATE_COUNT);
+ assertThrowResultNotAllowed(parser, query, allowedResultTypes);
+ checkResultTypeAllowed(parser.parse(Statement.of(dml)), allowedResultTypes);
+ assertThrowResultNotAllowed(parser, dmlReturning, allowedResultTypes);
+ assertThrowResultNotAllowed(parser, ddl, allowedResultTypes);
+ assertThrowResultNotAllowed(parser, set, allowedResultTypes);
+ assertThrowResultNotAllowed(parser, show, allowedResultTypes);
+ assertThrowResultNotAllowed(parser, start, allowedResultTypes);
+
+ allowedResultTypes = ImmutableSet.of(ResultType.NO_RESULT);
+ assertThrowResultNotAllowed(parser, query, allowedResultTypes);
+ assertThrowResultNotAllowed(parser, dml, allowedResultTypes);
+ assertThrowResultNotAllowed(parser, dmlReturning, allowedResultTypes);
+ checkResultTypeAllowed(parser.parse(Statement.of(ddl)), allowedResultTypes);
+ checkResultTypeAllowed(parser.parse(Statement.of(set)), allowedResultTypes);
+ assertThrowResultNotAllowed(parser, show, allowedResultTypes);
+ checkResultTypeAllowed(parser.parse(Statement.of(start)), allowedResultTypes);
+
+ allowedResultTypes = ImmutableSet.of(ResultType.RESULT_SET, ResultType.UPDATE_COUNT);
+ checkResultTypeAllowed(parser.parse(Statement.of(query)), allowedResultTypes);
+ checkResultTypeAllowed(parser.parse(Statement.of(dml)), allowedResultTypes);
+ checkResultTypeAllowed(parser.parse(Statement.of(dmlReturning)), allowedResultTypes);
+ assertThrowResultNotAllowed(parser, ddl, allowedResultTypes);
+ assertThrowResultNotAllowed(parser, set, allowedResultTypes);
+ checkResultTypeAllowed(parser.parse(Statement.of(show)), allowedResultTypes);
+ assertThrowResultNotAllowed(parser, start, allowedResultTypes);
+
+ allowedResultTypes = ImmutableSet.of(ResultType.RESULT_SET, ResultType.NO_RESULT);
+ checkResultTypeAllowed(parser.parse(Statement.of(query)), allowedResultTypes);
+ assertThrowResultNotAllowed(parser, dml, allowedResultTypes);
+ checkResultTypeAllowed(parser.parse(Statement.of(dmlReturning)), allowedResultTypes);
+ checkResultTypeAllowed(parser.parse(Statement.of(ddl)), allowedResultTypes);
+ checkResultTypeAllowed(parser.parse(Statement.of(set)), allowedResultTypes);
+ checkResultTypeAllowed(parser.parse(Statement.of(show)), allowedResultTypes);
+ checkResultTypeAllowed(parser.parse(Statement.of(start)), allowedResultTypes);
+
+ allowedResultTypes = ImmutableSet.of(ResultType.UPDATE_COUNT, ResultType.NO_RESULT);
+ assertThrowResultNotAllowed(parser, query, allowedResultTypes);
+ checkResultTypeAllowed(parser.parse(Statement.of(dml)), allowedResultTypes);
+ assertThrowResultNotAllowed(parser, dmlReturning, allowedResultTypes);
+ checkResultTypeAllowed(parser.parse(Statement.of(ddl)), allowedResultTypes);
+ checkResultTypeAllowed(parser.parse(Statement.of(set)), allowedResultTypes);
+ assertThrowResultNotAllowed(parser, show, allowedResultTypes);
+ checkResultTypeAllowed(parser.parse(Statement.of(start)), allowedResultTypes);
+
+ allowedResultTypes =
+ ImmutableSet.of(ResultType.RESULT_SET, ResultType.UPDATE_COUNT, ResultType.NO_RESULT);
+ checkResultTypeAllowed(parser.parse(Statement.of(query)), allowedResultTypes);
+ checkResultTypeAllowed(parser.parse(Statement.of(dml)), allowedResultTypes);
+ checkResultTypeAllowed(parser.parse(Statement.of(dmlReturning)), allowedResultTypes);
+ checkResultTypeAllowed(parser.parse(Statement.of(ddl)), allowedResultTypes);
+ checkResultTypeAllowed(parser.parse(Statement.of(set)), allowedResultTypes);
+ checkResultTypeAllowed(parser.parse(Statement.of(show)), allowedResultTypes);
+ checkResultTypeAllowed(parser.parse(Statement.of(start)), allowedResultTypes);
+ }
+
+ private void assertThrowResultNotAllowed(
+ AbstractStatementParser parser, String sql, ImmutableSet allowedResultTypes) {
+ SpannerException exception =
+ assertThrows(
+ SpannerException.class,
+ () -> checkResultTypeAllowed(parser.parse(Statement.of(sql)), allowedResultTypes));
+ assertEquals(ErrorCode.INVALID_ARGUMENT, exception.getErrorCode());
+ assertTrue(
+ exception.getMessage(),
+ exception
+ .getMessage()
+ .contains(
+ "Only statements that return a result of one of the following types are allowed"));
+ }
}
diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/EmulatorUtilTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/EmulatorUtilTest.java
index b9c556a9605..db1b232a37f 100644
--- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/EmulatorUtilTest.java
+++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/EmulatorUtilTest.java
@@ -16,8 +16,9 @@
package com.google.cloud.spanner.connection;
+import static com.google.cloud.spanner.connection.EmulatorUtil.maybeCreateInstanceAndDatabase;
import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.fail;
+import static org.junit.Assert.assertThrows;
import static org.mockito.Mockito.any;
import static org.mockito.Mockito.eq;
import static org.mockito.Mockito.mock;
@@ -29,6 +30,7 @@
import com.google.cloud.spanner.Database;
import com.google.cloud.spanner.DatabaseAdminClient;
import com.google.cloud.spanner.DatabaseId;
+import com.google.cloud.spanner.Dialect;
import com.google.cloud.spanner.ErrorCode;
import com.google.cloud.spanner.Instance;
import com.google.cloud.spanner.InstanceAdminClient;
@@ -45,10 +47,18 @@
import java.util.concurrent.ExecutionException;
import org.junit.Test;
import org.junit.runner.RunWith;
-import org.junit.runners.JUnit4;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameter;
+import org.junit.runners.Parameterized.Parameters;
-@RunWith(JUnit4.class)
+@RunWith(Parameterized.class)
public class EmulatorUtilTest {
+ @Parameter public Dialect dialect;
+
+ @Parameters(name = "dialect = {0}")
+ public static Object[] data() {
+ return Dialect.values();
+ }
@Test
public void testCreateInstanceAndDatabase_bothSucceed()
@@ -75,12 +85,15 @@ public void testCreateInstanceAndDatabase_bothSucceed()
when(spanner.getDatabaseAdminClient()).thenReturn(databaseClient);
when(databaseClient.createDatabase(
- eq("test-instance"), eq("test-database"), eq(ImmutableList.of())))
+ eq("test-instance"),
+ eq(dialect.createDatabaseStatementFor("test-database")),
+ eq(dialect),
+ eq(ImmutableList.of())))
.thenReturn(databaseOperationFuture);
when(databaseOperationFuture.get()).thenReturn(mock(Database.class));
- EmulatorUtil.maybeCreateInstanceAndDatabase(
- spanner, DatabaseId.of("test-project", "test-instance", "test-database"));
+ maybeCreateInstanceAndDatabase(
+ spanner, DatabaseId.of("test-project", "test-instance", "test-database"), dialect);
// Verify that both the instance and the database was created.
verify(instanceClient)
@@ -90,7 +103,12 @@ public void testCreateInstanceAndDatabase_bothSucceed()
.setInstanceConfigId(InstanceConfigId.of("test-project", "emulator-config"))
.setNodeCount(1)
.build());
- verify(databaseClient).createDatabase("test-instance", "test-database", ImmutableList.of());
+ verify(databaseClient)
+ .createDatabase(
+ "test-instance",
+ dialect.createDatabaseStatementFor("test-database"),
+ dialect,
+ ImmutableList.of());
}
@Test
@@ -122,7 +140,10 @@ public void testCreateInstanceAndDatabase_bothFailWithAlreadyExists()
when(spanner.getDatabaseAdminClient()).thenReturn(databaseClient);
when(databaseClient.createDatabase(
- eq("test-instance"), eq("test-database"), eq(ImmutableList.of())))
+ eq("test-instance"),
+ eq(dialect.createDatabaseStatementFor("test-database")),
+ eq(dialect),
+ eq(ImmutableList.of())))
.thenReturn(databaseOperationFuture);
when(databaseOperationFuture.get())
.thenThrow(
@@ -130,8 +151,8 @@ public void testCreateInstanceAndDatabase_bothFailWithAlreadyExists()
SpannerExceptionFactory.newSpannerException(
ErrorCode.ALREADY_EXISTS, "Database already exists")));
- EmulatorUtil.maybeCreateInstanceAndDatabase(
- spanner, DatabaseId.of("test-project", "test-instance", "test-database"));
+ maybeCreateInstanceAndDatabase(
+ spanner, DatabaseId.of("test-project", "test-instance", "test-database"), dialect);
// Verify that both the instance and the database was created.
verify(instanceClient)
@@ -141,7 +162,12 @@ public void testCreateInstanceAndDatabase_bothFailWithAlreadyExists()
.setInstanceConfigId(InstanceConfigId.of("test-project", "emulator-config"))
.setNodeCount(1)
.build());
- verify(databaseClient).createDatabase("test-instance", "test-database", ImmutableList.of());
+ verify(databaseClient)
+ .createDatabase(
+ "test-instance",
+ dialect.createDatabaseStatementFor("test-database"),
+ dialect,
+ ImmutableList.of());
}
@Test
@@ -166,13 +192,15 @@ public void testCreateInstanceAndDatabase_propagatesOtherErrorsOnInstanceCreatio
SpannerExceptionFactory.newSpannerException(
ErrorCode.INVALID_ARGUMENT, "Invalid instance options")));
- try {
- EmulatorUtil.maybeCreateInstanceAndDatabase(
- spanner, DatabaseId.of("test-project", "test-instance", "test-database"));
- fail("missing expected exception");
- } catch (SpannerException e) {
- assertEquals(ErrorCode.INVALID_ARGUMENT, e.getErrorCode());
- }
+ SpannerException exception =
+ assertThrows(
+ SpannerException.class,
+ () ->
+ maybeCreateInstanceAndDatabase(
+ spanner,
+ DatabaseId.of("test-project", "test-instance", "test-database"),
+ dialect));
+ assertEquals(ErrorCode.INVALID_ARGUMENT, exception.getErrorCode());
}
@Test
@@ -193,13 +221,15 @@ public void testCreateInstanceAndDatabase_propagatesInterruptsOnInstanceCreation
.thenReturn(instanceOperationFuture);
when(instanceOperationFuture.get()).thenThrow(new InterruptedException());
- try {
- EmulatorUtil.maybeCreateInstanceAndDatabase(
- spanner, DatabaseId.of("test-project", "test-instance", "test-database"));
- fail("missing expected exception");
- } catch (SpannerException e) {
- assertEquals(ErrorCode.CANCELLED, e.getErrorCode());
- }
+ SpannerException exception =
+ assertThrows(
+ SpannerException.class,
+ () ->
+ maybeCreateInstanceAndDatabase(
+ spanner,
+ DatabaseId.of("test-project", "test-instance", "test-database"),
+ dialect));
+ assertEquals(ErrorCode.CANCELLED, exception.getErrorCode());
}
@Test
@@ -227,7 +257,10 @@ public void testCreateInstanceAndDatabase_propagatesOtherErrorsOnDatabaseCreatio
when(spanner.getDatabaseAdminClient()).thenReturn(databaseClient);
when(databaseClient.createDatabase(
- eq("test-instance"), eq("test-database"), eq(ImmutableList.of())))
+ eq("test-instance"),
+ eq(dialect.createDatabaseStatementFor("test-database")),
+ eq(dialect),
+ eq(ImmutableList.of())))
.thenReturn(databaseOperationFuture);
when(databaseOperationFuture.get())
.thenThrow(
@@ -235,13 +268,15 @@ public void testCreateInstanceAndDatabase_propagatesOtherErrorsOnDatabaseCreatio
SpannerExceptionFactory.newSpannerException(
ErrorCode.INVALID_ARGUMENT, "Invalid database options")));
- try {
- EmulatorUtil.maybeCreateInstanceAndDatabase(
- spanner, DatabaseId.of("test-project", "test-instance", "test-database"));
- fail("missing expected exception");
- } catch (SpannerException e) {
- assertEquals(ErrorCode.INVALID_ARGUMENT, e.getErrorCode());
- }
+ SpannerException exception =
+ assertThrows(
+ SpannerException.class,
+ () ->
+ maybeCreateInstanceAndDatabase(
+ spanner,
+ DatabaseId.of("test-project", "test-instance", "test-database"),
+ dialect));
+ assertEquals(ErrorCode.INVALID_ARGUMENT, exception.getErrorCode());
}
@Test
@@ -269,16 +304,21 @@ public void testCreateInstanceAndDatabase_propagatesInterruptsOnDatabaseCreation
when(spanner.getDatabaseAdminClient()).thenReturn(databaseClient);
when(databaseClient.createDatabase(
- eq("test-instance"), eq("test-database"), eq(ImmutableList.of())))
+ eq("test-instance"),
+ eq(dialect.createDatabaseStatementFor("test-database")),
+ eq(dialect),
+ eq(ImmutableList.of())))
.thenReturn(databaseOperationFuture);
when(databaseOperationFuture.get()).thenThrow(new InterruptedException());
- try {
- EmulatorUtil.maybeCreateInstanceAndDatabase(
- spanner, DatabaseId.of("test-project", "test-instance", "test-database"));
- fail("missing expected exception");
- } catch (SpannerException e) {
- assertEquals(ErrorCode.CANCELLED, e.getErrorCode());
- }
+ SpannerException exception =
+ assertThrows(
+ SpannerException.class,
+ () ->
+ maybeCreateInstanceAndDatabase(
+ spanner,
+ DatabaseId.of("test-project", "test-instance", "test-database"),
+ dialect));
+ assertEquals(ErrorCode.CANCELLED, exception.getErrorCode());
}
}
diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/ExceptionMockServerTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/ExceptionMockServerTest.java
index 4b239e4cf8c..fcc05405b2d 100644
--- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/ExceptionMockServerTest.java
+++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/ExceptionMockServerTest.java
@@ -22,11 +22,18 @@
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
+import com.google.cloud.spanner.ErrorCode;
import com.google.cloud.spanner.MockSpannerServiceImpl.SimulatedExecutionTime;
import com.google.cloud.spanner.MockSpannerServiceImpl.StatementResult;
import com.google.cloud.spanner.Mutation;
import com.google.cloud.spanner.SpannerException;
import com.google.cloud.spanner.Statement;
+import com.google.cloud.spanner.connection.StatementResult.ResultType;
+import com.google.common.collect.ImmutableSet;
+import com.google.longrunning.Operation;
+import com.google.protobuf.Any;
+import com.google.protobuf.Empty;
+import com.google.spanner.admin.database.v1.UpdateDatabaseDdlMetadata;
import io.grpc.Status;
import io.grpc.StatusRuntimeException;
import java.util.Arrays;
@@ -152,4 +159,122 @@ public void testCommitException() {
assertEquals(0, exception.getSuppressed().length);
}
}
+
+ @Test
+ public void testAllowedResultType() {
+ mockSpanner.putStatementResult(
+ StatementResult.query(SELECT_COUNT_STATEMENT, SELECT_COUNT_RESULTSET_BEFORE_INSERT));
+ mockSpanner.putStatementResult(StatementResult.update(INSERT_STATEMENT, 1L));
+ mockSpanner.putStatementResult(
+ StatementResult.updateReturning(
+ INSERT_RETURNING_STATEMENT, SELECT_COUNT_RESULTSET_AFTER_INSERT));
+ mockDatabaseAdmin.addResponse(
+ Operation.newBuilder()
+ .setDone(true)
+ .setResponse(Any.pack(Empty.getDefaultInstance()))
+ .setMetadata(Any.pack(UpdateDatabaseDdlMetadata.getDefaultInstance()))
+ .build());
+ Statement ddl = Statement.of("create table foo");
+
+ try (Connection connection = createConnection()) {
+ assertEquals(
+ ErrorCode.INVALID_ARGUMENT,
+ assertThrows(
+ SpannerException.class,
+ () -> connection.execute(SELECT_COUNT_STATEMENT, ImmutableSet.of()))
+ .getErrorCode());
+ assertEquals(
+ ErrorCode.INVALID_ARGUMENT,
+ assertThrows(
+ SpannerException.class,
+ () ->
+ connection.execute(
+ SELECT_COUNT_STATEMENT, ImmutableSet.of(ResultType.UPDATE_COUNT)))
+ .getErrorCode());
+ assertEquals(
+ ErrorCode.INVALID_ARGUMENT,
+ assertThrows(
+ SpannerException.class,
+ () ->
+ connection.execute(
+ SELECT_COUNT_STATEMENT, ImmutableSet.of(ResultType.NO_RESULT)))
+ .getErrorCode());
+ assertNotNull(
+ connection
+ .execute(SELECT_COUNT_STATEMENT, ImmutableSet.of(ResultType.RESULT_SET))
+ .getResultSet());
+
+ assertEquals(
+ ErrorCode.INVALID_ARGUMENT,
+ assertThrows(
+ SpannerException.class,
+ () -> connection.execute(INSERT_STATEMENT, ImmutableSet.of()))
+ .getErrorCode());
+ assertEquals(
+ ErrorCode.INVALID_ARGUMENT,
+ assertThrows(
+ SpannerException.class,
+ () ->
+ connection.execute(INSERT_STATEMENT, ImmutableSet.of(ResultType.RESULT_SET)))
+ .getErrorCode());
+ assertEquals(
+ ErrorCode.INVALID_ARGUMENT,
+ assertThrows(
+ SpannerException.class,
+ () -> connection.execute(INSERT_STATEMENT, ImmutableSet.of(ResultType.NO_RESULT)))
+ .getErrorCode());
+ assertNotNull(
+ connection
+ .execute(INSERT_STATEMENT, ImmutableSet.of(ResultType.UPDATE_COUNT))
+ .getUpdateCount());
+
+ assertEquals(
+ ErrorCode.INVALID_ARGUMENT,
+ assertThrows(
+ SpannerException.class,
+ () -> connection.execute(INSERT_RETURNING_STATEMENT, ImmutableSet.of()))
+ .getErrorCode());
+ assertEquals(
+ ErrorCode.INVALID_ARGUMENT,
+ assertThrows(
+ SpannerException.class,
+ () ->
+ connection.execute(
+ INSERT_RETURNING_STATEMENT, ImmutableSet.of(ResultType.UPDATE_COUNT)))
+ .getErrorCode());
+ assertEquals(
+ ErrorCode.INVALID_ARGUMENT,
+ assertThrows(
+ SpannerException.class,
+ () ->
+ connection.execute(
+ INSERT_RETURNING_STATEMENT, ImmutableSet.of(ResultType.NO_RESULT)))
+ .getErrorCode());
+ assertNotNull(
+ connection
+ .execute(INSERT_RETURNING_STATEMENT, ImmutableSet.of(ResultType.RESULT_SET))
+ .getResultSet());
+
+ // Commit the current transaction and switch to autocommit to allow DDL.
+ connection.commit();
+ connection.setAutocommit(true);
+ assertEquals(
+ ErrorCode.INVALID_ARGUMENT,
+ assertThrows(SpannerException.class, () -> connection.execute(ddl, ImmutableSet.of()))
+ .getErrorCode());
+ assertEquals(
+ ErrorCode.INVALID_ARGUMENT,
+ assertThrows(
+ SpannerException.class,
+ () -> connection.execute(ddl, ImmutableSet.of(ResultType.RESULT_SET)))
+ .getErrorCode());
+ assertEquals(
+ ErrorCode.INVALID_ARGUMENT,
+ assertThrows(
+ SpannerException.class,
+ () -> connection.execute(ddl, ImmutableSet.of(ResultType.UPDATE_COUNT)))
+ .getErrorCode());
+ assertNotNull(connection.execute(ddl, ImmutableSet.of(ResultType.NO_RESULT)));
+ }
+ }
}
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 8246ca10760..1c20bc94ec6 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
@@ -1143,121 +1143,198 @@ public void testGoogleStandardSQLDialectConvertPositionalParametersToNamedParame
+ "and col8 between @p12 and @p13")));
}
+ private enum CommentInjector {
+ NONE {
+ @Override
+ String inject(String sql, String comment) {
+ return String.format(sql, "");
+ }
+ },
+ BEFORE {
+ @Override
+ String inject(String sql, String comment) {
+ return comment + String.format(sql, "");
+ }
+ },
+ IN_THE_MIDDLE {
+ @Override
+ String inject(String sql, String comment) {
+ return String.format(sql, comment);
+ }
+ },
+ AFTER {
+ @Override
+ String inject(String sql, String comment) {
+ return String.format(sql, "") + comment;
+ }
+ };
+
+ abstract String inject(String sql, String comment);
+ }
+
@Test
public void testPostgreSQLDialectDialectConvertPositionalParametersToNamedParameters() {
assumeTrue(dialect == Dialect.POSTGRESQL);
- assertThat(
+ for (String comment :
+ new String[] {
+ "-- test comment\n",
+ "/* another test comment */",
+ "/* comment\nwith\nmultiple\nlines\n */",
+ "/* comment /* with nested */ comment */"
+ }) {
+ for (CommentInjector injector : CommentInjector.values()) {
+ assertThat(
+ parser.convertPositionalParametersToNamedParameters(
+ '?', injector.inject("select * %sfrom foo where name=?", comment))
+ .sqlWithNamedParameters)
+ .isEqualTo(injector.inject("select * %sfrom foo where name=$1", comment));
+ assertThat(
+ parser.convertPositionalParametersToNamedParameters(
+ '?', injector.inject("?%s'?test?\"?test?\"?'?", comment))
+ .sqlWithNamedParameters)
+ .isEqualTo(injector.inject("$1%s'?test?\"?test?\"?'$2", comment));
+ assertThat(
+ parser.convertPositionalParametersToNamedParameters(
+ '?', injector.inject("?'?it\\''?s'%s?", comment))
+ .sqlWithNamedParameters)
+ .isEqualTo(injector.inject("$1'?it\\''?s'%s$2", comment));
+ assertThat(
+ parser.convertPositionalParametersToNamedParameters(
+ '?', injector.inject("?'?it\\\"?s'%s?", comment))
+ .sqlWithNamedParameters)
+ .isEqualTo(injector.inject("$1'?it\\\"?s'%s$2", comment));
+ assertThat(
+ parser.convertPositionalParametersToNamedParameters(
+ '?', injector.inject("?\"?it\\\"\"?s\"%s?", comment))
+ .sqlWithNamedParameters)
+ .isEqualTo(injector.inject("$1\"?it\\\"\"?s\"%s$2", comment));
+ assertThat(
+ parser.convertPositionalParametersToNamedParameters(
+ '?', injector.inject("?%s'''?it\\''?s'''?", comment))
+ .sqlWithNamedParameters)
+ .isEqualTo(injector.inject("$1%s'''?it\\''?s'''$2", comment));
+ assertThat(
+ parser.convertPositionalParametersToNamedParameters(
+ '?', injector.inject("?\"\"\"?it\\\"\"?s\"\"\"%s?", comment))
+ .sqlWithNamedParameters)
+ .isEqualTo(injector.inject("$1\"\"\"?it\\\"\"?s\"\"\"%s$2", comment));
+
+ assertThat(
+ parser.convertPositionalParametersToNamedParameters(
+ '?', injector.inject("?$$?it$?s$$%s?", comment))
+ .sqlWithNamedParameters)
+ .isEqualTo(injector.inject("$1$$?it$?s$$%s$2", comment));
+ assertThat(
+ parser.convertPositionalParametersToNamedParameters(
+ '?', injector.inject("?$tag$?it$$?s$tag$%s?", comment))
+ .sqlWithNamedParameters)
+ .isEqualTo(injector.inject("$1$tag$?it$$?s$tag$%s$2", comment));
+ assertThat(
+ parser.convertPositionalParametersToNamedParameters(
+ '?', injector.inject("?%s$$?it\\'?s \n ?it\\'?s$$?", comment))
+ .sqlWithNamedParameters)
+ .isEqualTo(injector.inject("$1%s$$?it\\'?s \n ?it\\'?s$$$2", comment));
+
+ // Note: PostgreSQL allows a single-quoted string literal to contain line feeds.
+ assertEquals(
+ injector.inject("$1'?it\\''?s \n ?it\\''?s'%s$2", comment),
parser.convertPositionalParametersToNamedParameters(
- '?', "select * from foo where name=?")
- .sqlWithNamedParameters)
- .isEqualTo("select * from foo where name=$1");
- assertThat(
- parser.convertPositionalParametersToNamedParameters('?', "?'?test?\"?test?\"?'?")
- .sqlWithNamedParameters)
- .isEqualTo("$1'?test?\"?test?\"?'$2");
- assertThat(
- parser.convertPositionalParametersToNamedParameters('?', "?'?it\\''?s'?")
- .sqlWithNamedParameters)
- .isEqualTo("$1'?it\\''?s'$2");
- assertThat(
- parser.convertPositionalParametersToNamedParameters('?', "?'?it\\\"?s'?")
- .sqlWithNamedParameters)
- .isEqualTo("$1'?it\\\"?s'$2");
- assertThat(
- parser.convertPositionalParametersToNamedParameters('?', "?\"?it\\\"\"?s\"?")
- .sqlWithNamedParameters)
- .isEqualTo("$1\"?it\\\"\"?s\"$2");
- assertThat(
- parser.convertPositionalParametersToNamedParameters('?', "?'''?it\\''?s'''?")
- .sqlWithNamedParameters)
- .isEqualTo("$1'''?it\\''?s'''$2");
- assertThat(
- parser.convertPositionalParametersToNamedParameters('?', "?\"\"\"?it\\\"\"?s\"\"\"?")
- .sqlWithNamedParameters)
- .isEqualTo("$1\"\"\"?it\\\"\"?s\"\"\"$2");
-
- assertThat(
- parser.convertPositionalParametersToNamedParameters('?', "?$$?it$?s$$?")
- .sqlWithNamedParameters)
- .isEqualTo("$1$$?it$?s$$$2");
- assertThat(
- parser.convertPositionalParametersToNamedParameters('?', "?$tag$?it$$?s$tag$?")
- .sqlWithNamedParameters)
- .isEqualTo("$1$tag$?it$$?s$tag$$2");
- assertThat(
- parser.convertPositionalParametersToNamedParameters('?', "?$$?it\\'?s \n ?it\\'?s$$?")
- .sqlWithNamedParameters)
- .isEqualTo("$1$$?it\\'?s \n ?it\\'?s$$$2");
-
- // Note: PostgreSQL allows a single-quoted string literal to contain line feeds.
- assertEquals(
- "$1'?it\\''?s \n ?it\\''?s'$2",
- parser.convertPositionalParametersToNamedParameters('?', "?'?it\\''?s \n ?it\\''?s'?")
- .sqlWithNamedParameters);
- assertUnclosedLiteral("?'?it\\''?s \n ?it\\''?s?");
- assertEquals(
- "$1'''?it\\''?s \n ?it\\''?s'$2",
- parser.convertPositionalParametersToNamedParameters('?', "?'''?it\\''?s \n ?it\\''?s'?")
- .sqlWithNamedParameters);
-
- assertThat(
- parser.convertPositionalParametersToNamedParameters(
- '?',
- "select 1, ?, 'test?test', \"test?test\", foo.* from `foo` where col1=? and col2='test' and col3=? and col4='?' and col5=\"?\" and col6='?''?''?'")
- .sqlWithNamedParameters,
- is(
- equalTo(
- "select 1, $1, 'test?test', \"test?test\", foo.* from `foo` where col1=$2 and col2='test' and col3=$3 and col4='?' and col5=\"?\" and col6='?''?''?'")));
+ '?', injector.inject("?'?it\\''?s \n ?it\\''?s'%s?", comment))
+ .sqlWithNamedParameters);
+ assertUnclosedLiteral("?'?it\\''?s \n ?it\\''?s?");
+ assertEquals(
+ injector.inject("$1%s'''?it\\''?s \n ?it\\''?s'$2", comment),
+ parser.convertPositionalParametersToNamedParameters(
+ '?', injector.inject("?%s'''?it\\''?s \n ?it\\''?s'?", comment))
+ .sqlWithNamedParameters);
- assertThat(
- parser.convertPositionalParametersToNamedParameters(
- '?',
- "select * " + "from foo " + "where name=? " + "and col2 like ? " + "and col3 > ?")
- .sqlWithNamedParameters,
- is(
- equalTo(
- "select * "
- + "from foo "
- + "where name=$1 "
- + "and col2 like $2 "
- + "and col3 > $3")));
- assertThat(
- parser.convertPositionalParametersToNamedParameters(
- '?', "select * " + "from foo " + "where id between ? and ?")
- .sqlWithNamedParameters,
- is(equalTo("select * " + "from foo " + "where id between $1 and $2")));
- assertThat(
- parser.convertPositionalParametersToNamedParameters(
- '?', "select * " + "from foo " + "limit ? offset ?")
- .sqlWithNamedParameters,
- is(equalTo("select * " + "from foo " + "limit $1 offset $2")));
- assertThat(
- parser.convertPositionalParametersToNamedParameters(
- '?',
- "select * "
- + "from foo "
- + "where col1=? "
- + "and col2 like ? "
- + "and col3 > ? "
- + "and col4 < ? "
- + "and col5 != ? "
- + "and col6 not in (?, ?, ?) "
- + "and col7 in (?, ?, ?) "
- + "and col8 between ? and ?")
- .sqlWithNamedParameters,
- is(
- equalTo(
- "select * "
- + "from foo "
- + "where col1=$1 "
- + "and col2 like $2 "
- + "and col3 > $3 "
- + "and col4 < $4 "
- + "and col5 != $5 "
- + "and col6 not in ($6, $7, $8) "
- + "and col7 in ($9, $10, $11) "
- + "and col8 between $12 and $13")));
+ assertThat(
+ parser.convertPositionalParametersToNamedParameters(
+ '?',
+ injector.inject(
+ "select 1, ?, 'test?test', \"test?test\", %sfoo.* from `foo` where col1=? and col2='test' and col3=? and col4='?' and col5=\"?\" and col6='?''?''?'",
+ comment))
+ .sqlWithNamedParameters,
+ is(
+ equalTo(
+ injector.inject(
+ "select 1, $1, 'test?test', \"test?test\", %sfoo.* from `foo` where col1=$2 and col2='test' and col3=$3 and col4='?' and col5=\"?\" and col6='?''?''?'",
+ comment))));
+
+ assertThat(
+ parser.convertPositionalParametersToNamedParameters(
+ '?',
+ injector.inject(
+ "select * "
+ + "%sfrom foo "
+ + "where name=? "
+ + "and col2 like ? "
+ + "and col3 > ?",
+ comment))
+ .sqlWithNamedParameters,
+ is(
+ equalTo(
+ injector.inject(
+ "select * "
+ + "%sfrom foo "
+ + "where name=$1 "
+ + "and col2 like $2 "
+ + "and col3 > $3",
+ comment))));
+ assertThat(
+ parser.convertPositionalParametersToNamedParameters(
+ '?',
+ injector.inject(
+ "select * " + "from foo " + "where id between ?%s and ?", comment))
+ .sqlWithNamedParameters,
+ is(
+ equalTo(
+ injector.inject(
+ "select * " + "from foo " + "where id between $1%s and $2", comment))));
+ assertThat(
+ parser.convertPositionalParametersToNamedParameters(
+ '?',
+ injector.inject("select * " + "from foo " + "limit ? %s offset ?", comment))
+ .sqlWithNamedParameters,
+ is(
+ equalTo(
+ injector.inject(
+ "select * " + "from foo " + "limit $1 %s offset $2", comment))));
+ assertThat(
+ parser.convertPositionalParametersToNamedParameters(
+ '?',
+ injector.inject(
+ "select * "
+ + "from foo "
+ + "where col1=? "
+ + "and col2 like ? "
+ + " %s "
+ + "and col3 > ? "
+ + "and col4 < ? "
+ + "and col5 != ? "
+ + "and col6 not in (?, ?, ?) "
+ + "and col7 in (?, ?, ?) "
+ + "and col8 between ? and ?",
+ comment))
+ .sqlWithNamedParameters,
+ is(
+ equalTo(
+ injector.inject(
+ "select * "
+ + "from foo "
+ + "where col1=$1 "
+ + "and col2 like $2 "
+ + " %s "
+ + "and col3 > $3 "
+ + "and col4 < $4 "
+ + "and col5 != $5 "
+ + "and col6 not in ($6, $7, $8) "
+ + "and col7 in ($9, $10, $11) "
+ + "and col8 between $12 and $13",
+ comment))));
+ }
+ }
}
@Test
@@ -1277,6 +1354,11 @@ public void testPostgreSQLGetQueryParameters() {
assertEquals(
ImmutableSet.of("$1"),
parser.getQueryParameters("select '$2' from foo where bar=$1 and baz=$foo"));
+ assertEquals(
+ ImmutableSet.of("$1"),
+ parser.getQueryParameters(
+ "/* @lock_scanned_ranges = exclusive */ select -- random comment\n '$2' "
+ + "from foo /* comment /* with nested comment */ outside of nested comment */ where bar=$1 and baz=$foo"));
}
@Test
@@ -1488,6 +1570,45 @@ public void testPostgreSQLReturningClause() {
parser.parse(Statement.of("insert into t1 select 10.returning*")).hasReturningClause());
}
+ int skipSingleLineComment(String sql, int startIndex) {
+ return PostgreSQLStatementParser.skipSingleLineComment(sql, startIndex, null);
+ }
+
+ int skipMultiLineComment(String sql, int startIndex) {
+ return PostgreSQLStatementParser.skipMultiLineComment(sql, startIndex, null);
+ }
+
+ @Test
+ public void testSkipSingleLineComment() {
+ assumeTrue(dialect == Dialect.POSTGRESQL);
+
+ assertEquals(7, skipSingleLineComment("-- foo\n", 0));
+ assertEquals(7, skipSingleLineComment("-- foo\nbar", 0));
+ assertEquals(6, skipSingleLineComment("-- foo", 0));
+ assertEquals(11, skipSingleLineComment("bar -- foo\n", 4));
+ assertEquals(11, skipSingleLineComment("bar -- foo\nbar", 4));
+ assertEquals(10, skipSingleLineComment("bar -- foo", 4));
+ }
+
+ @Test
+ public void testSkipMultiLineComment() {
+ assumeTrue(dialect == Dialect.POSTGRESQL);
+
+ assertEquals(9, skipMultiLineComment("/* foo */", 0));
+ assertEquals(9, skipMultiLineComment("/* foo */ bar", 0));
+ assertEquals(6, skipMultiLineComment("/* foo", 0));
+ assertEquals(8, skipMultiLineComment("/* foo *", 0));
+ assertEquals(9, skipMultiLineComment("/* foo **", 0));
+ assertEquals(10, skipMultiLineComment("/* foo **/ ", 0));
+ assertEquals(13, skipMultiLineComment("bar /* foo */", 4));
+ assertEquals(13, skipMultiLineComment("bar /* foo */bar", 4));
+ assertEquals(10, skipMultiLineComment("bar /* foo", 4));
+
+ assertEquals(
+ "/* foo /* inner comment */ not in inner comment */".length(),
+ skipMultiLineComment("/* foo /* inner comment */ not in inner comment */ bar", 0));
+ }
+
private void assertUnclosedLiteral(String sql) {
try {
parser.convertPositionalParametersToNamedParameters('?', sql);
diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITBatchReadTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITBatchReadTest.java
index 11be7df0430..6598a3dca76 100644
--- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITBatchReadTest.java
+++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITBatchReadTest.java
@@ -258,8 +258,6 @@ public void readUsingIndex() {
@Test
public void dataBoostRead() {
- // TODO: Remove the following check during GA
- assumeFalse("DataBoost feature is not yet generally available", true);
assumeFalse("Emulator does not support data boost read", isUsingEmulator());
BitSet seenRows = new BitSet(numRows);
@@ -314,8 +312,6 @@ private PartitionOptions getRandomPartitionOptions() {
@Test
public void dataBoostQuery() {
- // TODO: Remove the following check during GA
- assumeFalse("DataBoost feature is not yet generally available", true);
assumeFalse("Emulator does not support data boost query", isUsingEmulator());
BitSet seenRows = new BitSet(numRows);
TimestampBound bound = getRandomBound();
diff --git a/grpc-google-cloud-spanner-admin-database-v1/pom.xml b/grpc-google-cloud-spanner-admin-database-v1/pom.xml
index d9c30274fb1..efbc3323034 100644
--- a/grpc-google-cloud-spanner-admin-database-v1/pom.xml
+++ b/grpc-google-cloud-spanner-admin-database-v1/pom.xml
@@ -4,13 +4,13 @@
4.0.0
com.google.api.grpc
grpc-google-cloud-spanner-admin-database-v1
- 6.45.3
+ 6.46.0
grpc-google-cloud-spanner-admin-database-v1
GRPC library for grpc-google-cloud-spanner-admin-database-v1
com.google.cloud
google-cloud-spanner-parent
- 6.45.3
+ 6.46.0
diff --git a/grpc-google-cloud-spanner-admin-instance-v1/pom.xml b/grpc-google-cloud-spanner-admin-instance-v1/pom.xml
index 6742ac954c4..07a676637d4 100644
--- a/grpc-google-cloud-spanner-admin-instance-v1/pom.xml
+++ b/grpc-google-cloud-spanner-admin-instance-v1/pom.xml
@@ -4,13 +4,13 @@
4.0.0
com.google.api.grpc
grpc-google-cloud-spanner-admin-instance-v1
- 6.45.3
+ 6.46.0
grpc-google-cloud-spanner-admin-instance-v1
GRPC library for grpc-google-cloud-spanner-admin-instance-v1
com.google.cloud
google-cloud-spanner-parent
- 6.45.3
+ 6.46.0
diff --git a/grpc-google-cloud-spanner-v1/pom.xml b/grpc-google-cloud-spanner-v1/pom.xml
index 0dec3e97221..c87f44d3c78 100644
--- a/grpc-google-cloud-spanner-v1/pom.xml
+++ b/grpc-google-cloud-spanner-v1/pom.xml
@@ -4,13 +4,13 @@
4.0.0
com.google.api.grpc
grpc-google-cloud-spanner-v1
- 6.45.3
+ 6.46.0
grpc-google-cloud-spanner-v1
GRPC library for grpc-google-cloud-spanner-v1
com.google.cloud
google-cloud-spanner-parent
- 6.45.3
+ 6.46.0
diff --git a/pom.xml b/pom.xml
index a7f2c7f2c72..0274cdc4543 100644
--- a/pom.xml
+++ b/pom.xml
@@ -4,7 +4,7 @@
com.google.cloud
google-cloud-spanner-parent
pom
- 6.45.3
+ 6.46.0
Google Cloud Spanner Parent
https://github.com/googleapis/java-spanner
@@ -62,37 +62,37 @@
com.google.api.grpc
proto-google-cloud-spanner-admin-instance-v1
- 6.45.3
+ 6.46.0
com.google.api.grpc
proto-google-cloud-spanner-v1
- 6.45.3
+ 6.46.0
com.google.api.grpc
proto-google-cloud-spanner-admin-database-v1
- 6.45.3
+ 6.46.0
com.google.api.grpc
grpc-google-cloud-spanner-v1
- 6.45.3
+ 6.46.0
com.google.api.grpc
grpc-google-cloud-spanner-admin-instance-v1
- 6.45.3
+ 6.46.0
com.google.api.grpc
grpc-google-cloud-spanner-admin-database-v1
- 6.45.3
+ 6.46.0
com.google.cloud
google-cloud-spanner
- 6.45.3
+ 6.46.0
diff --git a/proto-google-cloud-spanner-admin-database-v1/pom.xml b/proto-google-cloud-spanner-admin-database-v1/pom.xml
index 14d72213756..cf0137536c0 100644
--- a/proto-google-cloud-spanner-admin-database-v1/pom.xml
+++ b/proto-google-cloud-spanner-admin-database-v1/pom.xml
@@ -4,13 +4,13 @@
4.0.0
com.google.api.grpc
proto-google-cloud-spanner-admin-database-v1
- 6.45.3
+ 6.46.0
proto-google-cloud-spanner-admin-database-v1
PROTO library for proto-google-cloud-spanner-admin-database-v1
com.google.cloud
google-cloud-spanner-parent
- 6.45.3
+ 6.46.0
diff --git a/proto-google-cloud-spanner-admin-instance-v1/pom.xml b/proto-google-cloud-spanner-admin-instance-v1/pom.xml
index 6061e2162c2..fb7fc00a3a3 100644
--- a/proto-google-cloud-spanner-admin-instance-v1/pom.xml
+++ b/proto-google-cloud-spanner-admin-instance-v1/pom.xml
@@ -4,13 +4,13 @@
4.0.0
com.google.api.grpc
proto-google-cloud-spanner-admin-instance-v1
- 6.45.3
+ 6.46.0
proto-google-cloud-spanner-admin-instance-v1
PROTO library for proto-google-cloud-spanner-admin-instance-v1
com.google.cloud
google-cloud-spanner-parent
- 6.45.3
+ 6.46.0
diff --git a/proto-google-cloud-spanner-v1/pom.xml b/proto-google-cloud-spanner-v1/pom.xml
index 3e3c0f2a759..666edb481bc 100644
--- a/proto-google-cloud-spanner-v1/pom.xml
+++ b/proto-google-cloud-spanner-v1/pom.xml
@@ -4,13 +4,13 @@
4.0.0
com.google.api.grpc
proto-google-cloud-spanner-v1
- 6.45.3
+ 6.46.0
proto-google-cloud-spanner-v1
PROTO library for proto-google-cloud-spanner-v1
com.google.cloud
google-cloud-spanner-parent
- 6.45.3
+ 6.46.0
diff --git a/samples/snapshot/pom.xml b/samples/snapshot/pom.xml
index 7df52fb3301..b5e84778899 100644
--- a/samples/snapshot/pom.xml
+++ b/samples/snapshot/pom.xml
@@ -31,7 +31,7 @@
com.google.cloud
google-cloud-spanner
- 6.45.3
+ 6.46.0
diff --git a/samples/snippets/pom.xml b/samples/snippets/pom.xml
index 8f312776692..4731895c8e2 100644
--- a/samples/snippets/pom.xml
+++ b/samples/snippets/pom.xml
@@ -17,6 +17,7 @@
com.google.cloud.samples
shared-configuration
1.2.0
+