diff --git a/README.md b/README.md index e66377e2ad9..f1aa6d81faa 100644 --- a/README.md +++ b/README.md @@ -258,6 +258,7 @@ Samples are in the [`samples/`](https://github.com/googleapis/java-spanner/tree/ | Add Json Column Sample | [source code](https://github.com/googleapis/java-spanner/blob/main/samples/snippets/src/main/java/com/example/spanner/AddJsonColumnSample.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/AddJsonColumnSample.java) | | Add Jsonb Column Sample | [source code](https://github.com/googleapis/java-spanner/blob/main/samples/snippets/src/main/java/com/example/spanner/AddJsonbColumnSample.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/AddJsonbColumnSample.java) | | Add Numeric Column Sample | [source code](https://github.com/googleapis/java-spanner/blob/main/samples/snippets/src/main/java/com/example/spanner/AddNumericColumnSample.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/AddNumericColumnSample.java) | +| Alter Table With Foreign Key Delete Cascade Sample | [source code](https://github.com/googleapis/java-spanner/blob/main/samples/snippets/src/main/java/com/example/spanner/AlterTableWithForeignKeyDeleteCascadeSample.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/AlterTableWithForeignKeyDeleteCascadeSample.java) | | Async Dml Example | [source code](https://github.com/googleapis/java-spanner/blob/main/samples/snippets/src/main/java/com/example/spanner/AsyncDmlExample.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/AsyncDmlExample.java) | | Async Query Example | [source code](https://github.com/googleapis/java-spanner/blob/main/samples/snippets/src/main/java/com/example/spanner/AsyncQueryExample.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/AsyncQueryExample.java) | | Async Query To List Async Example | [source code](https://github.com/googleapis/java-spanner/blob/main/samples/snippets/src/main/java/com/example/spanner/AsyncQueryToListAsyncExample.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/AsyncQueryToListAsyncExample.java) | @@ -276,9 +277,11 @@ Samples are in the [`samples/`](https://github.com/googleapis/java-spanner/tree/ | Create Instance Config Sample | [source code](https://github.com/googleapis/java-spanner/blob/main/samples/snippets/src/main/java/com/example/spanner/CreateInstanceConfigSample.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/CreateInstanceConfigSample.java) | | Create Instance Example | [source code](https://github.com/googleapis/java-spanner/blob/main/samples/snippets/src/main/java/com/example/spanner/CreateInstanceExample.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/CreateInstanceExample.java) | | Create Instance With Processing Units Example | [source code](https://github.com/googleapis/java-spanner/blob/main/samples/snippets/src/main/java/com/example/spanner/CreateInstanceWithProcessingUnitsExample.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/CreateInstanceWithProcessingUnitsExample.java) | +| Create Table With Foreign Key Delete Cascade Sample | [source code](https://github.com/googleapis/java-spanner/blob/main/samples/snippets/src/main/java/com/example/spanner/CreateTableWithForeignKeyDeleteCascadeSample.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/CreateTableWithForeignKeyDeleteCascadeSample.java) | | Custom Timeout And Retry Settings Example | [source code](https://github.com/googleapis/java-spanner/blob/main/samples/snippets/src/main/java/com/example/spanner/CustomTimeoutAndRetrySettingsExample.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/CustomTimeoutAndRetrySettingsExample.java) | | Delete Instance Config Sample | [source code](https://github.com/googleapis/java-spanner/blob/main/samples/snippets/src/main/java/com/example/spanner/DeleteInstanceConfigSample.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/DeleteInstanceConfigSample.java) | | Delete Using Dml Returning Sample | [source code](https://github.com/googleapis/java-spanner/blob/main/samples/snippets/src/main/java/com/example/spanner/DeleteUsingDmlReturningSample.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/DeleteUsingDmlReturningSample.java) | +| Drop Foreign Key Constraint Delete Cascade Sample | [source code](https://github.com/googleapis/java-spanner/blob/main/samples/snippets/src/main/java/com/example/spanner/DropForeignKeyConstraintDeleteCascadeSample.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/DropForeignKeyConstraintDeleteCascadeSample.java) | | Enable Fine Grained Access | [source code](https://github.com/googleapis/java-spanner/blob/main/samples/snippets/src/main/java/com/example/spanner/EnableFineGrainedAccess.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/EnableFineGrainedAccess.java) | | Get Commit Stats Sample | [source code](https://github.com/googleapis/java-spanner/blob/main/samples/snippets/src/main/java/com/example/spanner/GetCommitStatsSample.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/GetCommitStatsSample.java) | | Get Database Ddl Sample | [source code](https://github.com/googleapis/java-spanner/blob/main/samples/snippets/src/main/java/com/example/spanner/GetDatabaseDdlSample.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/GetDatabaseDdlSample.java) | diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITForeignKeyDeleteCascadeTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITForeignKeyDeleteCascadeTest.java new file mode 100644 index 00000000000..fc7c860267a --- /dev/null +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITForeignKeyDeleteCascadeTest.java @@ -0,0 +1,514 @@ +/* + * Copyright 2023 Google LLC + * + * 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.google.cloud.spanner.it; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; +import static org.junit.Assume.assumeFalse; + +import com.google.cloud.spanner.Database; +import com.google.cloud.spanner.DatabaseAdminClient; +import com.google.cloud.spanner.DatabaseClient; +import com.google.cloud.spanner.Dialect; +import com.google.cloud.spanner.ErrorCode; +import com.google.cloud.spanner.IntegrationTestEnv; +import com.google.cloud.spanner.Key; +import com.google.cloud.spanner.Mutation; +import com.google.cloud.spanner.ParallelIntegrationTest; +import com.google.cloud.spanner.ResultSet; +import com.google.cloud.spanner.SpannerException; +import com.google.cloud.spanner.Statement; +import com.google.cloud.spanner.testing.EmulatorSpannerHelper; +import com.google.common.collect.ImmutableList; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.IntStream; +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.ClassRule; +import org.junit.Test; +import org.junit.experimental.categories.Category; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; + +@Category(ParallelIntegrationTest.class) +@RunWith(Parameterized.class) +public class ITForeignKeyDeleteCascadeTest { + + @ClassRule public static IntegrationTestEnv env = new IntegrationTestEnv(); + + @Parameterized.Parameters(name = "Dialect = {0}") + public static List data() { + List params = new ArrayList<>(); + params.add(new DialectTestParameter(Dialect.GOOGLE_STANDARD_SQL)); + params.add(new DialectTestParameter(Dialect.POSTGRESQL)); + return params; + } + + private static final String TABLE_NAME_SINGER = "Singer"; + private static final String TABLE_NAME_CONCERT = "Concert"; + private static final String DELETE_RULE_COLUMN_NAME = "DELETE_RULE"; + + private static Database GOOGLE_STANDARD_SQL_DATABASE; + private static Database POSTGRESQL_DATABASE; + private static List dbs = new ArrayList<>(); + + @Parameterized.Parameter(0) + public DialectTestParameter dialect; + + @BeforeClass + public static void setUpDatabase() { + if (!EmulatorSpannerHelper.isUsingEmulator()) { + GOOGLE_STANDARD_SQL_DATABASE = + env.getTestHelper() + .createTestDatabase( + ImmutableList.of( + "CREATE TABLE Singer (\n" + + " singer_id INT64 NOT NULL,\n" + + " first_name STRING(1024),\n" + + ") PRIMARY KEY(singer_id)\n", + "CREATE TABLE Concert (\n" + + " venue_id INT64 NOT NULL,\n" + + " singer_id INT64 NOT NULL,\n" + + " CONSTRAINT Fk_Concert_Singer FOREIGN KEY (singer_id) REFERENCES Singer (singer_id) ON DELETE CASCADE" + + ") PRIMARY KEY(venue_id, singer_id)")); + POSTGRESQL_DATABASE = + env.getTestHelper() + .createTestDatabase( + Dialect.POSTGRESQL, + ImmutableList.of( + "CREATE TABLE Singer (\n" + + " singer_id BIGINT PRIMARY KEY,\n" + + " first_name VARCHAR\n" + + ")", + "CREATE TABLE Concert (\n" + + " venue_id BIGINT NOT NULL,\n" + + " singer_id BIGINT NOT NULL,\n" + + " PRIMARY KEY (venue_id, singer_id),\n" + + " CONSTRAINT Fk_Concert_Singer FOREIGN KEY (singer_id) REFERENCES Singer (singer_id) ON DELETE CASCADE\n" + + " )")); + + dbs.add(GOOGLE_STANDARD_SQL_DATABASE); + dbs.add(POSTGRESQL_DATABASE); + } + } + + @AfterClass + public static void tearDown() { + for (Database db : dbs) { + db.drop(); + } + dbs.clear(); + } + + @Test + public void testForeignKeyDeleteCascadeConstraints_withCreateDDLStatements() { + assumeFalse( + "Emulator does not yet support foreign key delete cascade", + EmulatorSpannerHelper.isUsingEmulator()); + + final DatabaseClient databaseClient = getCreatedDatabaseClient(); + try (final ResultSet rs = + databaseClient + .singleUse() + .executeQuery( + Statement.of( + "SELECT DELETE_RULE\n" + + "FROM INFORMATION_SCHEMA.REFERENTIAL_CONSTRAINTS\n" + + "WHERE CONSTRAINT_NAME ='Fk_Concert_Singer'"))) { + while (rs.next()) { + assertEquals(rs.getString(DELETE_RULE_COLUMN_NAME), "CASCADE"); + } + } + } + + @Test + public void testForeignKeyDeleteCascadeConstraints_withAlterDDLStatements() throws Exception { + assumeFalse( + "Emulator does not yet support foreign key delete cascade", + EmulatorSpannerHelper.isUsingEmulator()); + // Creating new tables within this test to ensure we don't pollute tables used by other tests in + // this class. + List createStatements; + if (dialect.dialect == Dialect.POSTGRESQL) { + createStatements = + ImmutableList.of( + "CREATE TABLE Singer (\n" + + " singer_id BIGINT PRIMARY KEY,\n" + + " first_name VARCHAR\n" + + ")", + "CREATE TABLE ConcertV2 (\n" + + " venue_id BIGINT NOT NULL,\n" + + " singer_id BIGINT NOT NULL,\n" + + " PRIMARY KEY (venue_id, singer_id)\n" + + " )", + "ALTER TABLE ConcertV2 " + + "ADD CONSTRAINT Fk_Concert_Singer_V2 FOREIGN KEY(singer_id) REFERENCES Singer(singer_id) " + + "ON DELETE CASCADE"); + } else { + createStatements = + ImmutableList.of( + "CREATE TABLE Singer (\n" + + " singer_id INT64 NOT NULL,\n" + + " first_name STRING(1024),\n" + + ") PRIMARY KEY(singer_id)\n", + "CREATE TABLE ConcertV2 (\n" + + " venue_id INT64 NOT NULL,\n" + + " singer_id INT64 NOT NULL,\n" + + ") PRIMARY KEY(venue_id, singer_id)", + "ALTER TABLE ConcertV2 " + + "ADD CONSTRAINT Fk_Concert_Singer_V2 FOREIGN KEY(singer_id) REFERENCES Singer(singer_id) " + + "ON DELETE CASCADE"); + } + final Database createdDatabase = + env.getTestHelper().createTestDatabase(dialect.dialect, createStatements); + dbs.add(createdDatabase); + + final DatabaseClient databaseClient = getCreatedDatabaseClient(); + + try (final ResultSet rs = + databaseClient + .singleUse() + .executeQuery( + Statement.of( + "SELECT DELETE_RULE\n" + + "FROM INFORMATION_SCHEMA.REFERENTIAL_CONSTRAINTS\n" + + "WHERE CONSTRAINT_NAME ='Fk_Concert_Singer'"))) { + while (rs.next()) { + assertEquals(rs.getString(DELETE_RULE_COLUMN_NAME), "CASCADE"); + } + } + + // remove the foreign key delete cascade constraint + getDatabaseAdminClient() + .updateDatabaseDdl( + env.getTestHelper().getInstanceId().getInstance(), + createdDatabase.getId().getDatabase(), + ImmutableList.of( + "ALTER TABLE ConcertV2\n" + "DROP CONSTRAINT Fk_Concert_Singer_V2", + "ALTER TABLE ConcertV2 " + + "ADD CONSTRAINT Fk_Concert_Singer_V2 FOREIGN KEY(singer_id) REFERENCES Singer(singer_id) "), + null) + .get(); + + try (final ResultSet rs = + databaseClient + .singleUse() + .executeQuery( + Statement.of( + "SELECT DELETE_RULE\n" + + "FROM INFORMATION_SCHEMA.REFERENTIAL_CONSTRAINTS\n" + + "WHERE CONSTRAINT_NAME ='Fk_Concert_Singer_V2'"))) { + while (rs.next()) { + assertEquals(rs.getString(DELETE_RULE_COLUMN_NAME), "NO ACTION"); + } + } + } + + @Test + public void testForeignKeyDeleteCascadeConstraints_verifyValidInsertions() { + assumeFalse( + "Emulator does not yet support foreign key delete cascade", + EmulatorSpannerHelper.isUsingEmulator()); + + final DatabaseClient databaseClient = getCreatedDatabaseClient(); + final String singerInsertStatement = + "INSERT INTO Singer (singer_id, first_name) VALUES (" + generateQueryParameters(2) + ")"; + final Statement singerInsertStatementWithValues = + Statement.newBuilder(singerInsertStatement) + // Use 'p1' to bind to the parameter with index 1 etc. + .bind("p1") + .to(1L) + .bind("p2") + .to("singerName") + .build(); + + final String concertInsertStatement = + "INSERT INTO Concert (venue_id, singer_id) VALUES (" + generateQueryParameters(2) + ")"; + final Statement concertInsertStatementWithValues = + Statement.newBuilder(concertInsertStatement) + // Use 'p1' to bind to the parameter with index 1 etc. + .bind("p1") + .to(1L) + .bind("p2") + .to(1L) + .build(); + + // successful inserts into referenced and referencing tables + databaseClient + .readWriteTransaction() + .run( + transaction -> { + transaction.batchUpdate( + ImmutableList.of( + singerInsertStatementWithValues, concertInsertStatementWithValues)); + return null; + }); + + try (ResultSet resultSet = + databaseClient + .singleUse() + .executeQuery(Statement.of("SELECT * FROM " + TABLE_NAME_SINGER))) { + + resultSet.next(); + assertEquals(1, resultSet.getLong("singer_id")); + assertEquals("singerName", resultSet.getString("first_name")); + + assertFalse(resultSet.next()); + } + + try (ResultSet resultSet = + databaseClient + .singleUse() + .executeQuery(Statement.of("SELECT * FROM " + TABLE_NAME_CONCERT))) { + + resultSet.next(); + assertEquals(1, resultSet.getLong("singer_id")); + assertEquals(1, resultSet.getLong("venue_id")); + + assertFalse(resultSet.next()); + } + } + + @Test + public void testForeignKeyDeleteCascadeConstraints_verifyInvalidInsertions() { + assumeFalse( + "Emulator does not yet support foreign key delete cascade", + EmulatorSpannerHelper.isUsingEmulator()); + + final DatabaseClient databaseClient = getCreatedDatabaseClient(); + + // unsuccessful inserts into referencing tables when foreign key is not inserted into referenced + // table + final String concertInsertStatement = + "INSERT INTO Concert (venue_id, singer_id) VALUES (" + generateQueryParameters(2) + ")"; + final Statement concertInsertStatementWithInvalidValues = + Statement.newBuilder(concertInsertStatement) + // Use 'p1' to bind to the parameter with index 1 etc. + .bind("p1") + .to(2L) + .bind("p2") + .to(2L) + .build(); + + SpannerException ex = + assertThrows( + SpannerException.class, + () -> + databaseClient + .readWriteTransaction() + .run( + transaction -> { + transaction.executeUpdate(concertInsertStatementWithInvalidValues); + return null; + })); + assertEquals(ex.getErrorCode(), ErrorCode.FAILED_PRECONDITION); + assertTrue(ex.getMessage().contains("Cannot find referenced values")); + } + + @Test + public void testForeignKeyDeleteCascadeConstraints_forDeletions() { + assumeFalse( + "Emulator does not yet support foreign key delete cascade", + EmulatorSpannerHelper.isUsingEmulator()); + + final DatabaseClient databaseClient = getCreatedDatabaseClient(); + + final String singerInsertStatement = + "INSERT INTO Singer (singer_id, first_name) VALUES (" + generateQueryParameters(2) + ")"; + final Statement singerInsertStatementWithValues = + Statement.newBuilder(singerInsertStatement) + // Use 'p1' to bind to the parameter with index 1 etc. + .bind("p1") + .to(3L) + .bind("p2") + .to("singerName") + .build(); + + final String concertInsertStatement = + "INSERT INTO Concert (venue_id, singer_id) VALUES (" + generateQueryParameters(2) + ")"; + final Statement concertInsertStatementWithValues = + Statement.newBuilder(concertInsertStatement) + // Use 'p1' to bind to the parameter with index 1 etc. + .bind("p1") + .to(3L) + .bind("p2") + .to(3L) + .build(); + + // successful inserts into referenced and referencing tables + databaseClient + .readWriteTransaction() + .run( + transaction -> { + transaction.batchUpdate( + ImmutableList.of( + singerInsertStatementWithValues, concertInsertStatementWithValues)); + return null; + }); + + // execute delete + final Statement singerDeleteStatementWithValues = + Statement.newBuilder("DELETE FROM Singer WHERE singer_id = " + generateQueryParameters(1)) + // Use 'p1' to bind to the parameter with index 1 etc. + .bind("p1") + .to(3L) + .build(); + databaseClient + .readWriteTransaction() + .run( + transaction -> { + transaction.executeUpdate(singerDeleteStatementWithValues); + return null; + }); + + try (ResultSet resultSet = + databaseClient + .singleUse() + .executeQuery(Statement.of("SELECT * FROM " + TABLE_NAME_SINGER))) { + assertFalse(resultSet.next()); + } + + try (ResultSet resultSet = + databaseClient + .singleUse() + .executeQuery(Statement.of("SELECT * FROM " + TABLE_NAME_CONCERT))) { + assertFalse(resultSet.next()); + } + } + + @Test + public void testForeignKeyDeleteCascadeConstraints_forMutations_onConflictDueToParentTable() { + assumeFalse( + "Emulator does not yet support foreign key delete cascade", + EmulatorSpannerHelper.isUsingEmulator()); + + final DatabaseClient databaseClient = getCreatedDatabaseClient(); + + // inserting and deleting the referenced key within the same mutation are considered + // conflicting operations, thus this results in an exception. + SpannerException ex = + assertThrows( + SpannerException.class, + () -> + databaseClient + .readWriteTransaction() + .run( + transaction -> { + transaction.buffer( + Arrays.asList( + Mutation.newInsertBuilder("Singer") + .set("singer_id") + .to(4L) + .set("first_name") + .to("singerName") + .build(), + Mutation.delete("Singer", Key.of(4L)))); + return null; + })); + assertEquals(ex.getErrorCode(), ErrorCode.FAILED_PRECONDITION); + } + + @Test + public void testForeignKeyDeleteCascadeConstraints_forMutations_onConflictsDueToChildTable() { + assumeFalse( + "Emulator does not yet support foreign key delete cascade", + EmulatorSpannerHelper.isUsingEmulator()); + + final DatabaseClient databaseClient = getCreatedDatabaseClient(); + + // referencing a foreign key in child table and deleting the referenced key in parent table + // within the same mutations are considered conflicting operations. + final String singerInsertStatement = + "INSERT INTO Singer (singer_id, first_name) VALUES (" + generateQueryParameters(2) + ")"; + final Statement singerInsertStatementWithValues = + Statement.newBuilder(singerInsertStatement) + // Use 'p1' to bind to the parameter with index 1 etc. + .bind("p1") + .to(5L) + .bind("p2") + .to("singerName") + .build(); + + databaseClient + .readWriteTransaction() + .run( + transaction -> { + transaction.executeUpdate(singerInsertStatementWithValues); + return null; + }); + SpannerException ex = + assertThrows( + SpannerException.class, + () -> + databaseClient + .readWriteTransaction() + .run( + transaction -> { + transaction.buffer( + Arrays.asList( + Mutation.newInsertBuilder("Concert") + .set("first_name") + .to(5L) + .set("singer_id") + .to(5L) + .build(), + Mutation.delete("Singer", Key.of(5L)))); + return null; + })); + } + + private DatabaseAdminClient getDatabaseAdminClient() { + return env.getTestHelper().getClient().getDatabaseAdminClient(); + } + + private DatabaseClient getCreatedDatabaseClient() { + if (dialect.dialect == Dialect.POSTGRESQL) { + return env.getTestHelper().getDatabaseClient(this.POSTGRESQL_DATABASE); + } + return env.getTestHelper().getDatabaseClient(this.GOOGLE_STANDARD_SQL_DATABASE); + } + + /** + * Returns '@p1, @p2, ..., @pNumParams' for GoogleSQL and $1, $2, ..., $NumParams' for PostgreSQL + * + * @param numParams + * @return + */ + private String generateQueryParameters(final int numParams) { + final List params; + if (dialect.dialect == Dialect.POSTGRESQL) { + params = + IntStream.range(1, numParams + 1) + .mapToObj(paramIndex -> "$" + paramIndex) + .collect(Collectors.toList()); + + } else { + params = + IntStream.range(1, numParams + 1) + .mapToObj(paramIndex -> "@p" + paramIndex) + .collect(Collectors.toList()); + } + if (params.size() == 1) { + return params.get(0); + } + return String.join(",", params); + } +} diff --git a/samples/snippets/src/main/java/com/example/spanner/AlterTableWithForeignKeyDeleteCascadeSample.java b/samples/snippets/src/main/java/com/example/spanner/AlterTableWithForeignKeyDeleteCascadeSample.java new file mode 100644 index 00000000000..1caf26fb289 --- /dev/null +++ b/samples/snippets/src/main/java/com/example/spanner/AlterTableWithForeignKeyDeleteCascadeSample.java @@ -0,0 +1,59 @@ +/* + * Copyright 2023 Google LLC + * + * 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_alter_table_with_foreign_key_delete_cascade] +import com.google.cloud.spanner.DatabaseAdminClient; +import com.google.cloud.spanner.Spanner; +import com.google.cloud.spanner.SpannerOptions; +import com.google.common.collect.ImmutableList; + +class AlterTableWithForeignKeyDeleteCascadeSample { + + static void alterForeignKeyDeleteCascadeConstraint() { + // 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()) { + DatabaseAdminClient adminClient = spanner.getDatabaseAdminClient(); + alterForeignKeyDeleteCascadeConstraint(adminClient, instanceId, databaseId); + } + } + + static void alterForeignKeyDeleteCascadeConstraint( + DatabaseAdminClient adminClient, String instanceId, String databaseId) { + adminClient.updateDatabaseDdl( + instanceId, + databaseId, + ImmutableList.of( + "ALTER TABLE ShoppingCarts\n" + + " ADD CONSTRAINT FKShoppingCartsCustomerName\n" + + " FOREIGN KEY (CustomerName)\n" + + " REFERENCES Customers(CustomerName)\n" + + " ON DELETE CASCADE\n"), + null); + System.out.printf( + String.format( + "Altered ShoppingCarts table with FKShoppingCartsCustomerName\n" + + "foreign key constraint on database %s on instance %s", + databaseId, instanceId)); + } +} +// [END spanner_alter_table_with_foreign_key_delete_cascade] diff --git a/samples/snippets/src/main/java/com/example/spanner/CreateTableWithForeignKeyDeleteCascadeSample.java b/samples/snippets/src/main/java/com/example/spanner/CreateTableWithForeignKeyDeleteCascadeSample.java new file mode 100644 index 00000000000..dda09591ee4 --- /dev/null +++ b/samples/snippets/src/main/java/com/example/spanner/CreateTableWithForeignKeyDeleteCascadeSample.java @@ -0,0 +1,66 @@ +/* + * Copyright 2023 Google LLC + * + * 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_create_table_with_foreign_key_delete_cascade] +import com.google.cloud.spanner.DatabaseAdminClient; +import com.google.cloud.spanner.Spanner; +import com.google.cloud.spanner.SpannerOptions; +import com.google.common.collect.ImmutableList; + +class CreateTableWithForeignKeyDeleteCascadeSample { + + static void createForeignKeyDeleteCascadeConstraint() { + // 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()) { + DatabaseAdminClient adminClient = spanner.getDatabaseAdminClient(); + createForeignKeyDeleteCascadeConstraint(adminClient, instanceId, databaseId); + } + } + + static void createForeignKeyDeleteCascadeConstraint( + DatabaseAdminClient adminClient, String instanceId, String databaseId) { + adminClient.updateDatabaseDdl( + instanceId, + databaseId, + ImmutableList.of( + "CREATE TABLE Customers (\n" + + " CustomerId INT64 NOT NULL,\n" + + " CustomerName STRING(62) NOT NULL,\n" + + " ) PRIMARY KEY (CustomerId)", + "CREATE TABLE ShoppingCarts (\n" + + " CartId INT64 NOT NULL,\n" + + " CustomerId INT64 NOT NULL,\n" + + " CustomerName STRING(62) NOT NULL,\n" + + " CONSTRAINT FKShoppingCartsCustomerId FOREIGN KEY (CustomerId)\n" + + " REFERENCES Customers (CustomerId) ON DELETE CASCADE\n" + + " ) PRIMARY KEY (CartId)\n"), + null); + + System.out.printf( + String.format( + "Created Customers and ShoppingCarts table with FKShoppingCartsCustomerId\n" + + "foreign key constraint on database %s on instance %s\n", + databaseId, instanceId)); + } +} +// [END spanner_create_table_with_foreign_key_delete_cascade] diff --git a/samples/snippets/src/main/java/com/example/spanner/DropForeignKeyConstraintDeleteCascadeSample.java b/samples/snippets/src/main/java/com/example/spanner/DropForeignKeyConstraintDeleteCascadeSample.java new file mode 100644 index 00000000000..13f39d129f7 --- /dev/null +++ b/samples/snippets/src/main/java/com/example/spanner/DropForeignKeyConstraintDeleteCascadeSample.java @@ -0,0 +1,57 @@ +/* + * Copyright 2023 Google LLC + * + * 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_drop_foreign_key_constraint_delete_cascade] +import com.google.cloud.spanner.DatabaseAdminClient; +import com.google.cloud.spanner.Spanner; +import com.google.cloud.spanner.SpannerOptions; +import com.google.common.collect.ImmutableList; + +class DropForeignKeyConstraintDeleteCascadeSample { + + static void deleteForeignKeyDeleteCascadeConstraint() { + // 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()) { + DatabaseAdminClient adminClient = spanner.getDatabaseAdminClient(); + deleteForeignKeyDeleteCascadeConstraint(adminClient, instanceId, databaseId); + } + } + + static void deleteForeignKeyDeleteCascadeConstraint( + DatabaseAdminClient adminClient, String instanceId, String databaseId) { + adminClient.updateDatabaseDdl( + instanceId, + databaseId, + ImmutableList.of( + "ALTER TABLE ShoppingCarts\n" + + " DROP CONSTRAINT FKShoppingCartsCustomerName\n"), + null); + + System.out.printf( + String.format( + "Altered ShoppingCarts table to drop FKShoppingCartsCustomerName\n" + + "foreign key constraint on database %s on instance %s\n", + databaseId, instanceId)); + } +} +// [END spanner_drop_foreign_key_constraint_delete_cascade] diff --git a/samples/snippets/src/test/java/com/example/spanner/AlterTableWithForeignKeyDeleteCascadeSampleIT.java b/samples/snippets/src/test/java/com/example/spanner/AlterTableWithForeignKeyDeleteCascadeSampleIT.java new file mode 100644 index 00000000000..f7194247585 --- /dev/null +++ b/samples/snippets/src/test/java/com/example/spanner/AlterTableWithForeignKeyDeleteCascadeSampleIT.java @@ -0,0 +1,65 @@ +/* + * Copyright 2023 Google LLC + * + * 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 org.junit.Assert.assertTrue; + +import java.util.Arrays; +import java.util.concurrent.TimeUnit; +import org.junit.Test; + +public class AlterTableWithForeignKeyDeleteCascadeSampleIT extends SampleTestBase { + + @Test + public void testAlterTableWithForeignKeyDeleteCascade() throws Exception { + + // Creates database + final String databaseId = idGenerator.generateDatabaseId(); + databaseAdminClient + .createDatabase( + instanceId, + databaseId, + Arrays.asList( + "CREATE TABLE Customers (\n" + + " CustomerId INT64 NOT NULL,\n" + + " CustomerName STRING(62) NOT NULL,\n" + + " ) PRIMARY KEY (CustomerId)", + "CREATE TABLE ShoppingCarts (\n" + + " CartId INT64 NOT NULL,\n" + + " CustomerId INT64 NOT NULL,\n" + + " CustomerName STRING(62) NOT NULL,\n" + + " CONSTRAINT FKShoppingCartsCustomerId" + + " FOREIGN KEY (CustomerId)\n" + + " REFERENCES Customers (CustomerId)\n" + + " ) PRIMARY KEY (CartId)\n")) + .get(5, TimeUnit.MINUTES); + + // Runs sample + final String out = + SampleRunner.runSample( + () -> + AlterTableWithForeignKeyDeleteCascadeSample.alterForeignKeyDeleteCascadeConstraint( + databaseAdminClient, instanceId, databaseId)); + + assertTrue( + "Expected to have created database " + + databaseId + + " with tables containing " + + "foreign key constraints.", + out.contains("Altered ShoppingCarts table " + "with FKShoppingCartsCustomerName")); + } +} diff --git a/samples/snippets/src/test/java/com/example/spanner/CreateTableWithForeignKeyDeleteCascadeSampleIT.java b/samples/snippets/src/test/java/com/example/spanner/CreateTableWithForeignKeyDeleteCascadeSampleIT.java new file mode 100644 index 00000000000..b2dd6638cea --- /dev/null +++ b/samples/snippets/src/test/java/com/example/spanner/CreateTableWithForeignKeyDeleteCascadeSampleIT.java @@ -0,0 +1,52 @@ +/* + * Copyright 2023 Google LLC + * + * 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 org.junit.Assert.assertTrue; + +import java.util.Arrays; +import java.util.concurrent.TimeUnit; +import org.junit.Test; + +public class CreateTableWithForeignKeyDeleteCascadeSampleIT extends SampleTestBase { + + @Test + public void testCreateTableWithForeignKeyDeleteCascade() throws Exception { + + // Creates database + final String databaseId = idGenerator.generateDatabaseId(); + databaseAdminClient + .createDatabase(instanceId, databaseId, Arrays.asList()) + .get(5, TimeUnit.MINUTES); + + // Runs sample + final String out = + SampleRunner.runSample( + () -> + CreateTableWithForeignKeyDeleteCascadeSample + .createForeignKeyDeleteCascadeConstraint( + databaseAdminClient, instanceId, databaseId)); + + assertTrue( + "Expected to have created database " + + databaseId + + " with tables containing " + + "foreign key constraints.", + out.contains( + "Created Customers and ShoppingCarts table " + "with FKShoppingCartsCustomerId")); + } +} diff --git a/samples/snippets/src/test/java/com/example/spanner/DropForeignKeyConstraintDeleteCascadeSampleIT.java b/samples/snippets/src/test/java/com/example/spanner/DropForeignKeyConstraintDeleteCascadeSampleIT.java new file mode 100644 index 00000000000..1c58daded18 --- /dev/null +++ b/samples/snippets/src/test/java/com/example/spanner/DropForeignKeyConstraintDeleteCascadeSampleIT.java @@ -0,0 +1,63 @@ +/* + * Copyright 2023 Google LLC + * + * 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 org.junit.Assert.assertTrue; + +import java.util.Arrays; +import java.util.concurrent.TimeUnit; +import org.junit.Test; + +public class DropForeignKeyConstraintDeleteCascadeSampleIT extends SampleTestBase { + + @Test + public void testDropForeignKeyConstraintDeleteCascade() throws Exception { + + // Creates database + final String databaseId = idGenerator.generateDatabaseId(); + databaseAdminClient + .createDatabase( + instanceId, + databaseId, + Arrays.asList( + "CREATE TABLE Customers (\n" + + " CustomerId INT64 NOT NULL,\n" + + " CustomerName STRING(62) NOT NULL,\n" + + " ) PRIMARY KEY (CustomerId)", + "CREATE TABLE ShoppingCarts (\n" + + " CartId INT64 NOT NULL,\n" + + " CustomerId INT64 NOT NULL,\n" + + " CustomerName STRING(62) NOT NULL,\n" + + " CONSTRAINT FKShoppingCartsCustomerName" + + " FOREIGN KEY (CustomerName)\n" + + " REFERENCES Customers (CustomerName) ON DELETE CASCADE\n" + + " ) PRIMARY KEY (CartId)\n")) + .get(5, TimeUnit.MINUTES); + + // Runs sample + final String out = + SampleRunner.runSample( + () -> + DropForeignKeyConstraintDeleteCascadeSample.deleteForeignKeyDeleteCascadeConstraint( + databaseAdminClient, instanceId, databaseId)); + + assertTrue( + "Expected to have dropped foreign-key constraints from tables in created database " + + databaseId, + out.contains("Altered ShoppingCarts table to drop FKShoppingCartsCustomerName")); + } +} diff --git a/samples/snippets/src/test/java/com/example/spanner/SampleTestBase.java b/samples/snippets/src/test/java/com/example/spanner/SampleTestBase.java index 47c6f4f0230..39424c8d080 100644 --- a/samples/snippets/src/test/java/com/example/spanner/SampleTestBase.java +++ b/samples/snippets/src/test/java/com/example/spanner/SampleTestBase.java @@ -44,10 +44,14 @@ public class SampleTestBase { @BeforeClass public static void beforeClass() { - final SpannerOptions options = SpannerOptions + final String serverUrl = ""; + final SpannerOptions.Builder optionsBuilder = SpannerOptions .newBuilder() - .setAutoThrottleAdministrativeRequests() - .build(); + .setAutoThrottleAdministrativeRequests(); + if (!serverUrl.isEmpty()) { + optionsBuilder.setHost(serverUrl); + } + final SpannerOptions options = optionsBuilder.build(); projectId = options.getProjectId(); spanner = options.getService(); databaseAdminClient = spanner.getDatabaseAdminClient();