Skip to content

Commit fbf1df9

Browse files
feat: support PostgreSQL for autoConfigEmulator (#2601)
* 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>
1 parent bfa777b commit fbf1df9

File tree

4 files changed

+107
-46
lines changed

4 files changed

+107
-46
lines changed

google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ConnectionImpl.java

+2-1
Original file line numberDiff line numberDiff line change
@@ -249,7 +249,8 @@ static UnitOfWorkType of(TransactionMode transactionMode) {
249249
this.options = options;
250250
this.spanner = spannerPool.getSpanner(options, this);
251251
if (options.isAutoConfigEmulator()) {
252-
EmulatorUtil.maybeCreateInstanceAndDatabase(spanner, options.getDatabaseId());
252+
EmulatorUtil.maybeCreateInstanceAndDatabase(
253+
spanner, options.getDatabaseId(), options.getDialect());
253254
}
254255
this.dbClient = spanner.getDatabaseClient(options.getDatabaseId());
255256
this.batchClient = spanner.getBatchClient(options.getDatabaseId());

google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ConnectionOptions.java

+18-2
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
import com.google.cloud.NoCredentials;
2727
import com.google.cloud.ServiceOptions;
2828
import com.google.cloud.spanner.DatabaseId;
29+
import com.google.cloud.spanner.Dialect;
2930
import com.google.cloud.spanner.ErrorCode;
3031
import com.google.cloud.spanner.Options.RpcPriority;
3132
import com.google.cloud.spanner.SessionPoolOptions;
@@ -307,7 +308,9 @@ public String[] getValidValues() {
307308
ConnectionProperty.createBooleanProperty("returnCommitStats", "", false),
308309
ConnectionProperty.createBooleanProperty(
309310
"autoConfigEmulator",
310-
"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.",
311+
"Automatically configure the connection to try to connect to the Cloud Spanner emulator (true/false). "
312+
+ "The instance and database in the connection string will automatically be created if these do not yet exist on the emulator. "
313+
+ "Add dialect=postgresql to the connection string to make sure that the database that is created uses the PostgreSQL dialect.",
311314
false),
312315
ConnectionProperty.createBooleanProperty(
313316
LENIENT_PROPERTY_NAME,
@@ -317,7 +320,8 @@ public String[] getValidValues() {
317320
RPC_PRIORITY_NAME,
318321
"Sets the priority for all RPC invocations from this connection (HIGH/MEDIUM/LOW). The default is HIGH."),
319322
ConnectionProperty.createStringProperty(
320-
DIALECT_PROPERTY_NAME, "Sets the dialect to use for this connection."),
323+
DIALECT_PROPERTY_NAME,
324+
"Sets the dialect to use for new databases that are created by this connection."),
321325
ConnectionProperty.createStringProperty(
322326
DATABASE_ROLE_PROPERTY_NAME,
323327
"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() {
626630
private final QueryOptions queryOptions;
627631
private final boolean returnCommitStats;
628632
private final boolean autoConfigEmulator;
633+
private final Dialect dialect;
629634
private final RpcPriority rpcPriority;
630635
private final boolean delayTransactionStartUntilFirstWrite;
631636
private final boolean trackSessionLeaks;
@@ -677,6 +682,7 @@ private ConnectionOptions(Builder builder) {
677682
this.queryOptions = queryOptionsBuilder.build();
678683
this.returnCommitStats = parseReturnCommitStats(this.uri);
679684
this.autoConfigEmulator = parseAutoConfigEmulator(this.uri);
685+
this.dialect = parseDialect(this.uri);
680686
this.usePlainText = this.autoConfigEmulator || parseUsePlainText(this.uri);
681687
this.host = determineHost(matcher, autoConfigEmulator, usePlainText);
682688
this.rpcPriority = parseRPCPriority(this.uri);
@@ -939,6 +945,12 @@ static boolean parseAutoConfigEmulator(String uri) {
939945
return Boolean.parseBoolean(value);
940946
}
941947

948+
@VisibleForTesting
949+
static Dialect parseDialect(String uri) {
950+
String value = parseUriProperty(uri, DIALECT_PROPERTY_NAME);
951+
return value != null ? Dialect.valueOf(value.toUpperCase()) : Dialect.GOOGLE_STANDARD_SQL;
952+
}
953+
942954
@VisibleForTesting
943955
static boolean parseLenient(String uri) {
944956
String value = parseUriProperty(uri, LENIENT_PROPERTY_NAME);
@@ -1259,6 +1271,10 @@ public boolean isAutoConfigEmulator() {
12591271
return autoConfigEmulator;
12601272
}
12611273

1274+
public Dialect getDialect() {
1275+
return dialect;
1276+
}
1277+
12621278
/** The {@link RpcPriority} to use for the connection. */
12631279
RpcPriority getRPCPriority() {
12641280
return rpcPriority;

google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/EmulatorUtil.java

+6-2
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818

1919
import com.google.cloud.NoCredentials;
2020
import com.google.cloud.spanner.DatabaseId;
21+
import com.google.cloud.spanner.Dialect;
2122
import com.google.cloud.spanner.ErrorCode;
2223
import com.google.cloud.spanner.InstanceConfigId;
2324
import com.google.cloud.spanner.InstanceInfo;
@@ -41,8 +42,10 @@ class EmulatorUtil {
4142
*
4243
* @param spanner a {@link Spanner} instance that connects to an emulator instance
4344
* @param databaseId the id of the instance and the database to create
45+
* @param dialect the {@link Dialect} to use for the database to create
4446
*/
45-
static void maybeCreateInstanceAndDatabase(Spanner spanner, DatabaseId databaseId) {
47+
static void maybeCreateInstanceAndDatabase(
48+
Spanner spanner, DatabaseId databaseId, Dialect dialect) {
4649
Preconditions.checkArgument(
4750
NoCredentials.getInstance().equals(spanner.getOptions().getCredentials()));
4851
try {
@@ -70,7 +73,8 @@ static void maybeCreateInstanceAndDatabase(Spanner spanner, DatabaseId databaseI
7073
.getDatabaseAdminClient()
7174
.createDatabase(
7275
databaseId.getInstanceId().getInstance(),
73-
databaseId.getDatabase(),
76+
dialect.createDatabaseStatementFor(databaseId.getDatabase()),
77+
dialect,
7478
ImmutableList.of())
7579
.get();
7680
} catch (ExecutionException executionException) {

google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/EmulatorUtilTest.java

+81-41
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,9 @@
1616

1717
package com.google.cloud.spanner.connection;
1818

19+
import static com.google.cloud.spanner.connection.EmulatorUtil.maybeCreateInstanceAndDatabase;
1920
import static org.junit.Assert.assertEquals;
20-
import static org.junit.Assert.fail;
21+
import static org.junit.Assert.assertThrows;
2122
import static org.mockito.Mockito.any;
2223
import static org.mockito.Mockito.eq;
2324
import static org.mockito.Mockito.mock;
@@ -29,6 +30,7 @@
2930
import com.google.cloud.spanner.Database;
3031
import com.google.cloud.spanner.DatabaseAdminClient;
3132
import com.google.cloud.spanner.DatabaseId;
33+
import com.google.cloud.spanner.Dialect;
3234
import com.google.cloud.spanner.ErrorCode;
3335
import com.google.cloud.spanner.Instance;
3436
import com.google.cloud.spanner.InstanceAdminClient;
@@ -45,10 +47,18 @@
4547
import java.util.concurrent.ExecutionException;
4648
import org.junit.Test;
4749
import org.junit.runner.RunWith;
48-
import org.junit.runners.JUnit4;
50+
import org.junit.runners.Parameterized;
51+
import org.junit.runners.Parameterized.Parameter;
52+
import org.junit.runners.Parameterized.Parameters;
4953

50-
@RunWith(JUnit4.class)
54+
@RunWith(Parameterized.class)
5155
public class EmulatorUtilTest {
56+
@Parameter public Dialect dialect;
57+
58+
@Parameters(name = "dialect = {0}")
59+
public static Object[] data() {
60+
return Dialect.values();
61+
}
5262

5363
@Test
5464
public void testCreateInstanceAndDatabase_bothSucceed()
@@ -75,12 +85,15 @@ public void testCreateInstanceAndDatabase_bothSucceed()
7585

7686
when(spanner.getDatabaseAdminClient()).thenReturn(databaseClient);
7787
when(databaseClient.createDatabase(
78-
eq("test-instance"), eq("test-database"), eq(ImmutableList.of())))
88+
eq("test-instance"),
89+
eq(dialect.createDatabaseStatementFor("test-database")),
90+
eq(dialect),
91+
eq(ImmutableList.of())))
7992
.thenReturn(databaseOperationFuture);
8093
when(databaseOperationFuture.get()).thenReturn(mock(Database.class));
8194

82-
EmulatorUtil.maybeCreateInstanceAndDatabase(
83-
spanner, DatabaseId.of("test-project", "test-instance", "test-database"));
95+
maybeCreateInstanceAndDatabase(
96+
spanner, DatabaseId.of("test-project", "test-instance", "test-database"), dialect);
8497

8598
// Verify that both the instance and the database was created.
8699
verify(instanceClient)
@@ -90,7 +103,12 @@ public void testCreateInstanceAndDatabase_bothSucceed()
90103
.setInstanceConfigId(InstanceConfigId.of("test-project", "emulator-config"))
91104
.setNodeCount(1)
92105
.build());
93-
verify(databaseClient).createDatabase("test-instance", "test-database", ImmutableList.of());
106+
verify(databaseClient)
107+
.createDatabase(
108+
"test-instance",
109+
dialect.createDatabaseStatementFor("test-database"),
110+
dialect,
111+
ImmutableList.of());
94112
}
95113

96114
@Test
@@ -122,16 +140,19 @@ public void testCreateInstanceAndDatabase_bothFailWithAlreadyExists()
122140

123141
when(spanner.getDatabaseAdminClient()).thenReturn(databaseClient);
124142
when(databaseClient.createDatabase(
125-
eq("test-instance"), eq("test-database"), eq(ImmutableList.of())))
143+
eq("test-instance"),
144+
eq(dialect.createDatabaseStatementFor("test-database")),
145+
eq(dialect),
146+
eq(ImmutableList.of())))
126147
.thenReturn(databaseOperationFuture);
127148
when(databaseOperationFuture.get())
128149
.thenThrow(
129150
new ExecutionException(
130151
SpannerExceptionFactory.newSpannerException(
131152
ErrorCode.ALREADY_EXISTS, "Database already exists")));
132153

133-
EmulatorUtil.maybeCreateInstanceAndDatabase(
134-
spanner, DatabaseId.of("test-project", "test-instance", "test-database"));
154+
maybeCreateInstanceAndDatabase(
155+
spanner, DatabaseId.of("test-project", "test-instance", "test-database"), dialect);
135156

136157
// Verify that both the instance and the database was created.
137158
verify(instanceClient)
@@ -141,7 +162,12 @@ public void testCreateInstanceAndDatabase_bothFailWithAlreadyExists()
141162
.setInstanceConfigId(InstanceConfigId.of("test-project", "emulator-config"))
142163
.setNodeCount(1)
143164
.build());
144-
verify(databaseClient).createDatabase("test-instance", "test-database", ImmutableList.of());
165+
verify(databaseClient)
166+
.createDatabase(
167+
"test-instance",
168+
dialect.createDatabaseStatementFor("test-database"),
169+
dialect,
170+
ImmutableList.of());
145171
}
146172

147173
@Test
@@ -166,13 +192,15 @@ public void testCreateInstanceAndDatabase_propagatesOtherErrorsOnInstanceCreatio
166192
SpannerExceptionFactory.newSpannerException(
167193
ErrorCode.INVALID_ARGUMENT, "Invalid instance options")));
168194

169-
try {
170-
EmulatorUtil.maybeCreateInstanceAndDatabase(
171-
spanner, DatabaseId.of("test-project", "test-instance", "test-database"));
172-
fail("missing expected exception");
173-
} catch (SpannerException e) {
174-
assertEquals(ErrorCode.INVALID_ARGUMENT, e.getErrorCode());
175-
}
195+
SpannerException exception =
196+
assertThrows(
197+
SpannerException.class,
198+
() ->
199+
maybeCreateInstanceAndDatabase(
200+
spanner,
201+
DatabaseId.of("test-project", "test-instance", "test-database"),
202+
dialect));
203+
assertEquals(ErrorCode.INVALID_ARGUMENT, exception.getErrorCode());
176204
}
177205

178206
@Test
@@ -193,13 +221,15 @@ public void testCreateInstanceAndDatabase_propagatesInterruptsOnInstanceCreation
193221
.thenReturn(instanceOperationFuture);
194222
when(instanceOperationFuture.get()).thenThrow(new InterruptedException());
195223

196-
try {
197-
EmulatorUtil.maybeCreateInstanceAndDatabase(
198-
spanner, DatabaseId.of("test-project", "test-instance", "test-database"));
199-
fail("missing expected exception");
200-
} catch (SpannerException e) {
201-
assertEquals(ErrorCode.CANCELLED, e.getErrorCode());
202-
}
224+
SpannerException exception =
225+
assertThrows(
226+
SpannerException.class,
227+
() ->
228+
maybeCreateInstanceAndDatabase(
229+
spanner,
230+
DatabaseId.of("test-project", "test-instance", "test-database"),
231+
dialect));
232+
assertEquals(ErrorCode.CANCELLED, exception.getErrorCode());
203233
}
204234

205235
@Test
@@ -227,21 +257,26 @@ public void testCreateInstanceAndDatabase_propagatesOtherErrorsOnDatabaseCreatio
227257

228258
when(spanner.getDatabaseAdminClient()).thenReturn(databaseClient);
229259
when(databaseClient.createDatabase(
230-
eq("test-instance"), eq("test-database"), eq(ImmutableList.of())))
260+
eq("test-instance"),
261+
eq(dialect.createDatabaseStatementFor("test-database")),
262+
eq(dialect),
263+
eq(ImmutableList.of())))
231264
.thenReturn(databaseOperationFuture);
232265
when(databaseOperationFuture.get())
233266
.thenThrow(
234267
new ExecutionException(
235268
SpannerExceptionFactory.newSpannerException(
236269
ErrorCode.INVALID_ARGUMENT, "Invalid database options")));
237270

