diff --git a/apis/Google.Cloud.Spanner.Data/Google.Cloud.Spanner.Data.IntegrationTests/AdminTests.cs b/apis/Google.Cloud.Spanner.Data/Google.Cloud.Spanner.Data.IntegrationTests/AdminTests.cs index af5608be00f1..521cfc674d54 100644 --- a/apis/Google.Cloud.Spanner.Data/Google.Cloud.Spanner.Data.IntegrationTests/AdminTests.cs +++ b/apis/Google.Cloud.Spanner.Data/Google.Cloud.Spanner.Data.IntegrationTests/AdminTests.cs @@ -12,7 +12,12 @@ // See the License for the specific language governing permissions and // limitations under the License. +using Google.Api.Gax.Grpc; +using Google.Cloud.Spanner.Admin.Database.V1; using Google.Cloud.Spanner.Data.CommonTesting; +using Google.LongRunning; +using Google.Protobuf; +using Google.Protobuf.WellKnownTypes; using System; using System.Threading.Tasks; using Xunit; @@ -252,5 +257,64 @@ AlbumTitle STRING(MAX), await dropCommand.ExecuteNonQueryAsync(); } } + + [Fact] + public async Task StartDdlReturnsOperationName() + { + string dbName = GenerateDatabaseName(); + var builder = new SpannerConnectionStringBuilder(_fixture.Database.NoDbConnectionString); + var connectionOptions = new SpannerClientCreationOptions(builder); + var adminClientBuilder = connectionOptions.CreateDatabaseAdminClientBuilder(); + var adminClient = await adminClientBuilder.BuildAsync(); + var channel = adminClientBuilder.LastCreatedChannel; + + try + { + using (var connection = new SpannerConnection(builder)) + { + var createDbCommand = connection.CreateDdlCommand($"CREATE DATABASE {dbName}"); + var operationName = await createDbCommand.StartDdlAsync(); + Assert.False(string.IsNullOrEmpty(operationName)); + + await HandleLro( + adminClient.CreateDatabaseOperationsClient, operationName); + } + + using (var connection = new SpannerConnection(builder.WithDatabase(dbName))) + { + var createTableCommand = connection.CreateDdlCommand( + "CREATE TABLE Singers (SingerId INT64 PRIMARY KEY, Name STRING(1024))"); + var operationName = await createTableCommand.StartDdlAsync(); + Assert.False(string.IsNullOrEmpty(operationName)); + + await HandleLro( + adminClient.UpdateDatabaseDdlOperationsClient, operationName); + } + + using (var connection = new SpannerConnection(builder)) + { + var dropCommand = connection.CreateDdlCommand($"DROP DATABASE {dbName}"); + var operationName = await dropCommand.StartDdlAsync(); + // DropDatabase does not return a long-running operation. + Assert.Null(operationName); + } + } + finally + { + channel?.Shutdown(); + } + + async Task HandleLro(OperationsClient client, string operationName) + where TResponse : class, IMessage, new() + where TMetadata : class, IMessage, new() + { + var rawOperation = await client.GetOperationAsync(operationName); + var operation = new Operation(rawOperation, client); + var completedOperation = await operation.PollUntilCompletedAsync(); + + Assert.True(completedOperation.IsCompleted); + Assert.Null(completedOperation.Exception); + } + } } } diff --git a/apis/Google.Cloud.Spanner.Data/Google.Cloud.Spanner.Data/SpannerCommand.ExecutableCommand.cs b/apis/Google.Cloud.Spanner.Data/Google.Cloud.Spanner.Data/SpannerCommand.ExecutableCommand.cs index 00ea89c7d8ae..ce03f2edb715 100644 --- a/apis/Google.Cloud.Spanner.Data/Google.Cloud.Spanner.Data/SpannerCommand.ExecutableCommand.cs +++ b/apis/Google.Cloud.Spanner.Data/Google.Cloud.Spanner.Data/SpannerCommand.ExecutableCommand.cs @@ -17,6 +17,7 @@ using Google.Cloud.Spanner.Admin.Database.V1; using Google.Cloud.Spanner.Common.V1; using Google.Cloud.Spanner.V1; +using Google.LongRunning; using Google.Protobuf; using Google.Protobuf.WellKnownTypes; using Grpc.Core; @@ -235,10 +236,30 @@ private async Task ExecuteDmlReaderAsync(CommandBehavior beha } private async Task ExecuteDdlAsync(CancellationToken cancellationToken) + { + await ExecuteDdlAsync(pollUntilCompleted: true, cancellationToken).ConfigureAwait(false); + return 0; + } + + /// + /// Starts a DDL operation, but does not wait for the long-running operation to finish. + /// + /// + /// The name of the long-running operation that was created or null if the DDL statement did not + /// create a long-running operation. + /// + internal async Task StartDdlAsync(CancellationToken cancellationToken) + { + var operation = await ExecuteDdlAsync(pollUntilCompleted: false, cancellationToken).ConfigureAwait(false); + return operation?.Name; + } + + private async Task ExecuteDdlAsync(bool pollUntilCompleted, CancellationToken cancellationToken) { string commandText = CommandTextBuilder.CommandText; var builder = Connection.Builder; var connectionOptions = new SpannerClientCreationOptions(builder); + Operation operation = null; // Create the builder separately from actually building, so we can note the channel that it created. // (This is fairly unpleasant, but we'll try to improve this in the next version of GAX.) @@ -257,12 +278,9 @@ private async Task ExecuteDdlAsync(CancellationToken cancellationToken) ExtraStatements = { CommandTextBuilder.ExtraStatements ?? new string[0] }, ProtoDescriptors = CommandTextBuilder.ProtobufDescriptors?.ToByteString() ?? ByteString.Empty, }; - var response = await databaseAdminClient.CreateDatabaseAsync(request).ConfigureAwait(false); - response = await response.PollUntilCompletedAsync().ConfigureAwait(false); - if (response.IsFaulted) - { - throw SpannerException.FromOperationFailedException(response.Exception); - } + var createDbOperation = await databaseAdminClient.CreateDatabaseAsync(request).ConfigureAwait(false); + var response = await HandleLro(createDbOperation).ConfigureAwait(false); + operation = response.RpcMessage; } else if (CommandTextBuilder.IsDropDatabaseCommand) { @@ -293,13 +311,9 @@ private async Task ExecuteDdlAsync(CancellationToken cancellationToken) Statements = { commandText, CommandTextBuilder.ExtraStatements ?? Enumerable.Empty() }, ProtoDescriptors = CommandTextBuilder.ProtobufDescriptors?.ToByteString() ?? ByteString.Empty, }; - - var response = await databaseAdminClient.UpdateDatabaseDdlAsync(request).ConfigureAwait(false); - response = await response.PollUntilCompletedAsync().ConfigureAwait(false); - if (response.IsFaulted) - { - throw SpannerException.FromOperationFailedException(response.Exception); - } + var updateDdlOperation = await databaseAdminClient.UpdateDatabaseDdlAsync(request).ConfigureAwait(false); + var response = await HandleLro(updateDdlOperation).ConfigureAwait(false); + operation = response.RpcMessage; } } catch (RpcException gRpcException) @@ -312,7 +326,22 @@ private async Task ExecuteDdlAsync(CancellationToken cancellationToken) channel?.Shutdown(); } - return 0; + return operation; + + async Task> HandleLro(Operation operationToPoll) + where TResponse : class, IMessage, new() + where TMetadata : class, IMessage, new() + { + if (pollUntilCompleted) + { + operationToPoll = await operationToPoll.PollUntilCompletedAsync().ConfigureAwait(false); + } + if (operationToPoll.IsFaulted) + { + throw SpannerException.FromOperationFailedException(operationToPoll.Exception); + } + return operationToPoll; + } } private async Task ExecuteMutationsAsync(CancellationToken cancellationToken) diff --git a/apis/Google.Cloud.Spanner.Data/Google.Cloud.Spanner.Data/SpannerCommand.cs b/apis/Google.Cloud.Spanner.Data/Google.Cloud.Spanner.Data/SpannerCommand.cs index e77434f9229e..df86123dafb0 100644 --- a/apis/Google.Cloud.Spanner.Data/Google.Cloud.Spanner.Data/SpannerCommand.cs +++ b/apis/Google.Cloud.Spanner.Data/Google.Cloud.Spanner.Data/SpannerCommand.cs @@ -513,6 +513,22 @@ public long ExecutePartitionedUpdate() => public Task ExecutePartitionedUpdateAsync(CancellationToken cancellationToken = default) => CreateExecutableCommand().ExecutePartitionedUpdateAsync(cancellationToken); + /// + /// Executes this command as DDL, but does not wait for the execution of the DDL statements to finish. The + /// method returns the name of the long-running operation that was started. The cancellation token can only be + /// used to cancel the request to start the execution of the DDL statements. It cannot be used to cancel the + /// long-running operation once it has been started. + /// The command must contain one or more DDL statements; + /// for details. + /// + /// An optional token for canceling the call. + /// + /// The name of the long-running operation that was started for the DDL statement(s). + /// Note: The ID is empty for DropDatabase commands. + /// + public Task StartDdlAsync(CancellationToken cancellationToken = default) => + CreateExecutableCommand().StartDdlAsync(cancellationToken); + /// /// Creates an executable command that captures all the necessary information from this command. ///