Skip to content
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

feat: support DATABASE statements #1143

Closed
wants to merge 5 commits into from
Closed
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
28 changes: 28 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,32 @@
<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>

<!-- Adds DATABASE statements support -->
<!-- These are not breaking changes, since we provide default interface implementation -->
<difference>
<differenceType>7012</differenceType>
<className>com/google/cloud/spanner/connection/Connection</className>
<method>void alterDatabase(java.lang.String)</method>
</difference>
<difference>
<differenceType>7012</differenceType>
<className>com/google/cloud/spanner/connection/Connection</className>
<method>void createDatabase(java.lang.String)</method>
</difference>
<difference>
<differenceType>7012</differenceType>
<className>com/google/cloud/spanner/connection/Connection</className>
<method>void dropDatabase(java.lang.String)</method>
</difference>
<difference>
<differenceType>7012</differenceType>
<className>com/google/cloud/spanner/connection/Connection</className>
<method>java.lang.Iterable listDatabases()</method>
</difference>
<difference>
<differenceType>7012</differenceType>
<className>com/google/cloud/spanner/connection/Connection</className>
<method>void useDatabase(java.lang.String)</method>
</difference>
</differences>
Original file line number Diff line number Diff line change
Expand Up @@ -457,7 +457,77 @@ private boolean statementStartsWith(String sql, Iterable<String> checkStatements
static final char DOLLAR = '$';

/**
* Removes comments from and trims the given sql statement using the dialect of this parser.
* Removes any (optional) quotes (`) or triple-quotes (```) around the given identifier.
*
* @param identifier the identifier to remove the quotes from
* @return an unquoted identifier that can be used in administrative methods
*/
@InternalApi
public String trimAndUnquoteIdentifier(String identifier) {
Preconditions.checkNotNull(identifier);
String trimmedIdentifier = removeCommentsAndTrim(identifier);
if (trimmedIdentifier.startsWith("```")
&& trimmedIdentifier.length() >= 6
&& trimmedIdentifier.endsWith("```")) {
return trimmedIdentifier.substring(3, trimmedIdentifier.length() - 3);
}
if (trimmedIdentifier.startsWith("`")
&& trimmedIdentifier.length() >= 2
&& trimmedIdentifier.endsWith("`")) {
return trimmedIdentifier.substring(1, trimmedIdentifier.length() - 1);
}
return trimmedIdentifier;
}

/**
* Parses the first token in the given expression as an identifier. The expression may contain
* additional tokens after the identifier. The returned identifier is stripped for any quotes or
* triple-quotes. The method returns the entire expression if the first token could not be parsed
* as an identifier, for example if the expression contains an unclosed literal.
*/
public String parseIdentifier(String expression) {
Preconditions.checkNotNull(expression);
String sql = removeCommentsAndTrim(expression);
boolean tripleQuote = sql.startsWith("```") && sql.length() >= 6;
boolean singleQuote = !tripleQuote && sql.startsWith("`") && sql.length() >= 2;
if (tripleQuote) {
// Find the second triple-quote and return everything in between.
int index = sql.indexOf("```", 3);
if (index > -1) {
return sql.substring(3, index);
}
}
if (!singleQuote) {
// Find the first whitespace character and return everything before it.
for (int index = 1; index < sql.length(); index++) {
if (Character.isWhitespace(sql.charAt(index))) {
return sql.substring(0, index);
}
}
return sql;
}
// Single-quoted identifiers are the 'hardest' as we need to take escaping into account.
for (int index = 1; index < sql.length(); index++) {
if (sql.charAt(index) == '`' && sql.charAt(index - 1) != '\\') {
return sql.substring(1, index);
}
}

return expression;
}

/**
* Removes comments from and trims the given sql statement. Spanner supports three types of
* comments:
*
* <ul>
* <li>Single line comments starting with '--'
* <li>Single line comments starting with '#'
* <li>Multi line comments between '/&#42;' and '&#42;/'
* </ul>
*
* Reference: https://cloud.google.com/spanner/docs/lexical#comments Removes comments from and
* trims the given sql statement using the dialect of this parser.
*
* @param sql The sql statement to remove comments from and to trim.
* @return the sql statement without the comments and leading and trailing spaces.
Expand Down
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,6 +23,8 @@
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.Dialect;
import com.google.cloud.spanner.ErrorCode;
import com.google.cloud.spanner.Mutation;
Expand All @@ -32,6 +34,7 @@
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 @@ -1117,4 +1120,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.Dialect;
import com.google.cloud.spanner.ErrorCode;
import com.google.cloud.spanner.Mutation;
Expand Down Expand Up @@ -64,6 +67,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 @@ -222,10 +227,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 @@ -263,7 +270,7 @@ private DdlClient createDdlClient() {
return DdlClient.newBuilder()
.setDatabaseAdminClient(spanner.getDatabaseAdminClient())
.setInstanceId(options.getInstanceId())
.setDatabaseName(options.getDatabaseName())
.setDatabaseId(options.getDatabaseName())
.build();
}

Expand Down Expand Up @@ -337,6 +344,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 @@ -732,6 +743,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 @@ -768,6 +780,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 @@ -787,6 +800,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 @@ -1134,6 +1148,7 @@ private ApiFuture<long[]> internalExecuteBatchUpdateAsync(
*/
@VisibleForTesting
UnitOfWork getCurrentUnitOfWorkOrStartNewUnitOfWork() {
ConnectionPreconditions.checkState(isConnectedToDatabase(), NOT_CONNECTED_TO_DB);
if (this.currentUnitOfWork == null || !this.currentUnitOfWork.isActive()) {
this.currentUnitOfWork = createNewUnitOfWork();
}
Expand Down Expand Up @@ -1256,18 +1271,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 @@ -1340,4 +1345,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 = getStatementParser().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 = getStatementParser().trimAndUnquoteIdentifier(database);
get(ddlClient.createDatabase(databaseId, Collections.emptyList()));
}

@Override
public void alterDatabase(String databaseStatement) {
checkDdlBatchOrDatabaseStatementAllowed("Cannot alter a database");
String databaseId = getStatementParser().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 = getStatementParser().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));
}
}
Loading