238-
try {
239-
EmulatorUtil.maybeCreateInstanceAndDatabase(
240-
spanner, DatabaseId.of("test-project", "test-instance", "test-database"));
241-
fail("missing expected exception");
242-
} catch (SpannerException e) {
243-
assertEquals(ErrorCode.INVALID_ARGUMENT, e.getErrorCode());
244-
}
271+
SpannerException exception =
272+
assertThrows(
273+
SpannerException.class,
274+
() ->
275+
maybeCreateInstanceAndDatabase(
276+
spanner,
277+
DatabaseId.of("test-project", "test-instance", "test-database"),
278+
dialect));
279+
assertEquals(ErrorCode.INVALID_ARGUMENT, exception.getErrorCode());
245280
}
246281

247282
@Test
@@ -269,16 +304,21 @@ public void testCreateInstanceAndDatabase_propagatesInterruptsOnDatabaseCreation
269304

270305
when(spanner.getDatabaseAdminClient()).thenReturn(databaseClient);
271306
when(databaseClient.createDatabase(
272-
eq("test-instance"), eq("test-database"), eq(ImmutableList.of())))
307+
eq("test-instance"),
308+
eq(dialect.createDatabaseStatementFor("test-database")),
309+
eq(dialect),
310+
eq(ImmutableList.of())))
273311
.thenReturn(databaseOperationFuture);
274312
when(databaseOperationFuture.get()).thenThrow(new InterruptedException());
275313

276-
try {
277-
EmulatorUtil.maybeCreateInstanceAndDatabase(
278-
spanner, DatabaseId.of("test-project", "test-instance", "test-database"));
279-
fail("missing expected exception");
280-
} catch (SpannerException e) {
281-
assertEquals(ErrorCode.CANCELLED, e.getErrorCode());
282-
}
314+
SpannerException exception =
315+
assertThrows(
316+
SpannerException.class,
317+
() ->
318+
maybeCreateInstanceAndDatabase(
319+
spanner,
320+
DatabaseId.of("test-project", "test-instance", "test-database"),
321+
dialect));
322+
assertEquals(ErrorCode.CANCELLED, exception.getErrorCode());
283323
}
284324
}

0 commit comments

Comments
 (0)