Skip to content

Commit

Permalink
feat: support PostgreSQL for autoConfigEmulator (#2601)
Browse files Browse the repository at this point in the history
* feat: support PostgreSQL for autoConfigEmulator

The autoConfigEmulator=true flag in the Connection API can be used to
automatically connect to the emulator and automatically create the
instance and database that is being referenced. This makes running a
quick test on the emulator much easier, as all you need to do is to
configure the correct (JDBC) connection URL, and it will automatically
work. This mode would always create a GoogleSQL database. This change
adds support for creating a PostgreSQL database if the user specifically
sets the dialect in the connection string.

* 🦉 Updates from OwlBot post-processor

See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md

---------

Co-authored-by: Owl Bot <gcf-owl-bot[bot]@users.noreply.github.com>
  • Loading branch information
olavloite and gcf-owl-bot[bot] committed Aug 30, 2023
1 parent bfa777b commit fbf1df9
Show file tree
Hide file tree
Showing 4 changed files with 107 additions and 46 deletions.
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
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
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
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());
}
}

0 comments on commit fbf1df9

Please sign in to comment.