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 311d14adbb5..12b31380bf2 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 @@ -16,11 +16,6 @@ package com.datastax.driver.core; import java.util.*; -import java.util.Map.Entry; - -import com.google.common.base.Function; -import com.google.common.collect.ImmutableMap; -import com.google.common.collect.Maps; /** * Describes a Column. @@ -28,15 +23,21 @@ public class ColumnMetadata { static final String COLUMN_NAME = "column_name"; - static final String VALIDATOR = "validator"; - static final String COMPONENT_INDEX = "component_index"; - static final String KIND = "type"; + + static final String VALIDATOR = "validator"; // v2 only + static final String TYPE = "type"; // replaces validator, v3 onwards + + static final String COMPONENT_INDEX = "component_index"; // v2 only + static final String POSITION = "position"; // replaces component_index, v3 onwards + + static final String KIND_V2 = "type"; // v2 only + static final String KIND_V3 = "kind"; // replaces type, v3 onwards static final String INDEX_TYPE = "index_type"; static final String INDEX_OPTIONS = "index_options"; static final String INDEX_NAME = "index_name"; - private final TableMetadata table; + private final TableOrView parent; private final String name; private final DataType type; private final boolean isStatic; @@ -45,14 +46,14 @@ public class ColumnMetadata { // between columns and indexes, and is updated only after the column is created final Map indexes = new LinkedHashMap(); - private ColumnMetadata(TableMetadata table, String name, DataType type, boolean isStatic) { - this.table = table; + private ColumnMetadata(TableOrView parent, String name, DataType type, boolean isStatic) { + this.parent = parent; this.name = name; this.type = type; this.isStatic = isStatic; } - static ColumnMetadata fromRaw(TableMetadata tm, Raw raw) { + static ColumnMetadata fromRaw(TableOrView tm, Raw raw) { return new ColumnMetadata(tm, raw.name, raw.dataType, raw.kind == Raw.Kind.STATIC); } @@ -70,12 +71,13 @@ public String getName() { } /** - * Returns the metadata of the table this column is part of. + * Returns the parent object of this column. This can be a {@link TableMetadata} + * or a {@link MaterializedViewMetadata} object. * - * @return the {@code TableMetadata} for the table this column is part of. + * @return the parent object of this column. */ - public TableMetadata getTable() { - return table; + public TableOrView getParent() { + return parent; } /** @@ -163,35 +165,50 @@ static Kind fromStringV3(String s) { public final String name; public Kind kind; - public final int componentIndex; + public final int position; public final DataType dataType; public final boolean isReversed; public final Map indexColumns = new HashMap(); - Raw(String name, Kind kind, int componentIndex, DataType dataType, boolean isReversed) { + Raw(String name, Kind kind, int position, DataType dataType, boolean isReversed) { this.name = name; this.kind = kind; - this.componentIndex = componentIndex; + this.position = position; this.dataType = dataType; this.isReversed = isReversed; } static Raw fromRow(Row row, VersionNumber version, ProtocolVersion protocolVersion, CodecRegistry codecRegistry) { String name = row.getString(COLUMN_NAME); + Kind kind; - if(version.getMajor() < 2 || row.isNull(KIND)) { + if(version.getMajor() < 2) { kind = Kind.REGULAR; } else if (version.getMajor() < 3) { - kind = Kind.fromStringV2(row.getString(KIND)); + kind = row.isNull(KIND_V2) ? Kind.REGULAR : Kind.fromStringV2(row.getString(KIND_V2)); + } else { + kind = row.isNull(KIND_V3) ? Kind.REGULAR : Kind.fromStringV3(row.getString(KIND_V3)); + } + + int position; + if(version.getMajor() >= 3) { + position = row.getInt(POSITION); // cannot be null, -1 is used as a special value instead of null to avoid tombstones + if(position == -1) position = 0; + } else { + position = row.isNull(COMPONENT_INDEX) ? 0 : row.getInt(COMPONENT_INDEX); + } + + String dataTypeStr; + if(version.getMajor() >= 3) { + dataTypeStr = row.getString(TYPE); } else { - kind = Kind.fromStringV3(row.getString(KIND)); + dataTypeStr = row.getString(VALIDATOR); } - int componentIndex = row.isNull(COMPONENT_INDEX) ? 0 : row.getInt(COMPONENT_INDEX); - String validatorStr = row.getString(VALIDATOR); - boolean reversed = CassandraTypeParser.isReversed(validatorStr); - DataType dataType = CassandraTypeParser.parseOne(validatorStr, protocolVersion, codecRegistry); - Raw c = new Raw(name, kind, componentIndex, dataType, reversed); + DataType dataType = CassandraTypeParser.parseOne(dataTypeStr, protocolVersion, codecRegistry); + boolean reversed = CassandraTypeParser.isReversed(dataTypeStr); + + Raw c = new Raw(name, kind, position, dataType, reversed); // secondary indexes (C* < 3.0.0) // from C* 3.0 onwards 2i are defined in a separate table 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 80778565c37..443414843d8 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 @@ -116,7 +116,7 @@ static IndexMetadata fromLegacy(ColumnMetadata column, ColumnMetadata.Raw raw) { } else { options = SimpleJSONParser.parseStringMap(indexOptionsCol); } - return new IndexMetadata(column.getTable(), indexName, columns, indexType, TargetType.COLUMN, options); + return new IndexMetadata((TableMetadata)column.getParent(), indexName, columns, indexType, TargetType.COLUMN, options); } /** diff --git a/driver-core/src/main/java/com/datastax/driver/core/KeyspaceMetadata.java b/driver-core/src/main/java/com/datastax/driver/core/KeyspaceMetadata.java index a742c37d24d..bf2d4916e13 100644 --- a/driver-core/src/main/java/com/datastax/driver/core/KeyspaceMetadata.java +++ b/driver-core/src/main/java/com/datastax/driver/core/KeyspaceMetadata.java @@ -42,6 +42,7 @@ public class KeyspaceMetadata { private final Map replication; private final Map tables = new ConcurrentHashMap(); + private final Map views = new ConcurrentHashMap(); private final Map userTypes = new ConcurrentHashMap(); final Map functions = new ConcurrentHashMap(); private final Map aggregates = new ConcurrentHashMap(); @@ -123,6 +124,31 @@ public Collection getTables() { return Collections.unmodifiableCollection(tables.values()); } + /** + * Returns the metadata for a materialized view contained in this keyspace. + * + * @param name the name of materialized view to retrieve + * @return the metadata for materialized view {@code name} if it exists in this keyspace, + * {@code null} otherwise. + */ + public MaterializedViewMetadata getMaterializedView(String name) { + return views.get(Metadata.handleId(name)); + } + + void removeMaterializedView(String materializedView) { + views.remove(materializedView); + } + + /** + * Returns the materialized views defined in this keyspace. + * + * @return a collection of the metadata for the materialized views defined in this + * keyspace. + */ + public Collection getMaterializedViews() { + return Collections.unmodifiableCollection(views.values()); + } + /** * Returns the definition for a user defined type (UDT) in this keyspace. * @@ -291,6 +317,10 @@ void add(TableMetadata tm) { tables.put(tm.getName(), tm); } + void add(MaterializedViewMetadata view) { + views.put(view.getName(), view); + } + void add(FunctionMetadata function) { functions.put(function.getFullName(), function); } 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 new file mode 100644 index 00000000000..31018df7d39 --- /dev/null +++ b/driver-core/src/main/java/com/datastax/driver/core/MaterializedViewMetadata.java @@ -0,0 +1,218 @@ +/* + * Copyright (C) 2012-2015 DataStax Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.driver.core; + +import java.util.*; + +import com.google.common.base.Function; +import com.google.common.base.Joiner; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import static com.google.common.collect.Iterables.transform; + +/** + * An immutable representation of a materialized view. + * Materialized views are available starting from Cassandra 3.0. + */ +public class MaterializedViewMetadata extends TableOrView { + + private static final Logger logger = LoggerFactory.getLogger(MaterializedViewMetadata.class); + + private final TableMetadata baseTable; + + private final boolean includeAllColumns; + + private MaterializedViewMetadata( + KeyspaceMetadata keyspace, + TableMetadata baseTable, + String name, + UUID id, + List partitionKey, + List clusteringColumns, + Map columns, + boolean includeAllColumns, + Options options, + List clusteringOrder, + VersionNumber cassandraVersion) { + super(keyspace, name, id, partitionKey, clusteringColumns, columns, options, clusteringOrder, cassandraVersion); + this.baseTable = baseTable; + this.includeAllColumns = includeAllColumns; + } + + static MaterializedViewMetadata build(KeyspaceMetadata keyspace, Row row, Map rawCols, VersionNumber cassandraVersion) { + + String name = row.getString("view_name"); + String tableName = row.getString("base_table_name"); + TableMetadata baseTable = keyspace.getTable(tableName); + if(baseTable == null) { + logger.trace(String.format("Cannot find base table %s for materialized view %s.%s: " + + "Cluster.getMetadata().getKeyspace(\"%s\").getView(\"%s\") will return null", + tableName, keyspace.getName(), name, keyspace.getName(), name)); + return null; + } + + UUID id = row.getUUID("id"); + boolean includeAllColumns = row.getBool("include_all_columns"); + + int partitionKeySize = findCollectionSize(rawCols.values(), ColumnMetadata.Raw.Kind.PARTITION_KEY); + int clusteringSize = findCollectionSize(rawCols.values(), ColumnMetadata.Raw.Kind.CLUSTERING_COLUMN); + List partitionKey = nullInitializedList(partitionKeySize); + List clusteringColumns = 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; + try { + options = new Options(row, false, cassandraVersion); + } catch (RuntimeException e) { + // See ControlConnection#refreshSchema for why we'd rather not probably this further. Since table options is one thing + // that tends to change often in Cassandra, it's worth special casing this. + logger.error(String.format("Error parsing schema options for view %s.%s: " + + "Cluster.getMetadata().getKeyspace(\"%s\").getView(\"%s\").getOptions() will return null", + keyspace.getName(), name, keyspace.getName(), name), e); + } + + MaterializedViewMetadata view = new MaterializedViewMetadata( + keyspace, baseTable, name, id, partitionKey, clusteringColumns, columns, + includeAllColumns, options, clusteringOrder, cassandraVersion); + + // We use this temporary set just so non PK columns are added in lexicographical order, which is the one of a + // 'SELECT * FROM ...' + Set otherColumns = new TreeSet(columnMetadataComparator); + for (ColumnMetadata.Raw rawCol : rawCols.values()) { + ColumnMetadata col = ColumnMetadata.fromRaw(view, rawCol); + switch (rawCol.kind) { + case PARTITION_KEY: + partitionKey.set(rawCol.position, col); + break; + case CLUSTERING_COLUMN: + clusteringColumns.set(rawCol.position, col); + clusteringOrder.set(rawCol.position, rawCol.isReversed ? Order.DESC : Order.ASC); + break; + default: + otherColumns.add(col); + break; + } + } + for (ColumnMetadata c : partitionKey) + columns.put(c.getName(), c); + for (ColumnMetadata c : clusteringColumns) + columns.put(c.getName(), c); + for (ColumnMetadata c : otherColumns) + columns.put(c.getName(), c); + + baseTable.add(view); + + return view; + + } + + private static int findCollectionSize(Collection cols, ColumnMetadata.Raw.Kind kind) { + int maxId = -1; + for (ColumnMetadata.Raw col : cols) + if (col.kind == kind) + maxId = Math.max(maxId, col.position); + return maxId + 1; + } + + /** + * Return this materialized view's base table. + * @return this materialized view's base table. + */ + public TableMetadata getBaseTable() { + return baseTable; + } + + @Override + protected String asCQLQuery(boolean formatted) { + + String keyspaceName = Metadata.escapeId(keyspace.getName()); + String baseTableName = Metadata.escapeId(baseTable.getName()); + String viewName = Metadata.escapeId(name); + String whereClause = Joiner.on(" AND ").join(transform(getPartitionKey(), new Function() { + @Override + public String apply(ColumnMetadata input) { + return Metadata.escapeId(input.getName()) + " IS NOT NULL"; + } + })); + + + StringBuilder sb = new StringBuilder(); + sb.append("CREATE MATERIALIZED VIEW ") + .append(keyspaceName).append('.').append(viewName) + .append(" AS "); + newLine(sb, formatted); + + // SELECT + sb.append("SELECT "); + if(includeAllColumns) { + sb.append(" * "); + } else { + Iterator it = columns.values().iterator(); + while(it.hasNext()) { + ColumnMetadata column = it.next(); + sb.append(spaces(4, formatted)).append(Metadata.escapeId(column.getName())); + if(it.hasNext()) sb.append(","); + sb.append(" "); + newLine(sb, formatted); + } + } + + // FROM + newLine(sb.append("FROM ").append(keyspaceName).append('.').append(baseTableName).append(" "), formatted); + + // WHERE + newLine(sb.append("WHERE "), formatted); + Iterator it = getPrimaryKey().iterator(); + while(it.hasNext()) { + ColumnMetadata column = it.next(); + sb.append(spaces(4, formatted)).append(Metadata.escapeId(column.getName())); + sb.append(" IS NOT NULL"); + if(it.hasNext()) sb.append(" AND"); + sb.append(" "); + newLine(sb, formatted); + } + + // PK + sb.append("PRIMARY KEY ("); + if (partitionKey.size() == 1) { + sb.append(partitionKey.get(0).getName()); + } else { + sb.append('('); + boolean first = true; + for (ColumnMetadata cm : partitionKey) { + if (first) + first = false; + else + sb.append(", "); + sb.append(Metadata.escapeId(cm.getName())); + } + sb.append(')'); + } + for (ColumnMetadata cm : clusteringColumns) + sb.append(", ").append(Metadata.escapeId(cm.getName())); + sb.append(')'); + newLine(sb, formatted); + + appendOptions(sb, formatted); + return sb.toString(); + + } + +} diff --git a/driver-core/src/main/java/com/datastax/driver/core/SchemaElement.java b/driver-core/src/main/java/com/datastax/driver/core/SchemaElement.java index 10e962a7506..b132ba72746 100644 --- a/driver-core/src/main/java/com/datastax/driver/core/SchemaElement.java +++ b/driver-core/src/main/java/com/datastax/driver/core/SchemaElement.java @@ -15,6 +15,11 @@ */ package com.datastax.driver.core; +/** + * Values for a SCHEMA_CHANGE event. + * See protocol v4 section 4.2.6. + * Note that {@code VIEW} is not a valid string under protocol v4 or lower, but is included for internal use only. + */ enum SchemaElement { - KEYSPACE, TABLE, TYPE, FUNCTION, AGGREGATE + KEYSPACE, TABLE, TYPE, FUNCTION, AGGREGATE, VIEW } 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 f474cd5ff13..e8b18bf7966 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 @@ -197,6 +197,7 @@ private boolean supportsUdfs(VersionNumber cassandraVersion) { private static final String SELECT_FUNCTIONS = "SELECT * FROM system_schema.functions"; private static final String SELECT_AGGREGATES = "SELECT * FROM system_schema.aggregates"; private static final String SELECT_INDEXES = "SELECT * FROM system_schema.indexes"; + private static final String SELECT_VIEWS = "SELECT * FROM system_schema.views"; private static final String TABLE_NAME = "table_name"; @@ -210,44 +211,33 @@ void refresh(Metadata metadata, ProtocolVersion protocolVersion = metadata.cluster.protocolVersion(); CodecRegistry codecRegistry = metadata.cluster.configuration.getCodecRegistry(); - String whereClause = ""; - if (targetType != null) { - whereClause = " WHERE keyspace_name = '" + targetKeyspace + '\''; - if (targetType == TABLE) - whereClause += " AND table_name = '" + targetName + '\''; - else if (targetType == TYPE) - whereClause += " AND type_name = '" + targetName + '\''; - else if (targetType == FUNCTION) - whereClause += " AND function_name = '" + targetName + "' AND signature = " + LIST_OF_TEXT_CODEC.format(targetSignature); - else if (targetType == AGGREGATE) - whereClause += " AND aggregate_name = '" + targetName + "' AND signature = " + LIST_OF_TEXT_CODEC.format(targetSignature); - } - ResultSetFuture ksFuture = null, udtFuture = null, cfFuture = null, colsFuture = null, functionsFuture = null, aggregatesFuture = null, - indexesFuture = null; + indexesFuture = null, + viewsFuture = null; if (isSchemaOrKeyspace) - ksFuture = queryAsync(SELECT_KEYSPACES + whereClause, connection, protocolVersion); + ksFuture = queryAsync(SELECT_KEYSPACES + whereClause(targetType, targetKeyspace, targetName, targetSignature), connection, protocolVersion); if (isSchemaOrKeyspace || targetType == TYPE) - udtFuture = queryAsync(SELECT_USERTYPES + whereClause, connection, protocolVersion); + udtFuture = queryAsync(SELECT_USERTYPES + whereClause(targetType, targetKeyspace, targetName, targetSignature), connection, protocolVersion); if (isSchemaOrKeyspace || targetType == TABLE) { - cfFuture = queryAsync(SELECT_TABLES + whereClause, connection, protocolVersion); - colsFuture = queryAsync(SELECT_COLUMNS + whereClause, connection, protocolVersion); - indexesFuture = queryAsync(SELECT_INDEXES + whereClause, connection, protocolVersion); + cfFuture = queryAsync(SELECT_TABLES + whereClause(targetType, targetKeyspace, targetName, targetSignature), connection, protocolVersion); + colsFuture = queryAsync(SELECT_COLUMNS + whereClause(targetType, targetKeyspace, targetName, targetSignature), connection, protocolVersion); + indexesFuture = queryAsync(SELECT_INDEXES + whereClause(targetType, targetKeyspace, targetName, targetSignature), connection, protocolVersion); + viewsFuture = queryAsync(SELECT_VIEWS + whereClause(targetType == null ? null : VIEW, targetKeyspace, targetName, targetSignature), connection, protocolVersion); } - if ((isSchemaOrKeyspace || targetType == FUNCTION)) - functionsFuture = queryAsync(SELECT_FUNCTIONS + whereClause, connection, protocolVersion); + if (isSchemaOrKeyspace || targetType == FUNCTION) + functionsFuture = queryAsync(SELECT_FUNCTIONS + whereClause(targetType, targetKeyspace, targetName, targetSignature), connection, protocolVersion); if (isSchemaOrKeyspace || targetType == AGGREGATE) - aggregatesFuture = queryAsync(SELECT_AGGREGATES + whereClause, connection, protocolVersion); + aggregatesFuture = queryAsync(SELECT_AGGREGATES + whereClause(targetType, targetKeyspace, targetName, targetSignature), connection, protocolVersion); ResultSet ks = get(ksFuture); Map> udtDefs = groupByKeyspace(get(udtFuture)); @@ -255,6 +245,7 @@ else if (targetType == AGGREGATE) Map>> colsDefs = groupByKeyspaceAndCf(get(colsFuture), cassandraVersion, protocolVersion, codecRegistry, TABLE_NAME); Map> functionDefs = groupByKeyspace(get(functionsFuture)); Map> aggregateDefs = groupByKeyspace(get(aggregatesFuture)); + Map> viewDefs = groupByKeyspace(get(viewsFuture)); Map>> indexDefs = groupByKeyspaceAndCf(get(indexesFuture), TABLE_NAME); metadata.lock.lock(); @@ -272,6 +263,9 @@ else if (targetType == AGGREGATE) if (cfDefs.containsKey(ksName)) { buildTableMetadata(ksm, cfDefs.get(ksName), colsDefs.get(ksName), indexDefs.get(ksName), cassandraVersion, protocolVersion, codecRegistry, TABLE_NAME); } + if (viewDefs.containsKey(ksName)) { + buildViewMetadata(ksm, viewDefs.get(ksName), colsDefs.get(ksName), cassandraVersion); + } if (functionDefs.containsKey(ksName)) { buildFunctionMetadata(ksm, functionDefs.get(ksName), protocolVersion, codecRegistry); } @@ -307,6 +301,8 @@ else if (targetType == AGGREGATE) case TABLE: if (cfDefs.containsKey(targetKeyspace)) buildTableMetadata(ksm, cfDefs.get(targetKeyspace), colsDefs.get(targetKeyspace), indexDefs.get(targetKeyspace), cassandraVersion, protocolVersion, codecRegistry, TABLE_NAME); + if (viewDefs.containsKey(targetKeyspace)) + buildViewMetadata(ksm, viewDefs.get(targetKeyspace), colsDefs.get(targetKeyspace), cassandraVersion); break; case TYPE: if (udtDefs.containsKey(targetKeyspace)) @@ -328,6 +324,25 @@ else if (targetType == AGGREGATE) metadata.lock.unlock(); } } + + private String whereClause(SchemaElement targetType, String targetKeyspace, String targetName, List targetSignature) { + String whereClause = ""; + if (targetType != null) { + whereClause = " WHERE keyspace_name = '" + targetKeyspace + '\''; + if (targetType == TABLE) + whereClause += " AND table_name = '" + targetName + '\''; + else if (targetType == VIEW) + whereClause += " AND view_name = '" + targetName + '\''; + else if (targetType == TYPE) + whereClause += " AND type_name = '" + targetName + '\''; + else if (targetType == FUNCTION) + whereClause += " AND function_name = '" + targetName + "' AND signature = " + LIST_OF_TEXT_CODEC.format(targetSignature); + else if (targetType == AGGREGATE) + whereClause += " AND aggregate_name = '" + targetName + "' AND signature = " + LIST_OF_TEXT_CODEC.format(targetSignature); + } + return whereClause; + } + }; static void buildTableMetadata(KeyspaceMetadata ksm, List cfRows, Map> colsDefs, Map> ksIndexes, VersionNumber cassandraVersion, ProtocolVersion protocolVersion, CodecRegistry codecRegistry, String tableName) { @@ -365,6 +380,24 @@ static void buildTableMetadata(KeyspaceMetadata ksm, List cfRows, Map viewRows, Map> colsDefs, VersionNumber cassandraVersion) { + for (Row viewRow : viewRows) { + String viewName = viewRow.getString("view_name"); + try { + 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); + } catch (RuntimeException e) { + // See ControlConnection#refreshSchema for why we'd rather not probably this further + logger.error(String.format("Error parsing schema for view %s.%s: " + + "Cluster.getMetadata().getKeyspace(\"%s\").getView(\"%s\") will be missing or incomplete", + ksm.getName(), viewName, ksm.getName(), viewName), e); + } + } + } static void buildUserTypes(KeyspaceMetadata ksm, List rows, ProtocolVersion protocolVersion, CodecRegistry codecRegistry) { for (Row row : rows) ksm.add(UserType.build(row, protocolVersion, codecRegistry)); 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 47066bf6ce0..c30c097938a 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 @@ -17,16 +17,13 @@ import java.util.*; -import com.google.common.base.Predicate; -import com.google.common.collect.ImmutableMap; -import com.google.common.collect.Iterables; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * Describes a Table. */ -public class TableMetadata { +public class TableMetadata extends TableOrView { private static final Logger logger = LoggerFactory.getLogger(TableMetadata.class); @@ -52,40 +49,8 @@ public class TableMetadata { private static final String EMPTY_TYPE = "org.apache.cassandra.db.marshal.EmptyType"; - private static final Comparator columnMetadataComparator = new Comparator() { - public int compare(ColumnMetadata c1, ColumnMetadata c2) { - return c1.getName().compareTo(c2.getName()); - } - }; - - private final KeyspaceMetadata keyspace; - private final String name; - private final UUID id; - private final List partitionKey; - private final List clusteringColumns; - private final Map columns; private final Map indexes; - private final Options options; - private final List clusteringOrder; - - - private final VersionNumber cassandraVersion; - - /** - * Clustering orders. - *

- * This is used by {@link #getClusteringOrder} to indicate the clustering - * order of a table. - */ - public static enum Order { - ASC, DESC; - - static final Predicate isAscending = new Predicate() { - public boolean apply(Order o) { - return o == ASC; - } - }; - } + private final Map views; private TableMetadata(KeyspaceMetadata keyspace, String name, @@ -97,16 +62,9 @@ private TableMetadata(KeyspaceMetadata keyspace, Options options, List clusteringOrder, VersionNumber cassandraVersion) { - this.keyspace = keyspace; - this.name = name; - this.id = id; - this.partitionKey = partitionKey; - this.clusteringColumns = clusteringColumns; - this.columns = columns; + super(keyspace, name, id, partitionKey, clusteringColumns, columns, options, clusteringOrder, cassandraVersion); this.indexes = indexes; - this.options = options; - this.clusteringOrder = clusteringOrder; - this.cassandraVersion = cassandraVersion; + this.views = new HashMap(); } static TableMetadata build(KeyspaceMetadata ksm, Row row, Map rawCols, List indexRows, String nameColumn, VersionNumber cassandraVersion, ProtocolVersion protocolVersion, CodecRegistry codecRegistry) { @@ -225,11 +183,11 @@ else if (cassandraVersion.getMajor() > 2) ColumnMetadata col = ColumnMetadata.fromRaw(tm, rawCol); switch (rawCol.kind) { case PARTITION_KEY: - partitionKey.set(rawCol.componentIndex, col); + partitionKey.set(rawCol.position, col); break; case CLUSTERING_COLUMN: - clusteringColumns.set(rawCol.componentIndex, col); - clusteringOrder.set(rawCol.componentIndex, rawCol.isReversed ? Order.DESC : Order.ASC); + clusteringColumns.set(rawCol.position, col); + clusteringOrder.set(rawCol.position, rawCol.isReversed ? Order.DESC : Order.ASC); break; default: otherColumns.add(col); @@ -270,6 +228,7 @@ else if (cassandraVersion.getMajor() > 2) column.indexes.put(index.getName(), index); } } + return tm; } @@ -347,7 +306,7 @@ private static int findPartitionKeySize(Collection cols, Cas int maxId = -1; for (ColumnMetadata.Raw col : cols) if (col.kind == ColumnMetadata.Raw.Kind.PARTITION_KEY) - maxId = Math.max(maxId, col.componentIndex); + maxId = Math.max(maxId, col.position); return maxId + 1; } @@ -355,14 +314,14 @@ private static int findClusteringSize(CassandraTypeParser.ParseResult comparator Collection cols, List columnAliases, VersionNumber cassandraVersion) { - // In 2.0 onwards, this is relatively easy, we just find the biggest 'componentIndex' amongst the clustering columns. + // In 2.0 onwards, this is relatively easy, we just find the biggest 'position' amongst the clustering columns. // For 1.2 however, this is slightly more subtle: we need to infer it based on whether the comparator is composite or not, and whether we have // regular columns or not. if (cassandraVersion.getMajor() >= 2) { int maxId = -1; for (ColumnMetadata.Raw col : cols) if (col.kind == ColumnMetadata.Raw.Kind.CLUSTERING_COLUMN) - maxId = Math.max(maxId, col.componentIndex); + maxId = Math.max(maxId, col.position); return maxId + 1; } else { int size = comparator.types.size(); @@ -374,109 +333,6 @@ private static int findClusteringSize(CassandraTypeParser.ParseResult comparator } } - private static List nullInitializedList(int size) { - List l = new ArrayList(size); - for (int i = 0; i < size; ++i) - l.add(null); - return l; - } - - /** - * Returns the name of this table. - * - * @return the name of this CQL table. - */ - public String getName() { - return name; - } - - /** - * Returns the unique id of this table. - *

- * Note: this id is available in Cassandra 2.1 and above. It will be - * {@code null} for earlier versions. - * - * @return the unique id of the table. - */ - public UUID getId() { - return id; - } - - /** - * Returns the keyspace this table belong to. - * - * @return the keyspace metadata of the keyspace this table belong to. - */ - public KeyspaceMetadata getKeyspace() { - return keyspace; - } - - /** - * Returns metadata on a column of this table. - * - * @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. - */ - public ColumnMetadata getColumn(String name) { - return columns.get(Metadata.handleId(name)); - } - - /** - * Returns a list containing all the columns of this table. - * - * The order of the columns in the list is consistent with - * the order of the columns returned by a {@code SELECT * FROM thisTable}: - * the first column is the partition key, next are the clustering - * columns in their defined order, and then the rest of the - * columns follow in alphabetic order. - * - * @return a list containing the metadata for the columns of this table. - */ - public List getColumns() { - return new ArrayList(columns.values()); - } - - /** - * Returns the list of columns composing the primary key for this table. - * - * A table will always at least have a partition key (that - * may itself be one or more columns), so the returned list at least - * has one element. - * - * @return the list of columns composing the primary key for this table. - */ - public List getPrimaryKey() { - List pk = new ArrayList(partitionKey.size() + clusteringColumns.size()); - pk.addAll(partitionKey); - pk.addAll(clusteringColumns); - return pk; - } - - /** - * Returns the list of columns composing the partition key for this table. - * - * A table always has a partition key so the returned list has - * at least one element. - * - * @return the list of columns composing the partition key for this table. - */ - public List getPartitionKey() { - return Collections.unmodifiableList(partitionKey); - } - - /** - * Returns the list of clustering columns for this table. - * - * @return the list of clustering columns for this table. - * If there is no clustering columns, an empty list is returned. - */ - public List getClusteringColumns() { - return Collections.unmodifiableList(clusteringColumns); - } - /** * Returns metadata on a index of this table. * @@ -500,32 +356,29 @@ public List getIndexes() { } /** - * Returns the clustering order for this table. - *

- * The returned contains the clustering order of each clustering column. The - * {@code i}th element of the result correspond to the order (ascending or - * descending) of the {@code i}th clustering column (see - * {@link #getClusteringColumns}). Note that a table defined without any - * particular clustering order is equivalent to one for which all the - * clustering key are in ascending order. + * Returns metadata on a view of this table. * - * @return a list with the clustering order for each clustering column. + * @param name the name of the view 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} view if it exists, or + * {@code null} otherwise. */ - public List getClusteringOrder() { - return clusteringOrder; + public MaterializedViewMetadata getView(String name) { + return views.get(Metadata.handleId(name)); } /** - * Returns the options for this table. + * Returns a list containing all the viewes of this table. * - * @return the options for this table. + * @return a list containing the metadata for the viewes of this table. */ - public Options getOptions() { - return options; + public List getViews() { + return new ArrayList(views.values()); } - void add(ColumnMetadata column) { - columns.put(column.getName(), column); + void add(MaterializedViewMetadata view) { + views.put(view.getName(), view); } /** @@ -533,8 +386,8 @@ void add(ColumnMetadata column) { * table and the index on it. *

* In other words, this method returns the queries that would allow you to - * recreate the schema of this table, along with the indexes defined on - * columns of this table. + * recreate the schema of this table, along with the indexes and views defined on + * this table, if any. *

* Note that the returned String is formatted to be human readable (for * some definition of human readable at least). @@ -542,36 +395,25 @@ void add(ColumnMetadata column) { * @return the CQL queries representing this table schema as a {code * String}. */ + @Override public String exportAsString() { StringBuilder sb = new StringBuilder(); - sb.append(asCQLQuery(true)); + sb.append(super.exportAsString()); for (IndexMetadata index : indexes.values()) { sb.append('\n').append(index.asCQLQuery()); } - return sb.toString(); - } - /** - * Returns a CQL query representing this table. - *

- * This method returns a single 'CREATE TABLE' query with the options - * corresponding to this table definition. - *

- * Note that the returned string is a single line; the returned query - * is not formatted in any way. - * - * @return the 'CREATE TABLE' query corresponding to this table. - * @see #exportAsString - */ - public String asCQLQuery() { - return asCQLQuery(false); + for (MaterializedViewMetadata index : views.values()) { + sb.append('\n').append(index.asCQLQuery()); + } + + return sb.toString(); } - private String asCQLQuery(boolean formatted) { + protected String asCQLQuery(boolean formatted) { StringBuilder sb = new StringBuilder(); - sb.append("CREATE TABLE ").append(Metadata.escapeId(keyspace.getName())).append('.').append(Metadata.escapeId(name)).append(" ("); newLine(sb, formatted); for (ColumnMetadata cm : columns.values()) @@ -585,7 +427,10 @@ private String asCQLQuery(boolean formatted) { sb.append('('); boolean first = true; for (ColumnMetadata cm : partitionKey) { - if (first) first = false; else sb.append(", "); + if (first) + first = false; + else + sb.append(", "); sb.append(Metadata.escapeId(cm.getName())); } sb.append(')'); @@ -596,397 +441,8 @@ private String asCQLQuery(boolean formatted) { newLine(sb, formatted); // end PK - // Options - sb.append(") WITH "); - - if (options.isCompactStorage) - and(sb.append("COMPACT STORAGE"), formatted); - if (!Iterables.all(clusteringOrder, Order.isAscending)) - and(appendClusteringOrder(sb), formatted); - sb.append("read_repair_chance = ").append(options.readRepair); - and(sb, formatted).append("dclocal_read_repair_chance = ").append(options.localReadRepair); - if (cassandraVersion.getMajor() < 2 || (cassandraVersion.getMajor() == 2 && cassandraVersion.getMinor() == 0)) - and(sb, formatted).append("replicate_on_write = ").append(options.replicateOnWrite); - and(sb, formatted).append("gc_grace_seconds = ").append(options.gcGrace); - and(sb, formatted).append("bloom_filter_fp_chance = ").append(options.bfFpChance); - if (cassandraVersion.getMajor() < 2 || cassandraVersion.getMajor() == 2 && cassandraVersion.getMinor() < 1) - and(sb, formatted).append("caching = '").append(options.caching.get("keys")).append('\''); - else - and(sb, formatted).append("caching = ").append(formatOptionMap(options.caching)); - if (options.comment != null) - and(sb, formatted).append("comment = '").append(options.comment.replace("'","''")).append('\''); - and(sb, formatted).append("compaction = ").append(formatOptionMap(options.compaction)); - and(sb, formatted).append("compression = ").append(formatOptionMap(options.compression)); - if (cassandraVersion.getMajor() >= 2) { - and(sb, formatted).append("default_time_to_live = ").append(options.defaultTTL); - and(sb, formatted).append("speculative_retry = '").append(options.speculativeRetry).append('\''); - if (options.indexInterval != null) - and(sb, formatted).append("index_interval = ").append(options.indexInterval); - } - if (cassandraVersion.getMajor() > 2 || (cassandraVersion.getMajor() == 2 && cassandraVersion.getMinor() >= 1)) { - and(sb, formatted).append("min_index_interval = ").append(options.minIndexInterval); - and(sb, formatted).append("max_index_interval = ").append(options.maxIndexInterval); - } - sb.append(';'); - return sb.toString(); - } - - @Override - public String toString() { - return asCQLQuery(); - } - - private StringBuilder appendClusteringOrder(StringBuilder sb) { - sb.append("CLUSTERING ORDER BY ("); - for (int i = 0; i < clusteringColumns.size(); i++) { - if (i > 0) sb.append(", "); - sb.append(clusteringColumns.get(i).getName()).append(' ').append(clusteringOrder.get(i)); - } - return sb.append(')'); - } - - private static String formatOptionMap(Map m) { - StringBuilder sb = new StringBuilder(); - sb.append("{ "); - boolean first = true; - for (Map.Entry entry : m.entrySet()) { - if (first) first = false; else sb.append(", "); - sb.append('\'').append(entry.getKey()).append('\''); - sb.append(" : "); - try { - sb.append(Integer.parseInt(entry.getValue())); - } catch (NumberFormatException e) { - sb.append('\'').append(entry.getValue()).append('\''); - } - } - sb.append(" }"); + sb.append(")"); + appendOptions(sb, formatted); return sb.toString(); } - - private StringBuilder and(StringBuilder sb, boolean formatted) { - return newLine(sb, formatted).append(spaces(2, formatted)).append(" AND "); - } - - static String spaces(int n, boolean formatted) { - if (!formatted) - return ""; - - StringBuilder sb = new StringBuilder(); - for (int i = 0; i < n; i++) - sb.append(' '); - - return sb.toString(); - } - - static StringBuilder newLine(StringBuilder sb, boolean formatted) { - if (formatted) - sb.append('\n'); - return sb; - } - - static StringBuilder spaceOrNewLine(StringBuilder sb, boolean formatted) { - sb.append(formatted ? '\n' : ' '); - return sb; - } - - public static class Options { - - private static final String COMMENT = "comment"; - private static final String READ_REPAIR = "read_repair_chance"; - private static final String DCLOCAL_READ_REPAIR = "dclocal_read_repair_chance"; - private static final String LOCAL_READ_REPAIR = "local_read_repair_chance"; - private static final String REPLICATE_ON_WRITE = "replicate_on_write"; - private static final String GC_GRACE = "gc_grace_seconds"; - private static final String BF_FP_CHANCE = "bloom_filter_fp_chance"; - private static final String CACHING = "caching"; - private static final String COMPACTION = "compaction"; - private static final String COMPACTION_CLASS = "compaction_strategy_class"; - private static final String COMPACTION_OPTIONS = "compaction_strategy_options"; - private static final String POPULATE_CACHE_ON_FLUSH = "populate_io_cache_on_flush"; - private static final String COMPRESSION = "compression"; - private static final String COMPRESSION_PARAMS = "compression_parameters"; - private static final String MEMTABLE_FLUSH_PERIOD_MS = "memtable_flush_period_in_ms"; - private static final String DEFAULT_TTL = "default_time_to_live"; - private static final String SPECULATIVE_RETRY = "speculative_retry"; - private static final String INDEX_INTERVAL = "index_interval"; - private static final String MIN_INDEX_INTERVAL = "min_index_interval"; - private static final String MAX_INDEX_INTERVAL = "max_index_interval"; - - private static final boolean DEFAULT_REPLICATE_ON_WRITE = true; - private static final double DEFAULT_BF_FP_CHANCE = 0.01; - private static final boolean DEFAULT_POPULATE_CACHE_ON_FLUSH = false; - private static final int DEFAULT_MEMTABLE_FLUSH_PERIOD = 0; - private static final int DEFAULT_DEFAULT_TTL = 0; - private static final String DEFAULT_SPECULATIVE_RETRY = "NONE"; - private static final int DEFAULT_INDEX_INTERVAL = 128; - private static final int DEFAULT_MIN_INDEX_INTERVAL = 128; - private static final int DEFAULT_MAX_INDEX_INTERVAL = 2048; - - private final boolean isCompactStorage; - - private final String comment; - private final double readRepair; - private final double localReadRepair; - private final boolean replicateOnWrite; - private final int gcGrace; - private final double bfFpChance; - private final Map caching; - private final boolean populateCacheOnFlush; - private final int memtableFlushPeriodMs; - private final int defaultTTL; - private final String speculativeRetry; - private final Integer indexInterval; - private final Integer minIndexInterval; - private final Integer maxIndexInterval; - private final Map compaction; - private final Map compression; - - Options(Row row, boolean isCompactStorage, VersionNumber version) { - - boolean is120 = version.getMajor() < 2; - boolean is200 = version.getMajor() == 2 && version.getMinor() == 0; - boolean is210 = version.getMajor() == 2 && version.getMinor() >= 1; - boolean is300OrHigher = version.getMajor() > 2; - boolean is210OrHigher = is210 || is300OrHigher; - - this.isCompactStorage = isCompactStorage; - this.comment = isNullOrAbsent(row, COMMENT) ? "" : row.getString(COMMENT); - this.readRepair = row.getDouble(READ_REPAIR); - - if(is300OrHigher) - this.localReadRepair = row.getDouble(DCLOCAL_READ_REPAIR); - else - this.localReadRepair = row.getDouble(LOCAL_READ_REPAIR); - - this.replicateOnWrite = is210OrHigher || isNullOrAbsent(row, REPLICATE_ON_WRITE) ? DEFAULT_REPLICATE_ON_WRITE : row.getBool(REPLICATE_ON_WRITE); - this.gcGrace = row.getInt(GC_GRACE); - this.bfFpChance = isNullOrAbsent(row, BF_FP_CHANCE) ? DEFAULT_BF_FP_CHANCE : row.getDouble(BF_FP_CHANCE); - - this.populateCacheOnFlush = isNullOrAbsent(row, POPULATE_CACHE_ON_FLUSH) ? DEFAULT_POPULATE_CACHE_ON_FLUSH : row.getBool(POPULATE_CACHE_ON_FLUSH); - this.memtableFlushPeriodMs = is120 || isNullOrAbsent(row, MEMTABLE_FLUSH_PERIOD_MS) ? DEFAULT_MEMTABLE_FLUSH_PERIOD : row.getInt(MEMTABLE_FLUSH_PERIOD_MS); - this.defaultTTL = is120 || isNullOrAbsent(row, DEFAULT_TTL) ? DEFAULT_DEFAULT_TTL : row.getInt(DEFAULT_TTL); - this.speculativeRetry = is120 || isNullOrAbsent(row, SPECULATIVE_RETRY) ? DEFAULT_SPECULATIVE_RETRY : row.getString(SPECULATIVE_RETRY); - - if (is200) - this.indexInterval = isNullOrAbsent(row, INDEX_INTERVAL) ? DEFAULT_INDEX_INTERVAL : row.getInt(INDEX_INTERVAL); - else - this.indexInterval = null; - - if (is210OrHigher) { - this.minIndexInterval = isNullOrAbsent(row, MIN_INDEX_INTERVAL) - ? DEFAULT_MIN_INDEX_INTERVAL - : row.getInt(MIN_INDEX_INTERVAL); - this.maxIndexInterval = isNullOrAbsent(row, MAX_INDEX_INTERVAL) - ? DEFAULT_MAX_INDEX_INTERVAL - : row.getInt(MAX_INDEX_INTERVAL); - } else { - this.minIndexInterval = null; - this.maxIndexInterval = null; - } - - if (is300OrHigher) { - this.caching = ImmutableMap.copyOf(row.getMap(CACHING, String.class, String.class)); - } else if (is210) { - this.caching = ImmutableMap.copyOf(SimpleJSONParser.parseStringMap(row.getString(CACHING))); - } else { - this.caching = ImmutableMap.of("keys", row.getString(CACHING)); - } - - if (is300OrHigher) - this.compaction = ImmutableMap.copyOf(row.getMap(COMPACTION, String.class, String.class)); - else { - this.compaction = ImmutableMap.builder() - .put("class", row.getString(COMPACTION_CLASS)) - .putAll(SimpleJSONParser.parseStringMap(row.getString(COMPACTION_OPTIONS))) - .build(); - } - - if(is300OrHigher) - this.compression = ImmutableMap.copyOf(row.getMap(COMPRESSION, String.class, String.class)); - else - this.compression = ImmutableMap.copyOf(SimpleJSONParser.parseStringMap(row.getString(COMPRESSION_PARAMS))); - } - - private static boolean isNullOrAbsent(Row row, String name) { - return row.getColumnDefinitions().getIndexOf(name) < 0 - || row.isNull(name); - } - - /** - * Returns whether the table uses the {@code COMPACT STORAGE} option. - * - * @return whether the table uses the {@code COMPACT STORAGE} option. - */ - public boolean isCompactStorage() { - return isCompactStorage; - } - - /** - * Returns the commentary set for this table. - * - * @return the commentary set for this table, or {@code null} if noe has been set. - */ - public String getComment() { - return comment; - } - - /** - * Returns the chance with which a read repair is triggered for this table. - * - * @return the read repair chance set for table (in [0.0, 1.0]). - */ - public double getReadRepairChance() { - return readRepair; - } - - /** - * Returns the cluster local read repair chance set for this table. - * - * @return the local read repair chance set for table (in [0.0, 1.0]). - */ - public double getLocalReadRepairChance() { - return localReadRepair; - } - - /** - * Returns whether replicateOnWrite is set for this table. - * - * This is only meaningful for tables holding counters. - * - * @return whether replicateOnWrite is set for this table. - */ - public boolean getReplicateOnWrite() { - return replicateOnWrite; - } - - /** - * Returns the tombstone garbage collection grace time in seconds for this table. - * - * @return the tombstone garbage collection grace time in seconds for this table. - */ - public int getGcGraceInSeconds() { - return gcGrace; - } - - /** - * Returns the false positive chance for the Bloom filter of this table. - * - * @return the Bloom filter false positive chance for this table (in [0.0, 1.0]). - */ - public double getBloomFilterFalsePositiveChance() { - return bfFpChance; - } - - /** - * Returns the caching options for this table. - * - * @return an immutable map containing the caching options for this table. - */ - public Map getCaching() { - return caching; - } - - /** - * Whether the populate I/O cache on flush is set on this table. - * - * @return whether the populate I/O cache on flush is set on this table. - */ - public boolean getPopulateIOCacheOnFlush() { - return populateCacheOnFlush; - } - - /* - * Returns the memtable flush period (in milliseconds) option for this table. - *

- * Note: this option is not available in Cassandra 1.2 and will return 0 (no periodic - * flush) when connected to 1.2 nodes. - * - * @return the memtable flush period option for this table or 0 if no - * periodic flush is configured. - */ - public int getMemtableFlushPeriodInMs() { - return memtableFlushPeriodMs; - } - - /** - * Returns the default TTL for this table. - *

- * Note: this option is not available in Cassandra 1.2 and will return 0 (no default - * TTL) when connected to 1.2 nodes. - * - * @return the default TTL for this table or 0 if no default TTL is - * configured. - */ - public int getDefaultTimeToLive() { - return defaultTTL; - } - - /** - * Returns the speculative retry option for this table. - *

- * Note: this option is not available in Cassandra 1.2 and will return "NONE" (no - * speculative retry) when connected to 1.2 nodes. - * - * @return the speculative retry option this table. - */ - public String getSpeculativeRetry() { - return speculativeRetry; - } - - /** - * Returns the index interval option for this table. - *

- * Note: this option is not available in Cassandra 1.2 (more precisely, it is not - * configurable per-table) and will return 128 (the default index interval) when - * connected to 1.2 nodes. It is deprecated in Cassandra 2.1 and above, and will - * therefore return {@code null} for 2.1 nodes. - * - * @return the index interval option for this table. - */ - public Integer getIndexInterval() { - return indexInterval; - } - - /** - * Returns the minimum index interval option for this table. - *

- * Note: this option is available in Cassandra 2.1 and above, and will return - * {@code null} for earlier versions. - * - * @return the minimum index interval option for this table. - */ - public Integer getMinIndexInterval() { - return minIndexInterval; - } - - /** - * Returns the maximum index interval option for this table. - *

- * Note: this option is available in Cassandra 2.1 and above, and will return - * {@code null} for earlier versions. - * - * @return the maximum index interval option for this table. - */ - public Integer getMaxIndexInterval() { - return maxIndexInterval; - } - - /** - * Returns the compaction options for this table. - * - * @return an immutable map containing the compaction options for this table. - */ - public Map getCompaction() { - return compaction; - } - - /** - * Returns the compression options for this table. - * - * @return an immutable map containing the compression options for this table. - */ - public Map getCompression() { - return compression; - } - } } 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 new file mode 100644 index 00000000000..5d8873da94c --- /dev/null +++ b/driver-core/src/main/java/com/datastax/driver/core/TableOrView.java @@ -0,0 +1,642 @@ +/* + * Copyright (C) 2012-2015 DataStax Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.driver.core; + +import java.util.*; + +import com.google.common.base.Predicate; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.Iterables; + +/** + * Base class for Tables and Materialized Views. + */ +abstract class TableOrView { + + static final Comparator columnMetadataComparator = new Comparator() { + public int compare(ColumnMetadata c1, ColumnMetadata c2) { + return c1.getName().compareTo(c2.getName()); + } + }; + + protected final KeyspaceMetadata keyspace; + protected final String name; + protected final UUID id; + protected final List partitionKey; + protected final List clusteringColumns; + protected final Map columns; + protected final Options options; + protected final List clusteringOrder; + protected final VersionNumber cassandraVersion; + + /** + * Clustering orders. + *

+ * This is used by {@link #getClusteringOrder} to indicate the clustering + * order of a table. + */ + public enum Order { + ASC, DESC; + + static final Predicate isAscending = new Predicate() { + public boolean apply(Order o) { + return o == ASC; + } + }; + } + + TableOrView(KeyspaceMetadata keyspace, + String name, + UUID id, + List partitionKey, + List clusteringColumns, + Map columns, + Options options, + List clusteringOrder, + VersionNumber cassandraVersion) { + this.keyspace = keyspace; + this.name = name; + this.id = id; + this.partitionKey = partitionKey; + this.clusteringColumns = clusteringColumns; + this.columns = columns; + this.options = options; + this.clusteringOrder = clusteringOrder; + this.cassandraVersion = cassandraVersion; + } + + protected static List nullInitializedList(int size) { + List l = new ArrayList(size); + for (int i = 0; i < size; ++i) + l.add(null); + return l; + } + + /** + * Returns the name of this table. + * + * @return the name of this CQL table. + */ + public String getName() { + return name; + } + + /** + * Returns the unique id of this table. + *

+ * Note: this id is available in Cassandra 2.1 and above. It will be + * {@code null} for earlier versions. + * + * @return the unique id of the table. + */ + public UUID getId() { + return id; + } + + /** + * Returns the keyspace this table belong to. + * + * @return the keyspace metadata of the keyspace this table belong to. + */ + public KeyspaceMetadata getKeyspace() { + return keyspace; + } + + /** + * Returns metadata on a column of this table. + * + * @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. + */ + public ColumnMetadata getColumn(String name) { + return columns.get(Metadata.handleId(name)); + } + + /** + * Returns a list containing all the columns of this table. + * + * The order of the columns in the list is consistent with + * the order of the columns returned by a {@code SELECT * FROM thisTable}: + * the first column is the partition key, next are the clustering + * columns in their defined order, and then the rest of the + * columns follow in alphabetic order. + * + * @return a list containing the metadata for the columns of this table. + */ + public List getColumns() { + return new ArrayList(columns.values()); + } + + /** + * Returns the list of columns composing the primary key for this table. + * + * A table will always at least have a partition key (that + * may itself be one or more columns), so the returned list at least + * has one element. + * + * @return the list of columns composing the primary key for this table. + */ + public List getPrimaryKey() { + List pk = new ArrayList(partitionKey.size() + clusteringColumns.size()); + pk.addAll(partitionKey); + pk.addAll(clusteringColumns); + return pk; + } + + /** + * Returns the list of columns composing the partition key for this table. + * + * A table always has a partition key so the returned list has + * at least one element. + * + * @return the list of columns composing the partition key for this table. + */ + public List getPartitionKey() { + return Collections.unmodifiableList(partitionKey); + } + + /** + * Returns the list of clustering columns for this table. + * + * @return the list of clustering columns for this table. + * If there is no clustering columns, an empty list is returned. + */ + public List getClusteringColumns() { + return Collections.unmodifiableList(clusteringColumns); + } + + /** + * Returns the clustering order for this table. + *

+ * The returned contains the clustering order of each clustering column. The + * {@code i}th element of the result correspond to the order (ascending or + * descending) of the {@code i}th clustering column (see + * {@link #getClusteringColumns}). Note that a table defined without any + * particular clustering order is equivalent to one for which all the + * clustering key are in ascending order. + * + * @return a list with the clustering order for each clustering column. + */ + public List getClusteringOrder() { + return clusteringOrder; + } + + /** + * Returns the options for this table. + * + * @return the options for this table. + */ + public Options getOptions() { + return options; + } + + void add(ColumnMetadata column) { + columns.put(column.getName(), column); + } + + /** + * Returns a {@code String} containing CQL queries representing this + * table and the index on it. + *

+ * In other words, this method returns the queries that would allow you to + * recreate the schema of this table, along with the indexes and views defined on + * this table, if any. + *

+ * Note that the returned String is formatted to be human readable (for + * some definition of human readable at least). + * + * @return the CQL queries representing this table schema as a {code + * String}. + */ + public String exportAsString() { + return asCQLQuery(true); + } + + /** + * Returns a CQL query representing this table. + *

+ * This method returns a single 'CREATE TABLE' query with the options + * corresponding to this table definition. + *

+ * Note that the returned string is a single line; the returned query + * is not formatted in any way. + * + * @return the 'CREATE TABLE' query corresponding to this table. + * @see #exportAsString + */ + public String asCQLQuery() { + return asCQLQuery(false); + } + + protected abstract String asCQLQuery(boolean formatted); + + protected StringBuilder appendOptions(StringBuilder sb, boolean formatted){ + // Options + sb.append(" WITH "); + if (options.isCompactStorage) + and(sb.append("COMPACT STORAGE"), formatted); + if (!Iterables.all(clusteringOrder, Order.isAscending)) + and(appendClusteringOrder(sb), formatted); + sb.append("read_repair_chance = ").append(options.readRepair); + and(sb, formatted).append("dclocal_read_repair_chance = ").append(options.localReadRepair); + if (cassandraVersion.getMajor() < 2 || (cassandraVersion.getMajor() == 2 && cassandraVersion.getMinor() == 0)) + and(sb, formatted).append("replicate_on_write = ").append(options.replicateOnWrite); + and(sb, formatted).append("gc_grace_seconds = ").append(options.gcGrace); + and(sb, formatted).append("bloom_filter_fp_chance = ").append(options.bfFpChance); + if (cassandraVersion.getMajor() < 2 || cassandraVersion.getMajor() == 2 && cassandraVersion.getMinor() < 1) + and(sb, formatted).append("caching = '").append(options.caching.get("keys")).append('\''); + else + and(sb, formatted).append("caching = ").append(formatOptionMap(options.caching)); + if (options.comment != null) + and(sb, formatted).append("comment = '").append(options.comment.replace("'","''")).append('\''); + and(sb, formatted).append("compaction = ").append(formatOptionMap(options.compaction)); + and(sb, formatted).append("compression = ").append(formatOptionMap(options.compression)); + if (cassandraVersion.getMajor() >= 2) { + and(sb, formatted).append("default_time_to_live = ").append(options.defaultTTL); + and(sb, formatted).append("speculative_retry = '").append(options.speculativeRetry).append('\''); + if (options.indexInterval != null) + and(sb, formatted).append("index_interval = ").append(options.indexInterval); + } + if (cassandraVersion.getMajor() > 2 || (cassandraVersion.getMajor() == 2 && cassandraVersion.getMinor() >= 1)) { + and(sb, formatted).append("min_index_interval = ").append(options.minIndexInterval); + and(sb, formatted).append("max_index_interval = ").append(options.maxIndexInterval); + } + sb.append(';'); + return sb; + } + + @Override + public String toString() { + return asCQLQuery(); + } + + private StringBuilder appendClusteringOrder(StringBuilder sb) { + sb.append("CLUSTERING ORDER BY ("); + for (int i = 0; i < clusteringColumns.size(); i++) { + if (i > 0) sb.append(", "); + sb.append(clusteringColumns.get(i).getName()).append(' ').append(clusteringOrder.get(i)); + } + return sb.append(')'); + } + + private static String formatOptionMap(Map m) { + StringBuilder sb = new StringBuilder(); + sb.append("{ "); + boolean first = true; + for (Map.Entry entry : m.entrySet()) { + if (first) first = false; else sb.append(", "); + sb.append('\'').append(entry.getKey()).append('\''); + sb.append(" : "); + try { + sb.append(Integer.parseInt(entry.getValue())); + } catch (NumberFormatException e) { + sb.append('\'').append(entry.getValue()).append('\''); + } + } + sb.append(" }"); + return sb.toString(); + } + + private StringBuilder and(StringBuilder sb, boolean formatted) { + return newLine(sb, formatted).append(spaces(2, formatted)).append(" AND "); + } + + static String spaces(int n, boolean formatted) { + if (!formatted) + return ""; + + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < n; i++) + sb.append(' '); + + return sb.toString(); + } + + static StringBuilder newLine(StringBuilder sb, boolean formatted) { + if (formatted) + sb.append('\n'); + return sb; + } + + static StringBuilder spaceOrNewLine(StringBuilder sb, boolean formatted) { + sb.append(formatted ? '\n' : ' '); + return sb; + } + + public static class Options { + + private static final String COMMENT = "comment"; + private static final String READ_REPAIR = "read_repair_chance"; + private static final String DCLOCAL_READ_REPAIR = "dclocal_read_repair_chance"; + private static final String LOCAL_READ_REPAIR = "local_read_repair_chance"; + private static final String REPLICATE_ON_WRITE = "replicate_on_write"; + private static final String GC_GRACE = "gc_grace_seconds"; + private static final String BF_FP_CHANCE = "bloom_filter_fp_chance"; + private static final String CACHING = "caching"; + private static final String COMPACTION = "compaction"; + private static final String COMPACTION_CLASS = "compaction_strategy_class"; + private static final String COMPACTION_OPTIONS = "compaction_strategy_options"; + private static final String POPULATE_CACHE_ON_FLUSH = "populate_io_cache_on_flush"; + private static final String COMPRESSION = "compression"; + private static final String COMPRESSION_PARAMS = "compression_parameters"; + private static final String MEMTABLE_FLUSH_PERIOD_MS = "memtable_flush_period_in_ms"; + private static final String DEFAULT_TTL = "default_time_to_live"; + private static final String SPECULATIVE_RETRY = "speculative_retry"; + private static final String INDEX_INTERVAL = "index_interval"; + private static final String MIN_INDEX_INTERVAL = "min_index_interval"; + private static final String MAX_INDEX_INTERVAL = "max_index_interval"; + + private static final boolean DEFAULT_REPLICATE_ON_WRITE = true; + private static final double DEFAULT_BF_FP_CHANCE = 0.01; + private static final boolean DEFAULT_POPULATE_CACHE_ON_FLUSH = false; + private static final int DEFAULT_MEMTABLE_FLUSH_PERIOD = 0; + private static final int DEFAULT_DEFAULT_TTL = 0; + private static final String DEFAULT_SPECULATIVE_RETRY = "NONE"; + private static final int DEFAULT_INDEX_INTERVAL = 128; + private static final int DEFAULT_MIN_INDEX_INTERVAL = 128; + private static final int DEFAULT_MAX_INDEX_INTERVAL = 2048; + + private final boolean isCompactStorage; + + private final String comment; + private final double readRepair; + private final double localReadRepair; + private final boolean replicateOnWrite; + private final int gcGrace; + private final double bfFpChance; + private final Map caching; + private final boolean populateCacheOnFlush; + private final int memtableFlushPeriodMs; + private final int defaultTTL; + private final String speculativeRetry; + private final Integer indexInterval; + private final Integer minIndexInterval; + private final Integer maxIndexInterval; + private final Map compaction; + private final Map compression; + + Options(Row row, boolean isCompactStorage, VersionNumber version) { + + boolean is120 = version.getMajor() < 2; + boolean is200 = version.getMajor() == 2 && version.getMinor() == 0; + boolean is210 = version.getMajor() == 2 && version.getMinor() >= 1; + boolean is300OrHigher = version.getMajor() > 2; + boolean is210OrHigher = is210 || is300OrHigher; + + this.isCompactStorage = isCompactStorage; + this.comment = isNullOrAbsent(row, COMMENT) ? "" : row.getString(COMMENT); + this.readRepair = row.getDouble(READ_REPAIR); + + if(is300OrHigher) + this.localReadRepair = row.getDouble(DCLOCAL_READ_REPAIR); + else + this.localReadRepair = row.getDouble(LOCAL_READ_REPAIR); + + this.replicateOnWrite = is210OrHigher || isNullOrAbsent(row, REPLICATE_ON_WRITE) ? DEFAULT_REPLICATE_ON_WRITE : row.getBool(REPLICATE_ON_WRITE); + this.gcGrace = row.getInt(GC_GRACE); + this.bfFpChance = isNullOrAbsent(row, BF_FP_CHANCE) ? DEFAULT_BF_FP_CHANCE : row.getDouble(BF_FP_CHANCE); + + this.populateCacheOnFlush = isNullOrAbsent(row, POPULATE_CACHE_ON_FLUSH) ? DEFAULT_POPULATE_CACHE_ON_FLUSH : row.getBool(POPULATE_CACHE_ON_FLUSH); + this.memtableFlushPeriodMs = is120 || isNullOrAbsent(row, MEMTABLE_FLUSH_PERIOD_MS) ? DEFAULT_MEMTABLE_FLUSH_PERIOD : row.getInt(MEMTABLE_FLUSH_PERIOD_MS); + this.defaultTTL = is120 || isNullOrAbsent(row, DEFAULT_TTL) ? DEFAULT_DEFAULT_TTL : row.getInt(DEFAULT_TTL); + this.speculativeRetry = is120 || isNullOrAbsent(row, SPECULATIVE_RETRY) ? DEFAULT_SPECULATIVE_RETRY : row.getString(SPECULATIVE_RETRY); + + if (is200) + this.indexInterval = isNullOrAbsent(row, INDEX_INTERVAL) ? DEFAULT_INDEX_INTERVAL : row.getInt(INDEX_INTERVAL); + else + this.indexInterval = null; + + if (is210OrHigher) { + this.minIndexInterval = isNullOrAbsent(row, MIN_INDEX_INTERVAL) + ? DEFAULT_MIN_INDEX_INTERVAL + : row.getInt(MIN_INDEX_INTERVAL); + this.maxIndexInterval = isNullOrAbsent(row, MAX_INDEX_INTERVAL) + ? DEFAULT_MAX_INDEX_INTERVAL + : row.getInt(MAX_INDEX_INTERVAL); + } else { + this.minIndexInterval = null; + this.maxIndexInterval = null; + } + + if (is300OrHigher) { + this.caching = ImmutableMap.copyOf(row.getMap(CACHING, String.class, String.class)); + } else if (is210) { + this.caching = ImmutableMap.copyOf(SimpleJSONParser.parseStringMap(row.getString(CACHING))); + } else { + this.caching = ImmutableMap.of("keys", row.getString(CACHING)); + } + + if (is300OrHigher) + this.compaction = ImmutableMap.copyOf(row.getMap(COMPACTION, String.class, String.class)); + else { + this.compaction = ImmutableMap.builder() + .put("class", row.getString(COMPACTION_CLASS)) + .putAll(SimpleJSONParser.parseStringMap(row.getString(COMPACTION_OPTIONS))) + .build(); + } + + if(is300OrHigher) + this.compression = ImmutableMap.copyOf(row.getMap(COMPRESSION, String.class, String.class)); + else + this.compression = ImmutableMap.copyOf(SimpleJSONParser.parseStringMap(row.getString(COMPRESSION_PARAMS))); + } + + private static boolean isNullOrAbsent(Row row, String name) { + return row.getColumnDefinitions().getIndexOf(name) < 0 + || row.isNull(name); + } + + /** + * Returns whether the table uses the {@code COMPACT STORAGE} option. + * + * @return whether the table uses the {@code COMPACT STORAGE} option. + */ + public boolean isCompactStorage() { + return isCompactStorage; + } + + /** + * Returns the commentary set for this table. + * + * @return the commentary set for this table, or {@code null} if noe has been set. + */ + public String getComment() { + return comment; + } + + /** + * Returns the chance with which a read repair is triggered for this table. + * + * @return the read repair chance set for table (in [0.0, 1.0]). + */ + public double getReadRepairChance() { + return readRepair; + } + + /** + * Returns the cluster local read repair chance set for this table. + * + * @return the local read repair chance set for table (in [0.0, 1.0]). + */ + public double getLocalReadRepairChance() { + return localReadRepair; + } + + /** + * Returns whether replicateOnWrite is set for this table. + * + * This is only meaningful for tables holding counters. + * + * @return whether replicateOnWrite is set for this table. + */ + public boolean getReplicateOnWrite() { + return replicateOnWrite; + } + + /** + * Returns the tombstone garbage collection grace time in seconds for this table. + * + * @return the tombstone garbage collection grace time in seconds for this table. + */ + public int getGcGraceInSeconds() { + return gcGrace; + } + + /** + * Returns the false positive chance for the Bloom filter of this table. + * + * @return the Bloom filter false positive chance for this table (in [0.0, 1.0]). + */ + public double getBloomFilterFalsePositiveChance() { + return bfFpChance; + } + + /** + * Returns the caching options for this table. + * + * @return an immutable map containing the caching options for this table. + */ + public Map getCaching() { + return caching; + } + + /** + * Whether the populate I/O cache on flush is set on this table. + * + * @return whether the populate I/O cache on flush is set on this table. + */ + public boolean getPopulateIOCacheOnFlush() { + return populateCacheOnFlush; + } + + /* + * Returns the memtable flush period (in milliseconds) option for this table. + *

+ * Note: this option is not available in Cassandra 1.2 and will return 0 (no periodic + * flush) when connected to 1.2 nodes. + * + * @return the memtable flush period option for this table or 0 if no + * periodic flush is configured. + */ + public int getMemtableFlushPeriodInMs() { + return memtableFlushPeriodMs; + } + + /** + * Returns the default TTL for this table. + *

+ * Note: this option is not available in Cassandra 1.2 and will return 0 (no default + * TTL) when connected to 1.2 nodes. + * + * @return the default TTL for this table or 0 if no default TTL is + * configured. + */ + public int getDefaultTimeToLive() { + return defaultTTL; + } + + /** + * Returns the speculative retry option for this table. + *

+ * Note: this option is not available in Cassandra 1.2 and will return "NONE" (no + * speculative retry) when connected to 1.2 nodes. + * + * @return the speculative retry option this table. + */ + public String getSpeculativeRetry() { + return speculativeRetry; + } + + /** + * Returns the index interval option for this table. + *

+ * Note: this option is not available in Cassandra 1.2 (more precisely, it is not + * configurable per-table) and will return 128 (the default index interval) when + * connected to 1.2 nodes. It is deprecated in Cassandra 2.1 and above, and will + * therefore return {@code null} for 2.1 nodes. + * + * @return the index interval option for this table. + */ + public Integer getIndexInterval() { + return indexInterval; + } + + /** + * Returns the minimum index interval option for this table. + *

+ * Note: this option is available in Cassandra 2.1 and above, and will return + * {@code null} for earlier versions. + * + * @return the minimum index interval option for this table. + */ + public Integer getMinIndexInterval() { + return minIndexInterval; + } + + /** + * Returns the maximum index interval option for this table. + *

+ * Note: this option is available in Cassandra 2.1 and above, and will return + * {@code null} for earlier versions. + * + * @return the maximum index interval option for this table. + */ + public Integer getMaxIndexInterval() { + return maxIndexInterval; + } + + /** + * Returns the compaction options for this table. + * + * @return an immutable map containing the compaction options for this table. + */ + public Map getCompaction() { + return compaction; + } + + /** + * Returns the compression options for this table. + * + * @return an immutable map containing the compression options for this table. + */ + public Map getCompression() { + return compression; + } + } +} diff --git a/driver-core/src/test/java/com/datastax/driver/core/Assertions.java b/driver-core/src/test/java/com/datastax/driver/core/Assertions.java index 86c5cc5dbfd..01357cbfefe 100644 --- a/driver-core/src/test/java/com/datastax/driver/core/Assertions.java +++ b/driver-core/src/test/java/com/datastax/driver/core/Assertions.java @@ -55,4 +55,8 @@ public static TypeCodecAssert assertThat(TypeCodec codec) { return new TypeCodecAssert(codec); } + public static MaterializedViewMetadataAssert assertThat(MaterializedViewMetadata view) { + return new MaterializedViewMetadataAssert(view); + } + } 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 5ffd34a6e15..45430993703 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 @@ -36,27 +36,27 @@ public ColumnMetadataAssert hasName(String name) { } public ColumnMetadataAssert isPrimaryKey() { - assertThat(actual.getTable().getPrimaryKey().contains(actual)); + assertThat(actual.getParent().getPrimaryKey().contains(actual)); return this; } public ColumnMetadataAssert isPartitionKey() { - assertThat(actual.getTable().getPartitionKey().contains(actual)); + assertThat(actual.getParent().getPartitionKey().contains(actual)); return this; } public ColumnMetadataAssert isClusteringColumn() { - assertThat(actual.getTable().getClusteringColumns().contains(actual)); + assertThat(actual.getParent().getClusteringColumns().contains(actual)); return this; } public ColumnMetadataAssert isRegularColumn() { - assertThat(!actual.getTable().getPrimaryKey().contains(actual)); + assertThat(!actual.getParent().getPrimaryKey().contains(actual)); return this; } - public ColumnMetadataAssert hasClusteringOrder(TableMetadata.Order order) { - assertThat(actual.getTable().getClusteringOrder().get(actual.getTable().getClusteringColumns().indexOf(actual))).isEqualTo(order); + public ColumnMetadataAssert hasClusteringOrder(TableOrView.Order order) { + assertThat(actual.getParent().getClusteringOrder().get(actual.getParent().getClusteringColumns().indexOf(actual))).isEqualTo(order); 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 9658125db72..4840384d7a0 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 @@ -41,7 +41,7 @@ public class IndexMetadataTest extends CCMBridge.PerClassSingleNodeCluster { private static final ColumnDefinitions defs = new ColumnDefinitions(new ColumnDefinitions.Definition[]{ definition(COLUMN_NAME, text()), definition(COMPONENT_INDEX, cint()), - definition(KIND, text()), + definition(KIND_V2, text()), definition(INDEX_NAME, text()), definition(INDEX_TYPE, text()), definition(VALIDATOR, text()), diff --git a/driver-core/src/test/java/com/datastax/driver/core/MaterializedViewMetadataAssert.java b/driver-core/src/test/java/com/datastax/driver/core/MaterializedViewMetadataAssert.java new file mode 100644 index 00000000000..336668b06a1 --- /dev/null +++ b/driver-core/src/test/java/com/datastax/driver/core/MaterializedViewMetadataAssert.java @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2012-2015 DataStax Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.driver.core; + +import org.assertj.core.api.AbstractAssert; + +import static org.assertj.core.api.Assertions.assertThat; + +public class MaterializedViewMetadataAssert extends AbstractAssert { + + public MaterializedViewMetadataAssert(MaterializedViewMetadata actual) { + super(actual, MaterializedViewMetadataAssert.class); + } + + public MaterializedViewMetadataAssert hasName(String name) { + assertThat(actual.getName()).isEqualTo(name); + return this; + } + + public MaterializedViewMetadataAssert hasBaseTable(TableMetadata table) { + assertThat(actual.getBaseTable()).isEqualTo(table); + return this; + } + + public MaterializedViewMetadataAssert hasNumberOfColumns(int expected) { + assertThat(actual.getColumns().size()).isEqualTo(expected); + return this; + } + +} 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 new file mode 100644 index 00000000000..1ea852e0111 --- /dev/null +++ b/driver-core/src/test/java/com/datastax/driver/core/MaterializedViewMetadataTest.java @@ -0,0 +1,120 @@ +/* + * Copyright (C) 2012-2015 DataStax Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.driver.core; + +import java.util.Collection; +import java.util.Collections; + +import org.testng.annotations.Test; + +import com.datastax.driver.core.utils.CassandraVersion; + +import static com.datastax.driver.core.Assertions.assertThat; +import static com.datastax.driver.core.DataType.*; +import static com.datastax.driver.core.TableOrView.Order.DESC; + +@CassandraVersion(major = 3) +public class MaterializedViewMetadataTest extends CCMBridge.PerClassSingleNodeCluster { + + @Test(groups = "short") + public void should_create_view_metadata() { + + // given + String createTable = String.format( + "CREATE TABLE %s.scores(" + + "user TEXT," + + "game TEXT," + + "year INT," + + "month INT," + + "day INT," + + "score INT," + + "PRIMARY KEY (user, game, year, month, day)" + + ")", + keyspace); + String createMV = String.format( + "CREATE MATERIALIZED VIEW %s.monthlyhigh AS " + + "SELECT user 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)", + keyspace, keyspace); + + // when + session.execute(createTable); + session.execute(createMV); + + // then + TableMetadata table = cluster.getMetadata().getKeyspace(keyspace).getTable("scores"); + MaterializedViewMetadata mv = cluster.getMetadata().getKeyspace(keyspace).getMaterializedView("monthlyhigh"); + + assertThat(table).isNotNull().hasName("scores").hasMaterializedView(mv).hasNumberOfColumns(6); + assertThat(table.getColumns().get(0)).isNotNull().hasName("user").isPartitionKey(); + assertThat(table.getColumns().get(1)).isNotNull().hasName("game").isClusteringColumn(); + assertThat(table.getColumns().get(2)).isNotNull().hasName("year").isClusteringColumn(); + assertThat(table.getColumns().get(3)).isNotNull().hasName("month").isClusteringColumn(); + assertThat(table.getColumns().get(4)).isNotNull().hasName("day").isClusteringColumn(); + assertThat(table.getColumns().get(5)).isNotNull().hasName("score").isRegularColumn(); + + assertThat(mv).isNotNull().hasName("monthlyhigh").hasBaseTable(table).hasNumberOfColumns(6).isEqualTo(table.getView("monthlyhigh")); + assertThat(mv.getColumns().get(0)).isNotNull().hasName("game").isPartitionKey(); + assertThat(mv.getColumns().get(1)).isNotNull().hasName("year").isPartitionKey(); + assertThat(mv.getColumns().get(2)).isNotNull().hasName("month").isPartitionKey(); + assertThat(mv.getColumns().get(3)).isNotNull().hasName("score").isClusteringColumn().hasClusteringOrder(DESC); + assertThat(mv.getColumns().get(4)).isNotNull().hasName("user").isClusteringColumn(); + assertThat(mv.getColumns().get(5)).isNotNull().hasName("day").isClusteringColumn(); + + assertThat(mv.asCQLQuery(false)).contains(createMV); + + } + + @Test(groups = "short") + public void should_create_view_metadata_with_case_sensitive_column_names() { + // given + String createTable = String.format( + "CREATE TABLE %s.t1 (" + + "\"theKey\" int, " + + "\"theClustering\" int, " + + "\"theValue\" int, " + + "PRIMARY KEY (\"theKey\", \"theClustering\"))", + keyspace); + String createMV = String.format( + "CREATE MATERIALIZED VIEW %s.mv1 AS " + + "SELECT \"theKey\", \"theClustering\", \"theValue\" " + + "FROM %s.t1 " + + "WHERE \"theKey\" IS NOT NULL AND \"theClustering\" IS NOT NULL AND \"theValue\" IS NOT NULL " + + "PRIMARY KEY (\"theKey\", \"theClustering\")", + keyspace, keyspace); + // when + session.execute(createTable); + session.execute(createMV); + // then + TableMetadata table = cluster.getMetadata().getKeyspace(keyspace).getTable("t1"); + MaterializedViewMetadata mv = cluster.getMetadata().getKeyspace(keyspace).getMaterializedView("mv1"); + assertThat(table).isNotNull().hasName("t1").hasMaterializedView(mv).hasNumberOfColumns(3); + assertThat(table.getColumns().get(0)).isNotNull().hasName("theKey").isPartitionKey().hasType(cint()); + assertThat(table.getColumns().get(1)).isNotNull().hasName("theClustering").isClusteringColumn().hasType(cint()); + assertThat(table.getColumns().get(2)).isNotNull().hasName("theValue").isRegularColumn().hasType(cint()); + assertThat(mv).isNotNull().hasName("mv1").hasBaseTable(table).hasNumberOfColumns(3); + assertThat(mv.getColumns().get(0)).isNotNull().hasName("theKey").isPartitionKey().hasType(cint()); + assertThat(mv.getColumns().get(1)).isNotNull().hasName("theClustering").isClusteringColumn().hasType(cint()); + assertThat(mv.getColumns().get(2)).isNotNull().hasName("theValue").isRegularColumn().hasType(cint()); + } + + @Override + protected Collection getTableDefinitions() { + return Collections.emptyList(); + } +} 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 7cf98d868d9..c5868e06db9 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 @@ -45,4 +45,8 @@ public TableMetadataAssert hasNumberOfColumns(int expected) { return this; } + public TableMetadataAssert hasMaterializedView(MaterializedViewMetadata expected) { + assertThat(actual.getView(expected.getName())).isNotNull().isEqualTo(expected); + return this; + } } diff --git a/driver-core/src/test/java/com/datastax/driver/core/TableMetadataTest.java b/driver-core/src/test/java/com/datastax/driver/core/TableMetadataTest.java index 6b7de515d89..77b029f125b 100644 --- a/driver-core/src/test/java/com/datastax/driver/core/TableMetadataTest.java +++ b/driver-core/src/test/java/com/datastax/driver/core/TableMetadataTest.java @@ -18,15 +18,14 @@ import java.util.Collection; import java.util.Collections; -import org.testng.SkipException; import org.testng.annotations.Test; import static org.assertj.core.api.Assertions.entry; import static com.datastax.driver.core.Assertions.assertThat; import static com.datastax.driver.core.DataType.*; -import static com.datastax.driver.core.TableMetadata.Order.ASC; -import static com.datastax.driver.core.TableMetadata.Order.DESC; +import static com.datastax.driver.core.TableOrView.Order.ASC; +import static com.datastax.driver.core.TableOrView.Order.DESC; public class TableMetadataTest extends CCMBridge.PerClassSingleNodeCluster { @@ -61,7 +60,7 @@ public void should_parse_table_with_clustering_columns() { + " l list,\n" + " v int,\n" + " PRIMARY KEY (k, c1, c2)\n" - + ");", keyspace); + + ") WITH CLUSTERING ORDER BY (c1 ASC, c2 DESC);", keyspace); // when session.execute(cql); TableMetadata table = cluster.getMetadata().getKeyspace(keyspace).getTable("sparse"); @@ -69,7 +68,7 @@ public void should_parse_table_with_clustering_columns() { assertThat(table).isNotNull().hasName("sparse").hasNumberOfColumns(5).isNotCompactStorage(); assertThat(table.getColumns().get(0)).isNotNull().hasName("k").isPartitionKey().hasType(text()); assertThat(table.getColumns().get(1)).isNotNull().hasName("c1").isClusteringColumn().hasClusteringOrder(ASC).hasType(cint()); - assertThat(table.getColumns().get(2)).isNotNull().hasName("c2").isClusteringColumn().hasClusteringOrder(ASC).hasType(cfloat()); + assertThat(table.getColumns().get(2)).isNotNull().hasName("c2").isClusteringColumn().hasClusteringOrder(DESC).hasType(cfloat()); assertThat(table.getColumns().get(3)).isNotNull().hasName("l").isRegularColumn().hasType(list(text())); assertThat(table.getColumns().get(4)).isNotNull().hasName("v").isRegularColumn().hasType(cint()); } @@ -174,32 +173,57 @@ public void should_parse_compact_table_with_multiple_clustering_columns() { @Test(groups = "short") public void should_parse_table_options() { VersionNumber version = TestUtils.findHost(cluster, 1).getCassandraVersion(); + + // given + String cql; + + // Cassandra 3.0 + if (version.getMajor() > 2) { - // TODO adapt test for C* 3.0 - throw new SkipException("Needs adjustments to work with Cassandra 3.0"); + cql = String.format("CREATE TABLE %s.with_options (\n" + + " k text,\n" + + " c1 int,\n" + + " c2 int,\n" + + " i int,\n" + + " PRIMARY KEY (k, c1, c2)\n" + + ") WITH CLUSTERING ORDER BY (c1 DESC, c2 ASC)\n" + + " AND read_repair_chance = 0.5\n" + + " AND dclocal_read_repair_chance = 0.6\n" + + " AND speculative_retry = '99.9PERCENTILE'\n" + // replicate_on_write not supported anymore in 3.0 + + " AND gc_grace_seconds = 42\n" + + " AND bloom_filter_fp_chance = 0.01\n" + // older caching formats not supported anymore in 3.0 + + " AND caching = { 'keys' : 'ALL', 'rows_per_partition' : 10 }\n" + + " AND comment = 'My awesome table'\n" + + " AND compaction = { 'class' : 'org.apache.cassandra.db.compaction.LeveledCompactionStrategy', 'sstable_size_in_mb' : 15 }\n" + + " AND compression = { 'sstable_compression' : 'org.apache.cassandra.io.compress.SnappyCompressor', 'chunk_length_kb' : 128 };", + keyspace); + + // older versions + } else { + cql = String.format("CREATE TABLE %s.with_options (\n" + + " k text,\n" + + " c1 int,\n" + + " c2 int,\n" + + " i int,\n" + + " PRIMARY KEY (k, c1, c2)\n" + + ") WITH CLUSTERING ORDER BY (c1 DESC, c2 ASC)\n" + + " AND read_repair_chance = 0.5\n" + + " AND dclocal_read_repair_chance = 0.6\n" + + " AND replicate_on_write = true\n" + + " AND gc_grace_seconds = 42\n" + + " AND bloom_filter_fp_chance = 0.01\n" + + " AND caching = 'ALL'\n" + + " AND comment = 'My awesome table'\n" + + " AND compaction = { 'class' : 'org.apache.cassandra.db.compaction.LeveledCompactionStrategy', 'sstable_size_in_mb' : 15 }\n" + + " AND compression = { 'sstable_compression' : 'org.apache.cassandra.io.compress.SnappyCompressor', 'chunk_length_kb' : 128 };", + keyspace); } - // given - String cql = String.format("CREATE TABLE %s.with_options (\n" - + " k text,\n" - + " c1 int,\n" - + " c2 int,\n" - + " i int,\n" - + " PRIMARY KEY (k, c1, c2)\n" - + ") WITH CLUSTERING ORDER BY (c1 DESC, c2 ASC)\n" - + " AND read_repair_chance = 0.5\n" - + " AND dclocal_read_repair_chance = 0.6\n" - + " AND replicate_on_write = true\n" - + " AND gc_grace_seconds = 42\n" - + " AND bloom_filter_fp_chance = 0.01\n" - + " AND caching = 'ALL'\n" - + " AND comment = 'My awesome table'\n" - + " AND compaction = { 'class' : 'org.apache.cassandra.db.compaction.LeveledCompactionStrategy', 'sstable_size_in_mb' : 15 }\n" - + " AND compression = { 'sstable_compression' : 'org.apache.cassandra.io.compress.SnappyCompressor', 'chunk_length_kb' : 128 };", - keyspace); // when session.execute(cql); TableMetadata table = cluster.getMetadata().getKeyspace(keyspace).getTable("with_options"); + // then assertThat(table).isNotNull().hasName("with_options").hasNumberOfColumns(4).isNotCompactStorage(); assertThat(table.getColumns().get(0)).isNotNull().hasName("k").isPartitionKey().hasType(text()); @@ -217,13 +241,13 @@ public void should_parse_table_options() { assertThat(table.getOptions().getBloomFilterFalsePositiveChance()).isEqualTo(0.01); assertThat(table.getOptions().getComment()).isEqualTo("My awesome table"); assertThat(table.getOptions().getCaching()).contains(entry("keys", "ALL" )); - assertThat(table.getOptions().getCaching()).contains(entry("rows_per_partition", "ALL")); + assertThat(table.getOptions().getCaching()).contains(entry("rows_per_partition", "10")); assertThat(table.getOptions().getCompaction()).contains(entry("class", "org.apache.cassandra.db.compaction.LeveledCompactionStrategy")); assertThat(table.getOptions().getCompaction()).contains(entry("sstable_size_in_mb", "15")); assertThat(table.getOptions().getCompression()).contains(entry("class", "org.apache.cassandra.io.compress.SnappyCompressor")); // sstable_compression becomes class assertThat(table.getOptions().getCompression()).contains(entry("chunk_length_in_kb", "128")); // note the "in" prefix assertThat(table.getOptions().getDefaultTimeToLive()).isEqualTo(0); - assertThat(table.getOptions().getSpeculativeRetry()).isEqualTo("99.0PERCENTILE"); + assertThat(table.getOptions().getSpeculativeRetry()).isEqualTo("99.9PERCENTILE"); assertThat(table.getOptions().getIndexInterval()).isNull(); assertThat(table.getOptions().getMinIndexInterval()).isEqualTo(128); assertThat(table.getOptions().getMaxIndexInterval()).isEqualTo(2048); @@ -235,13 +259,13 @@ public void should_parse_table_options() { .contains("bloom_filter_fp_chance = 0.01") .contains("comment = 'My awesome table'") .contains("'keys' : 'ALL'") - .contains("'rows_per_partition' : 'ALL'") + .contains("'rows_per_partition' : 10") .contains("'class' : 'org.apache.cassandra.db.compaction.LeveledCompactionStrategy'") .contains("'sstable_size_in_mb' : 15") .contains("'class' : 'org.apache.cassandra.io.compress.SnappyCompressor'") // sstable_compression becomes class .contains("'chunk_length_in_kb' : 128") // note the "in" prefix .contains("default_time_to_live = 0") - .contains("speculative_retry = '99.0PERCENTILE'") + .contains("speculative_retry = '99.9PERCENTILE'") .contains("min_index_interval = 128") .contains("max_index_interval = 2048") .doesNotContain(" index_interval") diff --git a/pom.xml b/pom.xml index b757c4c9b2a..5be5d5d6287 100644 --- a/pom.xml +++ b/pom.xml @@ -43,7 +43,7 @@ UTF-8 - 3.0.0-beta1 + 3.0.0-rc1 1.6 1.2.17 1.7.6