diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/CassandraAdminTemplate.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/CassandraAdminTemplate.java index 31e72bf1f..594d46ebe 100644 --- a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/CassandraAdminTemplate.java +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/CassandraAdminTemplate.java @@ -44,6 +44,7 @@ * @author John Blum * @author Vagif Zeynalov * @author Mikhail Polivakha + * @author Seungho Kang */ public class CassandraAdminTemplate extends CassandraTemplate implements CassandraAdminOperations { @@ -115,8 +116,10 @@ public void createTable(boolean ifNotExists, CqlIdentifier tableName, Class e if (!CollectionUtils.isEmpty(optionsByName)) { optionsByName.forEach((key, value) -> { - TableOption tableOption = TableOption.valueOfIgnoreCase(key); - if (tableOption.requiresValue()) { + TableOption tableOption = TableOption.findByNameIgnoreCase(key); + if (tableOption == null) { + addRawTableOption(key, value, createTableSpecification); + } else if (tableOption.requiresValue()) { createTableSpecification.with(tableOption, value); } else { createTableSpecification.with(tableOption); @@ -127,6 +130,14 @@ public void createTable(boolean ifNotExists, CqlIdentifier tableName, Class e getCqlOperations().execute(CqlGenerator.toCql(createTableSpecification)); } + private void addRawTableOption(String key, Object value, CreateTableSpecification createTableSpecification) { + if (value instanceof String) { + createTableSpecification.with(key, value, true, true); + return; + } + createTableSpecification.with(key, value, false, false); + } + @Override public void dropTable(Class entityClass) { dropTable(getTableName(entityClass)); diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/cql/keyspace/TableOption.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/cql/keyspace/TableOption.java index 3f2bfd2d1..3e971b035 100644 --- a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/cql/keyspace/TableOption.java +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/cql/keyspace/TableOption.java @@ -27,6 +27,7 @@ * @author Matthew T. Adams * @author Mark Paluch * @author Mikhail Polivakha + * @author Seungho Kang * @see CompactionOption * @see CompressionOption * @see CachingOption @@ -77,7 +78,39 @@ public enum TableOption implements Option { /** * {@code gc_grace_seconds} */ - GC_GRACE_SECONDS("gc_grace_seconds", Long.class, true, false, false); + GC_GRACE_SECONDS("gc_grace_seconds", Long.class, true, false, false), + /** + * {@code default_time_to_live} + */ + DEFAULT_TIME_TO_LIVE("default_time_to_live", Long.class, true, false, false), + /** + * {@code cdc} + */ + CDC("cdc", Boolean.class, true, false, false), + /** + * {@code speculative_retry} + */ + SPECULATIVE_RETRY("speculative_retry", String.class, true, true, true), + /** + * {@code memtable_flush_period_in_ms} + */ + MEMTABLE_FLUSH_PERIOD_IN_MS("memtable_flush_period_in_ms", Long.class, true, false, false), + /** + * {@code crc_check_chance} + */ + CRC_CHECK_CHANCE("crc_check_chance", Double.class, true, false, false), + /** + * {@code min_index_interval} + */ + MIN_INDEX_INTERVAL("min_index_interval", Long.class, true, false, false), + /** + * {@code max_index_interval} + */ + MAX_INDEX_INTERVAL("max_index_interval", Long.class, true, false, false), + /** + * {@code read_repair} + */ + READ_REPAIR("read_repair", String.class, true, true, true); private Option delegate; @@ -102,6 +135,23 @@ public static TableOption valueOfIgnoreCase(String optionName) { throw new IllegalArgumentException(String.format("Unable to recognize specified Table option '%s'", optionName)); } + /** + * Look up {@link TableOption} by name using case-insensitive lookups. + * + * @param optionName name of the option. + * @return the matching {@link TableOption}, or {@code null} if no match is found + * @since 4.5.2 + */ + @Nullable + public static TableOption findByNameIgnoreCase(String optionName) { + for (TableOption value : values()) { + if (value.getName().equalsIgnoreCase(optionName)) { + return value; + } + } + return null; + } + @Override public Class getType() { return this.delegate.getType(); diff --git a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/CassandraAdminTemplateIntegrationTests.java b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/CassandraAdminTemplateIntegrationTests.java index 499832929..14770be1c 100755 --- a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/CassandraAdminTemplateIntegrationTests.java +++ b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/CassandraAdminTemplateIntegrationTests.java @@ -43,6 +43,7 @@ * * @author Mark Paluch * @author Mikhail Polivakha + * @author Seungho Kang */ class CassandraAdminTemplateIntegrationTests extends AbstractKeyspaceCreatingIntegrationTests { @@ -66,11 +67,19 @@ private KeyspaceMetadata getKeyspaceMetadata() { return getSession().getKeyspace().flatMap(metadata::getKeyspace).get(); } - @Test // GH-359 + @Test // GH-359, GH-1584 void shouldApplyTableOptions() { Map options = Map.of(TableOption.COMMENT.getName(), "This is comment for table", // - TableOption.BLOOM_FILTER_FP_CHANCE.getName(), "0.3"); + TableOption.BLOOM_FILTER_FP_CHANCE.getName(), "0.3", // + TableOption.DEFAULT_TIME_TO_LIVE.getName(), "864000", // + TableOption.CDC.getName(), true, // + TableOption.SPECULATIVE_RETRY.getName(), "90percentile", // + TableOption.MEMTABLE_FLUSH_PERIOD_IN_MS.getName(), "1000", // + TableOption.CRC_CHECK_CHANCE.getName(), "0.9", // + TableOption.MIN_INDEX_INTERVAL.getName(), "128", // + TableOption.MAX_INDEX_INTERVAL.getName(), "2048", // + TableOption.READ_REPAIR.getName(), "BLOCKING"); CqlIdentifier tableName = CqlIdentifier.fromCql("someTable"); cassandraAdminTemplate.createTable(true, tableName, SomeTable.class, options); @@ -78,10 +87,64 @@ void shouldApplyTableOptions() { TableMetadata someTable = getKeyspaceMetadata().getTables().get(tableName); assertThat(someTable).isNotNull(); - assertThat(someTable.getOptions().get(CqlIdentifier.fromCql(TableOption.COMMENT.getName()))) + assertThat(someTable.getOptions().get(CqlIdentifier.fromCql(TableOption.COMMENT.getName()))) // + .isEqualTo("This is comment for table"); + assertThat(someTable.getOptions().get(CqlIdentifier.fromCql(TableOption.BLOOM_FILTER_FP_CHANCE.getName()))) // + .isEqualTo(0.3); + assertThat(someTable.getOptions().get(CqlIdentifier.fromCql(TableOption.DEFAULT_TIME_TO_LIVE.getName()))) // + .isEqualTo(864_000); + assertThat(someTable.getOptions().get(CqlIdentifier.fromCql(TableOption.SPECULATIVE_RETRY.getName()))) // + .isEqualTo("90p"); + assertThat(someTable.getOptions().get(CqlIdentifier.fromCql(TableOption.MEMTABLE_FLUSH_PERIOD_IN_MS.getName()))) // + .isEqualTo(1000); + assertThat(someTable.getOptions().get(CqlIdentifier.fromCql(TableOption.CRC_CHECK_CHANCE.getName()))) // + .isEqualTo(0.9); + assertThat(someTable.getOptions().get(CqlIdentifier.fromCql(TableOption.MIN_INDEX_INTERVAL.getName()))) // + .isEqualTo(128); + assertThat(someTable.getOptions().get(CqlIdentifier.fromCql(TableOption.MAX_INDEX_INTERVAL.getName()))) // + .isEqualTo(2048); + assertThat(someTable.getOptions().get(CqlIdentifier.fromCql(TableOption.READ_REPAIR.getName()))) // + .isEqualTo("BLOCKING"); + } + + @Test // GH-359, GH-1584 + void shouldApplyTableOptions_with_raw() { + + Map options = Map.of(TableOption.COMMENT.getName(), "This is comment for table", // + "bloom_filter_fp_chance", "0.3", // + "default_time_to_live", "864000", // + "cdc", true, // + "speculative_retry", "90percentile", // + "memtable_flush_period_in_ms", "1000", // + "crc_check_chance", "0.9", // + "min_index_interval", "128", // + "max_index_interval", "2048", // + "read_repair", "BLOCKING"); + + CqlIdentifier tableName = CqlIdentifier.fromCql("someTable"); + cassandraAdminTemplate.createTable(true, tableName, SomeTable.class, options); + + TableMetadata someTable = getKeyspaceMetadata().getTables().get(tableName); + + assertThat(someTable).isNotNull(); + assertThat(someTable.getOptions().get(CqlIdentifier.fromCql(TableOption.COMMENT.getName()))) // .isEqualTo("This is comment for table"); - assertThat(someTable.getOptions().get(CqlIdentifier.fromCql(TableOption.BLOOM_FILTER_FP_CHANCE.getName()))) + assertThat(someTable.getOptions().get(CqlIdentifier.fromCql(TableOption.BLOOM_FILTER_FP_CHANCE.getName()))) // .isEqualTo(0.3); + assertThat(someTable.getOptions().get(CqlIdentifier.fromCql(TableOption.DEFAULT_TIME_TO_LIVE.getName()))) // + .isEqualTo(864_000); + assertThat(someTable.getOptions().get(CqlIdentifier.fromCql(TableOption.SPECULATIVE_RETRY.getName()))) // + .isEqualTo("90p"); + assertThat(someTable.getOptions().get(CqlIdentifier.fromCql(TableOption.MEMTABLE_FLUSH_PERIOD_IN_MS.getName()))) // + .isEqualTo(1000); + assertThat(someTable.getOptions().get(CqlIdentifier.fromCql(TableOption.CRC_CHECK_CHANCE.getName()))) // + .isEqualTo(0.9); + assertThat(someTable.getOptions().get(CqlIdentifier.fromCql(TableOption.MIN_INDEX_INTERVAL.getName()))) // + .isEqualTo(128); + assertThat(someTable.getOptions().get(CqlIdentifier.fromCql(TableOption.MAX_INDEX_INTERVAL.getName()))) // + .isEqualTo(2048); + assertThat(someTable.getOptions().get(CqlIdentifier.fromCql(TableOption.READ_REPAIR.getName()))) // + .isEqualTo("BLOCKING"); } @Test // GH-1388 diff --git a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/cql/generator/AlterTableCqlGeneratorIntegrationTests.java b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/cql/generator/AlterTableCqlGeneratorIntegrationTests.java index e0fd92ca7..a8adbd888 100755 --- a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/cql/generator/AlterTableCqlGeneratorIntegrationTests.java +++ b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/cql/generator/AlterTableCqlGeneratorIntegrationTests.java @@ -32,6 +32,7 @@ import org.springframework.data.cassandra.test.util.AbstractKeyspaceCreatingIntegrationTests; import org.springframework.data.util.Version; +import com.datastax.oss.driver.api.core.CqlIdentifier; import com.datastax.oss.driver.api.core.metadata.schema.ColumnMetadata; import com.datastax.oss.driver.api.core.metadata.schema.KeyspaceMetadata; import com.datastax.oss.driver.api.core.metadata.schema.TableMetadata; @@ -41,6 +42,7 @@ * Integration tests tests for {@link AlterTableCqlGenerator}. * * @author Mark Paluch + * @author Seungho Kang */ class AlterTableCqlGeneratorIntegrationTests extends AbstractKeyspaceCreatingIntegrationTests { @@ -165,6 +167,39 @@ void alterTableAddCaching() { assertThat(getTableMetadata("users").getOptions().toString()).contains("caching").contains("keys").contains("NONE"); } + @Test // GH-1584 + void alterTableWithAllOptionsTest() { + + session.execute("CREATE TABLE users (user_name varchar PRIMARY KEY);"); + AlterTableSpecification spec = SpecificationBuilder.alterTable("users") // + .with(TableOption.GC_GRACE_SECONDS, 86400L).with(TableOption.DEFAULT_TIME_TO_LIVE, 36000L) + .with(TableOption.CDC, true).with(TableOption.SPECULATIVE_RETRY, "95PERCENTILE") + .with(TableOption.MEMTABLE_FLUSH_PERIOD_IN_MS, 20000L).with(TableOption.CRC_CHECK_CHANCE, 0.85d) + .with(TableOption.MIN_INDEX_INTERVAL, 256L).with(TableOption.MAX_INDEX_INTERVAL, 1048L) + .with(TableOption.READ_REPAIR, "NONE"); + + execute(spec); + + TableMetadata meta = getTableMetadata("users"); + + assertThat(meta.getOptions()) // + .containsEntry(CqlIdentifier.fromCql(TableOption.GC_GRACE_SECONDS.getName()), 86400); + assertThat(meta.getOptions()) // + .containsEntry(CqlIdentifier.fromCql(TableOption.DEFAULT_TIME_TO_LIVE.getName()), 36000); + assertThat(meta.getOptions()) // + .containsEntry(CqlIdentifier.fromCql(TableOption.SPECULATIVE_RETRY.getName()), "95p"); + assertThat(meta.getOptions()) // + .containsEntry(CqlIdentifier.fromCql(TableOption.MEMTABLE_FLUSH_PERIOD_IN_MS.getName()), 20000); + assertThat(meta.getOptions()) // + .containsEntry(CqlIdentifier.fromCql(TableOption.CRC_CHECK_CHANCE.getName()), 0.85); + assertThat(meta.getOptions()) // + .containsEntry(CqlIdentifier.fromCql(TableOption.MIN_INDEX_INTERVAL.getName()), 256); + assertThat(meta.getOptions()) // + .containsEntry(CqlIdentifier.fromCql(TableOption.MAX_INDEX_INTERVAL.getName()), 1048); + assertThat(meta.getOptions()) // + .containsEntry(CqlIdentifier.fromCql(TableOption.READ_REPAIR.getName()), "NONE"); + } + private void execute(AlterTableSpecification spec) { session.execute(CqlGenerator.toCql(spec)); } diff --git a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/cql/generator/AlterTableCqlGeneratorUnitTests.java b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/cql/generator/AlterTableCqlGeneratorUnitTests.java index 407ce557a..5488d9b76 100755 --- a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/cql/generator/AlterTableCqlGeneratorUnitTests.java +++ b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/cql/generator/AlterTableCqlGeneratorUnitTests.java @@ -35,6 +35,7 @@ * @author Matthew T. Adams * @author David Webb * @author Mark Paluch + * @author Seungho Kang */ class AlterTableCqlGeneratorUnitTests { @@ -123,6 +124,23 @@ void alterTableAddCaching() { .isEqualTo("ALTER TABLE users WITH caching = { 'keys' : 'none', 'rows_per_partition' : '15' };"); } + @Test // GH-1584 + void alterTableSetDefaultTimeToLive() { + + AlterTableSpecification spec = SpecificationBuilder.alterTable("users") + .with(TableOption.DEFAULT_TIME_TO_LIVE, 3600) + .with(TableOption.CDC, true) + .with(TableOption.SPECULATIVE_RETRY, "90percentile") + .with(TableOption.MEMTABLE_FLUSH_PERIOD_IN_MS, 1000L) + .with(TableOption.CRC_CHECK_CHANCE, 0.9) + .with(TableOption.MIN_INDEX_INTERVAL, 128L) + .with(TableOption.MAX_INDEX_INTERVAL, 2048L) + .with(TableOption.READ_REPAIR, "BLOCKING"); + + assertThat(toCql(spec)) + .isEqualTo("ALTER TABLE users WITH default_time_to_live = 3600 AND cdc = true AND speculative_retry = '90percentile' AND memtable_flush_period_in_ms = 1000 AND crc_check_chance = 0.9 AND min_index_interval = 128 AND max_index_interval = 2048 AND read_repair = 'BLOCKING';"); + } + private String toCql(AlterTableSpecification spec) { return CqlGenerator.toCql(spec); } diff --git a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/cql/generator/CreateTableCqlGeneratorIntegrationTests.java b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/cql/generator/CreateTableCqlGeneratorIntegrationTests.java index c98a292f4..01ce75481 100755 --- a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/cql/generator/CreateTableCqlGeneratorIntegrationTests.java +++ b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/cql/generator/CreateTableCqlGeneratorIntegrationTests.java @@ -35,6 +35,7 @@ * @author Matthew T. Adams * @author Oliver Gierke * @author Mark Paluch + * @author Seungho Kang */ class CreateTableCqlGeneratorIntegrationTests extends AbstractKeyspaceCreatingIntegrationTests { @@ -108,4 +109,39 @@ void shouldGenerateTableInOtherKeyspace() { assertThat(person.getPartitionKey()).hasSize(1); assertThat(person.getClusteringColumns()).hasSize(1); } + + @Test // GH-1584 + void shouldGenerateTableWithOptions() { + + CreateTableSpecification spec = CreateTableSpecification.createTable("person") + .partitionKeyColumn("id", DataTypes.INT) // + .clusteredKeyColumn("date_of_birth", DataTypes.DATE, Ordering.ASCENDING) // + .column("name", DataTypes.ASCII) // + .with(TableOption.GC_GRACE_SECONDS, 86400L).with(TableOption.DEFAULT_TIME_TO_LIVE, 3600L) + .with(TableOption.CDC, true).with(TableOption.SPECULATIVE_RETRY, "99PERCENTILE") + .with(TableOption.MEMTABLE_FLUSH_PERIOD_IN_MS, 10000L).with(TableOption.CRC_CHECK_CHANCE, 0.9d) + .with(TableOption.MIN_INDEX_INTERVAL, 128L).with(TableOption.MAX_INDEX_INTERVAL, 2048L) + .with(TableOption.READ_REPAIR, "BLOCKING"); + + session.execute(CqlGenerator.toCql(spec)); + + TableMetadata meta = session.getMetadata().getKeyspace(getKeyspace()).flatMap(it -> it.getTable("person")).get(); + assertThat(meta.getOptions()) // + .containsEntry(CqlIdentifier.fromCql(TableOption.GC_GRACE_SECONDS.getName()), 86400); + + assertThat(meta.getOptions()) // + .containsEntry(CqlIdentifier.fromCql(TableOption.DEFAULT_TIME_TO_LIVE.getName()), 3600); + assertThat(meta.getOptions()) // + .containsEntry(CqlIdentifier.fromCql(TableOption.SPECULATIVE_RETRY.getName()), "99p"); + assertThat(meta.getOptions()) // + .containsEntry(CqlIdentifier.fromCql(TableOption.MEMTABLE_FLUSH_PERIOD_IN_MS.getName()), 10000); + assertThat(meta.getOptions()) // + .containsEntry(CqlIdentifier.fromCql(TableOption.CRC_CHECK_CHANCE.getName()), 0.9); + assertThat(meta.getOptions()) // + .containsEntry(CqlIdentifier.fromCql(TableOption.MIN_INDEX_INTERVAL.getName()), 128); + assertThat(meta.getOptions()) // + .containsEntry(CqlIdentifier.fromCql(TableOption.MAX_INDEX_INTERVAL.getName()), 2048); + assertThat(meta.getOptions()) // + .containsEntry(CqlIdentifier.fromCql(TableOption.READ_REPAIR.getName()), "BLOCKING"); + } } diff --git a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/cql/generator/CreateTableCqlGeneratorUnitTests.java b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/cql/generator/CreateTableCqlGeneratorUnitTests.java index 781d7820a..4f4b5857d 100755 --- a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/cql/generator/CreateTableCqlGeneratorUnitTests.java +++ b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/cql/generator/CreateTableCqlGeneratorUnitTests.java @@ -41,6 +41,7 @@ * @author David Webb * @author Mark Paluch * @author Aleksei Zotov + * @author Seungho Kang */ class CreateTableCqlGeneratorUnitTests { @@ -108,7 +109,7 @@ void shouldGenerateTableOptions() { assertDoubleOption(TableOption.READ_REPAIR_CHANCE.getName(), readRepairChance, cql); } - @Test + @Test // GH-1584 void shouldGenerateMultipleOptions() { CqlIdentifier name = CqlIdentifier.fromCql("timeseries_table"); @@ -121,8 +122,15 @@ void shouldGenerateMultipleOptions() { Double readRepairChance = 0.5; Double dcLocalReadRepairChance = 0.7; Double bloomFilterFpChance = 0.001; - Boolean replcateOnWrite = Boolean.FALSE; Long gcGraceSeconds = 600l; + Long defaultTimeToLive = 864_00L; + Boolean cdc = Boolean.TRUE; + String speculative_retry = "99percentile"; + Long memtableFlushPeriodInMs = 600L; + Double crcCheckChance = 0.9; + Long maxIndexInterval = 2048L; + Long minIndexInterval = 128L; + String comment = "This is My Table"; Map compactionMap = new LinkedHashMap<>(); Map compressionMap = new LinkedHashMap<>(); @@ -146,7 +154,11 @@ void shouldGenerateMultipleOptions() { .with(TableOption.COMPRESSION, compressionMap).with(TableOption.BLOOM_FILTER_FP_CHANCE, bloomFilterFpChance) .with(TableOption.CACHING, cachingMap).with(TableOption.COMMENT, comment) .with(TableOption.DCLOCAL_READ_REPAIR_CHANCE, dcLocalReadRepairChance) - .with(TableOption.GC_GRACE_SECONDS, gcGraceSeconds); + .with(TableOption.GC_GRACE_SECONDS, gcGraceSeconds).with(TableOption.DEFAULT_TIME_TO_LIVE, defaultTimeToLive) + .with(TableOption.CDC, cdc).with(TableOption.SPECULATIVE_RETRY, speculative_retry) + .with(TableOption.MEMTABLE_FLUSH_PERIOD_IN_MS, memtableFlushPeriodInMs) + .with(TableOption.CRC_CHECK_CHANCE, crcCheckChance).with(TableOption.MAX_INDEX_INTERVAL, maxIndexInterval) + .with(TableOption.MIN_INDEX_INTERVAL, minIndexInterval).with(TableOption.READ_REPAIR, "BLOCKING"); String cql = CqlGenerator.toCql(table); @@ -159,6 +171,79 @@ void shouldGenerateMultipleOptions() { assertDoubleOption(TableOption.BLOOM_FILTER_FP_CHANCE.getName(), bloomFilterFpChance, cql); assertStringOption(TableOption.COMMENT.getName(), comment, cql); assertLongOption(TableOption.GC_GRACE_SECONDS.getName(), gcGraceSeconds, cql); + assertLongOption(TableOption.DEFAULT_TIME_TO_LIVE.getName(), defaultTimeToLive, cql); + assertBooleanOption(TableOption.CDC.getName(), cdc, cql); + assertStringOption(TableOption.SPECULATIVE_RETRY.getName(), speculative_retry, cql); + assertLongOption(TableOption.MEMTABLE_FLUSH_PERIOD_IN_MS.getName(), memtableFlushPeriodInMs, cql); + assertDoubleOption(TableOption.CRC_CHECK_CHANCE.getName(), crcCheckChance, cql); + assertLongOption(TableOption.MAX_INDEX_INTERVAL.getName(), maxIndexInterval, cql); + assertLongOption(TableOption.MIN_INDEX_INTERVAL.getName(), minIndexInterval, cql); + assertStringOption(TableOption.READ_REPAIR.getName(), "BLOCKING", cql); + } + + @Test // GH-1584 + void shouldGenerateMultipleOptions_usingRawStringKeys() { + + CqlIdentifier name = CqlIdentifier.fromCql("timeseries_table"); + DataType partitionKeyType0 = DataTypes.TIMEUUID; + CqlIdentifier partitionKey0 = CqlIdentifier.fromCql("tid"); + DataType partitionKeyType1 = DataTypes.TIMESTAMP; + CqlIdentifier partitionKey1 = CqlIdentifier.fromCql("create_timestamp"); + DataType columnType1 = DataTypes.TEXT; + CqlIdentifier column1 = CqlIdentifier.fromCql("data_point"); + Double readRepairChance = 0.5; + Double dcLocalReadRepairChance = 0.7; + Double bloomFilterFpChance = 0.001; + Long gcGraceSeconds = 600l; + Long defaultTimeToLive = 864_00L; + Boolean cdc = Boolean.TRUE; + String speculative_retry = "99percentile"; + Long memtableFlushPeriodInMs = 600L; + Double crcCheckChance = 0.9; + Long maxIndexInterval = 2048L; + Long minIndexInterval = 128L; + + String comment = "This is My Table"; + Map compactionMap = new LinkedHashMap<>(); + Map compressionMap = new LinkedHashMap<>(); + Map cachingMap = new LinkedHashMap<>(); + + // Compaction + compactionMap.put(CompactionOption.CLASS, "SizeTieredCompactionStrategy"); + compactionMap.put(CompactionOption.MIN_THRESHOLD, "4"); + // Compression + compressionMap.put(CompressionOption.SSTABLE_COMPRESSION, "SnappyCompressor"); + compressionMap.put(CompressionOption.CHUNK_LENGTH_KB, 128); + compressionMap.put(CompressionOption.CRC_CHECK_CHANCE, 0.75); + // Caching + cachingMap.put(CachingOption.KEYS, KeyCachingOption.ALL); + cachingMap.put(CachingOption.ROWS_PER_PARTITION, "NONE"); + + CreateTableSpecification table = CreateTableSpecification.createTable(name) + .partitionKeyColumn(partitionKey0, partitionKeyType0).partitionKeyColumn(partitionKey1, partitionKeyType1) + .column(column1, columnType1).with("compact_storage", null, false, false) + .with("read_repair_chance", readRepairChance, false, false).with("compaction", compactionMap, false, false) + .with("compression", compressionMap, false, false) + .with("bloom_filter_fp_chance", bloomFilterFpChance, false, false).with("caching", cachingMap, false, false) + .with("comment", comment, true, true).with("dclocal_read_repair_chance", dcLocalReadRepairChance, false, false) + .with("gc_grace_seconds", gcGraceSeconds, false, false) + .with("default_time_to_live", defaultTimeToLive, false, false).with("cdc", cdc, false, false) + .with("speculative_retry", speculative_retry, true, true) + .with("memtable_flush_period_in_ms", memtableFlushPeriodInMs, false, false) + .with("crc_check_chance", crcCheckChance, false, false) + .with("max_index_interval", maxIndexInterval, false, false) + .with("min_index_interval", minIndexInterval, false, false).with("read_repair", "BLOCKING", true, true); + + String cql = CqlGenerator.toCql(table); + + assertPreamble(name, cql); + assertColumns("tid timeuuid, create_timestamp timestamp, data_point text", cql); + assertPrimaryKey(String.format("(%s, %s)", partitionKey0, partitionKey1), cql); + assertDoubleOption(TableOption.READ_REPAIR_CHANCE.getName(), readRepairChance, cql); + assertDoubleOption(TableOption.DCLOCAL_READ_REPAIR_CHANCE.getName(), dcLocalReadRepairChance, cql); + assertDoubleOption(TableOption.BLOOM_FILTER_FP_CHANCE.getName(), bloomFilterFpChance, cql); + assertStringOption(TableOption.COMMENT.getName(), comment, cql); + assertLongOption(TableOption.GC_GRACE_SECONDS.getName(), gcGraceSeconds, cql); } @Test // DATACASS-518 @@ -254,6 +339,10 @@ private static void assertLongOption(String name, Long value, String cql) { assertThat(cql).contains(name + " = " + value); } + private static void assertBooleanOption(String name, Boolean value, String cql) { + assertThat(cql).contains(name + " = " + value); + } + /** * Asserts that the read repair change is set properly */ diff --git a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/cql/keyspace/CreateTableSpecificationTest.java b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/cql/keyspace/CreateTableSpecificationTest.java new file mode 100644 index 000000000..f8a3b2da7 --- /dev/null +++ b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/cql/keyspace/CreateTableSpecificationTest.java @@ -0,0 +1,69 @@ +/* + * Copyright 2016-2025 the original author or authors. + * + * 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 + * + * https://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 org.springframework.data.cassandra.core.cql.keyspace; + +import static org.assertj.core.api.Assertions.*; +import com.datastax.oss.driver.api.core.CqlIdentifier; +import org.junit.jupiter.api.Test; + +/** + * Unit tests for {@link CreateTableSpecification}. + * + * @author Seungho Kang + */ +class CreateTableSpecificationTest { + + @Test // GH-1584 + void shouldSupportStandardTableOptions() { + + CreateTableSpecification specification = CreateTableSpecification.createTable(CqlIdentifier.fromCql("person")) + .with(TableOption.COMPACT_STORAGE) + .with(TableOption.CDC, true) + .with(TableOption.READ_REPAIR, "BLOCKING"); + + assertThat(specification.getOptions()).containsEntry("COMPACT STORAGE", null) + .containsEntry("cdc", true) + .containsEntry("read_repair", "'BLOCKING'"); + } + + @Test // GH-1584 + void shouldUseRawValueWhenEscapingIsDisabled() { + + CreateTableSpecification specification = CreateTableSpecification.createTable(CqlIdentifier.fromCql("person")) + .with("max_index_interval", 128, false, false); + + assertThat(specification.getOptions()).containsEntry("max_index_interval", 128); + } + + @Test // GH-1584 + void shouldEscapeStringValueWhenEscapeIsEnabled() { + + CreateTableSpecification specification = CreateTableSpecification.createTable(CqlIdentifier.fromCql("person")) + .with("read_repair", "BLOCKING", true, true); + + assertThat(specification.getOptions()).containsEntry("read_repair", "'BLOCKING'"); + } + + @Test // GH-1584 + void shouldPreserveEscapedStringWhenProvidedByCaller() { + + CreateTableSpecification specification = CreateTableSpecification.createTable(CqlIdentifier.fromCql("person")) + .with("read_repair", "'BLOCKING'", false, false); + + assertThat(specification.getOptions()).containsEntry("read_repair", "'BLOCKING'"); + } + +} diff --git a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/cql/keyspace/TableOptionTest.java b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/cql/keyspace/TableOptionTest.java new file mode 100644 index 000000000..3449cf2e2 --- /dev/null +++ b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/cql/keyspace/TableOptionTest.java @@ -0,0 +1,61 @@ +/* + * Copyright 2016-2025 the original author or authors. + * + * 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 + * + * https://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 org.springframework.data.cassandra.core.cql.keyspace; + +import static org.assertj.core.api.Assertions.*; + +import org.junit.jupiter.api.Test; + +/** + * Unit tests for {@link TableOption}. + * + * @author Seungho Kang + */ +class TableOptionTest { + + @Test // GH-1584 + void shouldResolveTableOptionUsingValueOfIgnoreCase() { + + TableOption option = TableOption.valueOfIgnoreCase("bloom_filter_fp_chance"); + + assertThat(option).isEqualTo(TableOption.BLOOM_FILTER_FP_CHANCE); + } + + @Test // GH-1584 + void shouldThrowExceptionWhenTableOptionNotFoundInValueOfIgnoreCase() { + + assertThatThrownBy(() -> TableOption.valueOfIgnoreCase("unknown_option")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("Unable to recognize specified Table option"); + } + + @Test // GH-1584 + void shouldResolveKnownTableOptionByName() { + + TableOption tableOption = TableOption.findByNameIgnoreCase("bloom_filter_fp_chance"); + + assertThat(tableOption).isEqualTo(TableOption.BLOOM_FILTER_FP_CHANCE); + } + + @Test // GH-1584 + void shouldReturnNullForUnknownTableOption() { + + TableOption tableOption = TableOption.findByNameIgnoreCase("unknown_option"); + + assertThat(tableOption).isNull(); + } + +}