(CQL3) Allow definitions with only a PK patch by slebresne; reviewed by jbellis for CASSANDRA-4361
Project: http://git-wip-us.apache.org/repos/asf/cassandra/repo Commit: http://git-wip-us.apache.org/repos/asf/cassandra/commit/79517461 Tree: http://git-wip-us.apache.org/repos/asf/cassandra/tree/79517461 Diff: http://git-wip-us.apache.org/repos/asf/cassandra/diff/79517461 Branch: refs/heads/cassandra-1.1 Commit: 795174611b7a9bea4ee2d64f8640c8bfebe07cfb Parents: d0d1b2c Author: Sylvain Lebresne <sylv...@datastax.com> Authored: Tue Jul 24 09:31:56 2012 +0200 Committer: Sylvain Lebresne <sylv...@datastax.com> Committed: Tue Jul 24 09:31:56 2012 +0200 ---------------------------------------------------------------------- CHANGES.txt | 1 + .../org/apache/cassandra/config/CFMetaData.java | 4 +- .../org/apache/cassandra/cql3/CFDefinition.java | 22 ++++-- .../cassandra/cql3/operations/ColumnOperation.java | 18 ++++- .../cassandra/cql3/statements/ColumnGroupMap.java | 6 -- .../statements/CreateColumnFamilyStatement.java | 66 +++++++++----- .../cassandra/cql3/statements/SelectStatement.java | 51 +++++++---- .../cassandra/cql3/statements/UpdateStatement.java | 43 ++++++++-- 8 files changed, 145 insertions(+), 66 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/cassandra/blob/79517461/CHANGES.txt ---------------------------------------------------------------------- diff --git a/CHANGES.txt b/CHANGES.txt index 8d61f64..979e3ef 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -30,6 +30,7 @@ * (cql3) Always use composite types by default (CASSANDRA-4329) * (cql3) Add support for set, map and list (CASSANDRA-3647) * Validate date type correctly (CASSANDRA-4441) + * (cql3) Allow definitions with only a PK (CASSANDRA-4361) 1.1.3 http://git-wip-us.apache.org/repos/asf/cassandra/blob/79517461/src/java/org/apache/cassandra/config/CFMetaData.java ---------------------------------------------------------------------- diff --git a/src/java/org/apache/cassandra/config/CFMetaData.java b/src/java/org/apache/cassandra/config/CFMetaData.java index 3cf41d7..906017c 100644 --- a/src/java/org/apache/cassandra/config/CFMetaData.java +++ b/src/java/org/apache/cassandra/config/CFMetaData.java @@ -1080,8 +1080,6 @@ public final class CFMetaData { if (alias != null) { - if (!alias.hasRemaining()) - throw new ConfigurationException(msg + " alias may not be empty"); try { UTF8Type.instance.validate(alias); @@ -1259,9 +1257,9 @@ public final class CFMetaData cfm.caching(Caching.valueOf(result.getString("caching"))); cfm.compactionStrategyClass(createCompactionStrategy(result.getString("compaction_strategy_class"))); cfm.compressionParameters(CompressionParameters.create(fromJsonMap(result.getString("compression_parameters")))); + cfm.columnAliases(columnAliasesFromStrings(fromJsonList(result.getString("column_aliases")))); if (result.has("value_alias")) cfm.valueAlias(result.getBytes("value_alias")); - cfm.columnAliases(columnAliasesFromStrings(fromJsonList(result.getString("column_aliases")))); cfm.compactionStrategyOptions(fromJsonMap(result.getString("compaction_strategy_options"))); return cfm; http://git-wip-us.apache.org/repos/asf/cassandra/blob/79517461/src/java/org/apache/cassandra/cql3/CFDefinition.java ---------------------------------------------------------------------- diff --git a/src/java/org/apache/cassandra/cql3/CFDefinition.java b/src/java/org/apache/cassandra/cql3/CFDefinition.java index 23edd62..d960a79 100644 --- a/src/java/org/apache/cassandra/cql3/CFDefinition.java +++ b/src/java/org/apache/cassandra/cql3/CFDefinition.java @@ -51,8 +51,8 @@ public class CFDefinition implements Iterable<CFDefinition.Name> public final Map<ColumnIdentifier, Name> metadata = new TreeMap<ColumnIdentifier, Name>(); public final boolean isComposite; - // Note that isCompact means here that no componet of the comparator correspond to the column names - // defined in the CREATE TABLE QUERY. This is not exactly equivalent to the 'WITH COMPACT STORAGE' + // Note that isCompact means here that no component of the comparator correspond to the column names + // defined in the CREATE TABLE QUERY. This is not exactly equivalent to using the 'WITH COMPACT STORAGE' // option when creating a table in that "static CF" without a composite type will have isCompact == false // even though one must use 'WITH COMPACT STORAGE' to declare them. public final boolean isCompact; @@ -66,7 +66,7 @@ public class CFDefinition implements Iterable<CFDefinition.Name> { this.isComposite = true; CompositeType composite = (CompositeType)cfm.comparator; - if (cfm.getColumn_metadata().isEmpty()) + if (cfm.getColumnAliases().size() == composite.types.size()) { // "dense" composite this.isCompact = true; @@ -76,7 +76,7 @@ public class CFDefinition implements Iterable<CFDefinition.Name> ColumnIdentifier id = getColumnId(cfm, i); this.columns.put(id, new Name(cfm.ksName, cfm.cfName, id, Name.Kind.COLUMN_ALIAS, i, composite.types.get(i))); } - this.value = new Name(cfm.ksName, cfm.cfName, getValueId(cfm), Name.Kind.VALUE_ALIAS, cfm.getDefaultValidator()); + this.value = createValue(cfm); } else { @@ -114,14 +114,14 @@ public class CFDefinition implements Iterable<CFDefinition.Name> { this.isComposite = false; this.hasCollections = false; - if (cfm.getColumn_metadata().isEmpty()) + if (!cfm.getColumnAliases().isEmpty() || cfm.getColumn_metadata().isEmpty()) { // dynamic CF this.isCompact = true; ColumnIdentifier id = getColumnId(cfm, 0); Name name = new Name(cfm.ksName, cfm.cfName, id, Name.Kind.COLUMN_ALIAS, 0, cfm.comparator); this.columns.put(id, name); - this.value = new Name(cfm.ksName, cfm.cfName, getValueId(cfm), Name.Kind.VALUE_ALIAS, cfm.getDefaultValidator()); + this.value = createValue(cfm); } else { @@ -171,6 +171,16 @@ public class CFDefinition implements Iterable<CFDefinition.Name> return (ColumnToCollectionType)composite.types.get(composite.types.size() - 1); } + private static Name createValue(CFMetaData cfm) + { + ColumnIdentifier alias = getValueId(cfm); + // That's how we distinguish between 'no value alias because coming from thrift' and 'I explicitely did not + // define a value' (see CreateColumnFamilyStatement) + return alias.key.equals(ByteBufferUtil.EMPTY_BYTE_BUFFER) + ? null + : new Name(cfm.ksName, cfm.cfName, alias, Name.Kind.VALUE_ALIAS, cfm.getDefaultValidator()); + } + public Name get(ColumnIdentifier name) { if (name.equals(key.name)) http://git-wip-us.apache.org/repos/asf/cassandra/blob/79517461/src/java/org/apache/cassandra/cql3/operations/ColumnOperation.java ---------------------------------------------------------------------- diff --git a/src/java/org/apache/cassandra/cql3/operations/ColumnOperation.java b/src/java/org/apache/cassandra/cql3/operations/ColumnOperation.java index 5e4f6b3..4f36775 100644 --- a/src/java/org/apache/cassandra/cql3/operations/ColumnOperation.java +++ b/src/java/org/apache/cassandra/cql3/operations/ColumnOperation.java @@ -36,6 +36,17 @@ public class ColumnOperation implements Operation { enum Kind { SET, COUNTER_INC, COUNTER_DEC } + private static final Operation setToEmptyOperation = new ColumnOperation(null, Kind.SET) + { + @Override + protected void doSet(ColumnFamily cf, ColumnNameBuilder builder, AbstractType<?> validator, UpdateParameters params) throws InvalidRequestException + { + ByteBuffer colName = builder.build(); + QueryProcessor.validateColumnName(colName); + cf.addColumn(params.makeColumn(colName, ByteBufferUtil.EMPTY_BYTE_BUFFER)); + } + }; + private final Term value; private final Kind kind; @@ -69,7 +80,7 @@ public class ColumnOperation implements Operation throw new InvalidRequestException("Column operations are only supported on simple types, but " + validator + " given."); } - private void doSet(ColumnFamily cf, ColumnNameBuilder builder, AbstractType<?> validator, UpdateParameters params) throws InvalidRequestException + protected void doSet(ColumnFamily cf, ColumnNameBuilder builder, AbstractType<?> validator, UpdateParameters params) throws InvalidRequestException { ByteBuffer colName = builder.build(); QueryProcessor.validateColumnName(colName); @@ -133,4 +144,9 @@ public class ColumnOperation implements Operation { return new ColumnOperation(value, Kind.COUNTER_DEC); } + + public static Operation SetToEmpty() + { + return setToEmptyOperation; + } } http://git-wip-us.apache.org/repos/asf/cassandra/blob/79517461/src/java/org/apache/cassandra/cql3/statements/ColumnGroupMap.java ---------------------------------------------------------------------- diff --git a/src/java/org/apache/cassandra/cql3/statements/ColumnGroupMap.java b/src/java/org/apache/cassandra/cql3/statements/ColumnGroupMap.java index e365e81..2625fef 100644 --- a/src/java/org/apache/cassandra/cql3/statements/ColumnGroupMap.java +++ b/src/java/org/apache/cassandra/cql3/statements/ColumnGroupMap.java @@ -148,12 +148,6 @@ public class ColumnGroupMap */ private boolean isSameGroup(ByteBuffer[] c) { - // Cql don't allow to insert columns who doesn't have all component of - // the composite set for sparse composite. Someone coming from thrift - // could hit that though. But since we have no way to handle this - // correctly, better fail here and tell whomever may hit that (if - // someone ever do) to change the definition to a dense composite. - assert c.length >= idx && previous.length >= idx : "Sparse composite should not have partial column names"; for (int i = 0; i < idx; i++) { if (!c[i].equals(previous[i])) http://git-wip-us.apache.org/repos/asf/cassandra/blob/79517461/src/java/org/apache/cassandra/cql3/statements/CreateColumnFamilyStatement.java ---------------------------------------------------------------------- diff --git a/src/java/org/apache/cassandra/cql3/statements/CreateColumnFamilyStatement.java b/src/java/org/apache/cassandra/cql3/statements/CreateColumnFamilyStatement.java index bf7a82a..f5466d6 100644 --- a/src/java/org/apache/cassandra/cql3/statements/CreateColumnFamilyStatement.java +++ b/src/java/org/apache/cassandra/cql3/statements/CreateColumnFamilyStatement.java @@ -44,6 +44,7 @@ import org.apache.cassandra.service.MigrationManager; import org.apache.cassandra.thrift.CqlResult; import org.apache.cassandra.thrift.InvalidRequestException; import org.apache.cassandra.io.compress.CompressionParameters; +import org.apache.cassandra.utils.ByteBufferUtil; import org.apache.cassandra.utils.Pair; /** A <code>CREATE COLUMNFAMILY</code> parsed from a CQL query statement. */ @@ -195,7 +196,26 @@ public class CreateColumnFamilyStatement extends SchemaAlteringStatement throw new InvalidRequestException(String.format("counter type is not supported for PRIMARY KEY part %s", stmt.keyAlias)); // Handle column aliases - if (!columnAliases.isEmpty()) + if (columnAliases.isEmpty()) + { + if (useCompactStorage) + { + // There should remain some column definition since it is a non-composite "static" CF + if (stmt.columns.isEmpty()) + throw new InvalidRequestException("No definition found that is not part of the PRIMARY KEY"); + + stmt.comparator = CFDefinition.definitionType; + } + else + { + List<AbstractType<?>> types = new ArrayList<AbstractType<?>>(definedCollections == null ? 1 : 2); + types.add(CFDefinition.definitionType); + if (definedCollections != null) + types.add(ColumnToCollectionType.getInstance(definedCollections)); + stmt.comparator = CompositeType.getInstance(types); + } + } + else { // If we use compact storage and have only one alias, it is a // standard "dynamic" CF, otherwise it's a composite @@ -240,42 +260,40 @@ public class CreateColumnFamilyStatement extends SchemaAlteringStatement stmt.comparator = CompositeType.getInstance(types); } } - else + + if (useCompactStorage && stmt.columns.size() <= 1) { - if (useCompactStorage) + if (stmt.columns.isEmpty()) { - if (definedCollections != null) - throw new InvalidRequestException("Collection types are not supported with COMPACT STORAGE"); - stmt.comparator = CFDefinition.definitionType; + if (columnAliases.isEmpty()) + throw new InvalidRequestException(String.format("COMPACT STORAGE with non-composite PRIMARY KEY require one column not part of the PRIMARY KEY (got: %s)", StringUtils.join(stmt.columns.keySet(), ", "))); + + // The only value we'll insert will be the empty one, so the default validator don't matter + stmt.defaultValidator = CFDefinition.definitionType; + // We need to distinguish between + // * I'm upgrading from thrift so the valueAlias is null + // * I've define my table with only a PK (and the column value will be empty) + // So, we use an empty valueAlias (rather than null) for the second case + stmt.valueAlias = ByteBufferUtil.EMPTY_BYTE_BUFFER; } else { - List<AbstractType<?>> types = new ArrayList<AbstractType<?>>(definedCollections == null ? 1 : 2); - types.add(CFDefinition.definitionType); - if (definedCollections != null) - types.add(ColumnToCollectionType.getInstance(definedCollections)); - stmt.comparator = CompositeType.getInstance(types); + Map.Entry<ColumnIdentifier, AbstractType> lastEntry = stmt.columns.entrySet().iterator().next(); + stmt.defaultValidator = lastEntry.getValue(); + stmt.valueAlias = lastEntry.getKey().key; + stmt.columns.remove(lastEntry.getKey()); } } - - if (stmt.columns.isEmpty()) - throw new InvalidRequestException("No definition found that is not part of the PRIMARY KEY"); - - if (useCompactStorage && stmt.columns.size() == 1) - { - Map.Entry<ColumnIdentifier, AbstractType> lastEntry = stmt.columns.entrySet().iterator().next(); - stmt.defaultValidator = lastEntry.getValue(); - stmt.valueAlias = lastEntry.getKey().key; - stmt.columns.remove(lastEntry.getKey()); - } else { if (useCompactStorage && !columnAliases.isEmpty()) - throw new InvalidRequestException(String.format("COMPACT STORAGE with composite PRIMARY KEY allows only one column not part of the PRIMARY KEY (got: %s)", StringUtils.join(stmt.columns.keySet(), ", "))); + throw new InvalidRequestException(String.format("COMPACT STORAGE with composite PRIMARY KEY allows no more than one column not part of the PRIMARY KEY (got: %s)", StringUtils.join(stmt.columns.keySet(), ", "))); // There is no way to insert/access a column that is not defined for non-compact storage, so // the actual validator don't matter much (except that we want to recognize counter CF as limitation apply to them). - stmt.defaultValidator = (stmt.columns.values().iterator().next() instanceof CounterColumnType) ? CounterColumnType.instance : CFDefinition.definitionType; + stmt.defaultValidator = !stmt.columns.isEmpty() && (stmt.columns.values().iterator().next() instanceof CounterColumnType) + ? CounterColumnType.instance + : CFDefinition.definitionType; } return new ParsedStatement.Prepared(stmt); http://git-wip-us.apache.org/repos/asf/cassandra/blob/79517461/src/java/org/apache/cassandra/cql3/statements/SelectStatement.java ---------------------------------------------------------------------- diff --git a/src/java/org/apache/cassandra/cql3/statements/SelectStatement.java b/src/java/org/apache/cassandra/cql3/statements/SelectStatement.java index 97ff931..d370c28 100644 --- a/src/java/org/apache/cassandra/cql3/statements/SelectStatement.java +++ b/src/java/org/apache/cassandra/cql3/statements/SelectStatement.java @@ -270,10 +270,11 @@ public class SelectStatement implements CQLStatement { if (isColumnRange()) { - // For sparse, we used to ask for 'defined columns' * 'asked limit' to account for the grouping of columns. - // Since that doesn't work for maps/sets/lists, we use the compositesToGroup option of SliceQueryFilter. - // But we must preserve backward compatibility too. - int multiplier = cfDef.isCompact ? 1 : cfDef.metadata.size(); + // For sparse, we used to ask for 'defined columns' * 'asked limit' (where defined columns includes the row marker) + // to account for the grouping of columns. + // Since that doesn't work for maps/sets/lists, we now use the compositesToGroup option of SliceQueryFilter. + // But we must preserve backward compatibility too (for mixed version cluster that is). + int multiplier = cfDef.isCompact ? 1 : (cfDef.metadata.size() + 1); int toGroup = cfDef.isCompact ? -1 : cfDef.columns.size(); ColumnSlice slice = new ColumnSlice(getRequestedBound(isReversed ? Bound.END : Bound.START, variables), getRequestedBound(isReversed ? Bound.START : Bound.END, variables)); @@ -395,6 +396,7 @@ public class SelectStatement implements CQLStatement assert r != null && r.isEquality(); if (r.eqValues.size() > 1) { + assert cfDef.isCompact; // We have a IN. We only support this for the last column, so just create all columns and return. SortedSet<ByteBuffer> columns = new TreeSet<ByteBuffer>(cfDef.cfm.comparator); Iterator<Term> iter = r.eqValues.iterator(); @@ -423,19 +425,31 @@ public class SelectStatement implements CQLStatement // non-know set of columns, so we shouldn't get there assert !cfDef.hasCollections; - // Adds all columns (even if the user selected a few columns, we - // need to query all columns to know if the row exists or not). - // Note that when we allow IS NOT NULL in queries and if all - // selected name are request 'not null', we will allow to only - // query those. SortedSet<ByteBuffer> columns = new TreeSet<ByteBuffer>(cfDef.cfm.comparator); - Iterator<ColumnIdentifier> iter = cfDef.metadata.keySet().iterator(); - while (iter.hasNext()) + + // We need to query the selected column as well as the marker + // column (for the case where the row exists but has no columns outside the PK) + // One exception is "static CF" (non-composite non-compact CF) that + // don't have marker and for which we must query all columns instead + if (cfDef.isComposite) + { + // marker + columns.add(builder.copy().add(ByteBufferUtil.EMPTY_BYTE_BUFFER).build()); + + // selected columns + for (Pair<CFDefinition.Name, Selector> p : getExpandedSelection()) + columns.add(builder.copy().add(p.right.id().key).build()); + } + else { - ColumnIdentifier name = iter.next(); - ColumnNameBuilder b = iter.hasNext() ? builder.copy() : builder; - ByteBuffer cname = b.add(name.key).build(); - columns.add(cname); + Iterator<ColumnIdentifier> iter = cfDef.metadata.keySet().iterator(); + while (iter.hasNext()) + { + ColumnIdentifier name = iter.next(); + ColumnNameBuilder b = iter.hasNext() ? builder.copy() : builder; + ByteBuffer cname = b.add(name.key).build(); + columns.add(cname); + } } return columns; } @@ -948,10 +962,11 @@ public class SelectStatement implements CQLStatement if (!cfDef.isComposite && (!restriction.isInclusive(Bound.START) || !restriction.isInclusive(Bound.END))) stmt.sliceRestriction = restriction; } - // We only support IN for the last name so far - else if (restriction.eqValues.size() > 1 && i != stmt.columnRestrictions.length - 1) + // We only support IN for the last name and for compact storage so far + // TODO: #3885 allows us to extend to non compact as well, but that remains to be done + else if (cfDef.isCompact && restriction.eqValues.size() > 1 && i != stmt.columnRestrictions.length - 1) { - throw new InvalidRequestException(String.format("PRIMARY KEY part %s cannot be restricted by IN relation (only the first and last parts can)", cname)); + throw new InvalidRequestException(String.format("PRIMARY KEY part %s cannot be restricted by IN relation", cname)); } previous = cname; http://git-wip-us.apache.org/repos/asf/cassandra/blob/79517461/src/java/org/apache/cassandra/cql3/statements/UpdateStatement.java ---------------------------------------------------------------------- diff --git a/src/java/org/apache/cassandra/cql3/statements/UpdateStatement.java b/src/java/org/apache/cassandra/cql3/statements/UpdateStatement.java index f84875a..908c321 100644 --- a/src/java/org/apache/cassandra/cql3/statements/UpdateStatement.java +++ b/src/java/org/apache/cassandra/cql3/statements/UpdateStatement.java @@ -25,12 +25,14 @@ import com.google.common.collect.ArrayListMultimap; import org.apache.cassandra.cql3.*; import org.apache.cassandra.config.CFMetaData; +import org.apache.cassandra.cql3.operations.ColumnOperation; import org.apache.cassandra.cql3.operations.Operation; import org.apache.cassandra.db.*; import org.apache.cassandra.db.marshal.*; import org.apache.cassandra.service.ClientState; import org.apache.cassandra.thrift.InvalidRequestException; import org.apache.cassandra.thrift.UnavailableException; +import org.apache.cassandra.utils.ByteBufferUtil; import org.apache.cassandra.utils.Pair; import static org.apache.cassandra.cql.QueryProcessor.validateKey; @@ -182,16 +184,41 @@ public class UpdateStatement extends ModificationStatement RowMutation rm = new RowMutation(cfDef.cfm.ksName, key); ColumnFamily cf = rm.addOrGet(cfDef.cfm.cfName); + // Inserting the CQL row marker (see #4361) + // We always need to insert a marker, because of the following situation: + // CREATE TABLE t ( k int PRIMARY KEY, c text ); + // INSERT INTO t(k, c) VALUES (1, 1) + // DELETE c FROM t WHERE k = 1; + // SELECT * FROM t; + // The last query should return one row (but with c == null). Adding + // the marker with the insert make sure the semantic is correct (while making sure a + // 'DELETE FROM t WHERE k = 1' does remove the row entirely) + if (cfDef.isComposite && !cfDef.isCompact) + { + ByteBuffer name = builder.copy().add(ByteBufferUtil.EMPTY_BYTE_BUFFER).build(); + cf.addColumn(params.makeColumn(name, ByteBufferUtil.EMPTY_BYTE_BUFFER)); + } + if (cfDef.isCompact) { if (builder.componentCount() == 0) throw new InvalidRequestException(String.format("Missing PRIMARY KEY part %s", cfDef.columns.values().iterator().next())); - List<Operation> operation = processedColumns.get(cfDef.value); - if (operation.isEmpty()) - throw new InvalidRequestException(String.format("Missing mandatory column %s", cfDef.value)); - assert operation.size() == 1; - hasCounterColumn = addToMutation(cf, builder, cfDef.value, operation.get(0), params, null); + Operation operation; + if (cfDef.value == null) + { + // No value was defined, we set to the empty value + operation = ColumnOperation.SetToEmpty(); + } + else + { + List<Operation> operations = processedColumns.get(cfDef.value); + if (operations.isEmpty()) + throw new InvalidRequestException(String.format("Missing mandatory column %s", cfDef.value)); + assert operations.size() == 1; + operation = operations.get(0); + } + hasCounterColumn = addToMutation(cf, builder, cfDef.value, operation, params, null); } else { @@ -212,15 +239,15 @@ public class UpdateStatement extends ModificationStatement UpdateParameters params, List<Pair<ByteBuffer, IColumn>> list) throws InvalidRequestException { - Operation.Type type = valueOperation.getType(); if (type == Operation.Type.COLUMN || type == Operation.Type.COUNTER) { - if (valueDef.type.isCollection()) + if (valueDef != null && valueDef.type.isCollection()) throw new InvalidRequestException("Can't apply operation on column with " + valueDef.type + " type."); - valueOperation.execute(cf, builder.copy(), valueDef.type, params); + AbstractType<?> validator = valueDef == null ? null : valueDef.type; + valueOperation.execute(cf, builder.copy(), validator, params); } else {