diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITCommitTimestampTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITCommitTimestampTest.java new file mode 100644 index 00000000000..be40f5986a4 --- /dev/null +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITCommitTimestampTest.java @@ -0,0 +1,319 @@ +/* + * Copyright 2017 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 com.google.cloud.spanner.SpannerMatchers.isSpannerException; +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.DatabaseClient; +import com.google.cloud.spanner.ErrorCode; +import com.google.cloud.spanner.IntegrationTest; +import com.google.cloud.spanner.IntegrationTestEnv; +import com.google.cloud.spanner.Key; +import com.google.cloud.spanner.Mutation; +import com.google.cloud.spanner.SpannerExceptionFactory; +import com.google.cloud.spanner.Struct; +import com.google.cloud.spanner.TimestampBound; +import com.google.cloud.spanner.Value; +import com.google.cloud.spanner.testing.RemoteSpannerHelper; +import com.google.common.collect.ImmutableList; +import java.time.Duration; +import java.time.Instant; +import java.util.Arrays; +import java.util.concurrent.ExecutionException; +import org.junit.Before; +import org.junit.ClassRule; +import org.junit.Rule; +import org.junit.Test; +import org.junit.experimental.categories.Category; +import org.junit.rules.ExpectedException; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +/** Integration test for commit timestamp of Cloud Spanner. */ +@Category(IntegrationTest.class) +@RunWith(JUnit4.class) +public class ITCommitTimestampTest { + @ClassRule public static IntegrationTestEnv env = new IntegrationTestEnv(); + @Rule public final ExpectedException expectedException = ExpectedException.none(); + private Database db; + private DatabaseClient client; + private DatabaseAdminClient dbAdminClient; + private RemoteSpannerHelper testHelper; + private String instanceId; + private String databaseId; + + @Before + public void setUp() throws Exception { + testHelper = env.getTestHelper(); + db = + testHelper.createTestDatabase( + "CREATE TABLE T (" + + "K STRING(MAX) NOT NULL," + + "T1 TIMESTAMP OPTIONS (allow_commit_timestamp = true)," + + "T2 TIMESTAMP OPTIONS (allow_commit_timestamp = true)," + + "T3 TIMESTAMP," + + ") PRIMARY KEY (K)"); + client = testHelper.getDatabaseClient(db); + dbAdminClient = testHelper.getClient().getDatabaseAdminClient(); + instanceId = testHelper.getInstanceId().getInstance(); + databaseId = db.getId().getDatabase(); + } + + private Timestamp write(Mutation m) { + return client.write(Arrays.asList(m)); + } + + private Struct readRow(DatabaseClient client, String table, Key key, String... columns) { + return client.singleUse(TimestampBound.strong()).readRow(table, key, Arrays.asList(columns)); + } + + @Test + public void writeCommitTimestamp() { + // 1. timestamps auto populated and returned should be the same + Timestamp commitTimestamp = + write( + Mutation.newInsertOrUpdateBuilder("T") + .set("K") + .to("a") + .set("T1") + .to(Value.COMMIT_TIMESTAMP) + .set("T2") + .to(Value.COMMIT_TIMESTAMP) + .build()); + Struct row = readRow(client, "T", Key.of("a"), "T1", "T2"); + assertThat(row.getTimestamp(0)).isEqualTo(commitTimestamp); + assertThat(row.getTimestamp(1)).isEqualTo(commitTimestamp); + + // 2. attempt to write CommitTimestamp to not enabled column should fail + // error_catalog error CommitTimestampOptionNotEnabled + expectedException.expect(isSpannerException(ErrorCode.FAILED_PRECONDITION)); + expectedException.expectMessage("allow_commit_timestamp column option is not"); + write( + Mutation.newInsertOrUpdateBuilder("T") + .set("K") + .to("a") + .set("T3") + .to(Value.COMMIT_TIMESTAMP) + .build()); + } + + @Test + public void consistency() { + // 1. timestamps populated are consistent in order + write( + Mutation.newInsertOrUpdateBuilder("T") + .set("K") + .to("a") + .set("T1") + .to(Value.COMMIT_TIMESTAMP) + .build()); + write( + Mutation.newInsertOrUpdateBuilder("T") + .set("K") + .to("b") + .set("T1") + .to(Value.COMMIT_TIMESTAMP) + .build()); + Struct row1 = readRow(client, "T", Key.of("a"), "T1"); + Struct row2 = readRow(client, "T", Key.of("b"), "T1"); + assertThat(row2.getTimestamp(0)).isGreaterThan(row1.getTimestamp(0)); + } + + @Test + public void schemaChangeTimestampInFuture() throws Exception { + write( + Mutation.newInsertOrUpdateBuilder("T") + .set("K") + .to("a") + .set("T3") + .to(Timestamp.MAX_VALUE) + .build()); + + // error_catalog error CommitTimestampNotInFuture + expectedException.expectCause(isSpannerException(ErrorCode.FAILED_PRECONDITION)); + expectedException.expectMessage("has a timestamp in the future at key"); + String statement = "ALTER TABLE T ALTER COLUMN T3 SET OPTIONS (allow_commit_timestamp=true)"; + try { + dbAdminClient + .updateDatabaseDdl(instanceId, databaseId, ImmutableList.of(statement), null) + .get(); + } catch (ExecutionException e) { + throw SpannerExceptionFactory.newSpannerException(e.getCause()); + } + } + + @Test + public void insertTimestampInFuture() { + // error_catalog error TimestampInFuture + expectedException.expect(isSpannerException(ErrorCode.FAILED_PRECONDITION)); + expectedException.expectMessage("in the future"); + write( + Mutation.newInsertOrUpdateBuilder("T") + .set("K") + .to("a") + .set("T1") + .to(Timestamp.MAX_VALUE) + .build()); + } + + @Test + public void invalidColumnOption() throws Exception { + // error_catalog error DDLStatementWithError + expectedException.expectCause(isSpannerException(ErrorCode.INVALID_ARGUMENT)); + expectedException.expectMessage("Option: bogus is unknown."); + String statement = "ALTER TABLE T ALTER COLUMN T3 SET OPTIONS (bogus=null)"; + dbAdminClient + .updateDatabaseDdl(instanceId, databaseId, ImmutableList.of(statement), null) + .get(); + } + + @Test + public void invalidColumnOptionValue() throws Exception { + // error_catalog error DDLStatementWithErrors + expectedException.expectCause(isSpannerException(ErrorCode.INVALID_ARGUMENT)); + expectedException.expectMessage("Errors parsing Spanner DDL statement"); + String statement = "ALTER TABLE T ALTER COLUMN T3 SET OPTIONS (allow_commit_timestamp=bogus)"; + dbAdminClient + .updateDatabaseDdl(instanceId, databaseId, ImmutableList.of(statement), null) + .get(); + } + + @Test + public void invalidColumnType() throws Exception { + // error_catalog error OptionErrorList + expectedException.expect(isSpannerException(ErrorCode.FAILED_PRECONDITION)); + expectedException.expectMessage("Option only allowed on TIMESTAMP columns"); + String statement = "ALTER TABLE T ADD COLUMN T4 INT64 OPTIONS (allow_commit_timestamp=true)"; + try { + dbAdminClient + .updateDatabaseDdl(instanceId, databaseId, ImmutableList.of(statement), null) + .get(); + } catch (ExecutionException e) { + throw SpannerExceptionFactory.newSpannerException(e.getCause()); + } + } + + private void alterColumnOption(String databaseId, String table, String opt) throws Exception { + String statement = + "ALTER TABLE " + + table + + " ALTER COLUMN ts" + + " SET OPTIONS (allow_commit_timestamp=" + + opt + + ")"; + dbAdminClient + .updateDatabaseDdl(instanceId, databaseId, ImmutableList.of(statement), null) + .get(); + } + + private void writeAndVerify(DatabaseClient client, Timestamp ts) { + Timestamp commitTimestamp = + client.write( + Arrays.asList( + Mutation.newInsertOrUpdateBuilder("T1").set("ts").to(ts).build(), + Mutation.newInsertOrUpdateBuilder("T2").set("ts").to(ts).build(), + Mutation.newInsertOrUpdateBuilder("T3").set("ts").to(ts).build())); + if (ts == Value.COMMIT_TIMESTAMP) { + ts = commitTimestamp; + } + assertThat(readRow(client, "T1", Key.of(ts), "ts").getTimestamp(0)).isEqualTo(ts); + assertThat(readRow(client, "T2", Key.of(ts), "ts").getTimestamp(0)).isEqualTo(ts); + assertThat(readRow(client, "T3", Key.of(ts), "ts").getTimestamp(0)).isEqualTo(ts); + } + + @Test + // 1) Write timestamps in the past + // 2) Set all interleaved tables allow_commmit_timestamp=true + // 3) Use commit timestamp in all tables + // 4) Set all interleaved tables allow_commmit_timestamp=null + // 5) Write timestamps in the future + public void interleavedTable() throws Exception { + Database db = + testHelper.createTestDatabase( + "CREATE TABLE T1 (ts TIMESTAMP) PRIMARY KEY (ts)", + "CREATE TABLE T2 (ts TIMESTAMP) PRIMARY KEY (ts), INTERLEAVE IN PARENT T1", + "CREATE TABLE T3 (ts TIMESTAMP) PRIMARY KEY (ts), INTERLEAVE IN PARENT T2"); + DatabaseClient client = testHelper.getDatabaseClient(db); + String databaseId = db.getId().getDatabase(); + + Timestamp timeNow = Timestamp.ofTimeMicroseconds(Instant.now().toEpochMilli() * 1000); + Timestamp timeFuture = + Timestamp.ofTimeMicroseconds( + Instant.now().plus(Duration.ofDays(300)).toEpochMilli() * 1000); + + writeAndVerify(client, timeNow); + + alterColumnOption(databaseId, "T1", "true"); + alterColumnOption(databaseId, "T2", "true"); + alterColumnOption(databaseId, "T3", "true"); + writeAndVerify(client, Value.COMMIT_TIMESTAMP); + + alterColumnOption(databaseId, "T1", "null"); + alterColumnOption(databaseId, "T2", "null"); + alterColumnOption(databaseId, "T3", "null"); + writeAndVerify(client, timeFuture); + } + + @Test + // In interleaved table, use of commit timestamp in child table is not allowed + // if parent tables are not allow_commmit_timestamp=true + public void interleavedTableHierarchy1() { + Database db = + testHelper.createTestDatabase( + "CREATE TABLE T1 (ts TIMESTAMP) PRIMARY KEY (ts)", + "CREATE TABLE T2 (ts TIMESTAMP) PRIMARY KEY (ts), INTERLEAVE IN PARENT T1", + "CREATE TABLE T3 (ts TIMESTAMP OPTIONS (allow_commit_timestamp = true)) " + + "PRIMARY KEY (ts), INTERLEAVE IN PARENT T2"); + DatabaseClient client = testHelper.getDatabaseClient(db); + db.getId().getDatabase(); + + // error_catalog error CommitTimestampOptionNotEnabled + expectedException.expect(isSpannerException(ErrorCode.FAILED_PRECONDITION)); + expectedException.expectMessage( + "corresponding shared key columns in this table's interleaved table hierarchy"); + client.write( + Arrays.asList( + Mutation.newInsertOrUpdateBuilder("T3").set("ts").to(Value.COMMIT_TIMESTAMP).build())); + } + + @Test + // In interleaved table, use of commit timestamp in parent table is not + // allowed if child tables are not allow_commmit_timestamp=true + public void interleavedTableHierarchy2() { + Database db = + testHelper.createTestDatabase( + "CREATE TABLE T1 (ts TIMESTAMP OPTIONS (allow_commit_timestamp = true)) " + + "PRIMARY KEY (ts)", + "CREATE TABLE T2 (ts TIMESTAMP) PRIMARY KEY (ts), INTERLEAVE IN PARENT T1", + "CREATE TABLE T3 (ts TIMESTAMP OPTIONS (allow_commit_timestamp = true)) " + + "PRIMARY KEY (ts), INTERLEAVE IN PARENT T2"); + DatabaseClient client = testHelper.getDatabaseClient(db); + db.getId().getDatabase(); + + // error_catalog error CommitTimestampOptionNotEnabled + expectedException.expect(isSpannerException(ErrorCode.FAILED_PRECONDITION)); + expectedException.expectMessage( + "corresponding shared key columns in this table's interleaved table hierarchy"); + client.write( + Arrays.asList( + Mutation.newInsertOrUpdateBuilder("T1").set("ts").to(Value.COMMIT_TIMESTAMP).build())); + } +}