Skip to content

feat: support PostgreSQL for autoConfigEmulator #2601

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Aug 30, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,13 +57,13 @@ implementation 'com.google.cloud:google-cloud-spanner'
If you are using Gradle without BOM, add this to your dependencies:

```Groovy
implementation 'com.google.cloud:google-cloud-spanner:6.45.2'
implementation 'com.google.cloud:google-cloud-spanner:6.45.3'
```

If you are using SBT, add this to your dependencies:

```Scala
libraryDependencies += "com.google.cloud" % "google-cloud-spanner" % "6.45.2"
libraryDependencies += "com.google.cloud" % "google-cloud-spanner" % "6.45.3"
```
<!-- {x-version-update-end} -->

Expand Down Expand Up @@ -430,7 +430,7 @@ Java is a registered trademark of Oracle and/or its affiliates.
[kokoro-badge-link-5]: http://storage.googleapis.com/cloud-devrel-public/java/badges/java-spanner/java11.html
[stability-image]: https://img.shields.io/badge/stability-stable-green
[maven-version-image]: https://img.shields.io/maven-central/v/com.google.cloud/google-cloud-spanner.svg
[maven-version-link]: https://central.sonatype.com/artifact/com.google.cloud/google-cloud-spanner/6.45.2
[maven-version-link]: https://central.sonatype.com/artifact/com.google.cloud/google-cloud-spanner/6.45.3
[authentication]: https://github.com/googleapis/google-cloud-java#authentication
[auth-scopes]: https://developers.google.com/identity/protocols/oauth2/scopes
[predefined-iam-roles]: https://cloud.google.com/iam/docs/understanding-roles#predefined_roles
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -249,7 +249,8 @@ static UnitOfWorkType of(TransactionMode transactionMode) {
this.options = options;
this.spanner = spannerPool.getSpanner(options, this);
if (options.isAutoConfigEmulator()) {
EmulatorUtil.maybeCreateInstanceAndDatabase(spanner, options.getDatabaseId());
EmulatorUtil.maybeCreateInstanceAndDatabase(
spanner, options.getDatabaseId(), options.getDialect());
}
this.dbClient = spanner.getDatabaseClient(options.getDatabaseId());
this.batchClient = spanner.getBatchClient(options.getDatabaseId());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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,
Expand All @@ -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"),
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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 {
Expand Down Expand Up @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -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()
Expand All @@ -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)
Expand All @@ -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
Expand Down Expand Up @@ -122,16 +140,19 @@ 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(
new ExecutionException(
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)
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -227,21 +257,26 @@ 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(
new ExecutionException(
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
Expand Down Expand Up @@ -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());
}
}