Skip to content

Commit

Permalink
feat: support DATABASE statements
Browse files Browse the repository at this point in the history
Adds support for the following DATABASE statements in the Connection API:
- CREATE DATABASE <database_id>
- ALTER DATABASE <database_id>
- DROP DATABASE <database_id>
- USE DATABASE <database_id>

Needed for googleapis/java-spanner-jdbc#457
  • Loading branch information
olavloite committed May 7, 2021
1 parent b3ebfcf commit 49b8321
Show file tree
Hide file tree
Showing 18 changed files with 906 additions and 30 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ class ClientSideStatementImpl implements ClientSideStatement {
* ClientSideSetStatementImpl} that defines how the value is set.
*/
static class ClientSideSetStatementImpl {
/** The keyword for this statement, e.g. SET. */
private String statementKeyword = "SET";
/** The property name that is to be set, e.g. AUTOCOMMIT. */
private String propertyName;
/** The separator between the property and the value (i.e. '=' or '\s+'). */
Expand All @@ -45,6 +47,10 @@ static class ClientSideSetStatementImpl {
/** The class name of the {@link ClientSideStatementValueConverter} to use. */
private String converterName;

String getStatementKeyword() {
return statementKeyword;
}

String getPropertyName() {
return propertyName;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,8 @@ class ClientSideStatementSetExecutor<T> implements ClientSideStatementExecutor {
this.allowedValuesPattern =
Pattern.compile(
String.format(
"(?is)\\A\\s*set\\s+%s\\s*%s\\s*%s\\s*\\z",
"(?is)\\A\\s*%s\\s+%s\\s*%s\\s*%s\\s*\\z",
statement.getSetStatement().getStatementKeyword(),
statement.getSetStatement().getPropertyName(),
statement.getSetStatement().getSeparator(),
statement.getSetStatement().getAllowedValues()));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,16 @@
import com.google.cloud.spanner.AbortedException;
import com.google.cloud.spanner.AsyncResultSet;
import com.google.cloud.spanner.CommitResponse;
import com.google.cloud.spanner.Database;
import com.google.cloud.spanner.DatabaseNotFoundException;
import com.google.cloud.spanner.ErrorCode;
import com.google.cloud.spanner.Mutation;
import com.google.cloud.spanner.Options.QueryOption;
import com.google.cloud.spanner.ReadContext.QueryAnalyzeMode;
import com.google.cloud.spanner.ResultSet;
import com.google.cloud.spanner.SpannerBatchUpdateException;
import com.google.cloud.spanner.SpannerException;
import com.google.cloud.spanner.SpannerExceptionFactory;
import com.google.cloud.spanner.Statement;
import com.google.cloud.spanner.TimestampBound;
import com.google.cloud.spanner.connection.StatementResult.ResultType;
Expand Down Expand Up @@ -1009,4 +1012,59 @@ final class InternalMetadataQuery implements QueryOption {

private InternalMetadataQuery() {}
}

// DATABASE statements.

/**
* Lists the databases on the instance that the connection is connected to.
*
* @return the databases on the instance that the connection is connected to
*/
default Iterable<Database> listDatabases() {
throw SpannerExceptionFactory.newSpannerException(
ErrorCode.UNIMPLEMENTED, "SHOW VARIABLE DATABASES is not implemented");
}

/**
* Changes the database that this connection is connected to.
*
* @param database The name of the database to connect to
* @throws DatabaseNotFoundException if the specified database does not exists on the instance
* that the connection is connected to
*/
default void useDatabase(String database) {
throw SpannerExceptionFactory.newSpannerException(
ErrorCode.UNIMPLEMENTED, "USE DATABASE is not implemented");
}

/**
* Creates a new database on the instance that this connection is connected to.
*
* @param database the name of the database that is to be created
*/
default void createDatabase(String database) {
throw SpannerExceptionFactory.newSpannerException(
ErrorCode.UNIMPLEMENTED, "CREATE DATABASE is not implemented");
}

/**
* Alters an existing database on the instance that this connection is connected to.
*
* @param databaseStatement the name of the database that is to be altered, followed by the
* altered options
*/
default void alterDatabase(String databaseStatement) {
throw SpannerExceptionFactory.newSpannerException(
ErrorCode.UNIMPLEMENTED, "ALTER DATABASE is not implemented");
}

/**
* Drops an existing database on the instance that this connection is connected to.
*
* @param database the name of the database that is to be dropped
*/
default void dropDatabase(String database) {
throw SpannerExceptionFactory.newSpannerException(
ErrorCode.UNIMPLEMENTED, "DROP DATABASE is not implemented");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,15 @@

import static com.google.cloud.spanner.SpannerApiFutures.get;

import com.google.api.client.util.Strings;
import com.google.api.core.ApiFuture;
import com.google.api.core.ApiFutures;
import com.google.cloud.Timestamp;
import com.google.cloud.spanner.AsyncResultSet;
import com.google.cloud.spanner.CommitResponse;
import com.google.cloud.spanner.Database;
import com.google.cloud.spanner.DatabaseClient;
import com.google.cloud.spanner.DatabaseId;
import com.google.cloud.spanner.ErrorCode;
import com.google.cloud.spanner.Mutation;
import com.google.cloud.spanner.Options.QueryOption;
Expand Down Expand Up @@ -59,6 +62,8 @@
/** Implementation for {@link Connection}, the generic Spanner connection API (not JDBC). */
class ConnectionImpl implements Connection {
private static final String CLOSED_ERROR_MSG = "This connection is closed";
private static final String NOT_CONNECTED_TO_DB =
"This connection is not connected to a database.";
private static final String ONLY_ALLOWED_IN_AUTOCOMMIT =
"This method may only be called while in autocommit mode";
private static final String NOT_ALLOWED_IN_AUTOCOMMIT =
Expand Down Expand Up @@ -213,10 +218,12 @@ static UnitOfWorkType of(TransactionMode transactionMode) {
this.spannerPool = SpannerPool.INSTANCE;
this.options = options;
this.spanner = spannerPool.getSpanner(options, this);
if (options.isAutoConfigEmulator()) {
EmulatorUtil.maybeCreateInstanceAndDatabase(spanner, options.getDatabaseId());
if (!Strings.isNullOrEmpty(options.getDatabaseName())) {
if (options.isAutoConfigEmulator()) {
EmulatorUtil.maybeCreateInstanceAndDatabase(spanner, options.getDatabaseId());
}
this.dbClient = spanner.getDatabaseClient(options.getDatabaseId());
}
this.dbClient = spanner.getDatabaseClient(options.getDatabaseId());
this.retryAbortsInternally = options.isRetryAbortsInternally();
this.readOnly = options.isReadOnly();
this.autocommit = options.isAutocommit();
Expand Down Expand Up @@ -253,7 +260,7 @@ private DdlClient createDdlClient() {
return DdlClient.newBuilder()
.setDatabaseAdminClient(spanner.getDatabaseAdminClient())
.setInstanceId(options.getInstanceId())
.setDatabaseName(options.getDatabaseName())
.setDatabaseId(options.getDatabaseName())
.build();
}

Expand Down Expand Up @@ -315,6 +322,10 @@ public boolean isClosed() {
return closed;
}

boolean isConnectedToDatabase() {
return dbClient != null;
}

@Override
public void setAutocommit(boolean autocommit) {
ConnectionPreconditions.checkState(!isClosed(), CLOSED_ERROR_MSG);
Expand Down Expand Up @@ -642,6 +653,7 @@ public void beginTransaction() {
@Override
public ApiFuture<Void> beginTransactionAsync() {
ConnectionPreconditions.checkState(!isClosed(), CLOSED_ERROR_MSG);
ConnectionPreconditions.checkState(isConnectedToDatabase(), NOT_CONNECTED_TO_DB);
ConnectionPreconditions.checkState(
!isBatchActive(), "This connection has an active batch and cannot begin a transaction");
ConnectionPreconditions.checkState(
Expand Down Expand Up @@ -678,6 +690,7 @@ public void commit() {

public ApiFuture<Void> commitAsync() {
ConnectionPreconditions.checkState(!isClosed(), CLOSED_ERROR_MSG);
ConnectionPreconditions.checkState(isConnectedToDatabase(), NOT_CONNECTED_TO_DB);
return endCurrentTransactionAsync(commit);
}

Expand All @@ -697,6 +710,7 @@ public void rollback() {

public ApiFuture<Void> rollbackAsync() {
ConnectionPreconditions.checkState(!isClosed(), CLOSED_ERROR_MSG);
ConnectionPreconditions.checkState(isConnectedToDatabase(), NOT_CONNECTED_TO_DB);
return endCurrentTransactionAsync(rollback);
}

Expand Down Expand Up @@ -981,6 +995,7 @@ private ApiFuture<long[]> internalExecuteBatchUpdateAsync(List<ParsedStatement>
*/
@VisibleForTesting
UnitOfWork getCurrentUnitOfWorkOrStartNewUnitOfWork() {
ConnectionPreconditions.checkState(isConnectedToDatabase(), NOT_CONNECTED_TO_DB);
if (this.currentUnitOfWork == null || !this.currentUnitOfWork.isActive()) {
this.currentUnitOfWork = createNewUnitOfWork();
}
Expand Down Expand Up @@ -1096,18 +1111,8 @@ public void bufferedWrite(Iterable<Mutation> mutations) {

@Override
public void startBatchDdl() {
ConnectionPreconditions.checkState(!isClosed(), CLOSED_ERROR_MSG);
ConnectionPreconditions.checkState(
!isBatchActive(), "Cannot start a DDL batch when a batch is already active");
ConnectionPreconditions.checkState(
!isReadOnly(), "Cannot start a DDL batch when the connection is in read-only mode");
ConnectionPreconditions.checkState(
!isTransactionStarted(), "Cannot start a DDL batch while a transaction is active");
ConnectionPreconditions.checkState(
!(isAutocommit() && isInTransaction()),
"Cannot start a DDL batch while in a temporary transaction");
ConnectionPreconditions.checkState(
!transactionBeginMarked, "Cannot start a DDL batch when a transaction has begun");
ConnectionPreconditions.checkState(isConnectedToDatabase(), NOT_CONNECTED_TO_DB);
checkDdlBatchOrDatabaseStatementAllowed("Cannot start a DDL batch");
this.batchMode = BatchMode.DDL;
this.unitOfWorkType = UnitOfWorkType.DDL_BATCH;
this.currentUnitOfWork = createNewUnitOfWork();
Expand Down Expand Up @@ -1180,4 +1185,62 @@ public boolean isDmlBatchActive() {
ConnectionPreconditions.checkState(!isClosed(), CLOSED_ERROR_MSG);
return this.batchMode == BatchMode.DML;
}

@Override
public Iterable<Database> listDatabases() {
ConnectionPreconditions.checkState(!isClosed(), CLOSED_ERROR_MSG);
return ddlClient.listDatabases();
}

@Override
public void useDatabase(String database) {
checkDdlBatchOrDatabaseStatementAllowed("Cannot change database");
String databaseId = StatementParser.trimAndUnquoteIdentifier(database);
// Check that the database actually exists before we try to change.
ddlClient.getDatabase(databaseId);
// Get a new database client.
this.dbClient =
spanner.getDatabaseClient(
DatabaseId.of(options.getProjectId(), options.getInstanceId(), databaseId));
this.ddlClient.setDefaultDatabaseId(databaseId);
}

@Override
public void createDatabase(String database) {
checkDdlBatchOrDatabaseStatementAllowed("Cannot create a database");
String databaseId = StatementParser.trimAndUnquoteIdentifier(database);
get(ddlClient.createDatabase(databaseId, Collections.emptyList()));
}

@Override
public void alterDatabase(String databaseStatement) {
checkDdlBatchOrDatabaseStatementAllowed("Cannot alter a database");
String databaseId = StatementParser.parseIdentifier(databaseStatement);
get(
ddlClient.executeDdl(
databaseId,
Collections.singletonList(String.format("ALTER DATABASE %s", databaseStatement))));
}

@Override
public void dropDatabase(String database) {
checkDdlBatchOrDatabaseStatementAllowed("Cannot drop a database");
String databaseId = StatementParser.trimAndUnquoteIdentifier(database);
ddlClient.dropDatabase(databaseId);
}

private void checkDdlBatchOrDatabaseStatementAllowed(String prefix) {
ConnectionPreconditions.checkState(!isClosed(), CLOSED_ERROR_MSG);
ConnectionPreconditions.checkState(
!isBatchActive(), String.format("%s when a batch is active", prefix));
ConnectionPreconditions.checkState(
!isReadOnly(), String.format("%s when the connection is in read-only mode", prefix));
ConnectionPreconditions.checkState(
!isTransactionStarted(), String.format("%s while a transaction is active", prefix));
ConnectionPreconditions.checkState(
!(isAutocommit() && isInTransaction()),
String.format("%s while in a temporary transaction", prefix));
ConnectionPreconditions.checkState(
!transactionBeginMarked, String.format("%s when a transaction has begun", prefix));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -85,4 +85,14 @@ interface ConnectionStatementExecutor {
StatementResult statementRunBatch();

StatementResult statementAbortBatch();

StatementResult statementShowDatabases();

StatementResult statementUseDatabase(String database);

StatementResult statementCreateDatabase(String database);

StatementResult statementAlterDatabase(String database);

StatementResult statementDropDatabase(String database);
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,11 @@
package com.google.cloud.spanner.connection;

import static com.google.cloud.spanner.connection.StatementResult.ClientSideStatementType.ABORT_BATCH;
import static com.google.cloud.spanner.connection.StatementResult.ClientSideStatementType.ALTER_DATABASE;
import static com.google.cloud.spanner.connection.StatementResult.ClientSideStatementType.BEGIN;
import static com.google.cloud.spanner.connection.StatementResult.ClientSideStatementType.COMMIT;
import static com.google.cloud.spanner.connection.StatementResult.ClientSideStatementType.CREATE_DATABASE;
import static com.google.cloud.spanner.connection.StatementResult.ClientSideStatementType.DROP_DATABASE;
import static com.google.cloud.spanner.connection.StatementResult.ClientSideStatementType.ROLLBACK;
import static com.google.cloud.spanner.connection.StatementResult.ClientSideStatementType.RUN_BATCH;
import static com.google.cloud.spanner.connection.StatementResult.ClientSideStatementType.SET_AUTOCOMMIT;
Expand All @@ -34,6 +37,7 @@
import static com.google.cloud.spanner.connection.StatementResult.ClientSideStatementType.SHOW_AUTOCOMMIT_DML_MODE;
import static com.google.cloud.spanner.connection.StatementResult.ClientSideStatementType.SHOW_COMMIT_RESPONSE;
import static com.google.cloud.spanner.connection.StatementResult.ClientSideStatementType.SHOW_COMMIT_TIMESTAMP;
import static com.google.cloud.spanner.connection.StatementResult.ClientSideStatementType.SHOW_DATABASES;
import static com.google.cloud.spanner.connection.StatementResult.ClientSideStatementType.SHOW_OPTIMIZER_VERSION;
import static com.google.cloud.spanner.connection.StatementResult.ClientSideStatementType.SHOW_READONLY;
import static com.google.cloud.spanner.connection.StatementResult.ClientSideStatementType.SHOW_READ_ONLY_STALENESS;
Expand All @@ -43,11 +47,13 @@
import static com.google.cloud.spanner.connection.StatementResult.ClientSideStatementType.SHOW_STATEMENT_TIMEOUT;
import static com.google.cloud.spanner.connection.StatementResult.ClientSideStatementType.START_BATCH_DDL;
import static com.google.cloud.spanner.connection.StatementResult.ClientSideStatementType.START_BATCH_DML;
import static com.google.cloud.spanner.connection.StatementResult.ClientSideStatementType.USE_DATABASE;
import static com.google.cloud.spanner.connection.StatementResultImpl.noResult;
import static com.google.cloud.spanner.connection.StatementResultImpl.resultSet;

import com.google.cloud.spanner.CommitResponse;
import com.google.cloud.spanner.CommitStats;
import com.google.cloud.spanner.Database;
import com.google.cloud.spanner.ResultSet;
import com.google.cloud.spanner.ResultSets;
import com.google.cloud.spanner.Struct;
Expand All @@ -56,6 +62,7 @@
import com.google.cloud.spanner.Type.StructField;
import com.google.cloud.spanner.connection.ReadOnlyStalenessUtil.DurationValueGetter;
import com.google.common.base.Preconditions;
import com.google.common.collect.Iterables;
import com.google.protobuf.Duration;
import java.util.Arrays;
import java.util.concurrent.TimeUnit;
Expand Down Expand Up @@ -289,4 +296,57 @@ public StatementResult statementAbortBatch() {
getConnection().abortBatch();
return noResult(ABORT_BATCH);
}

@Override
public StatementResult statementShowDatabases() {
Iterable<Database> databases = getConnection().listDatabases();
ResultSet resultSet =
ResultSets.forRows(
Type.struct(
StructField.of("NAME", Type.string()),
StructField.of("CREATE_TIME", Type.timestamp()),
StructField.of("VERSION_RETENTION_PERIOD", Type.string()),
StructField.of("EARLIEST_VERSION_TIME", Type.timestamp()),
StructField.of("STATE", Type.string())),
Iterables.transform(
databases,
database ->
Struct.newBuilder()
.set("NAME")
.to(database.getId().getDatabase())
.set("CREATE_TIME")
.to(database.getCreateTime())
.set("VERSION_RETENTION_PERIOD")
.to(database.getVersionRetentionPeriod())
.set("EARLIEST_VERSION_TIME")
.to(database.getEarliestVersionTime())
.set("STATE")
.to(database.getState().toString())
.build()));
return StatementResultImpl.of(resultSet, SHOW_DATABASES);
}

@Override
public StatementResult statementUseDatabase(String database) {
getConnection().useDatabase(database);
return noResult(USE_DATABASE);
}

@Override
public StatementResult statementCreateDatabase(String database) {
getConnection().createDatabase(database);
return noResult(CREATE_DATABASE);
}

@Override
public StatementResult statementAlterDatabase(String database) {
getConnection().alterDatabase(database);
return noResult(ALTER_DATABASE);
}

@Override
public StatementResult statementDropDatabase(String database) {
getConnection().dropDatabase(database);
return noResult(DROP_DATABASE);
}
}
Loading

0 comments on commit 49b8321

Please sign in to comment.