Skip to content

Commit 40110fe

Browse files
feat: support CREATE DATABASE in Connection API (#1845)
* feat: support CREATE DATABASE in Connection API Adds support for the CREATE DATABASE statement in the Connection API. The statement can only be used with a SingleUseTransaction (i.e. auto commit mode). It is not supported in DDL batches. The database that is created will have the same dialect as the current database that the user is connected to. Fixes #1884 * fix: add clirr ignore * 🦉 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 049635d commit 40110fe

File tree

10 files changed

+167
-5
lines changed

10 files changed

+167
-5
lines changed

google-cloud-spanner/clirr-ignored-differences.xml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,4 +60,10 @@
6060
<className>com/google/cloud/spanner/spi/v1/SpannerRpc</className>
6161
<method>com.google.api.gax.longrunning.OperationFuture copyBackup(com.google.cloud.spanner.BackupId, com.google.cloud.spanner.Backup)</method>
6262
</difference>
63+
<difference>
64+
<differenceType>7012</differenceType>
65+
<className>com/google/cloud/spanner/DatabaseAdminClient</className>
66+
<method>com.google.api.gax.longrunning.OperationFuture createDatabase(java.lang.String, java.lang.String, com.google.cloud.spanner.Dialect, java.lang.Iterable)</method>
67+
</difference>
68+
6369
</differences>

google-cloud-spanner/src/main/java/com/google/cloud/spanner/DatabaseAdminClient.java

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,39 @@ public interface DatabaseAdminClient {
7070
OperationFuture<Database, CreateDatabaseMetadata> createDatabase(
7171
String instanceId, String databaseId, Iterable<String> statements) throws SpannerException;
7272

73+
/**
74+
* Creates a new database in a Cloud Spanner instance with the given {@link Dialect}.
75+
*
76+
* <p>Example to create database.
77+
*
78+
* <pre>{@code
79+
* String instanceId = "my_instance_id";
80+
* String createDatabaseStatement = "CREATE DATABASE \"my-database\"";
81+
* Operation<Database, CreateDatabaseMetadata> op = dbAdminClient
82+
* .createDatabase(
83+
* instanceId,
84+
* createDatabaseStatement,
85+
* Dialect.POSTGRESQL
86+
* Collections.emptyList());
87+
* Database db = op.waitFor().getResult();
88+
* }</pre>
89+
*
90+
* @param instanceId the id of the instance in which to create the database.
91+
* @param createDatabaseStatement the CREATE DATABASE statement for the database. This statement
92+
* must use the dialect for the new database.
93+
* @param dialect the dialect that the new database should use.
94+
* @param statements DDL statements to run while creating the database, for example {@code CREATE
95+
* TABLE MyTable ( ... )}. This should not include {@code CREATE DATABASE} statement.
96+
*/
97+
default OperationFuture<Database, CreateDatabaseMetadata> createDatabase(
98+
String instanceId,
99+
String createDatabaseStatement,
100+
Dialect dialect,
101+
Iterable<String> statements)
102+
throws SpannerException {
103+
throw new UnsupportedOperationException("Unimplemented");
104+
}
105+
73106
/**
74107
* Creates a database in a Cloud Spanner instance. Any configuration options in the {@link
75108
* Database} instance will be included in the {@link CreateDatabaseRequest}.

google-cloud-spanner/src/main/java/com/google/cloud/spanner/DatabaseAdminClientImpl.java

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -332,6 +332,26 @@ public OperationFuture<Database, CreateDatabaseMetadata> createDatabase(
332332
final Dialect dialect = Preconditions.checkNotNull(database.getDialect());
333333
final String createStatement =
334334
dialect.createDatabaseStatementFor(database.getId().getDatabase());
335+
336+
return createDatabase(createStatement, database, statements);
337+
}
338+
339+
@Override
340+
public OperationFuture<Database, CreateDatabaseMetadata> createDatabase(
341+
String instanceId,
342+
String createDatabaseStatement,
343+
Dialect dialect,
344+
Iterable<String> statements)
345+
throws SpannerException {
346+
Database database =
347+
newDatabaseBuilder(DatabaseId.of(projectId, instanceId, "")).setDialect(dialect).build();
348+
349+
return createDatabase(createDatabaseStatement, database, statements);
350+
}
351+
352+
private OperationFuture<Database, CreateDatabaseMetadata> createDatabase(
353+
String createStatement, Database database, Iterable<String> statements)
354+
throws SpannerException {
335355
OperationFuture<com.google.spanner.admin.database.v1.Database, CreateDatabaseMetadata>
336356
rawOperationFuture =
337357
rpc.createDatabase(

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,9 @@ public ApiFuture<Void> executeDdlAsync(ParsedStatement ddl) {
188188
"Only DDL statements are allowed. \""
189189
+ ddl.getSqlWithoutComments()
190190
+ "\" is not a DDL-statement.");
191+
Preconditions.checkArgument(
192+
!DdlClient.isCreateDatabaseStatement(ddl.getSqlWithoutComments()),
193+
"CREATE DATABASE is not supported in DDL batches.");
191194
statements.add(ddl.getSqlWithoutComments());
192195
return ApiFutures.immediateFuture(null);
193196
}

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

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,14 @@
1717
package com.google.cloud.spanner.connection;
1818

1919
import com.google.api.gax.longrunning.OperationFuture;
20+
import com.google.cloud.spanner.Database;
2021
import com.google.cloud.spanner.DatabaseAdminClient;
22+
import com.google.cloud.spanner.Dialect;
23+
import com.google.cloud.spanner.ErrorCode;
24+
import com.google.cloud.spanner.SpannerExceptionFactory;
2125
import com.google.common.base.Preconditions;
2226
import com.google.common.base.Strings;
27+
import com.google.spanner.admin.database.v1.CreateDatabaseMetadata;
2328
import com.google.spanner.admin.database.v1.UpdateDatabaseDdlMetadata;
2429
import java.util.Collections;
2530
import java.util.List;
@@ -79,13 +84,32 @@ private DdlClient(Builder builder) {
7984
this.databaseName = builder.databaseName;
8085
}
8186

87+
OperationFuture<Database, CreateDatabaseMetadata> executeCreateDatabase(
88+
String createStatement, Dialect dialect) {
89+
Preconditions.checkArgument(isCreateDatabaseStatement(createStatement));
90+
return dbAdminClient.createDatabase(
91+
instanceId, createStatement, dialect, Collections.emptyList());
92+
}
93+
8294
/** Execute a single DDL statement. */
8395
OperationFuture<Void, UpdateDatabaseDdlMetadata> executeDdl(String ddl) {
8496
return executeDdl(Collections.singletonList(ddl));
8597
}
8698

8799
/** Execute a list of DDL statements as one operation. */
88100
OperationFuture<Void, UpdateDatabaseDdlMetadata> executeDdl(List<String> statements) {
101+
if (statements.stream().anyMatch(DdlClient::isCreateDatabaseStatement)) {
102+
throw SpannerExceptionFactory.newSpannerException(
103+
ErrorCode.INVALID_ARGUMENT, "CREATE DATABASE is not supported in a DDL batch");
104+
}
89105
return dbAdminClient.updateDatabaseDdl(instanceId, databaseName, statements, null);
90106
}
107+
108+
/** Returns true if the statement is a `CREATE DATABASE ...` statement. */
109+
static boolean isCreateDatabaseStatement(String statement) {
110+
String[] tokens = statement.split("\\s+", 3);
111+
return tokens.length >= 2
112+
&& tokens[0].equalsIgnoreCase("CREATE")
113+
&& tokens[1].equalsIgnoreCase("DATABASE");
114+
}
91115
}

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

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,6 @@
4444
import com.google.common.collect.ImmutableList;
4545
import com.google.common.collect.Iterables;
4646
import com.google.spanner.admin.database.v1.DatabaseAdminGrpc;
47-
import com.google.spanner.admin.database.v1.UpdateDatabaseDdlMetadata;
4847
import com.google.spanner.v1.SpannerGrpc;
4948
import java.util.concurrent.Callable;
5049

@@ -270,11 +269,17 @@ public ApiFuture<Void> executeDdlAsync(final ParsedStatement ddl) {
270269
Callable<Void> callable =
271270
() -> {
272271
try {
273-
OperationFuture<Void, UpdateDatabaseDdlMetadata> operation =
274-
ddlClient.executeDdl(ddl.getSqlWithoutComments());
275-
Void res = getWithStatementTimeout(operation, ddl);
272+
OperationFuture<?, ?> operation;
273+
if (DdlClient.isCreateDatabaseStatement(ddl.getSqlWithoutComments())) {
274+
operation =
275+
ddlClient.executeCreateDatabase(
276+
ddl.getSqlWithoutComments(), dbClient.getDialect());
277+
} else {
278+
operation = ddlClient.executeDdl(ddl.getSqlWithoutComments());
279+
}
280+
getWithStatementTimeout(operation, ddl);
276281
state = UnitOfWorkState.COMMITTED;
277-
return res;
282+
return null;
278283
} catch (Throwable t) {
279284
state = UnitOfWorkState.COMMIT_FAILED;
280285
throw t;

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

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import static org.hamcrest.MatcherAssert.assertThat;
2323
import static org.junit.Assert.assertEquals;
2424
import static org.junit.Assert.assertNull;
25+
import static org.junit.Assert.assertThrows;
2526
import static org.junit.Assert.fail;
2627
import static org.mockito.Mockito.anyList;
2728
import static org.mockito.Mockito.anyString;
@@ -143,6 +144,17 @@ public void testExecuteQuery() {
143144
}
144145
}
145146

147+
@Test
148+
public void testExecuteCreateDatabase() {
149+
DdlBatch batch = createSubject();
150+
assertThrows(
151+
IllegalArgumentException.class,
152+
() ->
153+
batch.executeDdlAsync(
154+
AbstractStatementParser.getInstance(Dialect.GOOGLE_STANDARD_SQL)
155+
.parse(Statement.of("CREATE DATABASE foo"))));
156+
}
157+
146158
@Test
147159
public void testExecuteMetadataQuery() {
148160
Statement statement = Statement.of("SELECT * FROM INFORMATION_SCHEMA.TABLES");

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

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616

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

19+
import static org.junit.Assert.assertFalse;
20+
import static org.junit.Assert.assertTrue;
1921
import static org.mockito.Mockito.anyList;
2022
import static org.mockito.Mockito.eq;
2123
import static org.mockito.Mockito.isNull;
@@ -66,4 +68,23 @@ public void testExecuteDdl() throws InterruptedException, ExecutionException {
6668
subject.executeDdl(ddlList);
6769
verify(client).updateDatabaseDdl(instanceId, databaseId, ddlList, null);
6870
}
71+
72+
@Test
73+
public void testIsCreateDatabase() {
74+
assertTrue(DdlClient.isCreateDatabaseStatement("CREATE DATABASE foo"));
75+
assertTrue(DdlClient.isCreateDatabaseStatement("CREATE DATABASE \"foo\""));
76+
assertTrue(DdlClient.isCreateDatabaseStatement("CREATE DATABASE `foo`"));
77+
assertTrue(DdlClient.isCreateDatabaseStatement("CREATE DATABASE\tfoo"));
78+
assertTrue(DdlClient.isCreateDatabaseStatement("CREATE DATABASE\n foo"));
79+
assertTrue(DdlClient.isCreateDatabaseStatement("CREATE DATABASE\t\n foo"));
80+
assertTrue(DdlClient.isCreateDatabaseStatement("CREATE DATABASE"));
81+
assertTrue(DdlClient.isCreateDatabaseStatement("CREATE\t \n DATABASE foo"));
82+
assertTrue(DdlClient.isCreateDatabaseStatement("create\t \n DATABASE foo"));
83+
assertTrue(DdlClient.isCreateDatabaseStatement("create database foo"));
84+
85+
assertFalse(DdlClient.isCreateDatabaseStatement("CREATE VIEW foo"));
86+
assertFalse(DdlClient.isCreateDatabaseStatement("CREATE DATABAS foo"));
87+
assertFalse(DdlClient.isCreateDatabaseStatement("CREATE DATABASEfoo"));
88+
assertFalse(DdlClient.isCreateDatabaseStatement("CREATE foo"));
89+
}
6990
}

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

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
import com.google.cloud.spanner.AsyncResultSet;
3535
import com.google.cloud.spanner.CommitResponse;
3636
import com.google.cloud.spanner.DatabaseClient;
37+
import com.google.cloud.spanner.Dialect;
3738
import com.google.cloud.spanner.ErrorCode;
3839
import com.google.cloud.spanner.Key;
3940
import com.google.cloud.spanner.KeySet;
@@ -365,6 +366,7 @@ private SingleUseTransaction createSubject(
365366
DatabaseClient dbClient = mock(DatabaseClient.class);
366367
com.google.cloud.spanner.ReadOnlyTransaction singleUse =
367368
new SimpleReadOnlyTransaction(staleness);
369+
when(dbClient.getDialect()).thenReturn(Dialect.GOOGLE_STANDARD_SQL);
368370
when(dbClient.singleUseReadOnlyTransaction(staleness)).thenReturn(singleUse);
369371

370372
final TransactionContext txContext = mock(TransactionContext.class);
@@ -537,6 +539,19 @@ public void testExecuteDdl() {
537539
verify(ddlClient).executeDdl(sql);
538540
}
539541

542+
@Test
543+
public void testExecuteCreateDatabase() {
544+
String sql = "CREATE DATABASE FOO";
545+
ParsedStatement ddl = createParsedDdl(sql);
546+
DdlClient ddlClient = createDefaultMockDdlClient();
547+
when(ddlClient.executeCreateDatabase(sql, Dialect.GOOGLE_STANDARD_SQL))
548+
.thenReturn(mock(OperationFuture.class));
549+
550+
SingleUseTransaction singleUseTransaction = createDdlSubject(ddlClient);
551+
get(singleUseTransaction.executeDdlAsync(ddl));
552+
verify(ddlClient).executeCreateDatabase(sql, Dialect.GOOGLE_STANDARD_SQL);
553+
}
554+
540555
@Test
541556
public void testExecuteQuery() {
542557
for (TimestampBound staleness : getTestTimestampBounds()) {

google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/it/ITDdlTest.java

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,14 @@
1616

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

19+
import static org.junit.Assert.assertNotNull;
20+
import static org.junit.Assert.assertThrows;
21+
22+
import com.google.cloud.spanner.DatabaseAdminClient;
23+
import com.google.cloud.spanner.DatabaseNotFoundException;
1924
import com.google.cloud.spanner.ParallelIntegrationTest;
25+
import com.google.cloud.spanner.Statement;
26+
import com.google.cloud.spanner.connection.Connection;
2027
import com.google.cloud.spanner.connection.ITAbstractSpannerTest;
2128
import com.google.cloud.spanner.connection.SqlScriptVerifier;
2229
import org.junit.Test;
@@ -34,4 +41,20 @@ public void testSqlScript() throws Exception {
3441
SqlScriptVerifier verifier = new SqlScriptVerifier(new ITConnectionProvider());
3542
verifier.verifyStatementsInFile("ITDdlTest.sql", SqlScriptVerifier.class, false);
3643
}
44+
45+
@Test
46+
public void testCreateDatabase() {
47+
DatabaseAdminClient client = getTestEnv().getTestHelper().getClient().getDatabaseAdminClient();
48+
String instance = getTestEnv().getTestHelper().getInstanceId().getInstance();
49+
String name = getTestEnv().getTestHelper().getUniqueDatabaseId();
50+
51+
assertThrows(DatabaseNotFoundException.class, () -> client.getDatabase(instance, name));
52+
53+
try (Connection connection = createConnection()) {
54+
connection.execute(Statement.of(String.format("CREATE DATABASE `%s`", name)));
55+
assertNotNull(client.getDatabase(instance, name));
56+
} finally {
57+
client.dropDatabase(instance, name);
58+
}
59+
}
3760
}

0 commit comments

Comments
 (0)