Skip to content

feat: support CREATE DATABASE in Connection API #1845

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 3 commits into from
Apr 28, 2022
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
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,13 +56,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.23.2'
implementation 'com.google.cloud:google-cloud-spanner:6.23.3'
```

If you are using SBT, add this to your dependencies

```Scala
libraryDependencies += "com.google.cloud" % "google-cloud-spanner" % "6.23.2"
libraryDependencies += "com.google.cloud" % "google-cloud-spanner" % "6.23.3"
```

## Authentication
Expand Down
6 changes: 6 additions & 0 deletions google-cloud-spanner/clirr-ignored-differences.xml
Original file line number Diff line number Diff line change
Expand Up @@ -60,4 +60,10 @@
<className>com/google/cloud/spanner/spi/v1/SpannerRpc</className>
<method>com.google.api.gax.longrunning.OperationFuture copyBackup(com.google.cloud.spanner.BackupId, com.google.cloud.spanner.Backup)</method>
</difference>
<difference>
<differenceType>7012</differenceType>
<className>com/google/cloud/spanner/DatabaseAdminClient</className>
<method>com.google.api.gax.longrunning.OperationFuture createDatabase(java.lang.String, java.lang.String, com.google.cloud.spanner.Dialect, java.lang.Iterable)</method>
</difference>

</differences>
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,39 @@ public interface DatabaseAdminClient {
OperationFuture<Database, CreateDatabaseMetadata> createDatabase(
String instanceId, String databaseId, Iterable<String> statements) throws SpannerException;

/**
* Creates a new database in a Cloud Spanner instance with the given {@link Dialect}.
*
* <p>Example to create database.
*
* <pre>{@code
* String instanceId = "my_instance_id";
* String createDatabaseStatement = "CREATE DATABASE \"my-database\"";
* Operation<Database, CreateDatabaseMetadata> op = dbAdminClient
* .createDatabase(
* instanceId,
* createDatabaseStatement,
* Dialect.POSTGRESQL
* Collections.emptyList());
* Database db = op.waitFor().getResult();
* }</pre>
*
* @param instanceId the id of the instance in which to create the database.
* @param createDatabaseStatement the CREATE DATABASE statement for the database. This statement
* must use the dialect for the new database.
* @param dialect the dialect that the new database should use.
* @param statements DDL statements to run while creating the database, for example {@code CREATE
* TABLE MyTable ( ... )}. This should not include {@code CREATE DATABASE} statement.
*/
default OperationFuture<Database, CreateDatabaseMetadata> createDatabase(
String instanceId,
String createDatabaseStatement,
Dialect dialect,
Iterable<String> statements)
throws SpannerException {
throw new UnsupportedOperationException("Unimplemented");
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We seem to be throwing SpannerException elsewhere with createDatabase in the interface. Why is it an UnsupportedOperationException("Unimplemented"); for this one?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is only to make this a non-breaking change. We add this when we add a new method to a public interface. This prevents compile errors for downstream users that might implement this interface in a test, as the method has a default implementation. See for example this example from the DatabaseClient where we did the same thing:

throw new UnsupportedOperationException("method should be overwritten");


/**
* Creates a database in a Cloud Spanner instance. Any configuration options in the {@link
* Database} instance will be included in the {@link CreateDatabaseRequest}.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -332,6 +332,26 @@ public OperationFuture<Database, CreateDatabaseMetadata> createDatabase(
final Dialect dialect = Preconditions.checkNotNull(database.getDialect());
final String createStatement =
dialect.createDatabaseStatementFor(database.getId().getDatabase());

return createDatabase(createStatement, database, statements);
}

@Override
public OperationFuture<Database, CreateDatabaseMetadata> createDatabase(
String instanceId,
String createDatabaseStatement,
Dialect dialect,
Iterable<String> statements)
throws SpannerException {
Database database =
newDatabaseBuilder(DatabaseId.of(projectId, instanceId, "")).setDialect(dialect).build();

return createDatabase(createDatabaseStatement, database, statements);
}

private OperationFuture<Database, CreateDatabaseMetadata> createDatabase(
String createStatement, Database database, Iterable<String> statements)
throws SpannerException {
OperationFuture<com.google.spanner.admin.database.v1.Database, CreateDatabaseMetadata>
rawOperationFuture =
rpc.createDatabase(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,9 @@ public ApiFuture<Void> executeDdlAsync(ParsedStatement ddl) {
"Only DDL statements are allowed. \""
+ ddl.getSqlWithoutComments()
+ "\" is not a DDL-statement.");
Preconditions.checkArgument(
!DdlClient.isCreateDatabaseStatement(ddl.getSqlWithoutComments()),
"CREATE DATABASE is not supported in DDL batches.");
statements.add(ddl.getSqlWithoutComments());
return ApiFutures.immediateFuture(null);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,14 @@
package com.google.cloud.spanner.connection;

import com.google.api.gax.longrunning.OperationFuture;
import com.google.cloud.spanner.Database;
import com.google.cloud.spanner.DatabaseAdminClient;
import com.google.cloud.spanner.Dialect;
import com.google.cloud.spanner.ErrorCode;
import com.google.cloud.spanner.SpannerExceptionFactory;
import com.google.common.base.Preconditions;
import com.google.common.base.Strings;
import com.google.spanner.admin.database.v1.CreateDatabaseMetadata;
import com.google.spanner.admin.database.v1.UpdateDatabaseDdlMetadata;
import java.util.Collections;
import java.util.List;
Expand Down Expand Up @@ -79,13 +84,32 @@ private DdlClient(Builder builder) {
this.databaseName = builder.databaseName;
}

OperationFuture<Database, CreateDatabaseMetadata> executeCreateDatabase(
String createStatement, Dialect dialect) {
Preconditions.checkArgument(isCreateDatabaseStatement(createStatement));
return dbAdminClient.createDatabase(
instanceId, createStatement, dialect, Collections.emptyList());
}

/** Execute a single DDL statement. */
OperationFuture<Void, UpdateDatabaseDdlMetadata> executeDdl(String ddl) {
return executeDdl(Collections.singletonList(ddl));
}

/** Execute a list of DDL statements as one operation. */
OperationFuture<Void, UpdateDatabaseDdlMetadata> executeDdl(List<String> statements) {
if (statements.stream().anyMatch(DdlClient::isCreateDatabaseStatement)) {
throw SpannerExceptionFactory.newSpannerException(
ErrorCode.INVALID_ARGUMENT, "CREATE DATABASE is not supported in a DDL batch");
}
return dbAdminClient.updateDatabaseDdl(instanceId, databaseName, statements, null);
}

/** Returns true if the statement is a `CREATE DATABASE ...` statement. */
static boolean isCreateDatabaseStatement(String statement) {
String[] tokens = statement.split("\\s+", 3);
return tokens.length >= 2
&& tokens[0].equalsIgnoreCase("CREATE")
&& tokens[1].equalsIgnoreCase("DATABASE");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,6 @@
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;
import com.google.spanner.admin.database.v1.DatabaseAdminGrpc;
import com.google.spanner.admin.database.v1.UpdateDatabaseDdlMetadata;
import com.google.spanner.v1.SpannerGrpc;
import java.util.concurrent.Callable;

Expand Down Expand Up @@ -270,11 +269,17 @@ public ApiFuture<Void> executeDdlAsync(final ParsedStatement ddl) {
Callable<Void> callable =
() -> {
try {
OperationFuture<Void, UpdateDatabaseDdlMetadata> operation =
ddlClient.executeDdl(ddl.getSqlWithoutComments());
Void res = getWithStatementTimeout(operation, ddl);
OperationFuture<?, ?> operation;
if (DdlClient.isCreateDatabaseStatement(ddl.getSqlWithoutComments())) {
operation =
ddlClient.executeCreateDatabase(
ddl.getSqlWithoutComments(), dbClient.getDialect());
} else {
operation = ddlClient.executeDdl(ddl.getSqlWithoutComments());
}
getWithStatementTimeout(operation, ddl);
state = UnitOfWorkState.COMMITTED;
return res;
return null;
} catch (Throwable t) {
state = UnitOfWorkState.COMMIT_FAILED;
throw t;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import static org.hamcrest.MatcherAssert.assertThat;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.fail;
import static org.mockito.Mockito.anyList;
import static org.mockito.Mockito.anyString;
Expand Down Expand Up @@ -143,6 +144,17 @@ public void testExecuteQuery() {
}
}

@Test
public void testExecuteCreateDatabase() {
DdlBatch batch = createSubject();
assertThrows(
IllegalArgumentException.class,
() ->
batch.executeDdlAsync(
AbstractStatementParser.getInstance(Dialect.GOOGLE_STANDARD_SQL)
.parse(Statement.of("CREATE DATABASE foo"))));
}

@Test
public void testExecuteMetadataQuery() {
Statement statement = Statement.of("SELECT * FROM INFORMATION_SCHEMA.TABLES");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@

package com.google.cloud.spanner.connection;

import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.anyList;
import static org.mockito.Mockito.eq;
import static org.mockito.Mockito.isNull;
Expand Down Expand Up @@ -66,4 +68,23 @@ public void testExecuteDdl() throws InterruptedException, ExecutionException {
subject.executeDdl(ddlList);
verify(client).updateDatabaseDdl(instanceId, databaseId, ddlList, null);
}

@Test
public void testIsCreateDatabase() {
assertTrue(DdlClient.isCreateDatabaseStatement("CREATE DATABASE foo"));
assertTrue(DdlClient.isCreateDatabaseStatement("CREATE DATABASE \"foo\""));
assertTrue(DdlClient.isCreateDatabaseStatement("CREATE DATABASE `foo`"));
assertTrue(DdlClient.isCreateDatabaseStatement("CREATE DATABASE\tfoo"));
assertTrue(DdlClient.isCreateDatabaseStatement("CREATE DATABASE\n foo"));
assertTrue(DdlClient.isCreateDatabaseStatement("CREATE DATABASE\t\n foo"));
assertTrue(DdlClient.isCreateDatabaseStatement("CREATE DATABASE"));
assertTrue(DdlClient.isCreateDatabaseStatement("CREATE\t \n DATABASE foo"));
assertTrue(DdlClient.isCreateDatabaseStatement("create\t \n DATABASE foo"));
assertTrue(DdlClient.isCreateDatabaseStatement("create database foo"));

assertFalse(DdlClient.isCreateDatabaseStatement("CREATE VIEW foo"));
assertFalse(DdlClient.isCreateDatabaseStatement("CREATE DATABAS foo"));
assertFalse(DdlClient.isCreateDatabaseStatement("CREATE DATABASEfoo"));
assertFalse(DdlClient.isCreateDatabaseStatement("CREATE foo"));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
import com.google.cloud.spanner.AsyncResultSet;
import com.google.cloud.spanner.CommitResponse;
import com.google.cloud.spanner.DatabaseClient;
import com.google.cloud.spanner.Dialect;
import com.google.cloud.spanner.ErrorCode;
import com.google.cloud.spanner.Key;
import com.google.cloud.spanner.KeySet;
Expand Down Expand Up @@ -365,6 +366,7 @@ private SingleUseTransaction createSubject(
DatabaseClient dbClient = mock(DatabaseClient.class);
com.google.cloud.spanner.ReadOnlyTransaction singleUse =
new SimpleReadOnlyTransaction(staleness);
when(dbClient.getDialect()).thenReturn(Dialect.GOOGLE_STANDARD_SQL);
when(dbClient.singleUseReadOnlyTransaction(staleness)).thenReturn(singleUse);

final TransactionContext txContext = mock(TransactionContext.class);
Expand Down Expand Up @@ -537,6 +539,19 @@ public void testExecuteDdl() {
verify(ddlClient).executeDdl(sql);
}

@Test
public void testExecuteCreateDatabase() {
String sql = "CREATE DATABASE FOO";
ParsedStatement ddl = createParsedDdl(sql);
DdlClient ddlClient = createDefaultMockDdlClient();
when(ddlClient.executeCreateDatabase(sql, Dialect.GOOGLE_STANDARD_SQL))
.thenReturn(mock(OperationFuture.class));

SingleUseTransaction singleUseTransaction = createDdlSubject(ddlClient);
get(singleUseTransaction.executeDdlAsync(ddl));
verify(ddlClient).executeCreateDatabase(sql, Dialect.GOOGLE_STANDARD_SQL);
}

@Test
public void testExecuteQuery() {
for (TimestampBound staleness : getTestTimestampBounds()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,14 @@

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

import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertThrows;

import com.google.cloud.spanner.DatabaseAdminClient;
import com.google.cloud.spanner.DatabaseNotFoundException;
import com.google.cloud.spanner.ParallelIntegrationTest;
import com.google.cloud.spanner.Statement;
import com.google.cloud.spanner.connection.Connection;
import com.google.cloud.spanner.connection.ITAbstractSpannerTest;
import com.google.cloud.spanner.connection.SqlScriptVerifier;
import org.junit.Test;
Expand All @@ -34,4 +41,20 @@ public void testSqlScript() throws Exception {
SqlScriptVerifier verifier = new SqlScriptVerifier(new ITConnectionProvider());
verifier.verifyStatementsInFile("ITDdlTest.sql", SqlScriptVerifier.class, false);
}

@Test
public void testCreateDatabase() {
DatabaseAdminClient client = getTestEnv().getTestHelper().getClient().getDatabaseAdminClient();
String instance = getTestEnv().getTestHelper().getInstanceId().getInstance();
String name = getTestEnv().getTestHelper().getUniqueDatabaseId();

assertThrows(DatabaseNotFoundException.class, () -> client.getDatabase(instance, name));

try (Connection connection = createConnection()) {
connection.execute(Statement.of(String.format("CREATE DATABASE `%s`", name)));
assertNotNull(client.getDatabase(instance, name));
} finally {
client.dropDatabase(instance, name);
}
}
}