diff --git a/driver-core/src/main/java/com/datastax/driver/core/ColumnMetadata.java b/driver-core/src/main/java/com/datastax/driver/core/ColumnMetadata.java index 12b31380bf2..c9c39084a49 100644 --- a/driver-core/src/main/java/com/datastax/driver/core/ColumnMetadata.java +++ b/driver-core/src/main/java/com/datastax/driver/core/ColumnMetadata.java @@ -15,7 +15,9 @@ */ package com.datastax.driver.core; -import java.util.*; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; /** * Describes a Column. @@ -42,10 +44,6 @@ public class ColumnMetadata { private final DataType type; private final boolean isStatic; - // this is the "reverse" side of the many-to-many relationship - // between columns and indexes, and is updated only after the column is created - final Map indexes = new LinkedHashMap(); - private ColumnMetadata(TableOrView parent, String name, DataType type, boolean isStatic) { this.parent = parent; this.name = name; @@ -98,28 +96,6 @@ public boolean isStatic() { return isStatic; } - /** - * Returns metadata on a index on this column. - * - * @param name the name of the index to retrieve ({@code name} will be - * interpreted as a case-insensitive identifier unless enclosed in double-quotes, - * see {@link Metadata#quote}). - * @return the metadata for the {@code name} index if it exists, or - * {@code null} otherwise. - */ - public IndexMetadata getIndex(String name) { - return indexes.get(Metadata.handleId(name)); - } - - /** - * Returns a list containing all the indexes on this column. - * - * @return a list containing the metadata for the indexes on this column. - */ - public List getIndexes() { - return new ArrayList(indexes.values()); - } - @Override public String toString() { String str = Metadata.escapeId(name) + ' ' + type; diff --git a/driver-core/src/main/java/com/datastax/driver/core/IndexMetadata.java b/driver-core/src/main/java/com/datastax/driver/core/IndexMetadata.java index 443414843d8..2f08d59ee2b 100644 --- a/driver-core/src/main/java/com/datastax/driver/core/IndexMetadata.java +++ b/driver-core/src/main/java/com/datastax/driver/core/IndexMetadata.java @@ -15,65 +15,62 @@ */ package com.datastax.driver.core; -import java.util.*; +import java.util.Iterator; +import java.util.Map; -import com.google.common.base.Function; import com.google.common.base.Objects; +import com.google.common.base.Predicate; import com.google.common.collect.ImmutableMap; +import com.google.common.collect.Iterables; /** * An immutable representation of secondary index metadata. */ public class IndexMetadata { - public enum IndexType { + public enum Kind { KEYS, CUSTOM, COMPOSITES } - public enum TargetType { - COLUMN, ROW - } + static final String NAME = "index_name"; - public static final Function INDEX_NAME = new Function() { - @Override - public String apply(IndexMetadata input) { - return input.getName(); - } - }; + static final String KIND = "kind"; - public static final String CUSTOM_INDEX_OPTION_NAME = "class_name"; + static final String OPTIONS = "options"; /** - * The name of the option used to specify that the index is on the collection keys. + * The name of the option used to specify the index target (Cassandra 3.0 onwards). */ - public static final String INDEX_KEYS_OPTION_NAME = "index_keys"; + public static final String TARGET_OPTION_NAME = "target"; /** - * The name of the option used to specify that the index is on the collection values. + * The name of the option used to specify a custom index class name. */ - public static final String INDEX_VALUES_OPTION_NAME = "index_values"; + public static final String CUSTOM_INDEX_OPTION_NAME = "class_name"; + + /** + * The name of the option used to specify that the index is on the collection (map) keys. + */ + public static final String INDEX_KEYS_OPTION_NAME = "index_keys"; /** * The name of the option used to specify that the index is on the collection (map) entries. */ public static final String INDEX_ENTRIES_OPTION_NAME = "index_keys_and_values"; - private final TableMetadata table; private final String name; - private final Map columns; - private final IndexType indexType; - private final TargetType targetType; + private final Kind kind; + private final String target; private final Map options; - private IndexMetadata(TableMetadata table, String name, Map columns, IndexType indexType, TargetType targetType, Map options) { + private IndexMetadata(TableMetadata table, String name, Kind kind, String target, Map options) { this.table = table; this.name = name; - this.columns = columns; - this.indexType = indexType; - this.targetType = targetType; + this.kind = kind; + this.target = target; this.options = options; } @@ -81,16 +78,11 @@ private IndexMetadata(TableMetadata table, String name, Map targetColumnNames = indexRow.getSet("target_columns", String.class); - LinkedHashMap targetColumns = new LinkedHashMap(targetColumnNames.size()); - for (String targetColumnName : targetColumnNames) { - targetColumns.put(targetColumnName, table.getColumn(targetColumnName)); - } - IndexMetadata.IndexType indexType = IndexMetadata.IndexType.valueOf(indexRow.getString("index_type")); - IndexMetadata.TargetType targetType = IndexMetadata.TargetType.valueOf(indexRow.getString("target_type")); - Map options = indexRow.getMap("options", String.class, String.class); - return new IndexMetadata(table, name, targetColumns, indexType, targetType, options); + String name = indexRow.getString(NAME); + Kind kind = Kind.valueOf(indexRow.getString(KIND)); + Map options = indexRow.getMap(OPTIONS, String.class, String.class); + String target = options.get(TARGET_OPTION_NAME); + return new IndexMetadata(table, name, kind, target, options); } /** @@ -98,31 +90,44 @@ static IndexMetadata fromRow(TableMetadata table, Row indexRow) { * along with indexed column). */ static IndexMetadata fromLegacy(ColumnMetadata column, ColumnMetadata.Raw raw) { - if (raw.indexColumns.isEmpty()) + Map indexColumns = raw.indexColumns; + if (indexColumns.isEmpty()) return null; - String type = raw.indexColumns.get(ColumnMetadata.INDEX_TYPE); + String type = indexColumns.get(ColumnMetadata.INDEX_TYPE); if (type == null) return null; - String indexName = raw.indexColumns.get(ColumnMetadata.INDEX_NAME); - String indexTypeStr = raw.indexColumns.get(ColumnMetadata.INDEX_TYPE); - IndexType indexType = indexTypeStr == null ? null : IndexType.valueOf(indexTypeStr); + String indexName = indexColumns.get(ColumnMetadata.INDEX_NAME); + String kindStr = indexColumns.get(ColumnMetadata.INDEX_TYPE); + Kind kind = kindStr == null ? null : Kind.valueOf(kindStr); // Special case check for the value of the index_options column being a string with value 'null' as this // column appears to be set this way (JAVA-834). - String indexOptionsCol = raw.indexColumns.get(ColumnMetadata.INDEX_OPTIONS); - ImmutableMap columns = ImmutableMap.of(column.getName(), column); + String indexOptionsCol = indexColumns.get(ColumnMetadata.INDEX_OPTIONS); Map options; if (indexOptionsCol == null || indexOptionsCol.isEmpty() || indexOptionsCol.equals("null")) { options = ImmutableMap.of(); } else { options = SimpleJSONParser.parseStringMap(indexOptionsCol); } - return new IndexMetadata((TableMetadata)column.getParent(), indexName, columns, indexType, TargetType.COLUMN, options); + String target = targetFromLegacyOptions(column, options); + return new IndexMetadata((TableMetadata)column.getParent(), indexName, kind, target, options); + } + + private static String targetFromLegacyOptions(ColumnMetadata column, Map options) { + String columnName = Metadata.escapeId(column.getName()); + if(options.containsKey(INDEX_KEYS_OPTION_NAME)) + return String.format("keys(%s)", columnName); + if(options.containsKey(INDEX_ENTRIES_OPTION_NAME)) + return String.format("entries(%s)", columnName); + if(column.getType() instanceof DataType.CollectionType && column.getType().isFrozen()) + return String.format("full(%s)", columnName); + // Note: the keyword 'values' is not accepted as a valid index target function until 3.0 + return columnName; } /** - * Returns the metadata of the table this column is part of. + * Returns the metadata of the table this index is part of. * - * @return the {@code TableMetadata} for the table this column is part of. + * @return the table this index is part of. */ public TableMetadata getTable() { return table; @@ -138,47 +143,21 @@ public String getName() { } /** - * Returns metadata on a column of this index. + * Returns the index kind. * - * @param name the name of the column to retrieve ({@code name} will be - * interpreted as a case-insensitive identifier unless enclosed in double-quotes, - * see {@link Metadata#quote}). - * @return the metadata for the column if it exists, or - * {@code null} otherwise. + * @return the index kind. */ - public ColumnMetadata getColumn(String name) { - return columns.get(Metadata.handleId(name)); + public Kind getKind() { + return kind; } /** - * Returns a list containing all the columns of this index. + * Returns the index target. * - * The order of the columns in the list is consistent with - * the order of the columns in the index. - * - * @return a list containing the metadata for the columns of this table. - */ - public List getColumns() { - return new ArrayList(columns.values()); - } - - /** - * Returns the index type. - * - * @return the index type. + * @return the index target. */ - public IndexType getIndexType() { - return indexType; - } - - /** - * Returns the index target type. - * Note: for legacy indexes, this is always {@link TargetType#COLUMN}. - * - * @return the index target type. - */ - public TargetType getTargetType() { - return targetType; + public String getTarget() { + return target; } /** @@ -204,56 +183,6 @@ public String getIndexClassName() { return getOption(CUSTOM_INDEX_OPTION_NAME); } - /** - * Return whether this index is a 'KEYS' index on a map, e.g., - * CREATE INDEX ON mytable (KEYS(mymap)) - * - * @return {@code true} if this is a 'KEYS' index on a map. - */ - public boolean isKeys() { - return getOption(INDEX_KEYS_OPTION_NAME) != null; - } - - /** - * Return whether this index is a 'VALUES' index on a map, e.g., - * CREATE INDEX ON mytable (VALUES(mymap)) - * - * @return {@code true} if this is an 'VALUES' index on a map. - */ - public boolean isValues() { - return getOption(INDEX_VALUES_OPTION_NAME) != null; - } - - /** - * Return whether this index is a 'ENTRIES' index on a map, e.g., - * CREATE INDEX ON mytable (ENTRIES(mymap)) - * - * @return {@code true} if this is an 'ENTRIES' index on a map. - */ - public boolean isEntries() { - return getOption(INDEX_ENTRIES_OPTION_NAME) != null; - } - - /** - * Return whether this index is a 'FULL' index on a frozen collection, e.g., - * CREATE INDEX ON mytable (FULL(mymap)) - * - * @return {@code true} if this is a 'FULL' index on a frozen collection. - */ - public boolean isFull() { - /* - * This check is analogous to the Cassandra counterpart - * in IndexTarget. - */ - ColumnMetadata column = columns.values().iterator().next(); - return - !isKeys() - && !isValues() - && !isEntries() - && column.getType() instanceof DataType.CollectionType - && column.getType().isFrozen(); - } - /** * Return the value for the given option name. * @@ -273,14 +202,12 @@ public String getOption(String name) { * @return the 'CREATE INDEX' query corresponding to this index. */ public String asCQLQuery() { - TableMetadata table = getTable(); - String ksName = Metadata.escapeId(table.getKeyspace().getName()); - String cfName = Metadata.escapeId(table.getName()); - // TODO indexes on multiple columns - String colName = Metadata.escapeId(columns.keySet().iterator().next()); + String keyspaceName = Metadata.escapeId(table.getKeyspace().getName()); + String tableName = Metadata.escapeId(table.getName()); + String indexName = Metadata.escapeId(this.name); return isCustomIndex() - ? String.format("CREATE CUSTOM INDEX %s ON %s.%s (%s) USING '%s' WITH OPTIONS = %s;", name, ksName, cfName, colName, getIndexClassName(), getOptionsAsCql()) - : String.format("CREATE INDEX %s ON %s.%s (%s);", name, ksName, cfName, getIndexFunction(colName)); + ? String.format("CREATE CUSTOM INDEX %s ON %s.%s (%s) USING '%s' %s;", indexName, keyspaceName, tableName, getTarget(), getIndexClassName(), getOptionsAsCql()) + : String.format("CREATE INDEX %s ON %s.%s (%s);", indexName, keyspaceName, tableName, getTarget()); } /** @@ -290,9 +217,18 @@ public String asCQLQuery() { * the 'index_options' column of the 'schema_columns' table in the 'system' keyspace. */ private String getOptionsAsCql() { + Iterable> filtered = Iterables.filter(options.entrySet(), new Predicate>() { + @Override + public boolean apply(Map.Entry input) { + return + !input.getKey().equals(TARGET_OPTION_NAME) && + !input.getKey().equals(CUSTOM_INDEX_OPTION_NAME); + } + }); + if(Iterables.isEmpty(filtered)) return ""; StringBuilder builder = new StringBuilder(); - builder.append("{"); - Iterator> it = options.entrySet().iterator(); + builder.append("WITH OPTIONS = {"); + Iterator> it = filtered.iterator(); while (it.hasNext()) { Map.Entry option = it.next(); builder.append(String.format("'%s' : '%s'", option.getKey(), option.getValue())); @@ -303,24 +239,8 @@ private String getOptionsAsCql() { return builder.toString(); } - /** - * Wraps the column name with the appropriate index function (KEYS, FULL, ENTRIES), - * if necessary. - * - * @return Column name wrapped with the appropriate index function. - */ - private String getIndexFunction(String colName) { - if (isKeys()) - return String.format("KEYS(%s)", colName); - else if (isFull()) - return String.format("FULL(%s)", colName); - else if (isEntries()) - return String.format("ENTRIES(%s)", colName); - return colName; - } - public int hashCode() { - return Objects.hashCode(name, columns, indexType, targetType, options); + return Objects.hashCode(name, kind, target, options); } public boolean equals(Object obj) { @@ -333,9 +253,8 @@ public boolean equals(Object obj) { IndexMetadata other = (IndexMetadata)obj; return Objects.equal(name, other.name) - && Objects.equal(columns, other.columns) - && Objects.equal(indexType, other.indexType) - && Objects.equal(targetType, other.targetType) + && Objects.equal(kind, other.kind) + && Objects.equal(target, other.target) && Objects.equal(options, other.options); } diff --git a/driver-core/src/main/java/com/datastax/driver/core/MaterializedViewMetadata.java b/driver-core/src/main/java/com/datastax/driver/core/MaterializedViewMetadata.java index 31018df7d39..fcc00b67831 100644 --- a/driver-core/src/main/java/com/datastax/driver/core/MaterializedViewMetadata.java +++ b/driver-core/src/main/java/com/datastax/driver/core/MaterializedViewMetadata.java @@ -72,12 +72,12 @@ static MaterializedViewMetadata build(KeyspaceMetadata keyspace, Row row, Map partitionKey = nullInitializedList(partitionKeySize); List clusteringColumns = nullInitializedList(clusteringSize); - List clusteringOrder = nullInitializedList(clusteringSize); + List clusteringOrder = nullInitializedList(clusteringSize); // We use a linked hashmap because we will keep this in the order of a 'SELECT * FROM ...'. LinkedHashMap columns = new LinkedHashMap(); - TableMetadata.Options options = null; + Options options = null; try { options = new Options(row, false, cassandraVersion); } catch (RuntimeException e) { diff --git a/driver-core/src/main/java/com/datastax/driver/core/SchemaParser.java b/driver-core/src/main/java/com/datastax/driver/core/SchemaParser.java index e8b18bf7966..a33b6e7d36f 100644 --- a/driver-core/src/main/java/com/datastax/driver/core/SchemaParser.java +++ b/driver-core/src/main/java/com/datastax/driver/core/SchemaParser.java @@ -387,6 +387,7 @@ static void buildViewMetadata(KeyspaceMetadata ksm, List viewRows, Map cols = colsDefs.get(viewName); if (cols == null || cols.isEmpty()) continue; // we probably raced, we will update the metadata next time + MaterializedViewMetadata view = MaterializedViewMetadata.build(ksm, viewRow, cols, cassandraVersion); if(view != null) ksm.add(view); diff --git a/driver-core/src/main/java/com/datastax/driver/core/TableMetadata.java b/driver-core/src/main/java/com/datastax/driver/core/TableMetadata.java index c30c097938a..5a10cd5a988 100644 --- a/driver-core/src/main/java/com/datastax/driver/core/TableMetadata.java +++ b/driver-core/src/main/java/com/datastax/driver/core/TableMetadata.java @@ -50,6 +50,7 @@ public class TableMetadata extends TableOrView { private static final String EMPTY_TYPE = "org.apache.cassandra.db.marshal.EmptyType"; private final Map indexes; + private final Map views; private TableMetadata(KeyspaceMetadata keyspace, @@ -177,8 +178,6 @@ else if (cassandraVersion.getMajor() > 2) } } - Map> indexedColumns = new LinkedHashMap>(); - for (ColumnMetadata.Raw rawCol : rawCols.values()) { ColumnMetadata col = ColumnMetadata.fromRaw(tm, rawCol); switch (rawCol.kind) { @@ -193,15 +192,13 @@ else if (cassandraVersion.getMajor() > 2) otherColumns.add(col); break; } - // create legacy secondary indexes (C* < 3.0) + + // legacy secondary indexes (C* < 3.0) IndexMetadata index = IndexMetadata.fromLegacy(col, rawCol); - if(index != null) { + if(index != null) indexes.put(index.getName(), index); - addIndexToColumn(index, col, indexedColumns); - } } - for (ColumnMetadata c : partitionKey) columns.put(c.getName(), c); for (ColumnMetadata c : clusteringColumns) @@ -214,36 +211,11 @@ else if (cassandraVersion.getMajor() > 2) for (Row indexRow : indexRows) { IndexMetadata index = IndexMetadata.fromRow(tm, indexRow); indexes.put(index.getName(), index); - // update the many-to-many relationship between indexes and columns - for (ColumnMetadata column : index.getColumns()) { - addIndexToColumn(index, column, indexedColumns); - } } - // update indexed columns - for (Map.Entry> entry : indexedColumns.entrySet()) { - ColumnMetadata column = entry.getKey(); - for (IndexMetadata index : entry.getValue()) { - // update the "reverse" side of the many-to-many relationship between indexes and columns - column.indexes.put(index.getName(), index); - } - } - return tm; } - /** - * Associate the given index with the given column and store it in the given map. - */ - private static void addIndexToColumn(IndexMetadata index, ColumnMetadata column, Map> indexedColumns) { - Set indexes = indexedColumns.get(column); - if(indexes == null) { - indexes = new LinkedHashSet(); - indexedColumns.put(column, indexes); - } - indexes.add(index); - } - /** * Upon migration from thrift to CQL, we internally create a pair of surrogate clustering/regular columns * for compact static tables. These columns shouldn't be exposed to the user but are currently returned by C*. @@ -405,8 +377,8 @@ public String exportAsString() { sb.append('\n').append(index.asCQLQuery()); } - for (MaterializedViewMetadata index : views.values()) { - sb.append('\n').append(index.asCQLQuery()); + for (MaterializedViewMetadata view : views.values()) { + sb.append('\n').append(view.asCQLQuery()); } return sb.toString(); diff --git a/driver-core/src/main/java/com/datastax/driver/core/TableOrView.java b/driver-core/src/main/java/com/datastax/driver/core/TableOrView.java index 5d8873da94c..2de3cc8088a 100644 --- a/driver-core/src/main/java/com/datastax/driver/core/TableOrView.java +++ b/driver-core/src/main/java/com/datastax/driver/core/TableOrView.java @@ -225,7 +225,11 @@ void add(ColumnMetadata column) { * String}. */ public String exportAsString() { - return asCQLQuery(true); + StringBuilder sb = new StringBuilder(); + + sb.append(asCQLQuery(true)); + + return sb.toString(); } /** diff --git a/driver-core/src/test/java/com/datastax/driver/core/ColumnMetadataAssert.java b/driver-core/src/test/java/com/datastax/driver/core/ColumnMetadataAssert.java index 45430993703..1f62829ed79 100644 --- a/driver-core/src/test/java/com/datastax/driver/core/ColumnMetadataAssert.java +++ b/driver-core/src/test/java/com/datastax/driver/core/ColumnMetadataAssert.java @@ -70,14 +70,4 @@ public ColumnMetadataAssert isNotStatic() { return this; } - public ColumnMetadataAssert hasIndex(IndexMetadata index) { - assertThat(actual.getIndex(index.getName())).isEqualTo(index); - return this; - } - - public ColumnMetadataAssert hasSingleIndex(IndexMetadata index) { - assertThat(actual.getIndexes()).containsOnly(index); - return this; - } - } diff --git a/driver-core/src/test/java/com/datastax/driver/core/IndexMetadataAssert.java b/driver-core/src/test/java/com/datastax/driver/core/IndexMetadataAssert.java index f95b06a1454..d0a6fad556c 100644 --- a/driver-core/src/test/java/com/datastax/driver/core/IndexMetadataAssert.java +++ b/driver-core/src/test/java/com/datastax/driver/core/IndexMetadataAssert.java @@ -19,6 +19,8 @@ import static org.assertj.core.api.Assertions.assertThat; +import com.datastax.driver.core.IndexMetadata.Kind; + public class IndexMetadataAssert extends AbstractAssert { public IndexMetadataAssert(IndexMetadata actual) { @@ -29,7 +31,12 @@ public IndexMetadataAssert hasName(String name) { assertThat(actual.getName()).isEqualTo(name); return this; } - + + public IndexMetadataAssert hasParent(TableMetadata parent) { + assertThat(actual.getTable()).isEqualTo(parent); + return this; + } + public IndexMetadataAssert hasOption(String name, String value) { assertThat(actual.getOption(name)).isEqualTo(value); return this; @@ -39,37 +46,7 @@ public IndexMetadataAssert asCqlQuery(String cqlQuery) { assertThat(actual.asCQLQuery()).isEqualTo(cqlQuery); return this; } - - public IndexMetadataAssert isKeys(){ - assertThat(actual.isKeys()).isTrue(); - return this; - } - - public IndexMetadataAssert isNotKeys(){ - assertThat(actual.isKeys()).isFalse(); - return this; - } - - public IndexMetadataAssert isFull() { - assertThat(actual.isFull()).isTrue(); - return this; - } - - public IndexMetadataAssert isNotFull() { - assertThat(actual.isFull()).isFalse(); - return this; - } - - public IndexMetadataAssert isEntries() { - assertThat(actual.isEntries()).isTrue(); - return this; - } - - public IndexMetadataAssert isNotEntries() { - assertThat(actual.isEntries()).isFalse(); - return this; - } - + public IndexMetadataAssert isCustomIndex() { assertThat(actual.isCustomIndex()).isTrue(); return this; @@ -80,13 +57,13 @@ public IndexMetadataAssert isNotCustomIndex() { return this; } - public IndexMetadataAssert hasColumn(ColumnMetadata column) { - assertThat(actual.getColumn(column.getName())).isEqualTo(column); + public IndexMetadataAssert hasTarget(String target) { + assertThat(actual.getTarget()).isEqualTo(target); return this; } - public IndexMetadataAssert hasSingleColumn(ColumnMetadata column) { - assertThat(actual.getColumns()).containsOnly(column); + public IndexMetadataAssert hasKind(Kind kind) { + assertThat(actual.getKind()).isEqualTo(kind); return this; } } diff --git a/driver-core/src/test/java/com/datastax/driver/core/IndexMetadataTest.java b/driver-core/src/test/java/com/datastax/driver/core/IndexMetadataTest.java index 4840384d7a0..1984f8cf88d 100644 --- a/driver-core/src/test/java/com/datastax/driver/core/IndexMetadataTest.java +++ b/driver-core/src/test/java/com/datastax/driver/core/IndexMetadataTest.java @@ -20,7 +20,9 @@ import java.util.List; import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; import com.google.common.primitives.Ints; +import org.testng.annotations.BeforeClass; import org.testng.annotations.Test; import com.datastax.driver.core.ColumnMetadata.*; @@ -30,15 +32,17 @@ import static com.datastax.driver.core.Assertions.assertThat; import static com.datastax.driver.core.ColumnMetadata.*; import static com.datastax.driver.core.DataType.cint; +import static com.datastax.driver.core.DataType.map; import static com.datastax.driver.core.DataType.text; +import static com.datastax.driver.core.IndexMetadata.Kind.*; @CassandraVersion(major = 1.2) public class IndexMetadataTest extends CCMBridge.PerClassSingleNodeCluster { /** - * Column definitions for schema_columns table. + * Column definitions for schema_columns table (legacy pre-3.0 layout). */ - private static final ColumnDefinitions defs = new ColumnDefinitions(new ColumnDefinitions.Definition[]{ + private static final ColumnDefinitions legacyColumnDefs = new ColumnDefinitions(new ColumnDefinitions.Definition[]{ definition(COLUMN_NAME, text()), definition(COMPONENT_INDEX, cint()), definition(KIND_V2, text()), @@ -48,192 +52,277 @@ public class IndexMetadataTest extends CCMBridge.PerClassSingleNodeCluster { definition(INDEX_OPTIONS, text()) }); - static { - defs.setCodecRegistry(new CodecRegistry()); + /** + * Column definitions for indexes table (post-3.0 layout). + */ + private static final ColumnDefinitions indexColumnDefs = new ColumnDefinitions(new ColumnDefinitions.Definition[]{ + definition(IndexMetadata.NAME, text()), + definition(IndexMetadata.KIND, text()), + definition(IndexMetadata.OPTIONS, map(text(), text())) + }); + public static final TypeCodec.MapCodec MAP_CODEC = new TypeCodec.MapCodec(TypeCodec.VarcharCodec.instance, TypeCodec.VarcharCodec.instance); + + private static ProtocolVersion protocolVersion; + private static CodecRegistry codecRegistry; + + @BeforeClass(groups = { "short", "long" }) + public void beforeClass() { + super.beforeClass(); + protocolVersion = cluster.getConfiguration().getProtocolOptions().getProtocolVersion(); + codecRegistry = cluster.getConfiguration().getCodecRegistry(); + legacyColumnDefs.setCodecRegistry(codecRegistry); + indexColumnDefs.setCodecRegistry(codecRegistry); } @Override protected Collection getTableDefinitions() { - String createTable = "CREATE TABLE indexing (" - + "id int primary key," - + "map_values map," - + "map_keys map," - + "map_entries map," - + "text_column text" - + - // Frozen collections was introduced only in C* 2.1.3 - (cassandraVersion.compareTo(VersionNumber.parse("2.1.3")) >= 0 - ? - ", map_full frozen>," - + "set_full frozen>," - + "list_full frozen>);" - : - ")"); - return ImmutableList.of(createTable); + ImmutableList.Builder builder = ImmutableList.builder(); + builder.add("CREATE TABLE indexing (" + + "id int," + + "id2 int," + + "map_values map," + + "map_keys map," + + "map_entries map," + + "map_all map," + + "text_column text, " + + "\"MixedCaseColumn\" list," + + + // Frozen collections was introduced only in C* 2.1.3 + (cassandraVersion.compareTo(VersionNumber.parse("2.1.3")) >= 0 + ? + ", map_full frozen>," + + "set_full frozen>," + + "list_full frozen>," + : + "") + + "PRIMARY KEY (id, id2));" + ); + + return builder.build(); } @Test(groups = "short") - public void should_not_flag_text_column_index_type() { + public void should_create_metadata_for_simple_index() { String createValuesIndex = String.format("CREATE INDEX text_column_index ON %s.indexing (text_column);", keyspace); session.execute(createValuesIndex); ColumnMetadata column = getColumn("text_column"); IndexMetadata index = getIndex("text_column_index"); - assertThat(index).hasName("text_column_index") - .isNotKeys() - .isNotFull() - .isNotEntries() + assertThat(index) + .hasName("text_column_index") + .hasParent((TableMetadata)column.getParent()) + .isNotCustomIndex() + .hasTarget("text_column") + .hasKind(COMPOSITES) + .asCqlQuery(createValuesIndex); + assertThat((TableMetadata) column.getParent()).hasIndex(index); + } + + @Test(groups = "short") + @CassandraVersion(major = 2.1, description = "index names with quoted identifiers and collection indexes not supported until 2.1") + public void should_create_metadata_for_values_index_on_mixed_case_column() { + // 3.0 assumes the 'values' keyword if index on a collection + String createValuesIndex = cassandraVersion.getMajor() > 2 ? + String.format("CREATE INDEX \"MixedCaseIndex\" ON %s.indexing (values(\"MixedCaseColumn\"));", keyspace) : + String.format("CREATE INDEX \"MixedCaseIndex\" ON %s.indexing (\"MixedCaseColumn\");", keyspace); + session.execute(createValuesIndex); + ColumnMetadata column = getColumn("\"MixedCaseColumn\""); + IndexMetadata index = getIndex("\"MixedCaseIndex\""); + assertThat(index) + .hasName("MixedCaseIndex") + .hasParent((TableMetadata)column.getParent()) .isNotCustomIndex() - .hasSingleColumn(getColumn("text_column")) + .hasTarget(cassandraVersion.getMajor() > 2 ? "values(\"MixedCaseColumn\")" : "\"MixedCaseColumn\"") + .hasKind(COMPOSITES) .asCqlQuery(createValuesIndex); - assertThat(index).isSameAs(column.getIndex("text_column_index")); - assertThat(column).hasSingleIndex(index); - assertThat(column).isSameAs(index.getColumn("text_column")); + assertThat((TableMetadata) column.getParent()).hasIndex(index); } @Test(groups = "short") @CassandraVersion(major = 2.1) - public void should_not_flag_map_index_type() { - String createValuesIndex = String.format("CREATE INDEX map_values_index ON %s.indexing (map_values);", keyspace); + public void should_create_metadata_for_index_on_map_values() { + // 3.0 assumes the 'values' keyword if index on a collection + String createValuesIndex = cassandraVersion.getMajor() > 2 ? + String.format("CREATE INDEX map_values_index ON %s.indexing (values(map_values));", keyspace) : + String.format("CREATE INDEX map_values_index ON %s.indexing (map_values);", keyspace); session.execute(createValuesIndex); ColumnMetadata column = getColumn("map_values"); IndexMetadata index = getIndex("map_values_index"); - assertThat(index).hasName("map_values_index") - .isNotKeys() - .isNotFull() - .isNotEntries() + assertThat(index) + .hasName("map_values_index") + .hasParent((TableMetadata)column.getParent()) .isNotCustomIndex() - .hasSingleColumn(getColumn("map_values")) + .hasTarget(cassandraVersion.getMajor() > 2 ? "values(map_values)" : "map_values") + .hasKind(COMPOSITES) .asCqlQuery(createValuesIndex); - assertThat(index).isSameAs(column.getIndex("map_values_index")); - assertThat(column).hasSingleIndex(index); - assertThat(column).isSameAs(index.getColumn("map_values")); + assertThat((TableMetadata) column.getParent()).hasIndex(index); } @Test(groups = "short") @CassandraVersion(major = 2.1) - public void should_flag_map_index_type_as_keys() { - String createKeysIndex = String.format("CREATE INDEX map_keys_index ON %s.indexing (KEYS(map_keys));", keyspace); + public void should_create_metadata_for_index_on_map_keys() { + String createKeysIndex = String.format("CREATE INDEX map_keys_index ON %s.indexing (keys(map_keys));", keyspace); session.execute(createKeysIndex); ColumnMetadata column = getColumn("map_keys"); IndexMetadata index = getIndex("map_keys_index"); - assertThat(index).hasName("map_keys_index") - .isKeys() - .isNotFull() - .isNotEntries() + assertThat(index) + .hasName("map_keys_index") + .hasParent((TableMetadata)column.getParent()) .isNotCustomIndex() - .hasSingleColumn(getColumn("map_keys")) + .hasTarget("keys(map_keys)") + .hasKind(COMPOSITES) .asCqlQuery(createKeysIndex); - assertThat(index).isSameAs(column.getIndex("map_keys_index")); - assertThat(column).hasSingleIndex(index); - assertThat(column).isSameAs(index.getColumn("map_keys")); + assertThat((TableMetadata) column.getParent()).hasIndex(index); } @Test(groups = "short") @CassandraVersion(major = 2.1, minor = 3) - public void should_flag_map_index_type_as_full() { - String createFullIndex = String.format("CREATE INDEX map_full_index ON %s.indexing (FULL(map_full));", keyspace); + public void should_create_metadata_for_full_index_on_map() { + String createFullIndex = String.format("CREATE INDEX map_full_index ON %s.indexing (full(map_full));", keyspace); session.execute(createFullIndex); ColumnMetadata column = getColumn("map_full"); IndexMetadata index = getIndex("map_full_index"); - assertThat(index).hasName("map_full_index") - .isNotKeys() - .isFull() - .isNotEntries() + assertThat(index) + .hasName("map_full_index") + .hasParent((TableMetadata)column.getParent()) .isNotCustomIndex() - .hasSingleColumn(getColumn("map_full")) + .hasTarget("full(map_full)") + .hasKind(COMPOSITES) .asCqlQuery(createFullIndex); - assertThat(index).isSameAs(column.getIndex("map_full_index")); - assertThat(column).hasSingleIndex(index); - assertThat(column).isSameAs(index.getColumn("map_full")); + assertThat((TableMetadata) column.getParent()).hasIndex(index); } @Test(groups = "short") @CassandraVersion(major = 2.1, minor = 3) - public void should_flag_set_index_type_as_full() { - String createFullIndex = String.format("CREATE INDEX set_full_index ON %s.indexing (FULL(set_full));", keyspace); + public void should_create_metadata_for_full_index_on_set() { + String createFullIndex = String.format("CREATE INDEX set_full_index ON %s.indexing (full(set_full));", keyspace); session.execute(createFullIndex); ColumnMetadata column = getColumn("set_full"); IndexMetadata index = getIndex("set_full_index"); - assertThat(index).hasName("set_full_index") - .isNotKeys() - .isFull() - .isNotEntries() + assertThat(index) + .hasName("set_full_index") + .hasParent((TableMetadata)column.getParent()) .isNotCustomIndex() - .hasSingleColumn(getColumn("set_full")) + .hasTarget("full(set_full)") + .hasKind(COMPOSITES) .asCqlQuery(createFullIndex); - assertThat(index).isSameAs(column.getIndex("set_full_index")); - assertThat(column).hasSingleIndex(index); - assertThat(column).isSameAs(index.getColumn("set_full")); + assertThat((TableMetadata) column.getParent()).hasIndex(index); } @Test(groups = "short") @CassandraVersion(major = 2.1, minor = 3) - public void should_flag_list_index_type_as_full() { - String createFullIndex = String.format("CREATE INDEX list_full_index ON %s.indexing (FULL(list_full));", keyspace); + public void should_create_metadata_for_full_index_on_list() { + String createFullIndex = String.format("CREATE INDEX list_full_index ON %s.indexing (full(list_full));", keyspace); session.execute(createFullIndex); ColumnMetadata column = getColumn("list_full"); IndexMetadata index = getIndex("list_full_index"); - assertThat(index).hasName("list_full_index") - .isNotKeys() - .isFull() - .isNotEntries() + assertThat(index) + .hasName("list_full_index") + .hasParent((TableMetadata)column.getParent()) .isNotCustomIndex() - .hasSingleColumn(getColumn("list_full")) + .hasTarget("full(list_full)") + .hasKind(COMPOSITES) .asCqlQuery(createFullIndex); - assertThat(index).isSameAs(column.getIndex("list_full_index")); - assertThat(column).hasSingleIndex(index); - assertThat(column).isSameAs(index.getColumn("list_full")); + assertThat((TableMetadata) column.getParent()).hasIndex(index); } @Test(groups = "short") @CassandraVersion(major = 2.2) - public void should_flag_map_index_type_as_entries() { - String createEntriesIndex = String.format("CREATE INDEX map_entries_index ON %s.indexing (ENTRIES(map_entries));", keyspace); + public void should_create_metadata_for_index_on_map_entries() { + String createEntriesIndex = String.format("CREATE INDEX map_entries_index ON %s.indexing (entries(map_entries));", keyspace); session.execute(createEntriesIndex); ColumnMetadata column = getColumn("map_entries"); IndexMetadata index = getIndex("map_entries_index"); - assertThat(index).hasName("map_entries_index") - .isNotKeys() - .isNotFull() - .isEntries() + assertThat(index) + .hasName("map_entries_index") + .hasParent((TableMetadata)column.getParent()) .isNotCustomIndex() - .hasSingleColumn(getColumn("map_entries")) + .hasTarget("entries(map_entries)") + .hasKind(COMPOSITES) + .asCqlQuery(createEntriesIndex); + assertThat((TableMetadata) column.getParent()).hasIndex(index); + } + + @Test(groups = "short") + @CassandraVersion(major = 3.0) + public void should_allow_multiple_indexes_on_map_column() { + String createEntriesIndex = String.format("CREATE INDEX map_all_entries_index ON %s.indexing (entries(map_all));", keyspace); + session.execute(createEntriesIndex); + String createKeysIndex = String.format("CREATE INDEX map_all_keys_index ON %s.indexing (keys(map_all));", keyspace); + session.execute(createKeysIndex); + String createValuesIndex = String.format("CREATE INDEX map_all_values_index ON %s.indexing (values(map_all));", keyspace); + session.execute(createValuesIndex); + + ColumnMetadata column = getColumn("map_all"); + TableMetadata table = (TableMetadata)column.getParent(); + + assertThat(getIndex("map_all_entries_index")) + .hasParent(table) .asCqlQuery(createEntriesIndex); - assertThat(index).isSameAs(column.getIndex("map_entries_index")); - assertThat(column).hasSingleIndex(index); - assertThat(column).isSameAs(index.getColumn("map_entries")); + + assertThat(getIndex("map_all_keys_index")) + .hasParent(table) + .asCqlQuery(createKeysIndex); + + assertThat(getIndex("map_all_values_index")) + .hasParent(table) + .asCqlQuery(createValuesIndex); } @Test( groups = "short", - description = "This test case builds a ColumnMetadata object programmatically to test custom indices," + description = "This test case builds a ColumnMetadata object programmatically to test custom indices with pre-3.0 layout," + "otherwise, it would require deploying an actual custom index class into the C* test cluster") - public void should_parse_custom_index_options() { + public void should_parse_legacy_custom_index_options() { TableMetadata table = getTable("indexing"); - List data = ImmutableList.of( + List columnData = ImmutableList.of( wrap("text_column"), // column name wrap(0), // component index - wrap("regular"), // kind + wrap("regular"), // column kind wrap("custom_index"), // index name wrap("CUSTOM"), // index type wrap("org.apache.cassandra.db.marshal.UTF8Type"), // validator wrap("{\"foo\" : \"bar\", \"class_name\" : \"dummy.DummyIndex\"}") // index options ); - Row row = ArrayBackedRow.fromData(defs, M3PToken.FACTORY, cluster.getConfiguration().getProtocolOptions().getProtocolVersion(), data); - Raw raw = Raw.fromRow(row, VersionNumber.parse("2.1"), - cluster.getConfiguration().getProtocolOptions().getProtocolVersion(), - cluster.getConfiguration().getCodecRegistry()); - ColumnMetadata column = ColumnMetadata.fromRaw(table, raw); - IndexMetadata index = IndexMetadata.fromLegacy(column, raw); - assertThat(index).hasName("custom_index") - .isNotKeys() - .isNotFull() - .isNotEntries() + Row columnRow = ArrayBackedRow.fromData(legacyColumnDefs, M3PToken.FACTORY, protocolVersion, columnData); + Raw columnRaw = Raw.fromRow(columnRow, VersionNumber.parse("2.1"), protocolVersion, codecRegistry); + ColumnMetadata column = ColumnMetadata.fromRaw(table, columnRaw); + IndexMetadata index = IndexMetadata.fromLegacy(column, columnRaw); + assertThat(index) + .isNotNull() + .hasName("custom_index") .isCustomIndex() - .hasSingleColumn(column) .hasOption("foo", "bar") + .hasKind(CUSTOM) .asCqlQuery(String.format("CREATE CUSTOM INDEX custom_index ON %s.indexing (text_column) " - + "USING 'dummy.DummyIndex' WITH OPTIONS = {'foo' : 'bar', 'class_name' : 'dummy.DummyIndex'};", keyspace)); + + "USING 'dummy.DummyIndex' WITH OPTIONS = {'foo' : 'bar'};", keyspace)); + } + + @Test( + groups = "short", + description = "This test case builds a ColumnMetadata object programmatically to test custom indices with post-3.0 layout," + + "otherwise, it would require deploying an actual custom index class into the C* test cluster") + public void should_parse_custom_index_options() { + TableMetadata table = getTable("indexing"); + List indexData = ImmutableList.of( + wrap("custom_index"), // index name + wrap("CUSTOM"), // kind + MAP_CODEC.serialize(ImmutableMap.of( + "foo", "bar", + IndexMetadata.CUSTOM_INDEX_OPTION_NAME, "dummy.DummyIndex", + IndexMetadata.TARGET_OPTION_NAME, "a, b, keys(c)" + ), protocolVersion) // options + ); + Row indexRow = ArrayBackedRow.fromData(indexColumnDefs, M3PToken.FACTORY, protocolVersion, indexData); + IndexMetadata index = IndexMetadata.fromRow(table, indexRow); + assertThat(index) + .isNotNull() + .hasName("custom_index") + .isCustomIndex() + .hasOption("foo", "bar") + .hasTarget("a, b, keys(c)") + .hasKind(CUSTOM) + .asCqlQuery(String.format("CREATE CUSTOM INDEX custom_index ON %s.indexing (a, b, keys(c)) " + + "USING 'dummy.DummyIndex' WITH OPTIONS = {'foo' : 'bar'};", keyspace)); } /** @@ -257,19 +346,20 @@ public void should_parse_with_null_string_index_options() { wrap("org.apache.cassandra.db.marshal.BytesType"), // validator wrap("null") // index options ); - Row row = ArrayBackedRow.fromData(defs, M3PToken.FACTORY, cluster.getConfiguration().getProtocolOptions().getProtocolVersion(), data); + Row row = ArrayBackedRow.fromData(legacyColumnDefs, M3PToken.FACTORY, cluster.getConfiguration().getProtocolOptions().getProtocolVersion(), data); Raw raw = Raw.fromRow(row, VersionNumber.parse("2.1"), cluster.getConfiguration().getProtocolOptions().getProtocolVersion(), cluster.getConfiguration().getCodecRegistry()); ColumnMetadata column = ColumnMetadata.fromRaw(table, raw); IndexMetadata index = IndexMetadata.fromLegacy(column, raw); - assertThat(index).hasName("cfs_archive_parent_path") - .isNotKeys() // While the index type is KEYS, since it lacks index_options it does not considered. - .isNotFull() - .isNotEntries() + assertThat(index) + .isNotNull() + .hasName("cfs_archive_parent_path") .isNotCustomIndex() - .hasSingleColumn(column) + .hasTarget("\"b@706172656e745f70617468\"") + .hasKind(KEYS) .asCqlQuery(String.format("CREATE INDEX cfs_archive_parent_path ON %s.indexing (\"b@706172656e745f70617468\");", keyspace)); + assertThat(index.getOption(IndexMetadata.INDEX_KEYS_OPTION_NAME)).isNull(); // While the index type is KEYS, since it lacks index_options it does not get considered. } private static ColumnDefinitions.Definition definition(String name, DataType type) { @@ -285,7 +375,12 @@ private static ByteBuffer wrap(int number) { } private ColumnMetadata getColumn(String name) { - return getTable("indexing").getColumn(name); + return getColumn(name, true); + } + + private ColumnMetadata getColumn(String name, boolean fromTable) { + TableOrView target = fromTable ? getTable("indexing") : getMaterializedView("mv1"); + return target.getColumn(name); } private IndexMetadata getIndex(String name) { @@ -296,4 +391,7 @@ private TableMetadata getTable(String name) { return cluster.getMetadata().getKeyspace(keyspace).getTable(name); } + private MaterializedViewMetadata getMaterializedView(String name) { + return cluster.getMetadata().getKeyspace(keyspace).getMaterializedView(name); + } } diff --git a/driver-core/src/test/java/com/datastax/driver/core/MaterializedViewMetadataTest.java b/driver-core/src/test/java/com/datastax/driver/core/MaterializedViewMetadataTest.java index 1ea852e0111..c348c954d24 100644 --- a/driver-core/src/test/java/com/datastax/driver/core/MaterializedViewMetadataTest.java +++ b/driver-core/src/test/java/com/datastax/driver/core/MaterializedViewMetadataTest.java @@ -46,10 +46,10 @@ public void should_create_view_metadata() { keyspace); String createMV = String.format( "CREATE MATERIALIZED VIEW %s.monthlyhigh AS " - + "SELECT user FROM %s.scores " + + "SELECT game, year, month, score, user, day FROM %s.scores " + "WHERE game IS NOT NULL AND year IS NOT NULL AND month IS NOT NULL AND score IS NOT NULL AND user IS NOT NULL AND day IS NOT NULL " + "PRIMARY KEY ((game, year, month), score, user, day) " - + "WITH CLUSTERING ORDER BY (score DESC)", + + "WITH CLUSTERING ORDER BY (score DESC, user ASC, day ASC)", keyspace, keyspace); // when diff --git a/driver-core/src/test/java/com/datastax/driver/core/TableMetadataAssert.java b/driver-core/src/test/java/com/datastax/driver/core/TableMetadataAssert.java index c5868e06db9..46d545df2c0 100644 --- a/driver-core/src/test/java/com/datastax/driver/core/TableMetadataAssert.java +++ b/driver-core/src/test/java/com/datastax/driver/core/TableMetadataAssert.java @@ -49,4 +49,9 @@ public TableMetadataAssert hasMaterializedView(MaterializedViewMetadata expected assertThat(actual.getView(expected.getName())).isNotNull().isEqualTo(expected); return this; } + + public TableMetadataAssert hasIndex(IndexMetadata index) { + assertThat(actual.getIndexes()).contains(index); + return this; + } }