From 402e2209e5aba14c9a547e347b3bedea56f57492 Mon Sep 17 00:00:00 2001 From: Daniel Jatnieks Date: Fri, 14 Nov 2025 12:55:23 -0800 Subject: [PATCH 1/7] CNDB-15995 Add CC 4.0 schema backward compatibility mode --- .../config/CassandraRelevantProperties.java | 4 + .../statements/schema/TableAttributes.java | 14 +++- .../cassandra/schema/MemtableParams.java | 27 +++++++ .../apache/cassandra/schema/TableParams.java | 45 ++++++++--- .../cassandra/db/SchemaCQLHelperTest.java | 81 +++++++++++++++++++ 5 files changed, 156 insertions(+), 15 deletions(-) diff --git a/src/java/org/apache/cassandra/config/CassandraRelevantProperties.java b/src/java/org/apache/cassandra/config/CassandraRelevantProperties.java index 0711ead12f6a..29791ab3040f 100644 --- a/src/java/org/apache/cassandra/config/CassandraRelevantProperties.java +++ b/src/java/org/apache/cassandra/config/CassandraRelevantProperties.java @@ -811,6 +811,10 @@ public enum CassandraRelevantProperties SAI_WRITE_JVECTOR3_FORMAT("cassandra.sai.write_jv3_format", "false"), + /** + * Enable schema backward compatibility mode for CC 4.0. + */ + SCHEMA_BACKWARD_COMPATIBILITY_CC_4("cassandra.schema.backward_compatibility_cc_4", "false"), SCHEMA_PULL_INTERVAL_MS("cassandra.schema_pull_interval_ms", "60000"), SCHEMA_UPDATE_HANDLER_FACTORY_CLASS("cassandra.schema.update_handler_factory.class"), SEARCH_CONCURRENCY_FACTOR("cassandra.search_concurrency_factor", "1"), diff --git a/src/java/org/apache/cassandra/cql3/statements/schema/TableAttributes.java b/src/java/org/apache/cassandra/cql3/statements/schema/TableAttributes.java index d314c1ad11f3..25bbc862b3b4 100644 --- a/src/java/org/apache/cassandra/cql3/statements/schema/TableAttributes.java +++ b/src/java/org/apache/cassandra/cql3/statements/schema/TableAttributes.java @@ -184,13 +184,19 @@ private TableParams build(TableParams.Builder builder) .findFirst() .orElse(null); // Not exhaustive, but avoids raising an error upgrading from a CC 4.0 schema - if (memtableClass == null) + // Extract short class name if fully qualified (e.g., "org.apache.cassandra.db.memtable.TrieMemtable" -> "TrieMemtable") + String shortClassName = memtableClass != null && memtableClass.contains(".") + ? memtableClass.substring(memtableClass.lastIndexOf('.') + 1) + : memtableClass; + + if (shortClassName == null) builder.memtable(MemtableParams.get(null)); - else if ("SkipListMemtable".equalsIgnoreCase(memtableClass)) + else if ("SkipListMemtable".equalsIgnoreCase(shortClassName)) builder.memtable(MemtableParams.get("skiplist")); - else if ("PersistentMemoryMemtable".equalsIgnoreCase(memtableClass)) - builder.memtable(MemtableParams.get("persistent_memory")); + else if ("TrieMemtable".equalsIgnoreCase(shortClassName)) + builder.memtable(MemtableParams.get("trie")); else + // Default to trie for unknown memtable types builder.memtable(MemtableParams.get("trie")); } else diff --git a/src/java/org/apache/cassandra/schema/MemtableParams.java b/src/java/org/apache/cassandra/schema/MemtableParams.java index 27bee6dcc729..77450bf6fecf 100644 --- a/src/java/org/apache/cassandra/schema/MemtableParams.java +++ b/src/java/org/apache/cassandra/schema/MemtableParams.java @@ -69,6 +69,33 @@ public Memtable.Factory factory() return factory; } + /** + * Returns a map representation of the memtable configuration for backward compatibility with CC 4.0. + * This is used when outputting schema in a format compatible with CC 4.0. + * + * CC 4.0 accepts both short class names (e.g., 'TrieMemtable') and fully qualified names + * (e.g., 'org.apache.cassandra.db.memtable.TrieMemtable'), but using short names is preferred + * for consistency and readability. + */ + public Map toMapForCC4() + { + ParameterizedClass definition = CONFIGURATION_DEFINITIONS.get(configurationKey); + if (definition != null && definition.class_name != null) + { + Map map = new HashMap<>(); + String className = definition.class_name; + String shortClassName = className.contains(".") + ? className.substring(className.lastIndexOf('.') + 1) + : className; + map.put("class", shortClassName); + if (definition.parameters != null) + map.putAll(definition.parameters); + return map; + } + // Fallback for unknown configurations + return ImmutableMap.of("class", configurationKey); + } + @Override public String toString() { diff --git a/src/java/org/apache/cassandra/schema/TableParams.java b/src/java/org/apache/cassandra/schema/TableParams.java index e79f215b3643..03c90aeaa1a6 100644 --- a/src/java/org/apache/cassandra/schema/TableParams.java +++ b/src/java/org/apache/cassandra/schema/TableParams.java @@ -21,6 +21,7 @@ import java.util.Map; import java.util.Map.Entry; +import com.google.common.annotations.VisibleForTesting; import com.google.common.base.MoreObjects; import com.google.common.base.Objects; import com.google.common.collect.ImmutableMap; @@ -36,6 +37,7 @@ import static java.lang.String.format; import static java.util.stream.Collectors.toMap; +import static org.apache.cassandra.config.CassandraRelevantProperties.SCHEMA_BACKWARD_COMPATIBILITY_CC_4; import static org.apache.cassandra.schema.TableParams.Option.*; public final class TableParams @@ -69,6 +71,9 @@ public String toString() } } + @VisibleForTesting + public static boolean backwardCompatibilityCC4 = SCHEMA_BACKWARD_COMPATIBILITY_CC_4.getBoolean(); + public final String comment; public final boolean allowAutoSnapshot; public final double bloomFilterFpChance; @@ -287,10 +292,16 @@ public void appendCqlTo(CqlBuilder builder, boolean isView) { // option names should be in alphabetical order builder.append("additional_write_policy = ").appendWithSingleQuotes(additionalWritePolicy.toString()) - .newLine() - .append("AND allow_auto_snapshot = ").append(allowAutoSnapshot) - .newLine() - .append("AND bloom_filter_fp_chance = ").append(bloomFilterFpChance) + .newLine(); + + // Exclude allow_auto_snapshot in backward compatibility mode (new in 5.0) + if (!backwardCompatibilityCC4) + { + builder.append("AND allow_auto_snapshot = ").append(allowAutoSnapshot) + .newLine(); + } + + builder.append("AND bloom_filter_fp_chance = ").append(bloomFilterFpChance) .newLine() .append("AND caching = ").append(caching.asMap()) .newLine() @@ -301,9 +312,15 @@ public void appendCqlTo(CqlBuilder builder, boolean isView) .append("AND compaction = ").append(compaction.asMap()) .newLine() .append("AND compression = ").append(compression.asMap()) - .newLine() - .append("AND memtable = ").appendWithSingleQuotes(memtable.configurationKey()) - .newLine() + .newLine(); + + // Use map format for CC 4.0 compatibility, string format for 5.0 + if (backwardCompatibilityCC4) + builder.append("AND memtable = ").append(memtable.toMapForCC4()); + else + builder.append("AND memtable = ").appendWithSingleQuotes(memtable.configurationKey()); + + builder.newLine() .append("AND crc_check_chance = ").append(crcCheckChance) .newLine(); @@ -320,10 +337,16 @@ public void appendCqlTo(CqlBuilder builder, boolean isView) false) .newLine() .append("AND gc_grace_seconds = ").append(gcGraceSeconds) - .newLine() - .append("AND incremental_backups = ").append(incrementalBackups) - .newLine() - .append("AND max_index_interval = ").append(maxIndexInterval) + .newLine(); + + // Exclude incremental_backups in backward compatibility mode (new in 5.0) + if (!backwardCompatibilityCC4) + { + builder.append("AND incremental_backups = ").append(incrementalBackups) + .newLine(); + } + + builder.append("AND max_index_interval = ").append(maxIndexInterval) .newLine() .append("AND memtable_flush_period_in_ms = ").append(memtableFlushPeriodInMs) .newLine() diff --git a/test/unit/org/apache/cassandra/db/SchemaCQLHelperTest.java b/test/unit/org/apache/cassandra/db/SchemaCQLHelperTest.java index 883a58582525..7bd87068eef6 100644 --- a/test/unit/org/apache/cassandra/db/SchemaCQLHelperTest.java +++ b/test/unit/org/apache/cassandra/db/SchemaCQLHelperTest.java @@ -61,6 +61,7 @@ import org.apache.cassandra.schema.Indexes; import org.apache.cassandra.schema.KeyspaceParams; import org.apache.cassandra.schema.TableMetadata; +import org.apache.cassandra.schema.TableParams; import org.apache.cassandra.schema.Tables; import org.apache.cassandra.schema.Types; import org.apache.cassandra.utils.ByteBufferUtil; @@ -748,4 +749,84 @@ public void testParseCreateTableWithDuplicateDroppedColumns() containsString("Cannot have multiple dropped column record for column")); } } + + @Test + public void testSchemaBackwardCompatibilityCc40() + { + String keyspace = createKeyspace("CREATE KEYSPACE %s WITH replication={ 'class' : 'SimpleStrategy', 'replication_factor' : 1 }"); + + // Save original value + boolean originalValue = TableParams.backwardCompatibilityCC4; + + try + { + // Test filtering when CC 4.0 backward compatibility mode is enabled + TableParams.backwardCompatibilityCC4 = true; + + String tableName = createTable(keyspace, "CREATE TABLE %s (id int PRIMARY KEY, value text)"); + ColumnFamilyStore cfs = Keyspace.open(keyspace).getColumnFamilyStore(tableName); + String cql = SchemaCQLHelper.getTableMetadataAsCQL(cfs.metadata(), cfs.keyspace.getMetadata()); + + // Should not contain new 5.0 properties + Assertions.assertThat(cql).doesNotContain("allow_auto_snapshot"); + Assertions.assertThat(cql).doesNotContain("incremental_backups"); + // Should contain other properties + Assertions.assertThat(cql).contains("bloom_filter_fp_chance"); + // Should use map format for memtable (CC 4.0 compatible) with short class name + Assertions.assertThat(cql).contains("memtable = {'class': 'TrieMemtable'}"); + // Should NOT contain fully qualified class name + Assertions.assertThat(cql).doesNotContain("org.apache.cassandra.db.memtable"); + + // Test filtering when disabled (default) + TableParams.backwardCompatibilityCC4 = false; + + String tableName2 = createTable(keyspace, "CREATE TABLE %s (id int PRIMARY KEY, value text)"); + ColumnFamilyStore cfs2 = Keyspace.open(keyspace).getColumnFamilyStore(tableName2); + String cql2 = SchemaCQLHelper.getTableMetadataAsCQL(cfs2.metadata(), cfs2.keyspace.getMetadata()); + + // Should contain new 5.0 properties when filtering is disabled + Assertions.assertThat(cql2).contains("allow_auto_snapshot"); + Assertions.assertThat(cql2).contains("incremental_backups"); + // Should use string format for memtable (5.0 format) + Assertions.assertThat(cql2).contains("memtable = '"); + Assertions.assertThat(cql2).doesNotContain("memtable = {'class'"); + } + finally + { + // Restore original value + TableParams.backwardCompatibilityCC4 = originalValue; + } + } + + @Test + public void testParseCc40MemtableFormat() + { + String keyspace = createKeyspace("CREATE KEYSPACE %s WITH replication={ 'class' : 'SimpleStrategy', 'replication_factor' : 1 }"); + + // Test parsing CC 4.0 format with short class name + String tableName1 = createTable(keyspace, "CREATE TABLE %s (id int PRIMARY KEY, value text) WITH memtable = {'class': 'TrieMemtable'}"); + ColumnFamilyStore cfs1 = Keyspace.open(keyspace).getColumnFamilyStore(tableName1); + Assertions.assertThat(cfs1.metadata().params.memtable.configurationKey()).isEqualTo("trie"); + + // Test parsing CC 4.0 format with fully qualified class name + String tableName2 = createTable(keyspace, "CREATE TABLE %s (id int PRIMARY KEY, value text) WITH memtable = {'class': 'org.apache.cassandra.db.memtable.TrieMemtable'}"); + ColumnFamilyStore cfs2 = Keyspace.open(keyspace).getColumnFamilyStore(tableName2); + Assertions.assertThat(cfs2.metadata().params.memtable.configurationKey()).isEqualTo("trie"); + + // Test parsing CC 4.0 format with SkipListMemtable (short name) + String tableName3 = createTable(keyspace, "CREATE TABLE %s (id int PRIMARY KEY, value text) WITH memtable = {'class': 'SkipListMemtable'}"); + ColumnFamilyStore cfs3 = Keyspace.open(keyspace).getColumnFamilyStore(tableName3); + Assertions.assertThat(cfs3.metadata().params.memtable.configurationKey()).isEqualTo("skiplist"); + + // Test parsing CC 4.0 format with SkipListMemtable (fully qualified) + String tableName4 = createTable(keyspace, "CREATE TABLE %s (id int PRIMARY KEY, value text) WITH memtable = {'class': 'org.apache.cassandra.db.memtable.SkipListMemtable'}"); + ColumnFamilyStore cfs4 = Keyspace.open(keyspace).getColumnFamilyStore(tableName4); + Assertions.assertThat(cfs4.metadata().params.memtable.configurationKey()).isEqualTo("skiplist"); + + // Test parsing CC 4.0 format with empty map (default) + String tableName5 = createTable(keyspace, "CREATE TABLE %s (id int PRIMARY KEY, value text) WITH memtable = {}"); + ColumnFamilyStore cfs5 = Keyspace.open(keyspace).getColumnFamilyStore(tableName5); + // Empty map should use default memtable + Assertions.assertThat(cfs5.metadata().params.memtable).isNotNull(); + } } From 014857ae62a69d698a7523707219f1ad835be8c8 Mon Sep 17 00:00:00 2001 From: Daniel Jatnieks Date: Mon, 17 Nov 2025 11:24:47 -0800 Subject: [PATCH 2/7] Use StorageCompatibilityMode instead of SCHEMA_BACKWARD_COMPATIBILITY_CC_4 --- .../config/CassandraRelevantProperties.java | 4 ---- .../apache/cassandra/schema/TableParams.java | 17 +++++++++++------ .../utils/StorageCompatibilityMode.java | 5 +++++ .../cassandra/db/SchemaCQLHelperTest.java | 9 +++++---- 4 files changed, 21 insertions(+), 14 deletions(-) diff --git a/src/java/org/apache/cassandra/config/CassandraRelevantProperties.java b/src/java/org/apache/cassandra/config/CassandraRelevantProperties.java index 29791ab3040f..0711ead12f6a 100644 --- a/src/java/org/apache/cassandra/config/CassandraRelevantProperties.java +++ b/src/java/org/apache/cassandra/config/CassandraRelevantProperties.java @@ -811,10 +811,6 @@ public enum CassandraRelevantProperties SAI_WRITE_JVECTOR3_FORMAT("cassandra.sai.write_jv3_format", "false"), - /** - * Enable schema backward compatibility mode for CC 4.0. - */ - SCHEMA_BACKWARD_COMPATIBILITY_CC_4("cassandra.schema.backward_compatibility_cc_4", "false"), SCHEMA_PULL_INTERVAL_MS("cassandra.schema_pull_interval_ms", "60000"), SCHEMA_UPDATE_HANDLER_FACTORY_CLASS("cassandra.schema.update_handler_factory.class"), SEARCH_CONCURRENCY_FACTOR("cassandra.search_concurrency_factor", "1"), diff --git a/src/java/org/apache/cassandra/schema/TableParams.java b/src/java/org/apache/cassandra/schema/TableParams.java index 03c90aeaa1a6..809b19517915 100644 --- a/src/java/org/apache/cassandra/schema/TableParams.java +++ b/src/java/org/apache/cassandra/schema/TableParams.java @@ -22,6 +22,7 @@ import java.util.Map.Entry; import com.google.common.annotations.VisibleForTesting; +import org.apache.cassandra.utils.StorageCompatibilityMode; import com.google.common.base.MoreObjects; import com.google.common.base.Objects; import com.google.common.collect.ImmutableMap; @@ -37,7 +38,6 @@ import static java.lang.String.format; import static java.util.stream.Collectors.toMap; -import static org.apache.cassandra.config.CassandraRelevantProperties.SCHEMA_BACKWARD_COMPATIBILITY_CC_4; import static org.apache.cassandra.schema.TableParams.Option.*; public final class TableParams @@ -72,7 +72,7 @@ public String toString() } @VisibleForTesting - public static boolean backwardCompatibilityCC4 = SCHEMA_BACKWARD_COMPATIBILITY_CC_4.getBoolean(); + public static StorageCompatibilityMode storageCompatibilityModeOverride = null; public final String comment; public final boolean allowAutoSnapshot; @@ -290,12 +290,17 @@ public String toString() public void appendCqlTo(CqlBuilder builder, boolean isView) { + StorageCompatibilityMode compatibilityMode = storageCompatibilityModeOverride != null + ? storageCompatibilityModeOverride + : StorageCompatibilityMode.current(); + boolean usePre50Schema = compatibilityMode.isBefore(5); + // option names should be in alphabetical order builder.append("additional_write_policy = ").appendWithSingleQuotes(additionalWritePolicy.toString()) .newLine(); // Exclude allow_auto_snapshot in backward compatibility mode (new in 5.0) - if (!backwardCompatibilityCC4) + if (!usePre50Schema) { builder.append("AND allow_auto_snapshot = ").append(allowAutoSnapshot) .newLine(); @@ -314,8 +319,8 @@ public void appendCqlTo(CqlBuilder builder, boolean isView) .append("AND compression = ").append(compression.asMap()) .newLine(); - // Use map format for CC 4.0 compatibility, string format for 5.0 - if (backwardCompatibilityCC4) + // Use map format for pre-5.0 compatibility, string format for 5.0 + if (usePre50Schema) builder.append("AND memtable = ").append(memtable.toMapForCC4()); else builder.append("AND memtable = ").appendWithSingleQuotes(memtable.configurationKey()); @@ -340,7 +345,7 @@ public void appendCqlTo(CqlBuilder builder, boolean isView) .newLine(); // Exclude incremental_backups in backward compatibility mode (new in 5.0) - if (!backwardCompatibilityCC4) + if (!usePre50Schema) { builder.append("AND incremental_backups = ").append(incrementalBackups) .newLine(); diff --git a/src/java/org/apache/cassandra/utils/StorageCompatibilityMode.java b/src/java/org/apache/cassandra/utils/StorageCompatibilityMode.java index 2969597c2381..f070a5796d0e 100644 --- a/src/java/org/apache/cassandra/utils/StorageCompatibilityMode.java +++ b/src/java/org/apache/cassandra/utils/StorageCompatibilityMode.java @@ -35,6 +35,11 @@ public enum StorageCompatibilityMode */ CASSANDRA_4(4), + /** + * Same as {@link #CASSANDRA_4}, but allows the use of BTI format in {@link #validateSstableFormat}. + */ + CC_4(4), + /** * Use the storage formats of the current version, but disabling features that are not compatible with any * not-upgraded nodes in the cluster. Use this during rolling upgrades to a new major Cassandra version. Once all diff --git a/test/unit/org/apache/cassandra/db/SchemaCQLHelperTest.java b/test/unit/org/apache/cassandra/db/SchemaCQLHelperTest.java index 7bd87068eef6..aa2c62941a09 100644 --- a/test/unit/org/apache/cassandra/db/SchemaCQLHelperTest.java +++ b/test/unit/org/apache/cassandra/db/SchemaCQLHelperTest.java @@ -67,6 +67,7 @@ import org.apache.cassandra.utils.ByteBufferUtil; import org.apache.cassandra.utils.FBUtilities; import org.apache.cassandra.utils.JsonUtils; +import org.apache.cassandra.utils.StorageCompatibilityMode; import static org.hamcrest.CoreMatchers.allOf; import static org.hamcrest.CoreMatchers.containsString; @@ -756,12 +757,12 @@ public void testSchemaBackwardCompatibilityCc40() String keyspace = createKeyspace("CREATE KEYSPACE %s WITH replication={ 'class' : 'SimpleStrategy', 'replication_factor' : 1 }"); // Save original value - boolean originalValue = TableParams.backwardCompatibilityCC4; + StorageCompatibilityMode originalMode = TableParams.storageCompatibilityModeOverride; try { // Test filtering when CC 4.0 backward compatibility mode is enabled - TableParams.backwardCompatibilityCC4 = true; + TableParams.storageCompatibilityModeOverride = StorageCompatibilityMode.CC_4; String tableName = createTable(keyspace, "CREATE TABLE %s (id int PRIMARY KEY, value text)"); ColumnFamilyStore cfs = Keyspace.open(keyspace).getColumnFamilyStore(tableName); @@ -778,7 +779,7 @@ public void testSchemaBackwardCompatibilityCc40() Assertions.assertThat(cql).doesNotContain("org.apache.cassandra.db.memtable"); // Test filtering when disabled (default) - TableParams.backwardCompatibilityCC4 = false; + TableParams.storageCompatibilityModeOverride = StorageCompatibilityMode.NONE; String tableName2 = createTable(keyspace, "CREATE TABLE %s (id int PRIMARY KEY, value text)"); ColumnFamilyStore cfs2 = Keyspace.open(keyspace).getColumnFamilyStore(tableName2); @@ -794,7 +795,7 @@ public void testSchemaBackwardCompatibilityCc40() finally { // Restore original value - TableParams.backwardCompatibilityCC4 = originalValue; + TableParams.storageCompatibilityModeOverride = originalMode; } } From 73ea19eb18fac85a372a0ebb9e8fb21d2fd17cb3 Mon Sep 17 00:00:00 2001 From: Daniel Jatnieks Date: Mon, 17 Nov 2025 12:38:18 -0800 Subject: [PATCH 3/7] Fix StorageCompatibilityModeTest --- .../org/apache/cassandra/utils/StorageCompatibilityModeTest.java | 1 + 1 file changed, 1 insertion(+) diff --git a/test/unit/org/apache/cassandra/utils/StorageCompatibilityModeTest.java b/test/unit/org/apache/cassandra/utils/StorageCompatibilityModeTest.java index f8684edd6250..951dcbe76f0b 100644 --- a/test/unit/org/apache/cassandra/utils/StorageCompatibilityModeTest.java +++ b/test/unit/org/apache/cassandra/utils/StorageCompatibilityModeTest.java @@ -38,6 +38,7 @@ public void testBtiFormatAndStorageCompatibilityMode() { switch (mode) { + case CC_4: case UPGRADING: case NONE: mode.validateSstableFormat(big); From 02cc456e421cf147e8bb5bff983c9299794149ce Mon Sep 17 00:00:00 2001 From: Daniel Jatnieks Date: Mon, 17 Nov 2025 13:22:37 -0800 Subject: [PATCH 4/7] Fix CreateTest.testCreateTableWithMemtable --- .../cql3/statements/schema/TableAttributes.java | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/java/org/apache/cassandra/cql3/statements/schema/TableAttributes.java b/src/java/org/apache/cassandra/cql3/statements/schema/TableAttributes.java index 25bbc862b3b4..6f8bb5014801 100644 --- a/src/java/org/apache/cassandra/cql3/statements/schema/TableAttributes.java +++ b/src/java/org/apache/cassandra/cql3/statements/schema/TableAttributes.java @@ -173,7 +173,7 @@ private TableParams build(TableParams.Builder builder) if (hasOption(MEMTABLE)) { - // Handle deserialzation of Astra/CC 4.0 schema with memtable option as a map + // Handle deserialization of Astra/CC 4.0 schema with memtable option as a map if (properties.get(MEMTABLE.toString()) instanceof Map) { String memtableClass = getMap(MEMTABLE) @@ -183,7 +183,6 @@ private TableParams build(TableParams.Builder builder) .map(Map.Entry::getValue) .findFirst() .orElse(null); - // Not exhaustive, but avoids raising an error upgrading from a CC 4.0 schema // Extract short class name if fully qualified (e.g., "org.apache.cassandra.db.memtable.TrieMemtable" -> "TrieMemtable") String shortClassName = memtableClass != null && memtableClass.contains(".") ? memtableClass.substring(memtableClass.lastIndexOf('.') + 1) @@ -193,8 +192,14 @@ private TableParams build(TableParams.Builder builder) builder.memtable(MemtableParams.get(null)); else if ("SkipListMemtable".equalsIgnoreCase(shortClassName)) builder.memtable(MemtableParams.get("skiplist")); + else if ("PersistentMemoryMemtable".equalsIgnoreCase(shortClassName)) + builder.memtable(MemtableParams.get("persistent_memory")); else if ("TrieMemtable".equalsIgnoreCase(shortClassName)) builder.memtable(MemtableParams.get("trie")); + else if ("TrieMemtableStage1".equalsIgnoreCase(shortClassName)) + builder.memtable(MemtableParams.get("trie")); + else if ("ShardedSkipListMemtable".equalsIgnoreCase(shortClassName)) + builder.memtable(MemtableParams.get("skiplist_sharded")); else // Default to trie for unknown memtable types builder.memtable(MemtableParams.get("trie")); From 2d09b553f13df5dff1ec85b0729e9324d62336e3 Mon Sep 17 00:00:00 2001 From: Daniel Jatnieks Date: Tue, 18 Nov 2025 11:45:26 -0800 Subject: [PATCH 5/7] Extract short class names only for org.apache.cassandra.db.memtable class name; Add some more test coverage --- .../statements/schema/TableAttributes.java | 46 +++++++++------ .../cassandra/db/SchemaCQLHelperTest.java | 58 ++++++++++++++++++- 2 files changed, 84 insertions(+), 20 deletions(-) diff --git a/src/java/org/apache/cassandra/cql3/statements/schema/TableAttributes.java b/src/java/org/apache/cassandra/cql3/statements/schema/TableAttributes.java index 6f8bb5014801..dce26a5bd90d 100644 --- a/src/java/org/apache/cassandra/cql3/statements/schema/TableAttributes.java +++ b/src/java/org/apache/cassandra/cql3/statements/schema/TableAttributes.java @@ -183,26 +183,38 @@ private TableParams build(TableParams.Builder builder) .map(Map.Entry::getValue) .findFirst() .orElse(null); - // Extract short class name if fully qualified (e.g., "org.apache.cassandra.db.memtable.TrieMemtable" -> "TrieMemtable") - String shortClassName = memtableClass != null && memtableClass.contains(".") - ? memtableClass.substring(memtableClass.lastIndexOf('.') + 1) - : memtableClass; - if (shortClassName == null) + if (memtableClass == null) + { builder.memtable(MemtableParams.get(null)); - else if ("SkipListMemtable".equalsIgnoreCase(shortClassName)) - builder.memtable(MemtableParams.get("skiplist")); - else if ("PersistentMemoryMemtable".equalsIgnoreCase(shortClassName)) - builder.memtable(MemtableParams.get("persistent_memory")); - else if ("TrieMemtable".equalsIgnoreCase(shortClassName)) - builder.memtable(MemtableParams.get("trie")); - else if ("TrieMemtableStage1".equalsIgnoreCase(shortClassName)) - builder.memtable(MemtableParams.get("trie")); - else if ("ShardedSkipListMemtable".equalsIgnoreCase(shortClassName)) - builder.memtable(MemtableParams.get("skiplist_sharded")); + } + // Only process as a known memtable if it's in the standard package or is a short name (no package qualifier) + else if (memtableClass.startsWith("org.apache.cassandra.db.memtable.") || !memtableClass.contains(".")) + { + // Extract short class name for comparison against known types + String shortClassName = memtableClass.contains(".") + ? memtableClass.substring(memtableClass.lastIndexOf('.') + 1) + : memtableClass; + + if ("SkipListMemtable".equalsIgnoreCase(shortClassName)) + builder.memtable(MemtableParams.get("skiplist")); + else if ("PersistentMemoryMemtable".equalsIgnoreCase(shortClassName)) + builder.memtable(MemtableParams.get("persistent_memory")); + else if ("TrieMemtable".equalsIgnoreCase(shortClassName)) + builder.memtable(MemtableParams.get("trie")); + else if ("TrieMemtableStage1".equalsIgnoreCase(shortClassName)) + builder.memtable(MemtableParams.get("trie")); + else if ("ShardedSkipListMemtable".equalsIgnoreCase(shortClassName)) + builder.memtable(MemtableParams.get("skiplist_sharded")); + else + // Unknown short name or unknown class in standard package - use as configuration key + builder.memtable(MemtableParams.get(shortClassName)); + } else - // Default to trie for unknown memtable types - builder.memtable(MemtableParams.get("trie")); + { + // Custom fully qualified class name from a different package. + builder.memtable(MemtableParams.get(memtableClass)); + } } else builder.memtable(MemtableParams.get(getString(MEMTABLE))); diff --git a/test/unit/org/apache/cassandra/db/SchemaCQLHelperTest.java b/test/unit/org/apache/cassandra/db/SchemaCQLHelperTest.java index aa2c62941a09..2e8732d2fe23 100644 --- a/test/unit/org/apache/cassandra/db/SchemaCQLHelperTest.java +++ b/test/unit/org/apache/cassandra/db/SchemaCQLHelperTest.java @@ -824,10 +824,62 @@ public void testParseCc40MemtableFormat() ColumnFamilyStore cfs4 = Keyspace.open(keyspace).getColumnFamilyStore(tableName4); Assertions.assertThat(cfs4.metadata().params.memtable.configurationKey()).isEqualTo("skiplist"); - // Test parsing CC 4.0 format with empty map (default) - String tableName5 = createTable(keyspace, "CREATE TABLE %s (id int PRIMARY KEY, value text) WITH memtable = {}"); + // Test parsing CC 4.0 format with PersistentMemoryMemtable (short name) + String tableName5 = createTable(keyspace, "CREATE TABLE %s (id int PRIMARY KEY, value text) WITH memtable = {'class': 'PersistentMemoryMemtable'}"); ColumnFamilyStore cfs5 = Keyspace.open(keyspace).getColumnFamilyStore(tableName5); + Assertions.assertThat(cfs5.metadata().params.memtable.configurationKey()).isEqualTo("persistent_memory"); + + // Test parsing CC 4.0 format with PersistentMemoryMemtable (fully qualified) + String tableName6 = createTable(keyspace, "CREATE TABLE %s (id int PRIMARY KEY, value text) WITH memtable = {'class': 'org.apache.cassandra.db.memtable.PersistentMemoryMemtable'}"); + ColumnFamilyStore cfs6 = Keyspace.open(keyspace).getColumnFamilyStore(tableName6); + Assertions.assertThat(cfs6.metadata().params.memtable.configurationKey()).isEqualTo("persistent_memory"); + + // Test parsing CC 4.0 format with TrieMemtableStage1 (legacy class) + String tableName7 = createTable(keyspace, "CREATE TABLE %s (id int PRIMARY KEY, value text) WITH memtable = {'class': 'TrieMemtableStage1'}"); + ColumnFamilyStore cfs7 = Keyspace.open(keyspace).getColumnFamilyStore(tableName7); + Assertions.assertThat(cfs7.metadata().params.memtable.configurationKey()).isEqualTo("trie"); + + // Test parsing CC 4.0 format with ShardedSkipListMemtable (short name) + String tableName8 = createTable(keyspace, "CREATE TABLE %s (id int PRIMARY KEY, value text) WITH memtable = {'class': 'ShardedSkipListMemtable'}"); + ColumnFamilyStore cfs8 = Keyspace.open(keyspace).getColumnFamilyStore(tableName8); + Assertions.assertThat(cfs8.metadata().params.memtable.configurationKey()).isEqualTo("skiplist_sharded"); + + // Test parsing CC 4.0 format with empty map (default) + String tableName9 = createTable(keyspace, "CREATE TABLE %s (id int PRIMARY KEY, value text) WITH memtable = {}"); + ColumnFamilyStore cfs9 = Keyspace.open(keyspace).getColumnFamilyStore(tableName9); // Empty map should use default memtable - Assertions.assertThat(cfs5.metadata().params.memtable).isNotNull(); + Assertions.assertThat(cfs9.metadata().params.memtable).isNotNull(); + } + + @Test + public void testParseCc40MemtableFormatWithUnknownClass() + { + String keyspace = createKeyspace("CREATE KEYSPACE %s WITH replication={ 'class' : 'SimpleStrategy', 'replication_factor' : 1 }"); + + // Test parsing CC 4.0 format with unknown short class name (not in cassandra.yaml) + // This should throw ConfigurationException because the configuration key doesn't exist + try + { + createTable(keyspace, "CREATE TABLE %s (id int PRIMARY KEY, value text) WITH memtable = {'class': 'UnknownMemtable'}"); + fail("Expected ConfigurationException for unknown memtable class"); + } + catch (RuntimeException e) + { + assertThat(e.getCause(), instanceOf(ConfigurationException.class)); + assertThat(e.getCause().getMessage(), containsString("Memtable configuration \"UnknownMemtable\" not found")); + } + + // Test parsing CC 4.0 format with custom fully qualified class name from different package + // This should also throw ConfigurationException because it's not configured in cassandra.yaml + try + { + createTable(keyspace, "CREATE TABLE %s (id int PRIMARY KEY, value text) WITH memtable = {'class': 'com.example.CustomMemtable'}"); + fail("Expected ConfigurationException for unconfigured custom memtable class"); + } + catch (RuntimeException e) + { + assertThat(e.getCause(), instanceOf(ConfigurationException.class)); + assertThat(e.getCause().getMessage(), containsString("Memtable configuration \"com.example.CustomMemtable\" not found")); + } } } From 361868eaef964ff954a5beaf900d453de6fb90ac Mon Sep 17 00:00:00 2001 From: Daniel Jatnieks Date: Tue, 18 Nov 2025 14:28:49 -0800 Subject: [PATCH 6/7] Update MemtableParams.toMapForCC4 to extract short class names only for org.apache.cassandra.db.memtable class names. --- .../apache/cassandra/schema/MemtableParams.java | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/src/java/org/apache/cassandra/schema/MemtableParams.java b/src/java/org/apache/cassandra/schema/MemtableParams.java index 77450bf6fecf..6c957d3240af 100644 --- a/src/java/org/apache/cassandra/schema/MemtableParams.java +++ b/src/java/org/apache/cassandra/schema/MemtableParams.java @@ -74,8 +74,9 @@ public Memtable.Factory factory() * This is used when outputting schema in a format compatible with CC 4.0. * * CC 4.0 accepts both short class names (e.g., 'TrieMemtable') and fully qualified names - * (e.g., 'org.apache.cassandra.db.memtable.TrieMemtable'), but using short names is preferred - * for consistency and readability. + * (e.g., 'org.apache.cassandra.db.memtable.TrieMemtable'). For standard Cassandra memtables + * in the org.apache.cassandra.db.memtable package, we use short names and for custom memtables + * from other packages, we preserve the fully qualified class name. */ public Map toMapForCC4() { @@ -84,10 +85,13 @@ public Map toMapForCC4() { Map map = new HashMap<>(); String className = definition.class_name; - String shortClassName = className.contains(".") - ? className.substring(className.lastIndexOf('.') + 1) - : className; - map.put("class", shortClassName); + + if (className.startsWith("org.apache.cassandra.db.memtable.")) + { + className = className.substring("org.apache.cassandra.db.memtable.".length()); + } + + map.put("class", className); if (definition.parameters != null) map.putAll(definition.parameters); return map; From d5e0c6e7aa1ee4ae1253fb95688ae47133fd6fa2 Mon Sep 17 00:00:00 2001 From: Daniel Jatnieks Date: Tue, 18 Nov 2025 17:02:23 -0800 Subject: [PATCH 7/7] Add more tests for toMapForCC4 --- .../cassandra/db/SchemaCQLHelperTest.java | 60 +++++++++++++++++++ 1 file changed, 60 insertions(+) diff --git a/test/unit/org/apache/cassandra/db/SchemaCQLHelperTest.java b/test/unit/org/apache/cassandra/db/SchemaCQLHelperTest.java index 2e8732d2fe23..54313247db66 100644 --- a/test/unit/org/apache/cassandra/db/SchemaCQLHelperTest.java +++ b/test/unit/org/apache/cassandra/db/SchemaCQLHelperTest.java @@ -882,4 +882,64 @@ public void testParseCc40MemtableFormatWithUnknownClass() assertThat(e.getCause().getMessage(), containsString("Memtable configuration \"com.example.CustomMemtable\" not found")); } } + + @Test + public void testToMapForCC4OutputFormat() + { + String keyspace = createKeyspace("CREATE KEYSPACE %s WITH replication={ 'class' : 'SimpleStrategy', 'replication_factor' : 1 }"); + StorageCompatibilityMode originalMode = TableParams.storageCompatibilityModeOverride; + + try + { + // Enable CC 4.0 backward compatibility mode to test toMapForCC4() output + TableParams.storageCompatibilityModeOverride = StorageCompatibilityMode.CC_4; + + // Test that standard Cassandra memtables output short class names + String tableName1 = createTable(keyspace, "CREATE TABLE %s (id int PRIMARY KEY, value text) WITH memtable = 'trie'"); + ColumnFamilyStore cfs1 = Keyspace.open(keyspace).getColumnFamilyStore(tableName1); + String cql1 = SchemaCQLHelper.getTableMetadataAsCQL(cfs1.metadata(), cfs1.keyspace.getMetadata()); + // Should output short class name for standard Cassandra memtable + Assertions.assertThat(cql1).contains("memtable = {'class': 'TrieMemtable'"); + Assertions.assertThat(cql1).doesNotContain("org.apache.cassandra.db.memtable.TrieMemtable"); + + String tableName2 = createTable(keyspace, "CREATE TABLE %s (id int PRIMARY KEY, value text) WITH memtable = 'skiplist'"); + ColumnFamilyStore cfs2 = Keyspace.open(keyspace).getColumnFamilyStore(tableName2); + String cql2 = SchemaCQLHelper.getTableMetadataAsCQL(cfs2.metadata(), cfs2.keyspace.getMetadata()); + // Should output short class name for standard Cassandra memtable + Assertions.assertThat(cql2).contains("memtable = {'class': 'SkipListMemtable'"); + Assertions.assertThat(cql2).doesNotContain("org.apache.cassandra.db.memtable.SkipListMemtable"); + + String tableName3 = createTable(keyspace, "CREATE TABLE %s (id int PRIMARY KEY, value text) WITH memtable = 'skiplist_sharded'"); + ColumnFamilyStore cfs3 = Keyspace.open(keyspace).getColumnFamilyStore(tableName3); + String cql3 = SchemaCQLHelper.getTableMetadataAsCQL(cfs3.metadata(), cfs3.keyspace.getMetadata()); + // Should output short class name for standard Cassandra memtable + Assertions.assertThat(cql3).contains("memtable = {'class': 'ShardedSkipListMemtable'"); + Assertions.assertThat(cql3).doesNotContain("org.apache.cassandra.db.memtable.ShardedSkipListMemtable"); + + // Test that tables created with fully qualified class names also output short class names + String tableName4 = createTable(keyspace, "CREATE TABLE %s (id int PRIMARY KEY, value text) WITH memtable = {'class': 'org.apache.cassandra.db.memtable.TrieMemtable'}"); + ColumnFamilyStore cfs4 = Keyspace.open(keyspace).getColumnFamilyStore(tableName4); + String cql4 = SchemaCQLHelper.getTableMetadataAsCQL(cfs4.metadata(), cfs4.keyspace.getMetadata()); + // Should output short class name even though input was fully qualified + Assertions.assertThat(cql4).contains("memtable = {'class': 'TrieMemtable'"); + Assertions.assertThat(cql4).doesNotContain("org.apache.cassandra.db.memtable.TrieMemtable"); + + String tableName5 = createTable(keyspace, "CREATE TABLE %s (id int PRIMARY KEY, value text) WITH memtable = {'class': 'org.apache.cassandra.db.memtable.SkipListMemtable'}"); + ColumnFamilyStore cfs5 = Keyspace.open(keyspace).getColumnFamilyStore(tableName5); + String cql5 = SchemaCQLHelper.getTableMetadataAsCQL(cfs5.metadata(), cfs5.keyspace.getMetadata()); + // Should output short class name even though input was fully qualified + Assertions.assertThat(cql5).contains("memtable = {'class': 'SkipListMemtable'"); + Assertions.assertThat(cql5).doesNotContain("org.apache.cassandra.db.memtable.SkipListMemtable"); + + // Test that custom memtables from other packages preserve fully qualified class names + // Note: We can't easily test this without adding a custom memtable configuration to cassandra.yaml, + // but the logic in toMapForCC4() ensures that only classes starting with + // "org.apache.cassandra.db.memtable." get their short names extracted. + } + finally + { + // Restore original value + TableParams.storageCompatibilityModeOverride = originalMode; + } + } }