diff --git a/README.md b/README.md index 8f78233506a..473c27dff26 100644 --- a/README.md +++ b/README.md @@ -265,10 +265,15 @@ Samples are in the [`samples/`](https://github.com/googleapis/java-spanner/tree/ | Get Instance Config Sample | [source code](https://github.com/googleapis/java-spanner/blob/main/samples/snippets/src/main/java/com/example/spanner/GetInstanceConfigSample.java) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/java-spanner&page=editor&open_in_editor=samples/snippets/src/main/java/com/example/spanner/GetInstanceConfigSample.java) | | List Databases Sample | [source code](https://github.com/googleapis/java-spanner/blob/main/samples/snippets/src/main/java/com/example/spanner/ListDatabasesSample.java) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/java-spanner&page=editor&open_in_editor=samples/snippets/src/main/java/com/example/spanner/ListDatabasesSample.java) | | List Instance Configs Sample | [source code](https://github.com/googleapis/java-spanner/blob/main/samples/snippets/src/main/java/com/example/spanner/ListInstanceConfigsSample.java) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/java-spanner&page=editor&open_in_editor=samples/snippets/src/main/java/com/example/spanner/ListInstanceConfigsSample.java) | +| Pg Async Query To List Async Example | [source code](https://github.com/googleapis/java-spanner/blob/main/samples/snippets/src/main/java/com/example/spanner/PgAsyncQueryToListAsyncExample.java) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/java-spanner&page=editor&open_in_editor=samples/snippets/src/main/java/com/example/spanner/PgAsyncQueryToListAsyncExample.java) | +| Pg Async Runner Example | [source code](https://github.com/googleapis/java-spanner/blob/main/samples/snippets/src/main/java/com/example/spanner/PgAsyncRunnerExample.java) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/java-spanner&page=editor&open_in_editor=samples/snippets/src/main/java/com/example/spanner/PgAsyncRunnerExample.java) | +| Pg Async Transaction Manager Example | [source code](https://github.com/googleapis/java-spanner/blob/main/samples/snippets/src/main/java/com/example/spanner/PgAsyncTransactionManagerExample.java) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/java-spanner&page=editor&open_in_editor=samples/snippets/src/main/java/com/example/spanner/PgAsyncTransactionManagerExample.java) | | Pg Batch Dml Sample | [source code](https://github.com/googleapis/java-spanner/blob/main/samples/snippets/src/main/java/com/example/spanner/PgBatchDmlSample.java) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/java-spanner&page=editor&open_in_editor=samples/snippets/src/main/java/com/example/spanner/PgBatchDmlSample.java) | | Pg Case Sensitivity Sample | [source code](https://github.com/googleapis/java-spanner/blob/main/samples/snippets/src/main/java/com/example/spanner/PgCaseSensitivitySample.java) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/java-spanner&page=editor&open_in_editor=samples/snippets/src/main/java/com/example/spanner/PgCaseSensitivitySample.java) | | Pg Interleaved Table Sample | [source code](https://github.com/googleapis/java-spanner/blob/main/samples/snippets/src/main/java/com/example/spanner/PgInterleavedTableSample.java) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/java-spanner&page=editor&open_in_editor=samples/snippets/src/main/java/com/example/spanner/PgInterleavedTableSample.java) | | Pg Partitioned Dml Sample | [source code](https://github.com/googleapis/java-spanner/blob/main/samples/snippets/src/main/java/com/example/spanner/PgPartitionedDmlSample.java) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/java-spanner&page=editor&open_in_editor=samples/snippets/src/main/java/com/example/spanner/PgPartitionedDmlSample.java) | +| Pg Query With Numeric Parameter Sample | [source code](https://github.com/googleapis/java-spanner/blob/main/samples/snippets/src/main/java/com/example/spanner/PgQueryWithNumericParameterSample.java) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/java-spanner&page=editor&open_in_editor=samples/snippets/src/main/java/com/example/spanner/PgQueryWithNumericParameterSample.java) | +| Pg Spanner Sample | [source code](https://github.com/googleapis/java-spanner/blob/main/samples/snippets/src/main/java/com/example/spanner/PgSpannerSample.java) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/java-spanner&page=editor&open_in_editor=samples/snippets/src/main/java/com/example/spanner/PgSpannerSample.java) | | Query Information Schema Database Options Sample | [source code](https://github.com/googleapis/java-spanner/blob/main/samples/snippets/src/main/java/com/example/spanner/QueryInformationSchemaDatabaseOptionsSample.java) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/java-spanner&page=editor&open_in_editor=samples/snippets/src/main/java/com/example/spanner/QueryInformationSchemaDatabaseOptionsSample.java) | | Query With Json Parameter Sample | [source code](https://github.com/googleapis/java-spanner/blob/main/samples/snippets/src/main/java/com/example/spanner/QueryWithJsonParameterSample.java) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/java-spanner&page=editor&open_in_editor=samples/snippets/src/main/java/com/example/spanner/QueryWithJsonParameterSample.java) | | Query With Numeric Parameter Sample | [source code](https://github.com/googleapis/java-spanner/blob/main/samples/snippets/src/main/java/com/example/spanner/QueryWithNumericParameterSample.java) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/java-spanner&page=editor&open_in_editor=samples/snippets/src/main/java/com/example/spanner/QueryWithNumericParameterSample.java) | diff --git a/samples/snippets/src/main/java/com/example/spanner/AsyncQueryToListAsyncExample.java b/samples/snippets/src/main/java/com/example/spanner/AsyncQueryToListAsyncExample.java index 57082ba3e05..11da6a13fdd 100644 --- a/samples/snippets/src/main/java/com/example/spanner/AsyncQueryToListAsyncExample.java +++ b/samples/snippets/src/main/java/com/example/spanner/AsyncQueryToListAsyncExample.java @@ -31,6 +31,9 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; +/** + * Example code for using Async query on Cloud Spanner and convert it to list. + */ class AsyncQueryToListAsyncExample { static class Album { final long singerId; diff --git a/samples/snippets/src/main/java/com/example/spanner/CustomTimeoutAndRetrySettingsExample.java b/samples/snippets/src/main/java/com/example/spanner/CustomTimeoutAndRetrySettingsExample.java index e3e51875141..7a4a806fdee 100644 --- a/samples/snippets/src/main/java/com/example/spanner/CustomTimeoutAndRetrySettingsExample.java +++ b/samples/snippets/src/main/java/com/example/spanner/CustomTimeoutAndRetrySettingsExample.java @@ -80,7 +80,7 @@ static void executeSqlWithCustomTimeoutAndRetrySettings( .readWriteTransaction() .run(transaction -> { String sql = - "INSERT Singers (SingerId, FirstName, LastName)\n" + "INSERT INTO Singers (SingerId, FirstName, LastName)\n" + "VALUES (20, 'George', 'Washington')"; long rowCount = transaction.executeUpdate(Statement.of(sql)); System.out.printf("%d record inserted.%n", rowCount); diff --git a/samples/snippets/src/main/java/com/example/spanner/PgAsyncQueryToListAsyncExample.java b/samples/snippets/src/main/java/com/example/spanner/PgAsyncQueryToListAsyncExample.java new file mode 100644 index 00000000000..63e7205563b --- /dev/null +++ b/samples/snippets/src/main/java/com/example/spanner/PgAsyncQueryToListAsyncExample.java @@ -0,0 +1,90 @@ +/* + * Copyright 2022 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.spanner; + +// [START spanner_postgresql_async_query_to_list] +import com.google.api.core.ApiFuture; +import com.google.cloud.spanner.AsyncResultSet; +import com.google.cloud.spanner.DatabaseClient; +import com.google.cloud.spanner.DatabaseId; +import com.google.cloud.spanner.Spanner; +import com.google.cloud.spanner.SpannerOptions; +import com.google.cloud.spanner.Statement; +import java.util.List; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +public class PgAsyncQueryToListAsyncExample { + static class Album { + final long singerId; + final long albumId; + final String albumTitle; + + Album(long singerId, long albumId, String albumTitle) { + this.singerId = singerId; + this.albumId = albumId; + this.albumTitle = albumTitle; + } + } + + static void asyncQueryToList() throws InterruptedException, ExecutionException, TimeoutException { + // TODO(developer): Replace these variables before running the sample. + String projectId = "my-project"; + String instanceId = "my-instance"; + String databaseId = "my-database"; + + try (Spanner spanner = + SpannerOptions.newBuilder().setProjectId(projectId).build().getService()) { + DatabaseClient client = + spanner.getDatabaseClient(DatabaseId.of(projectId, instanceId, databaseId)); + asyncQueryToList(client); + } + } + + // Execute a query asynchronously and transform the result to a list. + static void asyncQueryToList(DatabaseClient client) + throws InterruptedException, ExecutionException, TimeoutException { + ExecutorService executor = Executors.newSingleThreadExecutor(); + ApiFuture> albums; + try (AsyncResultSet resultSet = + client + .singleUse() + .executeQueryAsync(Statement.of("SELECT singerid as \"SingerId\", " + + "albumid as \"AlbumId\", albumtitle as \"AlbumTitle\" " + + "FROM Albums"))) { + // Convert the result set to a list of Albums asynchronously. + albums = + resultSet.toListAsync( + reader -> { + return new Album( + reader.getLong("SingerId"), + reader.getLong("AlbumId"), + reader.getString("AlbumTitle")); + }, + executor); + } + + for (Album album : albums.get(30L, TimeUnit.SECONDS)) { + System.out.printf("%d %d %s%n", album.singerId, album.albumId, album.albumTitle); + } + executor.shutdown(); + } +} +//[END spanner_postgresql_async_query_to_list] diff --git a/samples/snippets/src/main/java/com/example/spanner/PgAsyncRunnerExample.java b/samples/snippets/src/main/java/com/example/spanner/PgAsyncRunnerExample.java new file mode 100644 index 00000000000..f05b509d4c8 --- /dev/null +++ b/samples/snippets/src/main/java/com/example/spanner/PgAsyncRunnerExample.java @@ -0,0 +1,124 @@ +/* + * Copyright 2022 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.spanner; + +//[START spanner_postgresql_async_read_write_transaction] +import com.google.api.core.ApiFuture; +import com.google.api.core.ApiFutures; +import com.google.cloud.spanner.AsyncRunner; +import com.google.cloud.spanner.DatabaseClient; +import com.google.cloud.spanner.DatabaseId; +import com.google.cloud.spanner.Key; +import com.google.cloud.spanner.Spanner; +import com.google.cloud.spanner.SpannerExceptionFactory; +import com.google.cloud.spanner.SpannerOptions; +import com.google.cloud.spanner.Statement; +import com.google.cloud.spanner.Struct; +import com.google.common.collect.ImmutableList; +import com.google.common.util.concurrent.MoreExecutors; +import java.util.Arrays; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +class PgAsyncRunnerExample { + + static void asyncRunner() throws InterruptedException, ExecutionException, TimeoutException { + // TODO(developer): Replace these variables before running the sample. + String projectId = "my-project"; + String instanceId = "my-instance"; + String databaseId = "my-database"; + + try (Spanner spanner = + SpannerOptions.newBuilder().setProjectId(projectId).build().getService()) { + DatabaseClient client = + spanner.getDatabaseClient(DatabaseId.of(projectId, instanceId, databaseId)); + asyncRunner(client); + } + } + + // Execute a read/write transaction asynchronously. + static void asyncRunner(DatabaseClient client) + throws InterruptedException, ExecutionException, TimeoutException { + ExecutorService executor = Executors.newSingleThreadExecutor(); + + // Create an async transaction runner. + AsyncRunner runner = client.runAsync(); + // The transaction returns the total number of rows that were updated as a future array of + // longs. + ApiFuture rowCounts = + runner.runAsync( + txn -> { + // Transfer marketing budget from one album to another. We do it in a + // transaction to ensure that the transfer is atomic. + ApiFuture album1BudgetFut = + txn.readRowAsync("Albums", Key.of(1, 1), ImmutableList.of("MarketingBudget")); + ApiFuture album2BudgetFut = + txn.readRowAsync("Albums", Key.of(2, 2), ImmutableList.of("MarketingBudget")); + + try { + // Transaction will only be committed if this condition still holds at the + // time of commit. Otherwise it will be aborted and the AsyncWork will be + // rerun by the client library. + long transfer = 200_000; + if (album2BudgetFut.get().getLong(0) >= transfer) { + long album1Budget = album1BudgetFut.get().getLong(0); + long album2Budget = album2BudgetFut.get().getLong(0); + + album1Budget += transfer; + album2Budget -= transfer; + Statement updateStatement1 = + Statement.newBuilder( + "UPDATE Albums " + + "SET MarketingBudget = $1 " + + "WHERE SingerId = 1 and AlbumId = 1") + .bind("p1") + .to(album1Budget) + .build(); + Statement updateStatement2 = + Statement.newBuilder( + "UPDATE Albums " + + "SET MarketingBudget = $1 " + + "WHERE SingerId = 2 and AlbumId = 2") + .bind("p1") + .to(album2Budget) + .build(); + return txn.batchUpdateAsync( + ImmutableList.of(updateStatement1, updateStatement2)); + } else { + return ApiFutures.immediateFuture(new long[] {0L, 0L}); + } + } catch (ExecutionException e) { + throw SpannerExceptionFactory.newSpannerException(e.getCause()); + } catch (InterruptedException e) { + throw SpannerExceptionFactory.propagateInterrupt(e); + } + }, + executor); + + ApiFuture totalUpdateCount = + ApiFutures.transform( + rowCounts, + input -> Arrays.stream(input).sum(), + MoreExecutors.directExecutor()); + System.out.printf("%d records updated.%n", totalUpdateCount.get(30L, TimeUnit.SECONDS)); + executor.shutdown(); + } +} +//[END spanner_postgresql_async_read_write_transaction] diff --git a/samples/snippets/src/main/java/com/example/spanner/PgAsyncTransactionManagerExample.java b/samples/snippets/src/main/java/com/example/spanner/PgAsyncTransactionManagerExample.java new file mode 100644 index 00000000000..d1b20decde6 --- /dev/null +++ b/samples/snippets/src/main/java/com/example/spanner/PgAsyncTransactionManagerExample.java @@ -0,0 +1,140 @@ +/* + * Copyright 2022 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.spanner; + +//[START spanner_postgresql_async_transaction_manager] +import com.google.api.core.ApiFuture; +import com.google.api.core.ApiFutures; +import com.google.cloud.spanner.AbortedException; +import com.google.cloud.spanner.AsyncTransactionManager; +import com.google.cloud.spanner.AsyncTransactionManager.AsyncTransactionStep; +import com.google.cloud.spanner.AsyncTransactionManager.CommitTimestampFuture; +import com.google.cloud.spanner.AsyncTransactionManager.TransactionContextFuture; +import com.google.cloud.spanner.DatabaseClient; +import com.google.cloud.spanner.DatabaseId; +import com.google.cloud.spanner.Key; +import com.google.cloud.spanner.Spanner; +import com.google.cloud.spanner.SpannerOptions; +import com.google.cloud.spanner.Statement; +import com.google.cloud.spanner.Struct; +import com.google.common.collect.ImmutableList; +import com.google.common.util.concurrent.MoreExecutors; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +class PgAsyncTransactionManagerExample { + + static void asyncTransactionManager() + throws InterruptedException, ExecutionException, TimeoutException { + // TODO(developer): Replace these variables before running the sample. + String projectId = "my-project"; + String instanceId = "my-instance"; + String databaseId = "my-database"; + + try (Spanner spanner = + SpannerOptions.newBuilder().setProjectId(projectId).build().getService()) { + DatabaseClient client = + spanner.getDatabaseClient(DatabaseId.of(projectId, instanceId, databaseId)); + asyncTransactionManager(client); + } + } + + static void asyncTransactionManager(DatabaseClient client) + throws InterruptedException, ExecutionException, TimeoutException { + ExecutorService executor = Executors.newSingleThreadExecutor(); + + AsyncTransactionStep, long[]> updateCounts; + try (AsyncTransactionManager mgr = client.transactionManagerAsync()) { + TransactionContextFuture txn = mgr.beginAsync(); + // Loop to retry aborted errors. + while (true) { + try { + updateCounts = + txn.then( + (transaction, v) -> { + // Execute two reads in parallel and return the result of these as the input + // for the next step of the transaction. + ApiFuture album1BudgetFut = + transaction.readRowAsync( + "Albums", Key.of(1, 1), ImmutableList.of("MarketingBudget")); + ApiFuture album2BudgetFut = + transaction.readRowAsync( + "Albums", Key.of(2, 2), ImmutableList.of("MarketingBudget")); + return ApiFutures.allAsList( + Arrays.asList(album1BudgetFut, album2BudgetFut)); + }, + executor) + // The input of the next step of the transaction is the return value of the + // previous step, i.e. a list containing the marketing budget of two Albums. + .then( + (transaction, budgets) -> { + long album1Budget = budgets.get(0).getLong(0); + long album2Budget = budgets.get(1).getLong(0); + long transfer = 200_000; + if (album2Budget >= transfer) { + album1Budget += transfer; + album2Budget -= transfer; + Statement updateStatement1 = + Statement.newBuilder( + "UPDATE Albums " + + "SET MarketingBudget = $1 " + + "WHERE SingerId = 1 and AlbumId = 1") + .bind("p1") + .to(album1Budget) + .build(); + Statement updateStatement2 = + Statement.newBuilder( + "UPDATE Albums " + + "SET MarketingBudget = $1 " + + "WHERE SingerId = 2 and AlbumId = 2") + .bind("p1") + .to(album2Budget) + .build(); + return transaction.batchUpdateAsync( + ImmutableList.of(updateStatement1, updateStatement2)); + } else { + return ApiFutures.immediateFuture(new long[] {0L, 0L}); + } + }, + executor); + // Commit after the updates. + CommitTimestampFuture commitTsFut = updateCounts.commitAsync(); + // Wait for the transaction to finish and execute a retry if necessary. + commitTsFut.get(); + break; + } catch (AbortedException e) { + txn = mgr.resetForRetryAsync(); + } + } + } + + // Calculate the total update count. + ApiFuture totalUpdateCount = + ApiFutures.transform( + updateCounts, + input -> Arrays.stream(input).sum(), + MoreExecutors.directExecutor()); + System.out.printf("%d records updated.%n", totalUpdateCount.get(30L, TimeUnit.SECONDS)); + executor.shutdown(); + } +} +//[END spanner_postgresql_async_transaction_manager] diff --git a/samples/snippets/src/main/java/com/example/spanner/PgQueryWithNumericParameterSample.java b/samples/snippets/src/main/java/com/example/spanner/PgQueryWithNumericParameterSample.java new file mode 100644 index 00000000000..144a26def38 --- /dev/null +++ b/samples/snippets/src/main/java/com/example/spanner/PgQueryWithNumericParameterSample.java @@ -0,0 +1,60 @@ +/* + * Copyright 2022 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.spanner; + +// [START spanner_postgresql_query_with_numeric_parameter] +import com.google.cloud.spanner.DatabaseClient; +import com.google.cloud.spanner.DatabaseId; +import com.google.cloud.spanner.ResultSet; +import com.google.cloud.spanner.Spanner; +import com.google.cloud.spanner.SpannerOptions; +import com.google.cloud.spanner.Statement; +import com.google.cloud.spanner.Value; + +class PgQueryWithNumericParameterSample { + + static void queryWithNumericParameter() { + // TODO(developer): Replace these variables before running the sample. + String projectId = "my-project"; + String instanceId = "my-instance"; + String databaseId = "my-database"; + + try (Spanner spanner = + SpannerOptions.newBuilder().setProjectId(projectId).build().getService()) { + DatabaseClient client = + spanner.getDatabaseClient(DatabaseId.of(projectId, instanceId, databaseId)); + queryWithNumericParameter(client); + } + } + + static void queryWithNumericParameter(DatabaseClient client) { + Statement statement = + Statement.newBuilder( + "SELECT venueid as \"VenueId\", revenue as \"Revenue\" FROM Venues WHERE Revenue " + + "< $1") + .bind("p1") + .to(Value.pgNumeric("100000")) + .build(); + try (ResultSet resultSet = client.singleUse().executeQuery(statement)) { + while (resultSet.next()) { + System.out.printf( + "%d %s%n", resultSet.getLong("VenueId"), resultSet.getValue("Revenue")); + } + } + } +} +// [END spanner_postgresql_query_with_numeric_parameter] diff --git a/samples/snippets/src/main/java/com/example/spanner/PgSpannerSample.java b/samples/snippets/src/main/java/com/example/spanner/PgSpannerSample.java new file mode 100644 index 00000000000..9cc1d5d8497 --- /dev/null +++ b/samples/snippets/src/main/java/com/example/spanner/PgSpannerSample.java @@ -0,0 +1,1578 @@ +/* + * Copyright 2022 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.spanner; + +import com.google.api.gax.longrunning.OperationFuture; +import com.google.api.gax.paging.Page; +import com.google.cloud.ByteArray; +import com.google.cloud.Date; +import com.google.cloud.Timestamp; +import com.google.cloud.spanner.Database; +import com.google.cloud.spanner.DatabaseAdminClient; +import com.google.cloud.spanner.DatabaseClient; +import com.google.cloud.spanner.DatabaseId; +import com.google.cloud.spanner.Dialect; +import com.google.cloud.spanner.Instance; +import com.google.cloud.spanner.InstanceAdminClient; +import com.google.cloud.spanner.InstanceId; +import com.google.cloud.spanner.Key; +import com.google.cloud.spanner.KeyRange; +import com.google.cloud.spanner.KeySet; +import com.google.cloud.spanner.Mutation; +import com.google.cloud.spanner.Options; +import com.google.cloud.spanner.ReadOnlyTransaction; +import com.google.cloud.spanner.ResultSet; +import com.google.cloud.spanner.Spanner; +import com.google.cloud.spanner.SpannerBatchUpdateException; +import com.google.cloud.spanner.SpannerException; +import com.google.cloud.spanner.SpannerExceptionFactory; +import com.google.cloud.spanner.SpannerOptions; +import com.google.cloud.spanner.Statement; +import com.google.cloud.spanner.Struct; +import com.google.cloud.spanner.TimestampBound; +import com.google.cloud.spanner.Value; +import com.google.common.io.BaseEncoding; +import com.google.longrunning.Operation; +import com.google.protobuf.InvalidProtocolBufferException; +import com.google.spanner.admin.database.v1.CreateBackupMetadata; +import com.google.spanner.admin.database.v1.CreateDatabaseMetadata; +import com.google.spanner.admin.database.v1.OptimizeRestoredDatabaseMetadata; +import com.google.spanner.admin.database.v1.UpdateDatabaseDdlMetadata; +import com.google.spanner.v1.ExecuteSqlRequest; +import java.math.BigDecimal; +import java.time.Instant; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; + +/** + * Example code for using the Cloud Spanner PostgreSQL interface. + */ +public class PgSpannerSample { + static final List SINGERS = + Arrays.asList( + new Singer(1, "Marc", "Richards"), + new Singer(2, "Catalina", "Smith"), + new Singer(3, "Alice", "Trentor"), + new Singer(4, "Lea", "Martin"), + new Singer(5, "David", "Lomond")); + static final List ALBUMS = + Arrays.asList( + new Album(1, 1, "Total Junk"), + new Album(1, 2, "Go, Go, Go"), + new Album(2, 1, "Green"), + new Album(2, 2, "Forever Hold Your Peace"), + new Album(2, 3, "Terrified")); + + /** Class to contain performance sample data. */ + static class Performance { + + final long singerId; + final long venueId; + final String eventDate; + final long revenue; + + Performance(long singerId, long venueId, String eventDate, long revenue) { + this.singerId = singerId; + this.venueId = venueId; + this.eventDate = eventDate; + this.revenue = revenue; + } + } + + // [START spanner_postgresql_insert_data_with_timestamp_column] + static final List PERFORMANCES = + Arrays.asList( + new Performance(1, 4, "2017-10-05", 11000), + new Performance(1, 19, "2017-11-02", 15000), + new Performance(2, 42, "2017-12-23", 7000)); + // [START spanner_postgresql_insert_datatypes_data] + + static Value availableDates1 = + Value.dateArray( + Arrays.asList( + Date.parseDate("2020-12-01"), + Date.parseDate("2020-12-02"), + Date.parseDate("2020-12-03"))); + static Value availableDates2 = + Value.dateArray( + Arrays.asList( + Date.parseDate("2020-11-01"), + Date.parseDate("2020-11-05"), + Date.parseDate("2020-11-15"))); + static Value availableDates3 = + Value.dateArray(Arrays.asList(Date.parseDate("2020-10-01"), Date.parseDate("2020-10-07"))); + // [END spanner_postgresql_insert_data_with_timestamp_column] + static String exampleBytes1 = BaseEncoding.base64().encode("Hello World 1".getBytes()); + static String exampleBytes2 = BaseEncoding.base64().encode("Hello World 2".getBytes()); + static String exampleBytes3 = BaseEncoding.base64().encode("Hello World 3".getBytes()); + static final List VENUES = + Arrays.asList( + new Venue( + 4, + "Venue 4", + exampleBytes1, + 1800, + availableDates1, + "2018-09-02", + false, + 0.85543f, + new BigDecimal("215100.10")), + new Venue( + 19, + "Venue 19", + exampleBytes2, + 6300, + availableDates2, + "2019-01-15", + true, + 0.98716f, + new BigDecimal("1200100.00")), + new Venue( + 42, + "Venue 42", + exampleBytes3, + 3000, + availableDates3, + "2018-10-01", + false, + 0.72598f, + new BigDecimal("390650.99"))); + // [END spanner_postgresql_insert_datatypes_data] + + /** Class to contain venue sample data. */ + static class Venue { + + final long venueId; + final String venueName; + final String venueInfo; + final long capacity; + final Value availableDates; + final String lastContactDate; + final boolean outdoorVenue; + final float popularityScore; + final BigDecimal revenue; + + Venue( + long venueId, + String venueName, + String venueInfo, + long capacity, + Value availableDates, + String lastContactDate, + boolean outdoorVenue, + float popularityScore, + BigDecimal revenue) { + this.venueId = venueId; + this.venueName = venueName; + this.venueInfo = venueInfo; + this.capacity = capacity; + this.availableDates = availableDates; + this.lastContactDate = lastContactDate; + this.outdoorVenue = outdoorVenue; + this.popularityScore = popularityScore; + this.revenue = revenue; + } + } + + // [START spanner_postgresql_create_database] + static void createPostgreSqlDatabase(DatabaseAdminClient dbAdminClient, DatabaseId id) { + OperationFuture op = dbAdminClient.createDatabase( + dbAdminClient.newDatabaseBuilder(id).setDialect(Dialect.POSTGRESQL).build(), + Collections.emptyList()); + try { + // Initiate the request which returns an OperationFuture. + Database db = op.get(); + System.out.println("Created database [" + db.getId() + "]"); + } catch (ExecutionException e) { + // If the operation failed during execution, expose the cause. + throw (SpannerException) e.getCause(); + } catch (InterruptedException e) { + // Throw when a thread is waiting, sleeping, or otherwise occupied, + // and the thread is interrupted, either before or during the activity. + throw SpannerExceptionFactory.propagateInterrupt(e); + } + } + // [END spanner_postgresql_create_database] + + // [START spanner_postgresql_insert_data] + static void writeExampleData(DatabaseClient dbClient) { + List mutations = new ArrayList<>(); + for (Singer singer : SINGERS) { + mutations.add( + Mutation.newInsertBuilder("Singers") + .set("SingerId") + .to(singer.singerId) + .set("FirstName") + .to(singer.firstName) + .set("LastName") + .to(singer.lastName) + .build()); + } + for (Album album : ALBUMS) { + mutations.add( + Mutation.newInsertBuilder("Albums") + .set("SingerId") + .to(album.singerId) + .set("AlbumId") + .to(album.albumId) + .set("AlbumTitle") + .to(album.albumTitle) + .build()); + } + dbClient.write(mutations); + } + // [END spanner_postgresql_insert_data] + + // [START spanner_postgresql_delete_data] + static void deleteExampleData(DatabaseClient dbClient) { + List mutations = new ArrayList<>(); + + // KeySet.Builder can be used to delete a specific set of rows. + // Delete the Albums with the key values (2,1) and (2,3). + mutations.add( + Mutation.delete( + "Albums", KeySet.newBuilder().addKey(Key.of(2, 1)).addKey(Key.of(2, 3)).build())); + + // KeyRange can be used to delete rows with a key in a specific range. + // Delete a range of rows where the column key is >=3 and <5 + mutations.add( + Mutation.delete("Singers", KeySet.range(KeyRange.closedOpen(Key.of(3), Key.of(5))))); + + // KeySet.all() can be used to delete all the rows in a table. + // Delete remaining Singers rows, which will also delete the remaining Albums rows since it was + // defined with ON DELETE CASCADE. + mutations.add(Mutation.delete("Singers", KeySet.all())); + + dbClient.write(mutations); + System.out.printf("Records deleted.\n"); + } + // [END spanner_postgresql_delete_data] + + // [START spanner_postgresql_query_data] + static void query(DatabaseClient dbClient) { + try (ResultSet resultSet = + dbClient + .singleUse() // Execute a single read or query against Cloud Spanner. + .executeQuery(Statement.of("SELECT SingerId, AlbumId, AlbumTitle FROM Albums"))) { + while (resultSet.next()) { + System.out.printf( + "%d %d %s\n", resultSet.getLong(0), resultSet.getLong(1), + resultSet.getString(2)); + } + } + } + // [END spanner_postgresql_query_data] + + // [START spanner_postgresql_read_data] + static void read(DatabaseClient dbClient) { + try (ResultSet resultSet = + dbClient + .singleUse() + .read( + "Albums", + KeySet.all(), // Read all rows in a table. + Arrays.asList("SingerId", "AlbumId", "AlbumTitle"))) { + while (resultSet.next()) { + System.out.printf( + "%d %d %s\n", resultSet.getLong(0), resultSet.getLong(1), + resultSet.getString(2)); + } + } + } + // [END spanner_postgresql_read_data] + + // [START spanner_postgresql_add_column] + static void addMarketingBudget(DatabaseAdminClient adminClient, DatabaseId dbId) { + OperationFuture op = adminClient.updateDatabaseDdl( + dbId.getInstanceId().getInstance(), + dbId.getDatabase(), + Arrays.asList("ALTER TABLE Albums ADD COLUMN MarketingBudget bigint"), + null); + try { + // Initiate the request which returns an OperationFuture. + op.get(); + System.out.println("Added MarketingBudget column"); + } catch (ExecutionException e) { + // If the operation failed during execution, expose the cause. + throw (SpannerException) e.getCause(); + } catch (InterruptedException e) { + // Throw when a thread is waiting, sleeping, or otherwise occupied, + // and the thread is interrupted, either before or during the activity. + throw SpannerExceptionFactory.propagateInterrupt(e); + } + } + // [END spanner_postgresql_add_column] + + // Before executing this method, a new column MarketingBudget has to be added to the Albums + // table by applying the DDL statement "ALTER TABLE Albums ADD COLUMN MarketingBudget INT64". + // [START spanner_postgresql_update_data] + static void update(DatabaseClient dbClient) { + // Mutation can be used to update/insert/delete a single row in a table. Here we use + // newUpdateBuilder to create update mutations. + List mutations = + Arrays.asList( + Mutation.newUpdateBuilder("Albums") + .set("SingerId") + .to(1) + .set("AlbumId") + .to(1) + .set("MarketingBudget") + .to(100000) + .build(), + Mutation.newUpdateBuilder("Albums") + .set("SingerId") + .to(2) + .set("AlbumId") + .to(2) + .set("MarketingBudget") + .to(500000) + .build()); + // This writes all the mutations to Cloud Spanner atomically. + dbClient.write(mutations); + } + // [END spanner_postgresql_update_data] + + // [START spanner_postgresql_read_write_transaction] + static void writeWithTransaction(DatabaseClient dbClient) { + dbClient + .readWriteTransaction() + .run(transaction -> { + // Transfer marketing budget from one album to another. We do it in a transaction to + // ensure that the transfer is atomic. + Struct row = + transaction.readRow("Albums", Key.of(2, 2), Arrays.asList("MarketingBudget")); + long album2Budget = row.getLong(0); + // Transaction will only be committed if this condition still holds at the time of + // commit. Otherwise it will be aborted and the callable will be rerun by the + // client library. + long transfer = 200000; + if (album2Budget >= transfer) { + long album1Budget = + transaction + .readRow("Albums", Key.of(1, 1), Arrays.asList("MarketingBudget")) + .getLong(0); + album1Budget += transfer; + album2Budget -= transfer; + transaction.buffer( + Mutation.newUpdateBuilder("Albums") + .set("SingerId") + .to(1) + .set("AlbumId") + .to(1) + .set("MarketingBudget") + .to(album1Budget) + .build()); + transaction.buffer( + Mutation.newUpdateBuilder("Albums") + .set("SingerId") + .to(2) + .set("AlbumId") + .to(2) + .set("MarketingBudget") + .to(album2Budget) + .build()); + } + return null; + }); + } + // [END spanner_postgresql_read_write_transaction] + + // [START spanner_postgresql_query_data_with_new_column] + static void queryMarketingBudget(DatabaseClient dbClient) { + // Rows without an explicit value for MarketingBudget will have a MarketingBudget equal to + // null. A try-with-resource block is used to automatically release resources held by + // ResultSet. + try (ResultSet resultSet = + dbClient + .singleUse() + .executeQuery(Statement.of("SELECT singerid as \"SingerId\", " + + "albumid as \"AlbumId\", marketingbudget as \"MarketingBudget\" " + + "FROM Albums"))) { + while (resultSet.next()) { + System.out.printf( + "%d %d %s\n", + resultSet.getLong("SingerId"), + resultSet.getLong("AlbumId"), + // We check that the value is non null. ResultSet getters can only be used to retrieve + // non null values. + resultSet.isNull("MarketingBudget") ? "NULL" : + resultSet.getLong("MarketingBudget")); + } + } + } + // [END spanner_postgresql_query_data_with_new_column] + + // [START spanner_postgresql_create_index] + static void addIndex(DatabaseAdminClient adminClient, DatabaseId dbId) { + OperationFuture op = + adminClient.updateDatabaseDdl( + dbId.getInstanceId().getInstance(), + dbId.getDatabase(), + Arrays.asList("CREATE INDEX AlbumsByAlbumTitle ON Albums(AlbumTitle)"), + null); + try { + // Initiate the request which returns an OperationFuture. + op.get(); + System.out.println("Added AlbumsByAlbumTitle index"); + } catch (ExecutionException e) { + // If the operation failed during execution, expose the cause. + throw (SpannerException) e.getCause(); + } catch (InterruptedException e) { + // Throw when a thread is waiting, sleeping, or otherwise occupied, + // and the thread is interrupted, either before or during the activity. + throw SpannerExceptionFactory.propagateInterrupt(e); + } + } + // [END spanner_postgresql_create_index] + + // [START spanner_postgresql_read_data_with_index] + static void readUsingIndex(DatabaseClient dbClient) { + try (ResultSet resultSet = + dbClient + .singleUse() + .readUsingIndex( + "Albums", + "AlbumsByAlbumTitle", + KeySet.all(), + Arrays.asList("AlbumId", "AlbumTitle"))) { + while (resultSet.next()) { + System.out.printf("%d %s\n", resultSet.getLong(0), resultSet.getString(1)); + } + } + } + // [END spanner_postgresql_read_data_with_index] + + // [START spanner_postgresql_create_storing_index] + static void addStoringIndex(DatabaseAdminClient adminClient, DatabaseId dbId) { + OperationFuture op = adminClient.updateDatabaseDdl( + dbId.getInstanceId().getInstance(), + dbId.getDatabase(), + Arrays.asList( + "CREATE INDEX AlbumsByAlbumTitle2 ON Albums(AlbumTitle) " + + "INCLUDE (MarketingBudget)"), + null); + try { + // Initiate the request which returns an OperationFuture. + op.get(); + System.out.println("Added AlbumsByAlbumTitle2 index"); + } catch (ExecutionException e) { + // If the operation failed during execution, expose the cause. + throw (SpannerException) e.getCause(); + } catch (InterruptedException e) { + // Throw when a thread is waiting, sleeping, or otherwise occupied, + // and the thread is interrupted, either before or during the activity. + throw SpannerExceptionFactory.propagateInterrupt(e); + } + } + // [END spanner_postgresql_create_storing_index] + + // Before running this example, create a storing index AlbumsByAlbumTitle2 by applying the DDL + // statement "CREATE INDEX AlbumsByAlbumTitle2 ON Albums(AlbumTitle) INCLUDE (MarketingBudget)". + // [START spanner_postgresql_read_data_with_storing_index] + static void readStoringIndex(DatabaseClient dbClient) { + // We can read MarketingBudget also from the index since it stores a copy of MarketingBudget. + try (ResultSet resultSet = + dbClient + .singleUse() + .readUsingIndex( + "Albums", + "AlbumsByAlbumTitle2", + KeySet.all(), + Arrays.asList("AlbumId", "AlbumTitle", "MarketingBudget"))) { + while (resultSet.next()) { + System.out.printf( + "%d %s %s\n", + resultSet.getLong(0), + resultSet.getString(1), + resultSet.isNull("marketingbudget") ? "NULL" : resultSet.getLong(2)); + } + } + } + // [END spanner_postgresql_read_data_with_storing_index] + + // [START spanner_postgresql_read_only_transaction] + static void readOnlyTransaction(DatabaseClient dbClient) { + // ReadOnlyTransaction must be closed by calling close() on it to release resources held by it. + // We use a try-with-resource block to automatically do so. + try (ReadOnlyTransaction transaction = dbClient.readOnlyTransaction()) { + ResultSet queryResultSet = + transaction.executeQuery( + Statement.of("SELECT SingerId, AlbumId, AlbumTitle FROM Albums")); + while (queryResultSet.next()) { + System.out.printf( + "%d %d %s\n", + queryResultSet.getLong(0), queryResultSet.getLong(1), + queryResultSet.getString(2)); + } + try (ResultSet readResultSet = + transaction.read( + "Albums", KeySet.all(), Arrays.asList("SingerId", "AlbumId", "AlbumTitle"))) { + while (readResultSet.next()) { + System.out.printf( + "%d %d %s\n", + readResultSet.getLong(0), readResultSet.getLong(1), + readResultSet.getString(2)); + } + } + } + } + // [END spanner_postgresql_read_only_transaction] + + // [START spanner_postgresql_query_singers_table] + static void querySingersTable(DatabaseClient dbClient) { + try (ResultSet resultSet = + dbClient + .singleUse() + .executeQuery(Statement.of("SELECT singerid as \"SingerId\", " + + "firstname as \"FirstName\", lastname as \"LastName\" FROM Singers"))) { + while (resultSet.next()) { + System.out.printf( + "%s %s %s\n", + resultSet.getLong("SingerId"), + resultSet.getString("FirstName"), + resultSet.getString("LastName")); + } + } + } + // [END spanner_postgresql_query_singers_table] + + + // [START spanner_postgresql_dml_getting_started_insert] + static void writeUsingDml(DatabaseClient dbClient) { + // Insert 4 singer records + dbClient + .readWriteTransaction() + .run(transaction -> { + String sql = + "INSERT INTO Singers (SingerId, FirstName, LastName) VALUES " + + "(12, 'Melissa', 'Garcia'), " + + "(13, 'Russell', 'Morales'), " + + "(14, 'Jacqueline', 'Long'), " + + "(15, 'Dylan', 'Shaw')"; + long rowCount = transaction.executeUpdate(Statement.of(sql)); + System.out.printf("%d records inserted.\n", rowCount); + return null; + }); + } + // [END spanner_postgresql_dml_getting_started_insert] + + // [START spanner_postgresql_query_with_parameter] + static void queryWithParameter(DatabaseClient dbClient) { + Statement statement = + Statement.newBuilder( + "SELECT singerid AS \"SingerId\", " + + "firstname as \"FirstName\", lastname as \"LastName\" " + + "FROM Singers " + + "WHERE LastName = $1") + .bind("p1") + .to("Garcia") + .build(); + try (ResultSet resultSet = dbClient.singleUse().executeQuery(statement)) { + while (resultSet.next()) { + System.out.printf( + "%d %s %s\n", + resultSet.getLong("SingerId"), + resultSet.getString("FirstName"), + resultSet.getString("LastName")); + } + } + } + // [END spanner_postgresql_query_with_parameter] + + // [START spanner_postgresql_dml_getting_started_update] + static void writeWithTransactionUsingDml(DatabaseClient dbClient) { + dbClient + .readWriteTransaction() + .run(transaction -> { + // Transfer marketing budget from one album to another. We do it in a transaction to + // ensure that the transfer is atomic. + String sql1 = + "SELECT marketingbudget as \"MarketingBudget\" from Albums WHERE " + + "SingerId = 2 and AlbumId = 2"; + ResultSet resultSet = transaction.executeQuery(Statement.of(sql1)); + long album2Budget = 0; + while (resultSet.next()) { + album2Budget = resultSet.getLong("MarketingBudget"); + } + // Transaction will only be committed if this condition still holds at the time of + // commit. Otherwise it will be aborted and the callable will be rerun by the + // client library. + long transfer = 200000; + if (album2Budget >= transfer) { + String sql2 = + "SELECT marketingbudget as \"MarketingBudget\" from Albums WHERE " + + "SingerId = 1 and AlbumId = 1"; + ResultSet resultSet2 = transaction.executeQuery(Statement.of(sql2)); + long album1Budget = 0; + while (resultSet2.next()) { + album1Budget = resultSet2.getLong("MarketingBudget"); + } + album1Budget += transfer; + album2Budget -= transfer; + Statement updateStatement = + Statement.newBuilder( + "UPDATE Albums " + + "SET MarketingBudget = $1" + + "WHERE SingerId = 1 and AlbumId = 1") + .bind("p1") + .to(album1Budget) + .build(); + transaction.executeUpdate(updateStatement); + Statement updateStatement2 = + Statement.newBuilder( + "UPDATE Albums " + + "SET MarketingBudget = $1" + + "WHERE SingerId = 2 and AlbumId = 2") + .bind("p1") + .to(album2Budget) + .build(); + transaction.executeUpdate(updateStatement2); + } + return null; + }); + } + // [END spanner_postgresql_dml_getting_started_update] + + // [START spanner_postgresql_create_table_using_ddl] + static void createTableUsingDdl(DatabaseAdminClient dbAdminClient, DatabaseId id) { + OperationFuture op = + dbAdminClient.updateDatabaseDdl( + id.getInstanceId().getInstance(), + id.getDatabase(), + Arrays.asList( + "CREATE TABLE Singers (" + + " SingerId bigint NOT NULL," + + " FirstName character varying(1024)," + + " LastName character varying(1024)," + + " SingerInfo bytea," + + " PRIMARY KEY (SingerId)" + + ")", + "CREATE TABLE Albums (" + + " SingerId bigint NOT NULL," + + " AlbumId bigint NOT NULL," + + " AlbumTitle character varying(1024)," + + " PRIMARY KEY (SingerId, AlbumId)" + + ") INTERLEAVE IN PARENT Singers ON DELETE CASCADE"), + null); + try { + // Initiate the request which returns an OperationFuture. + op.get(); + System.out.println("Created Singers & Albums tables in database: [" + id + "]"); + } catch (ExecutionException e) { + // If the operation failed during execution, expose the cause. + throw (SpannerException) e.getCause(); + } catch (InterruptedException e) { + // Throw when a thread is waiting, sleeping, or otherwise occupied, + // and the thread is interrupted, either before or during the activity. + throw SpannerExceptionFactory.propagateInterrupt(e); + } + } + // [END spanner_postgresql_create_table_using_ddl] + + // [START spanner_postgresql_read_stale_data] + static void readStaleData(DatabaseClient dbClient) { + try (ResultSet resultSet = + dbClient + .singleUse(TimestampBound.ofExactStaleness(15, TimeUnit.SECONDS)) + .read( + "Albums", KeySet.all(), + Arrays.asList("SingerId", "AlbumId", "MarketingBudget"))) { + while (resultSet.next()) { + System.out.printf( + "%d %d %s\n", + resultSet.getLong(0), + resultSet.getLong(1), + resultSet.isNull(2) ? "NULL" : resultSet.getLong(2)); + } + } + } + // [END spanner_postgresql_read_stale_data] + + // Before executing this method, a new column MarketingBudget has to be added to the Albums + // table by applying the DDL statement "ALTER TABLE Albums ADD COLUMN MarketingBudget BIGINT". + // In addition this update expects the LastUpdateTime column added by applying the DDL statement + // "ALTER TABLE Albums ADD COLUMN LastUpdateTime TIMESTAMPTZ" + // [START spanner_postgresql_update_data_with_timestamp_column] + static void updateWithTimestamp(DatabaseClient dbClient) { + // Mutation can be used to update/insert/delete a single row in a table. Here we use + // newUpdateBuilder to create update mutations. + List mutations = + Arrays.asList( + Mutation.newUpdateBuilder("Albums") + .set("SingerId") + .to(1) + .set("AlbumId") + .to(1) + .set("MarketingBudget") + .to(1000000) + .set("LastUpdateTime") + .to(Value.COMMIT_TIMESTAMP) + .build(), + Mutation.newUpdateBuilder("Albums") + .set("SingerId") + .to(2) + .set("AlbumId") + .to(2) + .set("MarketingBudget") + .to(750000) + .set("LastUpdateTime") + .to(Value.COMMIT_TIMESTAMP) + .build()); + // This writes all the mutations to Cloud Spanner atomically. + dbClient.write(mutations); + } + // [END spanner_postgresql_update_data_with_timestamp_column] + + // [START spanner_postgresql_add_timestamp_column] + static void addLastUpdateTimestampColumn(DatabaseAdminClient adminClient, DatabaseId dbId) { + OperationFuture op = + adminClient.updateDatabaseDdl( + dbId.getInstanceId().getInstance(), + dbId.getDatabase(), + Arrays.asList( + "ALTER TABLE Albums ADD COLUMN LastUpdateTime spanner.commit_timestamp"), + null); + try { + // Initiate the request which returns an OperationFuture. + op.get(); + System.out.println("Added LastUpdateTime as a timestamp column in Albums table."); + } catch (ExecutionException e) { + // If the operation failed during execution, expose the cause. + throw (SpannerException) e.getCause(); + } catch (InterruptedException e) { + // Throw when a thread is waiting, sleeping, or otherwise occupied, + // and the thread is interrupted, either before or during the activity. + throw SpannerExceptionFactory.propagateInterrupt(e); + } + } + // [END spanner_postgresql_add_timestamp_column] + + // [START spanner_postgresql_query_data_with_timestamp_column] + static void queryMarketingBudgetWithTimestamp(DatabaseClient dbClient) { + // Rows without an explicit value for MarketingBudget will have a MarketingBudget equal to + // null. A try-with-resource block is used to automatically release resources held by + // ResultSet. + try (ResultSet resultSet = + dbClient + .singleUse() + .executeQuery( + Statement.of( + "SELECT singerid as \"SingerId\", albumid as \"AlbumId\", " + + "marketingbudget as \"MarketingBudget\"," + + "lastupdatetime as \"LastUpdateTime\" FROM Albums" + + " ORDER BY LastUpdateTime DESC"))) { + while (resultSet.next()) { + System.out.printf( + "%d %d %s %s\n", + resultSet.getLong("SingerId"), + resultSet.getLong("AlbumId"), + // We check that the value is non null. ResultSet getters can only be used to retrieve + // non null values. + resultSet.isNull("MarketingBudget") ? "NULL" : resultSet.getLong("MarketingBudget"), + resultSet.isNull("LastUpdateTime") ? "NULL" : resultSet.getTimestamp("LastUpdateTime")); + } + } + } + // [END spanner_postgresql_query_data_with_timestamp_column] + + // [START spanner_postgresql_create_table_with_timestamp_column] + static void createTableWithTimestamp(DatabaseAdminClient dbAdminClient, DatabaseId id) { + OperationFuture op = + dbAdminClient.updateDatabaseDdl( + id.getInstanceId().getInstance(), + id.getDatabase(), + Arrays.asList( + "CREATE TABLE Performances (" + + " SingerId BIGINT NOT NULL," + + " VenueId BIGINT NOT NULL," + + " Revenue BIGINT," + + " LastUpdateTime SPANNER.COMMIT_TIMESTAMP NOT NULL," + + " PRIMARY KEY (SingerId, VenueId))" + + " INTERLEAVE IN PARENT Singers ON DELETE CASCADE"), + null); + try { + // Initiate the request which returns an OperationFuture. + op.get(); + System.out.println("Created Performances table in database: [" + id + "]"); + } catch (ExecutionException e) { + // If the operation failed during execution, expose the cause. + throw (SpannerException) e.getCause(); + } catch (InterruptedException e) { + // Throw when a thread is waiting, sleeping, or otherwise occupied, + // and the thread is interrupted, either before or during the activity. + throw SpannerExceptionFactory.propagateInterrupt(e); + } + } + // [END spanner_postgresql_create_table_with_timestamp_column] + + // [START spanner_postgresql_insert_data_with_timestamp_column] + static void writeExampleDataWithTimestamp(DatabaseClient dbClient) { + List mutations = new ArrayList<>(); + for (Performance performance : PERFORMANCES) { + mutations.add( + Mutation.newInsertBuilder("Performances") + .set("SingerId") + .to(performance.singerId) + .set("VenueId") + .to(performance.venueId) + .set("Revenue") + .to(performance.revenue) + .set("LastUpdateTime") + .to(Value.COMMIT_TIMESTAMP) + .build()); + } + dbClient.write(mutations); + } + // [END spanner_postgresql_insert_data_with_timestamp_column] + + static void queryPerformancesTable(DatabaseClient dbClient) { + // Rows without an explicit value for Revenue will have a Revenue equal to + // null. A try-with-resource block is used to automatically release resources held by + // ResultSet. + try (ResultSet resultSet = + dbClient + .singleUse() + .executeQuery( + Statement.of( + "SELECT singerid as \"SingerId\", venueid as \"VenueId\", " + + "revenue as \"Revenue\", lastupdatetime as \"LastUpdateTime\" " + + "FROM Performances ORDER BY LastUpdateTime DESC"))) { + while (resultSet.next()) { + System.out.printf( + "%d %d %s %s\n", + resultSet.getLong("SingerId"), + resultSet.getLong("VenueId"), + // We check that the value is non null. ResultSet getters can only be used to retrieve + // non null values. + resultSet.isNull("Revenue") ? "NULL" : resultSet.getLong("Revenue"), + resultSet.getTimestamp("LastUpdateTime")); + } + } + } + + // [START spanner_postgresql_dml_standard_insert] + static void insertUsingDml(DatabaseClient dbClient) { + dbClient + .readWriteTransaction() + .run(transaction -> { + String sql = + "INSERT INTO Singers (SingerId, FirstName, LastName) " + + " VALUES (10, 'Virginia', 'Watson')"; + long rowCount = transaction.executeUpdate(Statement.of(sql)); + System.out.printf("%d record inserted.\n", rowCount); + return null; + }); + } + // [END spanner_postgresql_dml_standard_insert] + + // [START spanner_postgresql_dml_standard_update] + static void updateUsingDml(DatabaseClient dbClient) { + dbClient + .readWriteTransaction() + .run(transaction -> { + String sql = + "UPDATE Albums " + + "SET MarketingBudget = MarketingBudget * 2 " + + "WHERE SingerId = 1 and AlbumId = 1"; + long rowCount = transaction.executeUpdate(Statement.of(sql)); + System.out.printf("%d record updated.\n", rowCount); + return null; + }); + } + // [END spanner_postgresql_dml_standard_update] + + // [START spanner_postgresql_dml_standard_delete] + static void deleteUsingDml(DatabaseClient dbClient) { + dbClient + .readWriteTransaction() + .run(transaction -> { + String sql = "DELETE FROM Singers WHERE FirstName = 'Alice'"; + long rowCount = transaction.executeUpdate(Statement.of(sql)); + System.out.printf("%d record deleted.\n", rowCount); + return null; + }); + } + // [END spanner_postgresql_dml_standard_delete] + + // [START spanner_postgresql_dml_write_then_read] + static void writeAndReadUsingDml(DatabaseClient dbClient) { + dbClient + .readWriteTransaction() + .run(transaction -> { + // Insert record. + String sql = + "INSERT INTO Singers (SingerId, FirstName, LastName) " + + " VALUES (11, 'Timothy', 'Campbell')"; + long rowCount = transaction.executeUpdate(Statement.of(sql)); + System.out.printf("%d record inserted.\n", rowCount); + // Read newly inserted record. + sql = "SELECT firstname as \"FirstName\", lastname as \"LastName\" FROM Singers WHERE " + + "SingerId = 11"; + // We use a try-with-resource block to automatically release resources held by + // ResultSet. + try (ResultSet resultSet = transaction.executeQuery(Statement.of(sql))) { + while (resultSet.next()) { + System.out.printf( + "%s %s\n", + resultSet.getString("FirstName"), resultSet.getString("LastName")); + } + } + return null; + }); + } + // [END spanner_postgresql_dml_write_then_read] + + // [START spanner_postgresql_dml_partitioned_update] + static void updateUsingPartitionedDml(DatabaseClient dbClient) { + String sql = "UPDATE Albums SET MarketingBudget = 100000 WHERE SingerId > 1"; + long rowCount = dbClient.executePartitionedUpdate(Statement.of(sql)); + System.out.printf("%d records updated.\n", rowCount); + } + // [END spanner_postgresql_dml_partitioned_update] + + // [START spanner_postgresql_dml_partitioned_delete] + static void deleteUsingPartitionedDml(DatabaseClient dbClient) { + String sql = "DELETE FROM Singers WHERE SingerId > 10"; + long rowCount = dbClient.executePartitionedUpdate(Statement.of(sql)); + System.out.printf("%d records deleted.\n", rowCount); + } + // [END spanner_postgresql_dml_partitioned_delete] + + // [START spanner_postgresql_dml_batch_update] + static void updateUsingBatchDml(DatabaseClient dbClient) { + dbClient + .readWriteTransaction() + .run(transaction -> { + List stmts = new ArrayList(); + String sql = + "INSERT INTO Albums " + + "(SingerId, AlbumId, AlbumTitle, MarketingBudget) " + + "VALUES (1, 3, 'Test Album Title', 10000) "; + stmts.add(Statement.of(sql)); + sql = + "UPDATE Albums " + + "SET MarketingBudget = MarketingBudget * 2 " + + "WHERE SingerId = 1 and AlbumId = 3"; + stmts.add(Statement.of(sql)); + long[] rowCounts; + try { + rowCounts = transaction.batchUpdate(stmts); + } catch (SpannerBatchUpdateException e) { + rowCounts = e.getUpdateCounts(); + } + for (int i = 0; i < rowCounts.length; i++) { + System.out.printf("%d record updated by stmt %d.\n", rowCounts[i], i); + } + return null; + }); + } + // [END spanner_postgresql_dml_batch_update] + + // [START spanner_postgresql_create_table_with_datatypes] + static void createTableWithDatatypes(DatabaseAdminClient dbAdminClient, DatabaseId id) { + OperationFuture op = + dbAdminClient.updateDatabaseDdl( + id.getInstanceId().getInstance(), + id.getDatabase(), + Arrays.asList( + "CREATE TABLE Venues (" + + " VenueId BIGINT NOT NULL," + + " VenueName character varying(100)," + + " VenueInfo bytea," + + " Capacity BIGINT," + + " OutdoorVenue BOOL, " + + " PopularityScore FLOAT8, " + + " Revenue NUMERIC, " + + " LastUpdateTime SPANNER.COMMIT_TIMESTAMP NOT NULL," + + " PRIMARY KEY (VenueId))"), + null); + try { + // Initiate the request which returns an OperationFuture. + op.get(); + System.out.println("Created Venues table in database: [" + id + "]"); + } catch (ExecutionException e) { + // If the operation failed during execution, expose the cause. + throw (SpannerException) e.getCause(); + } catch (InterruptedException e) { + // Throw when a thread is waiting, sleeping, or otherwise occupied, + // and the thread is interrupted, either before or during the activity. + throw SpannerExceptionFactory.propagateInterrupt(e); + } + } + // [END spanner_postgresql_create_table_with_datatypes] + + // [START spanner_postgresql_insert_datatypes_data] + static void writeDatatypesData(DatabaseClient dbClient) { + List mutations = new ArrayList<>(); + for (Venue venue : VENUES) { + mutations.add( + Mutation.newInsertBuilder("Venues") + .set("VenueId") + .to(venue.venueId) + .set("VenueName") + .to(venue.venueName) + .set("VenueInfo") + .to(venue.venueInfo) + .set("Capacity") + .to(venue.capacity) + .set("OutdoorVenue") + .to(venue.outdoorVenue) + .set("PopularityScore") + .to(venue.popularityScore) + .set("Revenue") + .to(venue.revenue) + .set("LastUpdateTime") + .to(Value.COMMIT_TIMESTAMP) + .build()); + } + dbClient.write(mutations); + } + // [END spanner_postgresql_insert_datatypes_data] + + // [START spanner_postgresql_query_with_bool_parameter] + static void queryWithBool(DatabaseClient dbClient) { + boolean exampleBool = true; + Statement statement = + Statement.newBuilder( + "SELECT venueid as \"VenueId\", venuename as \"VenueName\"," + + " outdoorvenue as \"OutdoorVenue\" FROM Venues " + + "WHERE OutdoorVenue = $1") + .bind("p1") + .to(exampleBool) + .build(); + try (ResultSet resultSet = dbClient.singleUse().executeQuery(statement)) { + while (resultSet.next()) { + System.out.printf( + "%d %s %b\n", + resultSet.getLong("VenueId"), + resultSet.getString("VenueName"), + resultSet.getBoolean("OutdoorVenue")); + } + } + } + // [END spanner_postgresql_query_with_bool_parameter] + + // [START spanner_postgresql_query_with_bytes_parameter] + static void queryWithBytes(DatabaseClient dbClient) { + ByteArray exampleBytes = + ByteArray.fromBase64(BaseEncoding.base64().encode("Hello World 1".getBytes())); + Statement statement = + Statement.newBuilder( + "SELECT venueid as \"VenueId\", venuename as \"VenueName\" FROM Venues " + + "WHERE VenueInfo = $1") + .bind("p1") + .to(exampleBytes) + .build(); + try (ResultSet resultSet = dbClient.singleUse().executeQuery(statement)) { + while (resultSet.next()) { + System.out.printf( + "%d %s\n", resultSet.getLong("VenueId"), resultSet.getString("VenueName")); + } + } + } + // [END spanner_postgresql_query_with_bytes_parameter] + + // [START spanner_postgresql_query_with_float_parameter] + static void queryWithFloat(DatabaseClient dbClient) { + float exampleFloat = 0.8f; + Statement statement = + Statement.newBuilder( + "SELECT venueid as \"VenueId\", venuename as \"VenueName\", " + + "popularityscore as \"PopularityScore\" FROM Venues " + + "WHERE PopularityScore > $1") + .bind("p1") + .to(exampleFloat) + .build(); + try (ResultSet resultSet = dbClient.singleUse().executeQuery(statement)) { + while (resultSet.next()) { + System.out.printf( + "%d %s %f\n", + resultSet.getLong("VenueId"), + resultSet.getString("VenueName"), + resultSet.getDouble("PopularityScore")); + } + } + } + // [END spanner_postgresql_query_with_float_parameter] + + // [START spanner_postgresql_query_with_int_parameter] + static void queryWithInt(DatabaseClient dbClient) { + long exampleInt = 3000; + Statement statement = + Statement.newBuilder( + "SELECT venueid as \"VenueId\", venuename as \"VenueName\", " + + "capacity as \"Capacity\" " + + "FROM Venues " + "WHERE Capacity >= $1") + .bind("p1") + .to(exampleInt) + .build(); + try (ResultSet resultSet = dbClient.singleUse().executeQuery(statement)) { + while (resultSet.next()) { + System.out.printf( + "%d %s %d\n", + resultSet.getLong("VenueId"), + resultSet.getString("VenueName"), + resultSet.getLong("Capacity")); + } + } + } + // [END spanner_postgresql_query_with_int_parameter] + + // [START spanner_postgresql_query_with_string_parameter] + static void queryWithString(DatabaseClient dbClient) { + String exampleString = "Venue 42"; + Statement statement = + Statement.newBuilder( + "SELECT venueid as \"VenueId\", venuename as \"VenueName\" FROM Venues WHERE" + + " VenueName = $1") + .bind("p1") + .to(exampleString) + .build(); + try (ResultSet resultSet = dbClient.singleUse().executeQuery(statement)) { + while (resultSet.next()) { + System.out.printf( + "%d %s\n", resultSet.getLong("VenueId"), resultSet.getString("VenueName")); + } + } + } + // [END spanner_postgresql_query_with_string_parameter] + + // [START spanner_postgresql_query_with_timestamp_parameter] + static void queryWithTimestampParameter(DatabaseClient dbClient) { + Statement statement = + Statement.newBuilder( + "SELECT venueid as \"VenueId\", venuename as \"VenueName\", " + + "lastupdatetime as \"LastUpdateTime\" FROM Venues " + + "WHERE LastUpdateTime < $1") + .bind("p1") + .to(Timestamp.now()) + .build(); + try (ResultSet resultSet = dbClient.singleUse().executeQuery(statement)) { + while (resultSet.next()) { + System.out.printf( + "%d %s %s\n", + resultSet.getLong("VenueId"), + resultSet.getString("VenueName"), + resultSet.getTimestamp("LastUpdateTime")); + } + } + } + // [END spanner_postgresql_query_with_timestamp_parameter] + + // [START spanner_postgresql_query_with_numeric_parameter] + static void queryWithNumeric(DatabaseClient dbClient) { + Statement statement = + Statement.newBuilder( + "SELECT venueid as \"VenueId\", venuename as \"VenueName\", " + + "revenue as \"Revenue\" FROM Venues\n" + + "WHERE Revenue >= $1") + .bind("p1") + .to(Value.pgNumeric("300000")) + .build(); + try (ResultSet resultSet = dbClient.singleUse().executeQuery(statement)) { + while (resultSet.next()) { + System.out.printf( + "%d %s %s%n", + resultSet.getLong("VenueId"), + resultSet.getString("VenueName"), + resultSet.getValue("Revenue")); + } + } + } + // [END spanner_postgresql_query_with_numeric_parameter] + + // [START spanner_postgresql_create_client_with_query_options] + static void clientWithQueryOptions(DatabaseId db) { + SpannerOptions options = + SpannerOptions.newBuilder() + .setDefaultQueryOptions( + db, ExecuteSqlRequest.QueryOptions + .newBuilder() + .setOptimizerVersion("1") + // The list of available statistics packages can be found by querying the + // "INFORMATION_SCHEMA.spanner_postgresql_STATISTICS" table. + .setOptimizerStatisticsPackage("latest") + .build()) + .build(); + Spanner spanner = options.getService(); + DatabaseClient dbClient = spanner.getDatabaseClient(db); + try (ResultSet resultSet = + dbClient + .singleUse() + .executeQuery(Statement.of("SELECT SingerId, AlbumId, AlbumTitle FROM Albums"))) { + while (resultSet.next()) { + System.out.printf( + "%d %d %s\n", resultSet.getLong(0), resultSet.getLong(1), resultSet.getString(2)); + } + } + } + // [END spanner_postgresql_create_client_with_query_options] + + // [START spanner_postgresql_query_with_query_options] + static void queryWithQueryOptions(DatabaseClient dbClient) { + try (ResultSet resultSet = + dbClient + .singleUse() + .executeQuery( + Statement + .newBuilder("SELECT SingerId, AlbumId, AlbumTitle FROM Albums") + .withQueryOptions(ExecuteSqlRequest.QueryOptions + .newBuilder() + .setOptimizerVersion("1") + // The list of available statistics packages can be found by querying + // the "INFORMATION_SCHEMA.spanner_postgresql_STATISTICS" table. + .setOptimizerStatisticsPackage("latest") + .build()) + .build())) { + while (resultSet.next()) { + System.out.printf( + "%d %d %s\n", resultSet.getLong(0), resultSet.getLong(1), resultSet.getString(2)); + } + } + } + // [END spanner_postgresql_query_with_query_options] + + // [START spanner_postgresql_list_backup_operations] + static void listBackupOperations(InstanceAdminClient instanceAdminClient, DatabaseId databaseId) { + Instance instance = instanceAdminClient.getInstance(databaseId.getInstanceId().getInstance()); + // Get create backup operations for the sample database. + Timestamp last24Hours = Timestamp.ofTimeSecondsAndNanos(TimeUnit.SECONDS.convert( + TimeUnit.HOURS.convert(Timestamp.now().getSeconds(), TimeUnit.SECONDS) - 24, + TimeUnit.HOURS), 0); + String filter = + String.format( + "(metadata.database:%s) AND " + + "(metadata.@type:type.googleapis.com/" + + "google.spanner.admin.database.v1.CreateBackupMetadata) AND " + + "(metadata.progress.start_time > \"%s\")", + databaseId.getName(), last24Hours); + Page operations = instance + .listBackupOperations(Options.filter(filter)); + for (com.google.longrunning.Operation op : operations.iterateAll()) { + try { + CreateBackupMetadata metadata = op.getMetadata().unpack(CreateBackupMetadata.class); + System.out.println( + String.format( + "Backup %s on database %s pending: %d%% complete", + metadata.getName(), + metadata.getDatabase(), + metadata.getProgress().getProgressPercent())); + } catch (InvalidProtocolBufferException e) { + // The returned operation does not contain CreateBackupMetadata. + System.err.println(e.getMessage()); + } + } + } + // [END spanner_postgresql_list_backup_operations] + + // [START spanner_postgresql_list_database_operations] + static void listDatabaseOperations( + InstanceAdminClient instanceAdminClient, + DatabaseAdminClient dbAdminClient, + InstanceId instanceId) { + Instance instance = instanceAdminClient.getInstance(instanceId.getInstance()); + // Get optimize restored database operations. + Timestamp last24Hours = Timestamp.ofTimeSecondsAndNanos(TimeUnit.SECONDS.convert( + TimeUnit.HOURS.convert(Timestamp.now().getSeconds(), TimeUnit.SECONDS) - 24, + TimeUnit.HOURS), 0); + String filter = String.format("(metadata.@type:type.googleapis.com/" + + "google.spanner.admin.database.v1.OptimizeRestoredDatabaseMetadata) AND " + + "(metadata.progress.start_time > \"%s\")", last24Hours); + for (Operation op : instance.listDatabaseOperations(Options.filter(filter)).iterateAll()) { + try { + OptimizeRestoredDatabaseMetadata metadata = + op.getMetadata().unpack(OptimizeRestoredDatabaseMetadata.class); + System.out.println(String.format( + "Database %s restored from backup is %d%% optimized", + metadata.getName(), + metadata.getProgress().getProgressPercent())); + } catch (InvalidProtocolBufferException e) { + // The returned operation does not contain OptimizeRestoredDatabaseMetadata. + System.err.println(e.getMessage()); + } + } + } + // [END spanner_postgresql_list_database_operations] + + static void run( + DatabaseClient dbClient, + DatabaseAdminClient dbAdminClient, + InstanceAdminClient instanceAdminClient, + String command, + DatabaseId database) { + switch (command) { + case "createpgdatabase": + createPostgreSqlDatabase(dbAdminClient, database); + break; + case "write": + writeExampleData(dbClient); + break; + case "delete": + deleteExampleData(dbClient); + break; + case "query": + query(dbClient); + break; + case "read": + read(dbClient); + break; + case "addmarketingbudget": + addMarketingBudget(dbAdminClient, database); + break; + case "update": + update(dbClient); + break; + case "writetransaction": + writeWithTransaction(dbClient); + break; + case "querymarketingbudget": + queryMarketingBudget(dbClient); + break; + case "addindex": + addIndex(dbAdminClient, database); + break; + case "readindex": + readUsingIndex(dbClient); + break; + case "addstoringindex": + addStoringIndex(dbAdminClient, database); + break; + case "readstoringindex": + readStoringIndex(dbClient); + break; + case "readonlytransaction": + readOnlyTransaction(dbClient); + break; + case "querysingerstable": + querySingersTable(dbClient); + break; + case "writeusingdml": + writeUsingDml(dbClient); + break; + case "querywithparameter": + queryWithParameter(dbClient); + break; + case "writewithtransactionusingdml": + writeWithTransactionUsingDml(dbClient); + break; + case "createtableusingddl": + createTableUsingDdl(dbAdminClient, database); + break; + case "readstaledata": + readStaleData(dbClient); + break; + case "addlastupdatetimestampcolumn": + addLastUpdateTimestampColumn(dbAdminClient, database); + break; + case "updatewithtimestamp": + updateWithTimestamp(dbClient); + break; + case "querywithtimestamp": + queryMarketingBudgetWithTimestamp(dbClient); + break; + case "createtablewithtimestamp": + createTableWithTimestamp(dbAdminClient, database); + break; + case "writewithtimestamp": + writeExampleDataWithTimestamp(dbClient); + break; + case "queryperformancestable": + queryPerformancesTable(dbClient); + break; + case "insertusingdml": + insertUsingDml(dbClient); + break; + case "updateusingdml": + updateUsingDml(dbClient); + break; + case "deleteusingdml": + deleteUsingDml(dbClient); + break; + case "writeandreadusingdml": + writeAndReadUsingDml(dbClient); + break; + case "updateusingpartitioneddml": + updateUsingPartitionedDml(dbClient); + break; + case "deleteusingpartitioneddml": + deleteUsingPartitionedDml(dbClient); + break; + case "updateusingbatchdml": + updateUsingBatchDml(dbClient); + break; + case "createtablewithdatatypes": + createTableWithDatatypes(dbAdminClient, database); + break; + case "writedatatypesdata": + writeDatatypesData(dbClient); + break; + case "querywithbool": + queryWithBool(dbClient); + break; + case "querywithbytes": + queryWithBytes(dbClient); + break; + case "querywithfloat": + queryWithFloat(dbClient); + break; + case "querywithint": + queryWithInt(dbClient); + break; + case "querywithstring": + queryWithString(dbClient); + break; + case "querywithtimestampparameter": + queryWithTimestampParameter(dbClient); + break; + case "querywithnumeric": + queryWithNumeric(dbClient); + break; + case "clientwithqueryoptions": + clientWithQueryOptions(database); + break; + case "querywithqueryoptions": + queryWithQueryOptions(dbClient); + break; + case "listbackupoperations": + listBackupOperations(instanceAdminClient, database); + break; + case "listdatabaseoperations": + listDatabaseOperations(instanceAdminClient, dbAdminClient, database.getInstanceId()); + break; + default: + printUsageAndExit(); + } + } + + static void printUsageAndExit() { + System.err.println("Usage:"); + System.err.println(" PgSpannerExample "); + System.err.println(); + System.err.println("Examples:"); + System.err.println(" PgSpannerExample createpgdatabase my-instance example-db"); + System.err.println(" PgSpannerExample write my-instance example-db"); + System.err.println(" PgSpannerExample delete my-instance example-db"); + System.err.println(" PgSpannerExample query my-instance example-db"); + System.err.println(" PgSpannerExample read my-instance example-db"); + System.err.println(" PgSpannerExample addmarketingbudget my-instance example-db"); + System.err.println(" PgSpannerExample update my-instance example-db"); + System.err.println(" PgSpannerExample writetransaction my-instance example-db"); + System.err.println(" PgSpannerExample querymarketingbudget my-instance example-db"); + System.err.println(" PgSpannerExample addindex my-instance example-db"); + System.err.println(" PgSpannerExample readindex my-instance example-db"); + System.err.println(" PgSpannerExample addstoringindex my-instance example-db"); + System.err.println(" PgSpannerExample readstoringindex my-instance example-db"); + System.err.println(" PgSpannerExample readonlytransaction my-instance example-db"); + System.err.println(" PgSpannerExample querysingerstable my-instance example-db"); + System.err.println(" PgSpannerExample writeusingdml my-instance example-db"); + System.err.println(" PgSpannerExample querywithparameter my-instance example-db"); + System.err.println(" PgSpannerExample writewithtransactionusingdml my-instance example-db"); + System.err.println(" PgSpannerExample createtableforsamples my-instance example-db"); + System.err.println(" PgSpannerExample writewithtimestamp my-instance example-db"); + System.err.println(" PgSpannerExample queryperformancestable my-instance example-db"); + System.err.println(" PgSpannerExample writestructdata my-instance example-db"); + System.err.println(" PgSpannerExample insertusingdml my-instance example-db"); + System.err.println(" PgSpannerExample updateusingdml my-instance example-db"); + System.err.println(" PgSpannerExample deleteusingdml my-instance example-db"); + System.err.println(" PgSpannerExample writeandreadusingdml my-instance example-db"); + System.err.println(" PgSpannerExample writeusingdml my-instance example-db"); + System.err.println(" PgSpannerExample deleteusingpartitioneddml my-instance example-db"); + System.err.println(" PgSpannerExample updateusingbatchdml my-instance example-db"); + System.err.println(" PgSpannerExample createtablewithdatatypes my-instance example-db"); + System.err.println(" PgSpannerExample writedatatypesdata my-instance example-db"); + System.err.println(" PgSpannerExample querywithbool my-instance example-db"); + System.err.println(" PgSpannerExample querywithbytes my-instance example-db"); + System.err.println(" PgSpannerExample querywithfloat my-instance example-db"); + System.err.println(" PgSpannerExample querywithint my-instance example-db"); + System.err.println(" PgSpannerExample querywithstring my-instance example-db"); + System.err.println(" PgSpannerExample querywithtimestampparameter my-instance example-db"); + System.err.println(" PgSpannerExample clientwithqueryoptions my-instance example-db"); + System.err.println(" PgSpannerExample querywithqueryoptions my-instance example-db"); + System.err.println(" PgSpannerExample listbackupoperations my-instance example-db"); + System.err.println(" PgSpannerExample listdatabaseoperations my-instance example-db"); + System.exit(1); + } + + public static void main(String[] args) { + if (args.length != 3) { + printUsageAndExit(); + } + // [START spanner_init_client] + SpannerOptions options = SpannerOptions.newBuilder().build(); + Spanner spanner = options.getService(); + try { + // [END spanner_init_client] + String command = args[0]; + DatabaseId db = DatabaseId.of(options.getProjectId(), args[1], args[2]); + + // This will return the default project id based on the environment. + String clientProject = spanner.getOptions().getProjectId(); + if (!db.getInstanceId().getProject().equals(clientProject)) { + System.err.println( + "Invalid project specified. Project in the database id should match the" + + "project name set in the environment variable GOOGLE_CLOUD_PROJECT. Expected: " + + clientProject); + printUsageAndExit(); + } + // [START spanner_init_client] + DatabaseClient dbClient = spanner.getDatabaseClient(db); + DatabaseAdminClient dbAdminClient = spanner.getDatabaseAdminClient(); + InstanceAdminClient instanceAdminClient = spanner.getInstanceAdminClient(); + // [END spanner_init_client] + + // Use client here... + run(dbClient, dbAdminClient, instanceAdminClient, command, db); + // [START spanner_init_client] + } finally { + spanner.close(); + } + // [END spanner_init_client] + System.out.println("Closed client"); + } + + /** Class to contain singer sample data. */ + static class Singer { + + final long singerId; + final String firstName; + final String lastName; + + Singer(long singerId, String firstName, String lastName) { + this.singerId = singerId; + this.firstName = firstName; + this.lastName = lastName; + } + } + + /** Class to contain album sample data. */ + static class Album { + + final long singerId; + final long albumId; + final String albumTitle; + + Album(long singerId, long albumId, String albumTitle) { + this.singerId = singerId; + this.albumId = albumId; + this.albumTitle = albumTitle; + } + } +} diff --git a/samples/snippets/src/main/java/com/example/spanner/StatementTimeoutExample.java b/samples/snippets/src/main/java/com/example/spanner/StatementTimeoutExample.java index 6c45216b317..2a82467754f 100644 --- a/samples/snippets/src/main/java/com/example/spanner/StatementTimeoutExample.java +++ b/samples/snippets/src/main/java/com/example/spanner/StatementTimeoutExample.java @@ -65,7 +65,7 @@ public ApiCallContext configure(ApiCallContext context, ReqT reque // Run the transaction in the custom context. context.run(() -> client.readWriteTransaction().run(transaction -> { - String sql = "INSERT Singers (SingerId, FirstName, LastName)\n" + String sql = "INSERT INTO Singers (SingerId, FirstName, LastName)\n" + "VALUES (20, 'George', 'Washington')"; long rowCount = transaction.executeUpdate(Statement.of(sql)); System.out.printf("%d record inserted.%n", rowCount); diff --git a/samples/snippets/src/test/java/com/example/spanner/PgAsyncExamplesIT.java b/samples/snippets/src/test/java/com/example/spanner/PgAsyncExamplesIT.java new file mode 100644 index 00000000000..db5c7362cf8 --- /dev/null +++ b/samples/snippets/src/test/java/com/example/spanner/PgAsyncExamplesIT.java @@ -0,0 +1,251 @@ +/* + * Copyright 2022 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.spanner; + +import static com.example.spanner.SampleRunner.runSample; +import static com.google.common.truth.Truth.assertThat; + +import com.google.cloud.spanner.DatabaseClient; +import com.google.cloud.spanner.DatabaseId; +import com.google.cloud.spanner.Dialect; +import com.google.cloud.spanner.KeySet; +import com.google.cloud.spanner.Mutation; +import com.google.common.collect.ImmutableList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import org.junit.After; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +/** + * Integration tests for Cloud Spanner Async API examples for Postgresql. + */ +@RunWith(JUnit4.class) +@SuppressWarnings("checkstyle:AbbreviationAsWordInName") +public class PgAsyncExamplesIT extends SampleTestBase { + + private static DatabaseId databaseId; + + @BeforeClass + public static void createTestDatabase() throws Exception { + final String database = idGenerator.generateDatabaseId(); + databaseId = DatabaseId.of(projectId, instanceId, database); + databaseAdminClient + .createDatabase( + databaseAdminClient + .newDatabaseBuilder(databaseId) + .setDialect(Dialect.POSTGRESQL).build(), + Collections.emptyList()) + .get(); + databaseAdminClient.updateDatabaseDdl( + instanceId, + database, + Arrays.asList( + "CREATE TABLE Singers (" + + " SingerId bigint NOT NULL," + + " FirstName character varying(1024)," + + " LastName character varying(1024)," + + " SingerInfo bytea," + + " PRIMARY KEY (SingerId)" + + ")", + "CREATE TABLE Albums (" + + " SingerId bigint NOT NULL," + + " AlbumId bigint NOT NULL," + + " AlbumTitle character varying(1024)," + + " MarketingBudget bigint," + + " PRIMARY KEY (SingerId, AlbumId)" + + ") INTERLEAVE IN PARENT Singers ON DELETE CASCADE", + "CREATE INDEX AlbumsByAlbumTitle ON Albums(AlbumTitle)"), + null).get(); + } + + static class Singer { + + final long singerId; + final String firstName; + final String lastName; + + Singer(long singerId, String firstName, String lastName) { + this.singerId = singerId; + this.firstName = firstName; + this.lastName = lastName; + } + } + + static class Album { + + final long singerId; + final long albumId; + final String albumTitle; + final Long marketingBudget; + + Album(long singerId, long albumId, String albumTitle, Long marketingBudget) { + this.singerId = singerId; + this.albumId = albumId; + this.albumTitle = albumTitle; + this.marketingBudget = marketingBudget; + } + } + + private static final List TEST_SINGERS = + Arrays.asList( + new Singer(1, "Marc", "Richards"), + new Singer(2, "Catalina", "Smith"), + new Singer(3, "Alice", "Trentor"), + new Singer(4, "Lea", "Martin"), + new Singer(5, "David", "Lomond")); + private static final List ALBUMS = + Arrays.asList( + new Album(1, 1, "Total Junk", 300_000L), + new Album(1, 2, "Go, Go, Go", 400_000L), + new Album(2, 1, "Green", 150_000L), + new Album(2, 2, "Forever Hold Your Peace", 350_000L), + new Album(2, 3, "Terrified", null)); + + @Before + public void insertTestData() { + DatabaseClient client = spanner.getDatabaseClient(databaseId); + ImmutableList.Builder mutations = + ImmutableList.builderWithExpectedSize(TEST_SINGERS.size()); + for (Singer singer : TEST_SINGERS) { + mutations.add( + Mutation.newInsertBuilder("Singers") + .set("SingerId") + .to(singer.singerId) + .set("FirstName") + .to(singer.firstName) + .set("LastName") + .to(singer.lastName) + .build()); + } + for (Album album : ALBUMS) { + mutations.add( + Mutation.newInsertBuilder("Albums") + .set("SingerId") + .to(album.singerId) + .set("AlbumId") + .to(album.albumId) + .set("AlbumTitle") + .to(album.albumTitle) + .set("MarketingBudget") + .to(album.marketingBudget) + .build()); + } + client.write(mutations.build()); + } + + private void assertSingersOutput(String out) { + assertThat(out).contains("1 Marc Richard"); + assertThat(out).contains("2 Catalina Smith"); + assertThat(out).contains("3 Alice Trentor"); + assertThat(out).contains("4 Lea Martin"); + assertThat(out).contains("5 David Lomond"); + } + + private void assertAlbumsOutput(String out) { + assertThat(out).contains("1 1 Total Junk"); + assertThat(out).contains("1 2 Go, Go, Go"); + assertThat(out).contains("2 1 Green"); + assertThat(out).contains("2 2 Forever Hold Your Peace"); + assertThat(out).contains("2 3 Terrified"); + } + + @After + public void removeTestData() { + DatabaseClient client = spanner.getDatabaseClient(databaseId); + client.write(Arrays.asList(Mutation.delete("Singers", KeySet.all()))); + } + + @Test + public void asyncQuery_shouldReturnData() throws Exception { + String out = runSample( + () -> AsyncQueryExample.asyncQuery(spanner.getDatabaseClient(databaseId))); + assertAlbumsOutput(out); + } + + @Test + public void asyncQueryToListAsync_shouldReturnData() + throws Exception { + String out = runSample( + () -> PgAsyncQueryToListAsyncExample + .asyncQueryToList(spanner.getDatabaseClient(databaseId))); + assertAlbumsOutput(out); + } + + @Test + public void asyncRead_shouldReturnData() + throws Exception { + String out = runSample(() -> AsyncReadExample.asyncRead(spanner.getDatabaseClient(databaseId))); + assertAlbumsOutput(out); + } + + @Test + public void asyncReadUsingIndex_shouldReturnDataInCorrectOrder() throws Exception { + String out = runSample(() -> AsyncReadUsingIndexExample + .asyncReadUsingIndex(spanner.getDatabaseClient(databaseId))); + assertThat(out) + .contains( + "2 Forever Hold Your Peace\n" + + "2 Go, Go, Go\n" + + "1 Green\n" + + "3 Terrified\n" + + "1 Total Junk"); + } + + @Test + public void asyncReadOnlyTransaction_shouldReturnData() throws Exception { + String out = runSample(() -> AsyncReadOnlyTransactionExample + .asyncReadOnlyTransaction(spanner.getDatabaseClient(databaseId))); + assertAlbumsOutput(out); + assertSingersOutput(out); + } + + @Test + public void asyncDml_shouldInsertRows() throws Exception { + String out = runSample(() -> AsyncDmlExample.asyncDml(spanner.getDatabaseClient(databaseId))); + assertThat(out).contains("4 records inserted."); + } + + @Test + public void asyncRunner_shouldUpdateRows() throws Exception { + String out = runSample( + () -> PgAsyncRunnerExample.asyncRunner(spanner.getDatabaseClient(databaseId))); + assertThat(out).contains("2 records updated."); + } + + @Test + public void asyncTransactionManager_shouldUpdateRows() throws Exception { + String out = runSample(() -> PgAsyncTransactionManagerExample + .asyncTransactionManager(spanner.getDatabaseClient(databaseId))); + assertThat(out).contains("2 records updated."); + } + + @Test + public void asyncReadRow_shouldPrintRow() throws Exception { + String out = runSample( + () -> AsyncReadRowExample.asyncReadRow(spanner.getDatabaseClient(databaseId))); + assertThat(out).contains("1 1 Total Junk"); + assertThat(out).doesNotContain("1 2 Go, Go, Go"); + assertThat(out).doesNotContain("2 1 Green"); + assertThat(out).doesNotContain("2 2 Forever Hold Your Peace"); + assertThat(out).doesNotContain("2 3 Terrified"); + } +} diff --git a/samples/snippets/src/test/java/com/example/spanner/PgSpannerSampleIT.java b/samples/snippets/src/test/java/com/example/spanner/PgSpannerSampleIT.java new file mode 100644 index 00000000000..796df092c80 --- /dev/null +++ b/samples/snippets/src/test/java/com/example/spanner/PgSpannerSampleIT.java @@ -0,0 +1,304 @@ +/* + * Copyright 2022 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.spanner; + +import static com.google.common.truth.Truth.assertThat; + +import com.google.cloud.Timestamp; +import com.google.cloud.spanner.Database; +import com.google.cloud.spanner.DatabaseAdminClient; +import com.google.cloud.spanner.DatabaseId; +import com.google.cloud.spanner.Spanner; +import com.google.cloud.spanner.SpannerOptions; +import java.io.ByteArrayOutputStream; +import java.io.PrintStream; +import java.util.UUID; +import java.util.concurrent.TimeUnit; +import java.util.regex.Pattern; +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +/** + * Unit tests for {@code PgSpannerSample} + */ +@RunWith(JUnit4.class) +@SuppressWarnings("checkstyle:abbreviationaswordinname") +public class PgSpannerSampleIT { + private static final int DBID_LENGTH = 20; + // The instance needs to exist for tests to pass. + private static final String instanceId = System.getProperty("spanner.test.instance"); + private static final String baseDbId = System.getProperty("spanner.sample.database"); + private static final String databaseId = formatForTest(baseDbId); + private static final String encryptedDatabaseId = formatForTest(baseDbId); + private static final String encryptedBackupId = formatForTest(baseDbId); + private static final String encryptedRestoreId = formatForTest(baseDbId); + static Spanner spanner; + static DatabaseId dbId; + static DatabaseAdminClient dbClient; + + @BeforeClass + public static void setUp() { + SpannerOptions options = + SpannerOptions.newBuilder().setAutoThrottleAdministrativeRequests().build(); + spanner = options.getService(); + dbClient = spanner.getDatabaseAdminClient(); + dbId = DatabaseId.of(options.getProjectId(), instanceId, databaseId); + // Delete stale test databases that have been created earlier by this test, but not deleted. + deleteStaleTestDatabases(); + } + + static void deleteStaleTestDatabases() { + Timestamp now = Timestamp.now(); + Pattern samplePattern = getTestDbIdPattern(PgSpannerSampleIT.baseDbId); + Pattern restoredPattern = getTestDbIdPattern("restored"); + for (Database db : dbClient.listDatabases(PgSpannerSampleIT.instanceId).iterateAll()) { + if (TimeUnit.HOURS.convert(now.getSeconds() - db.getCreateTime().getSeconds(), + TimeUnit.SECONDS) > 24) { + if (db.getId().getDatabase().length() >= DBID_LENGTH) { + if (samplePattern.matcher(toComparableId(PgSpannerSampleIT.baseDbId, + db.getId().getDatabase())).matches()) { + db.drop(); + } + if (restoredPattern.matcher(toComparableId("restored", db.getId().getDatabase())) + .matches()) { + db.drop(); + } + } + } + } + } + + @AfterClass + public static void tearDown() { + dbClient.dropDatabase(dbId.getInstanceId().getInstance(), dbId.getDatabase()); + dbClient.dropDatabase( + dbId.getInstanceId().getInstance(), SpannerSample.createRestoredSampleDbId(dbId)); + dbClient.dropDatabase(instanceId, encryptedDatabaseId); + dbClient.dropDatabase(instanceId, encryptedRestoreId); + dbClient.deleteBackup(instanceId, encryptedBackupId); + spanner.close(); + } + + private static String toComparableId(String baseId, String existingId) { + String zeroUuid = "00000000-0000-0000-0000-0000-00000000"; + int shouldBeLength = (baseId + "-" + zeroUuid).length(); + int missingLength = shouldBeLength - existingId.length(); + return existingId + zeroUuid.substring(zeroUuid.length() - missingLength); + } + + private static Pattern getTestDbIdPattern(String baseDbId) { + return Pattern.compile( + baseDbId + "-[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{8}", + Pattern.CASE_INSENSITIVE); + } + + static String formatForTest(String name) { + return name + "-" + UUID.randomUUID().toString().substring(0, DBID_LENGTH); + } + + private String runSample(String command) { + final PrintStream stdOut = System.out; + final ByteArrayOutputStream bout = new ByteArrayOutputStream(); + final PrintStream out = new PrintStream(bout); + System.setOut(out); + System.out.println(instanceId + ":" + databaseId); + PgSpannerSample.main(new String[]{command, instanceId, databaseId}); + System.setOut(stdOut); + return bout.toString(); + } + + @Test + public void testSample() throws Exception { + assertThat(instanceId).isNotNull(); + assertThat(databaseId).isNotNull(); + + System.out.println("Create Database ..."); + String out = runSample("createpgdatabase"); + assertThat(out).contains("Created database"); + assertThat(out).contains(dbId.getName()); + + System.out.println("Create sample tables Singers and Albums ..."); + runSample("createtableusingddl"); + + System.out.println("Write data to sample tables ..."); + runSample("write"); + + System.out.println("Read data from sample tables ..."); + out = runSample("read"); + assertThat(out).contains("1 1 Total Junk"); + + System.out.println("Write data using DML to sample table ..."); + runSample("writeusingdml"); + System.out.println("Query Singers table ..."); + out = runSample("querysingerstable"); + assertThat(out).contains("Melissa Garcia"); + out = runSample("query"); + assertThat(out).contains("1 1 Total Junk"); + out = runSample("querywithparameter"); + assertThat(out).contains("12 Melissa Garcia"); + + System.out.println("Add column marketing budget ..."); + runSample("addmarketingbudget"); + + // wait for 15 seconds to elapse and then run an update, and query for stale data + long lastUpdateDataTimeInMillis = System.currentTimeMillis(); + while (System.currentTimeMillis() < lastUpdateDataTimeInMillis + 16000) { + Thread.sleep(1000); + } + System.out.println("Write data to marketing budget ..."); + runSample("update"); + + System.out.println("Query marketing budget ..."); + out = runSample("querymarketingbudget"); + assertThat(out).contains("1 1 100000"); + assertThat(out).contains("2 2 500000"); + + System.out.println("Write with transaction using dml..."); + runSample("writewithtransactionusingdml"); + out = runSample("querymarketingbudget"); + assertThat(out).contains("1 1 300000"); + assertThat(out).contains("1 1 300000"); + + System.out.println("Add index ..."); + runSample("addindex"); + + System.out.println("Read index ..."); + out = runSample("readindex"); + assertThat(out).contains("Go, Go, Go"); + assertThat(out).contains("Forever Hold Your Peace"); + assertThat(out).contains("Green"); + + System.out.println("Add Storing index ..."); + runSample("addstoringindex"); + + System.out.println("Read storing index ..."); + out = runSample("readstoringindex"); + assertThat(out).contains("300000"); + + System.out.println("Read only transaction ..."); + out = runSample("readonlytransaction"); + assertThat(out.replaceAll("[\r\n]+", " ")) + .containsMatch("(Total Junk.*){2}"); + + System.out.println("Add Timestamp column ..."); + out = runSample("addlastupdatetimestampcolumn"); + assertThat(out).contains("Added LastUpdateTime as a timestamp column"); + + System.out.println("Update values in Timestamp column ..."); + runSample("updatewithtimestamp"); + out = runSample("querywithtimestamp"); + assertThat(out).contains("1 1 1000000"); + assertThat(out).contains("2 2 750000"); + + System.out.println("Create table with Timestamp column ..."); + out = runSample("createtablewithtimestamp"); + assertThat(out).contains("Created Performances table in database"); + + System.out.println("Write with Timestamp ..."); + runSample("writewithtimestamp"); + out = runSample("queryperformancestable"); + assertThat(out).contains("1 4 11000"); + assertThat(out).contains("1 19 15000"); + assertThat(out).contains("2 42 7000"); + + System.out.println("Write using DML ..."); + runSample("insertusingdml"); + out = runSample("querysingerstable"); + assertThat(out).contains("Virginia Watson"); + + System.out.println("Update using DML ..."); + runSample("updateusingdml"); + out = runSample("querymarketingbudget"); + assertThat(out).contains("1 1 2000000"); + + System.out.println("Delete using DML ..."); + runSample("deleteusingdml"); + out = runSample("querysingerstable"); + assertThat(out).doesNotContain("Alice Trentor"); + + System.out.println("Write and Read using DML ..."); + out = runSample("writeandreadusingdml"); + assertThat(out).contains("Timothy Campbell"); + + System.out.println("Update using partitioned DML ..."); + runSample("updateusingpartitioneddml"); + out = runSample("querymarketingbudget"); + assertThat(out).contains("2 2 100000"); + assertThat(out).contains("1 1 2000000"); + + System.out.println("Delete using Partitioned DML ..."); + runSample("deleteusingpartitioneddml"); + out = runSample("querysingerstable"); + assertThat(out).doesNotContain("Timothy Grant"); + assertThat(out).doesNotContain("Melissa Garcia"); + assertThat(out).doesNotContain("Russell Morales"); + assertThat(out).doesNotContain("Jacqueline Long"); + assertThat(out).doesNotContain("Dylan Shaw"); + + System.out.println("Update in Batch using DML ..."); + out = runSample("updateusingbatchdml"); + assertThat(out).contains("1 record updated by stmt 0"); + assertThat(out).contains("1 record updated by stmt 1"); + + System.out.println("Create table with data types ..."); + out = runSample("createtablewithdatatypes"); + assertThat(out).contains("Created Venues table in database"); + + System.out.println("Write into table and Query Boolean Type ..."); + runSample("writedatatypesdata"); + out = runSample("querywithbool"); + assertThat(out).contains("19 Venue 19 true"); + + System.out.println("Query with Bytes ..."); + out = runSample("querywithbytes"); + assertThat(out).contains("4 Venue 4"); + + System.out.println("Query with Float ..."); + out = runSample("querywithfloat"); + assertThat(out).contains("4 Venue 4 0.8"); + assertThat(out).contains("19 Venue 19 0.9"); + + System.out.println("Query with Int ..."); + out = runSample("querywithint"); + assertThat(out).contains("19 Venue 19 6300"); + assertThat(out).contains("42 Venue 42 3000"); + + System.out.println("Query with String ..."); + out = runSample("querywithstring"); + assertThat(out).contains("42 Venue 42"); + + System.out.println("Query with Timestamp parameter ..."); + out = runSample("querywithtimestampparameter"); + assertThat(out).contains("4 Venue 4"); + assertThat(out).contains("19 Venue 19"); + assertThat(out).contains("42 Venue 42"); + + System.out.println("Query with Numeric Type ..."); + out = runSample("querywithnumeric"); + assertThat(out).contains("19 Venue 19 1200100"); + assertThat(out).contains("42 Venue 42 390650.99"); + + System.out.println("Query options ..."); + out = runSample("clientwithqueryoptions"); + assertThat(out).contains("1 1 Total Junk"); + out = runSample("querywithqueryoptions"); + assertThat(out).contains("1 1 Total Junk"); + } +} diff --git a/samples/snippets/src/test/java/com/example/spanner/PgSpannerStandaloneExamplesIT.java b/samples/snippets/src/test/java/com/example/spanner/PgSpannerStandaloneExamplesIT.java new file mode 100644 index 00000000000..035b7cdc36b --- /dev/null +++ b/samples/snippets/src/test/java/com/example/spanner/PgSpannerStandaloneExamplesIT.java @@ -0,0 +1,209 @@ +/* + * Copyright 2022 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.spanner; + +import static com.google.common.truth.Truth.assertThat; + +import com.google.api.gax.longrunning.OperationFuture; +import com.google.cloud.spanner.DatabaseAdminClient; +import com.google.cloud.spanner.DatabaseClient; +import com.google.cloud.spanner.DatabaseId; +import com.google.cloud.spanner.Dialect; +import com.google.cloud.spanner.Instance; +import com.google.cloud.spanner.KeySet; +import com.google.cloud.spanner.Mutation; +import com.google.cloud.spanner.Spanner; +import com.google.cloud.spanner.SpannerOptions; +import com.google.common.collect.ImmutableList; +import com.google.spanner.admin.database.v1.UpdateDatabaseDdlMetadata; +import java.io.ByteArrayOutputStream; +import java.io.PrintStream; +import java.math.BigDecimal; +import java.util.Arrays; +import java.util.Collections; +import java.util.Iterator; +import java.util.concurrent.ExecutionException; +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +/** Integration tests for Cloud Spanner cloud client examples. */ +@RunWith(JUnit4.class) +@SuppressWarnings("checkstyle:AbbreviationAsWordInName") +public class PgSpannerStandaloneExamplesIT { + // The instance needs to exist for tests to pass. + private static String instanceId = System.getProperty("spanner.test.instance"); + private static String baseDatabaseId = System.getProperty("spanner.sample.database", "mysample"); + private static String databaseId = SpannerSampleIT.formatForTest(baseDatabaseId); + private static DatabaseId dbId; + private static DatabaseAdminClient dbClient; + private static Spanner spanner; + + private String runExample(Runnable example) { + PrintStream stdOut = System.out; + ByteArrayOutputStream bout = new ByteArrayOutputStream(); + PrintStream out = new PrintStream(bout); + System.setOut(out); + example.run(); + System.setOut(stdOut); + return bout.toString(); + } + + @BeforeClass + public static void createTestDatabase() throws Exception { + SpannerOptions options = + SpannerOptions.newBuilder().setAutoThrottleAdministrativeRequests().build(); + spanner = options.getService(); + dbClient = spanner.getDatabaseAdminClient(); + if (instanceId == null) { + Iterator iterator = + spanner.getInstanceAdminClient().listInstances().iterateAll().iterator(); + if (iterator.hasNext()) { + instanceId = iterator.next().getId().getInstance(); + } + } + dbId = DatabaseId.of(options.getProjectId(), instanceId, databaseId); + dbClient + .createDatabase( + dbClient.newDatabaseBuilder(dbId).setDialect(Dialect.POSTGRESQL).build(), + Collections.emptyList()) + .get(); + dbClient.updateDatabaseDdl( + instanceId, + databaseId, + Arrays.asList( + "CREATE TABLE Singers (" + + " SingerId bigint NOT NULL," + + " FirstName character varying(1024)," + + " LastName character varying(1024)," + + " SingerInfo bytea," + + " PRIMARY KEY (SingerId)" + + ")", + "CREATE TABLE Venues (" + + "VenueId bigint NOT NULL," + + "Revenue NUMERIC," + + "PRIMARY KEY (VenueId))"), + null).get(); + } + + @AfterClass + public static void dropTestDatabase() throws Exception { + dbClient.dropDatabase(dbId.getInstanceId().getInstance(), dbId.getDatabase()); + spanner.close(); + } + + @Before + public void deleteTestData() { + String projectId = spanner.getOptions().getProjectId(); + DatabaseClient client = + spanner.getDatabaseClient(DatabaseId.of(projectId, instanceId, databaseId)); + client.write(Collections.singleton(Mutation.delete("Singers", KeySet.all()))); + client.write(Collections.singleton(Mutation.delete("Venues", KeySet.all()))); + } + + @Test + public void executeSqlWithCustomTimeoutAndRetrySettings_shouldWriteData() { + String projectId = spanner.getOptions().getProjectId(); + String out = + runExample( + () -> + CustomTimeoutAndRetrySettingsExample.executeSqlWithCustomTimeoutAndRetrySettings( + projectId, instanceId, databaseId)); + assertThat(out).contains("1 record inserted."); + } + + @Test + public void executeSqlWithTimeout_shouldWriteData() { + String projectId = spanner.getOptions().getProjectId(); + DatabaseClient client = + spanner.getDatabaseClient(DatabaseId.of(projectId, instanceId, databaseId)); + String out = runExample(() -> StatementTimeoutExample.executeSqlWithTimeout(client)); + assertThat(out).contains("1 record inserted."); + } + + @Test + public void addNumericColumn_shouldSuccessfullyAddColumn() + throws InterruptedException, ExecutionException { + OperationFuture operation = + spanner + .getDatabaseAdminClient() + .updateDatabaseDdl( + instanceId, + databaseId, + ImmutableList.of("ALTER TABLE Venues DROP COLUMN Revenue"), + null); + operation.get(); + String out = + runExample( + () -> { + try { + AddNumericColumnSample.addNumericColumn( + spanner.getDatabaseAdminClient(), instanceId, databaseId); + } catch (ExecutionException e) { + System.out.printf( + "Adding column `Revenue` failed: %s%n", e.getCause().getMessage()); + } catch (InterruptedException e) { + System.out.printf("Adding column `Revenue` was interrupted%n"); + } + }); + assertThat(out).contains("Successfully added column `Revenue`"); + } + + @Test + public void updateNumericData_shouldWriteData() { + String projectId = spanner.getOptions().getProjectId(); + String out = + runExample( + () -> + UpdateNumericDataSample.updateNumericData( + spanner.getDatabaseClient(DatabaseId.of(projectId, instanceId, databaseId)))); + assertThat(out).contains("Venues successfully updated"); + } + + @Test + public void queryWithNumericParameter_shouldReturnResults() { + String projectId = spanner.getOptions().getProjectId(); + DatabaseClient client = + spanner.getDatabaseClient(DatabaseId.of(projectId, instanceId, databaseId)); + client.write( + ImmutableList.of( + Mutation.newInsertOrUpdateBuilder("Venues") + .set("VenueId") + .to(4L) + .set("Revenue") + .to(new BigDecimal("35000")) + .build(), + Mutation.newInsertOrUpdateBuilder("Venues") + .set("VenueId") + .to(19L) + .set("Revenue") + .to(new BigDecimal("104500")) + .build(), + Mutation.newInsertOrUpdateBuilder("Venues") + .set("VenueId") + .to(42L) + .set("Revenue") + .to(new BigDecimal("99999999999999999999999999999.99")) + .build())); + String out = + runExample(() -> PgQueryWithNumericParameterSample.queryWithNumericParameter(client)); + assertThat(out).contains("4 35000"); + } +}