From bb66561142788270ab450c02de836b3952ed37b4 Mon Sep 17 00:00:00 2001 From: Stefan Miklosovic Date: Sun, 23 Mar 2025 23:37:11 +0100 Subject: [PATCH] Various fixes in constraint framework - fix some edge cases for NOT_NULL - ability to specify constraints when altering a column - ensure constraint is specified on a column it is bound to - fix nullity check on map type - fix satistfiability check on function constraints patch by Stefan Miklosovic; reviewed by Bernardo Botella for CASSANDRA-20481 --- CHANGES.txt | 1 + src/antlr/Parser.g | 10 +- ...AbstractFunctionSatisfiabilityChecker.java | 27 ++- .../cql3/constraints/ColumnConstraint.java | 5 +- .../cql3/constraints/ColumnConstraints.java | 18 +- .../cql3/constraints/ConstraintFunction.java | 10 +- .../cql3/constraints/JsonConstraint.java | 9 +- .../cql3/constraints/NotNullConstraint.java | 9 +- .../constraints/UnaryConstraintFunction.java | 37 ++++ .../cql3/statements/UpdateStatement.java | 4 +- .../schema/AlterTableStatement.java | 19 +- .../schema/CreateTableStatement.java | 2 +- .../cassandra/db/marshal/AbstractType.java | 14 +- .../cassandra/db/marshal/CollectionType.java | 6 + .../apache/cassandra/db/marshal/MapType.java | 6 - .../cassandra/schema/TableMetadata.java | 4 +- ...ableWithTableConstraintValidationTest.java | 42 +++- .../ConstraintsSatisfiabilityTest.java | 17 +- ...WithColumnCqlConstraintValidationTest.java | 18 ++ .../constraints/JsonConstraintTest.java | 2 +- .../constraints/NotNullConstraintTest.java | 165 ++++++++++++++ .../cql3/ColumnSpecificationTest.java | 206 ++++++++++++++++++ .../db/marshal/AbstractTypeTest.java | 9 +- 23 files changed, 568 insertions(+), 72 deletions(-) create mode 100644 src/java/org/apache/cassandra/cql3/constraints/UnaryConstraintFunction.java create mode 100644 test/unit/org/apache/cassandra/constraints/NotNullConstraintTest.java create mode 100644 test/unit/org/apache/cassandra/cql3/ColumnSpecificationTest.java diff --git a/CHANGES.txt b/CHANGES.txt index 0e155821c0..c1ee79f162 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,4 +1,5 @@ 5.1 + * Various fixes in constraint framework (CASSANDRA-20481) * Add support in CAS for -= on numeric types, and fixed improper handling of empty bytes which lead to NPE (CASSANDRA-20477) * Do not fail to start a node with materialized views after they are turned off in config (CASSANDRA-20452) * Fix nodetool gcstats output, support human-readable units and more output formats (CASSANDRA-19022) diff --git a/src/antlr/Parser.g b/src/antlr/Parser.g index dd9eb181df..c91dfe60d9 100644 --- a/src/antlr/Parser.g +++ b/src/antlr/Parser.g @@ -988,14 +988,14 @@ alterTableStatement returns [AlterTableStatement.Raw stmt] | K_ALTER ( K_IF K_EXISTS { $stmt.ifColumnExists(true); } )? id=cident ( mask=columnMask { $stmt.mask(id, mask); } + | constraints=columnConstraints { $stmt.constraint(id, constraints); } | K_DROP K_MASKED { $stmt.mask(id, null); } - | K_DROP K_CHECK { $stmt.constraint(id, null); } - | (constraints=columnConstraints) { $stmt.constraint(id, constraints); }) + | K_DROP K_CHECK { $stmt.constraint(id, null); }) | K_ADD ( K_IF K_NOT K_EXISTS { $stmt.ifColumnNotExists(true); } )? - ( id=ident v=comparatorType b=isStaticColumn (m=columnMask)? { $stmt.add(id, v, b, m); } - | ('(' id1=ident v1=comparatorType b1=isStaticColumn (m1=columnMask)? { $stmt.add(id1, v1, b1, m1); } - ( ',' idn=ident vn=comparatorType bn=isStaticColumn (mn=columnMask)? { $stmt.add(idn, vn, bn, mn); mn=null; } )* ')') ) + ( id=ident v=comparatorType b=isStaticColumn (m=columnMask)? (c=columnConstraints)? { $stmt.add(id, v, b, m, c); } + | ('(' id1=ident v1=comparatorType b1=isStaticColumn (m1=columnMask)? (c=columnConstraints)? { $stmt.add(id1, v1, b1, m1, c); } + ( ',' idn=ident vn=comparatorType bn=isStaticColumn (mn=columnMask)? (c=columnConstraints)? { $stmt.add(idn, vn, bn, mn, c); mn=null; c=null;} )* ')') ) | K_DROP ( K_IF K_EXISTS { $stmt.ifColumnExists(true); } )? ( id=ident { $stmt.drop(id); } diff --git a/src/java/org/apache/cassandra/cql3/constraints/AbstractFunctionSatisfiabilityChecker.java b/src/java/org/apache/cassandra/cql3/constraints/AbstractFunctionSatisfiabilityChecker.java index 91d448da01..cebc5c36d3 100644 --- a/src/java/org/apache/cassandra/cql3/constraints/AbstractFunctionSatisfiabilityChecker.java +++ b/src/java/org/apache/cassandra/cql3/constraints/AbstractFunctionSatisfiabilityChecker.java @@ -26,6 +26,8 @@ import java.util.TreeSet; import org.apache.cassandra.cql3.Operator; import org.apache.cassandra.cql3.functions.types.ParseUtils; +import org.apache.cassandra.db.marshal.AbstractType; +import org.apache.cassandra.db.marshal.Int32Type; import org.apache.cassandra.schema.ColumnMetadata; import org.apache.cassandra.utils.Pair; @@ -70,6 +72,8 @@ public abstract class AbstractFunctionSatisfiabilityChecker, List> categorizeConstraints(List> constraints, String functionName); + abstract AbstractType returnType(ColumnMetadata columnMetadata); + private void checkSupportedOperators(List allConstraints, String functionName) { for (CONSTRAINT_TYPE constraint : allConstraints) @@ -147,11 +151,12 @@ public abstract class AbstractFunctionSatisfiabilityChecker returnType = returnType(columnMetadata); + ByteBuffer firstTermBuffer = returnType.fromString(ParseUtils.unquote(firstTerm)); + ByteBuffer secondTermBuffer = returnType.fromString(ParseUtils.unquote(secondTerm)); - boolean firstSatisfaction = firstRelation.isSatisfiedBy(columnMetadata.type, secondTermBuffer, firstTermBuffer); - boolean secondSatisfaction = secondRelation.isSatisfiedBy(columnMetadata.type, firstTermBuffer, secondTermBuffer); + boolean firstSatisfaction = firstRelation.isSatisfiedBy(returnType, secondTermBuffer, firstTermBuffer); + boolean secondSatisfaction = secondRelation.isSatisfiedBy(returnType, firstTermBuffer, secondTermBuffer); if (!firstSatisfaction || !secondSatisfaction) throw new InvalidConstraintDefinitionException(format("Constraints of %s are not satisfiable: %s %s %s, %s %s %s", @@ -186,6 +191,14 @@ public abstract class AbstractFunctionSatisfiabilityChecker returnType(ColumnMetadata metadata) + { + // function constraints will always have terms of int32 type + // unlike scalar constraints where it will be a type of column + return metadata.type; + } }; public static final AbstractFunctionSatisfiabilityChecker FUNCTION_SATISFIABILITY_CHECKER = new AbstractFunctionSatisfiabilityChecker<>() @@ -215,5 +228,11 @@ public abstract class AbstractFunctionSatisfiabilityChecker returnType(ColumnMetadata columnMetadata) + { + return Int32Type.instance; + } }; } diff --git a/src/java/org/apache/cassandra/cql3/constraints/ColumnConstraint.java b/src/java/org/apache/cassandra/cql3/constraints/ColumnConstraint.java index ddcca653ea..eecc0b8ecc 100644 --- a/src/java/org/apache/cassandra/cql3/constraints/ColumnConstraint.java +++ b/src/java/org/apache/cassandra/cql3/constraints/ColumnConstraint.java @@ -30,6 +30,7 @@ import org.apache.cassandra.cql3.constraints.ScalarColumnConstraint.ScalarColumn import org.apache.cassandra.db.marshal.AbstractType; import org.apache.cassandra.schema.ColumnMetadata; import org.apache.cassandra.tcm.serialization.MetadataSerializer; +import org.apache.cassandra.utils.ByteBufferUtil; import static java.lang.String.format; @@ -116,8 +117,10 @@ public abstract class ColumnConstraint */ public void evaluate(AbstractType valueType, ByteBuffer columnValue) throws ConstraintViolationException { - if (columnValue.capacity() == 0) + if (columnValue == ByteBufferUtil.EMPTY_BYTE_BUFFER) throw new ConstraintViolationException("Column value does not satisfy value constraint for column '" + columnName + "' as it is null."); + else if (valueType.isEmptyValueMeaningless() && columnValue.capacity() == 0) + throw new ConstraintViolationException("Column value does not satisfy value constraint for column '" + columnName + "' as it is empty."); internalEvaluate(valueType, columnValue); } diff --git a/src/java/org/apache/cassandra/cql3/constraints/ColumnConstraints.java b/src/java/org/apache/cassandra/cql3/constraints/ColumnConstraints.java index 0acace098f..21b1195228 100644 --- a/src/java/org/apache/cassandra/cql3/constraints/ColumnConstraints.java +++ b/src/java/org/apache/cassandra/cql3/constraints/ColumnConstraints.java @@ -27,6 +27,7 @@ import java.util.Objects; import java.util.Set; import java.util.TreeSet; +import org.apache.cassandra.cql3.ColumnIdentifier; import org.apache.cassandra.cql3.CqlBuilder; import org.apache.cassandra.db.TypeSizes; import org.apache.cassandra.db.marshal.AbstractType; @@ -108,7 +109,7 @@ public class ColumnConstraints extends ColumnConstraint // Checks if there is at least one constraint that will perform checks public boolean hasRelevantConstraints() { - for (ColumnConstraint c : constraints) + for (ColumnConstraint c : constraints) { if (c != ColumnConstraints.NO_OP) return true; @@ -120,9 +121,12 @@ public class ColumnConstraints extends ColumnConstraint public void validate(ColumnMetadata columnMetadata) throws InvalidConstraintDefinitionException { if (!columnMetadata.type.isConstrainable()) + { throw new InvalidConstraintDefinitionException("Constraint cannot be defined on the column " + columnMetadata.name + " of type " + columnMetadata.type.asCQL3Type() - + " for the table " + columnMetadata.ksName + "." + columnMetadata.cfName); + + " for the table " + columnMetadata.ksName + '.' + columnMetadata.cfName + '.' + + (columnMetadata.type.isCollection() ? " When using collections, constraints can be used only of frozen collections." : "")); + } // this will look at constraints as a whole, // checking if combinations of a particular constraint make sense (duplicities, satisfiability etc.). @@ -207,10 +211,18 @@ public class ColumnConstraints extends ColumnConstraint this.constraints = Collections.emptyList(); } - public ColumnConstraints prepare() + public ColumnConstraints prepare(ColumnIdentifier column) { if (constraints.isEmpty()) return NO_OP; + + for (ColumnConstraint constraint : constraints) + { + if (constraint.columnName != null && !column.equals(constraint.columnName)) + throw new InvalidConstraintDefinitionException(format("Constraint %s was not specified on a column it operates on: %s but on: %s", + constraint, column.toCQLString(), constraint.columnName)); + } + return new ColumnConstraints(constraints); } } diff --git a/src/java/org/apache/cassandra/cql3/constraints/ConstraintFunction.java b/src/java/org/apache/cassandra/cql3/constraints/ConstraintFunction.java index 9952ab32d9..ad8424c3f5 100644 --- a/src/java/org/apache/cassandra/cql3/constraints/ConstraintFunction.java +++ b/src/java/org/apache/cassandra/cql3/constraints/ConstraintFunction.java @@ -25,6 +25,7 @@ import org.apache.cassandra.cql3.ColumnIdentifier; import org.apache.cassandra.cql3.Operator; import org.apache.cassandra.db.marshal.AbstractType; import org.apache.cassandra.schema.ColumnMetadata; +import org.apache.cassandra.utils.ByteBufferUtil; import static org.apache.cassandra.cql3.Operator.EQ; import static org.apache.cassandra.cql3.Operator.GT; @@ -55,8 +56,10 @@ public abstract class ConstraintFunction */ public void evaluate(AbstractType valueType, Operator relationType, String term, ByteBuffer columnValue) throws ConstraintViolationException { - if (columnValue.capacity() == 0) + if (columnValue == ByteBufferUtil.EMPTY_BYTE_BUFFER) throw new ConstraintViolationException("Column value does not satisfy value constraint for column '" + columnName + "' as it is null."); + else if (valueType.isEmptyValueMeaningless() && columnValue.capacity() == 0) + throw new ConstraintViolationException("Column value does not satisfy value constraint for column '" + columnName + "' as it is empty."); internalEvaluate(valueType, relationType, term, columnValue); } @@ -88,10 +91,7 @@ public abstract class ConstraintFunction * * @return list of operators this function is allowed to have. */ - public List getSupportedOperators() - { - return List.of(); - } + public abstract List getSupportedOperators(); /** * Tells what types of columns are supported by this constraint. diff --git a/src/java/org/apache/cassandra/cql3/constraints/JsonConstraint.java b/src/java/org/apache/cassandra/cql3/constraints/JsonConstraint.java index 99aeb6734e..95fbac5b3c 100644 --- a/src/java/org/apache/cassandra/cql3/constraints/JsonConstraint.java +++ b/src/java/org/apache/cassandra/cql3/constraints/JsonConstraint.java @@ -31,7 +31,7 @@ import org.apache.cassandra.utils.JsonUtils; import static java.lang.String.format; -public class JsonConstraint extends ConstraintFunction +public class JsonConstraint extends UnaryConstraintFunction { private static final List> SUPPORTED_TYPES = List.of(UTF8Type.instance, AsciiType.instance); @@ -39,12 +39,7 @@ public class JsonConstraint extends ConstraintFunction public JsonConstraint(ColumnIdentifier columnName) { - this(columnName, FUNCTION_NAME); - } - - public JsonConstraint(ColumnIdentifier columnName, String name) - { - super(columnName, name); + super(columnName, FUNCTION_NAME); } @Override diff --git a/src/java/org/apache/cassandra/cql3/constraints/NotNullConstraint.java b/src/java/org/apache/cassandra/cql3/constraints/NotNullConstraint.java index fb9f7de95b..af79086701 100644 --- a/src/java/org/apache/cassandra/cql3/constraints/NotNullConstraint.java +++ b/src/java/org/apache/cassandra/cql3/constraints/NotNullConstraint.java @@ -28,18 +28,13 @@ import org.apache.cassandra.schema.ColumnMetadata; import static java.lang.String.format; -public class NotNullConstraint extends ConstraintFunction +public class NotNullConstraint extends UnaryConstraintFunction { public static final String FUNCTION_NAME = "NOT_NULL"; public NotNullConstraint(ColumnIdentifier columnName) { - this(columnName, FUNCTION_NAME); - } - - public NotNullConstraint(ColumnIdentifier columnName, String name) - { - super(columnName, name); + super(columnName, FUNCTION_NAME); } @Override diff --git a/src/java/org/apache/cassandra/cql3/constraints/UnaryConstraintFunction.java b/src/java/org/apache/cassandra/cql3/constraints/UnaryConstraintFunction.java new file mode 100644 index 0000000000..8696e81a65 --- /dev/null +++ b/src/java/org/apache/cassandra/cql3/constraints/UnaryConstraintFunction.java @@ -0,0 +1,37 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 org.apache.cassandra.cql3.constraints; + +import java.util.List; + +import org.apache.cassandra.cql3.ColumnIdentifier; +import org.apache.cassandra.cql3.Operator; + +public abstract class UnaryConstraintFunction extends ConstraintFunction +{ + public UnaryConstraintFunction(ColumnIdentifier columnName, String name) + { + super(columnName, name); + } + + public List getSupportedOperators() + { + return List.of(); + } +} diff --git a/src/java/org/apache/cassandra/cql3/statements/UpdateStatement.java b/src/java/org/apache/cassandra/cql3/statements/UpdateStatement.java index d8310cdd2f..f6ecda2b87 100644 --- a/src/java/org/apache/cassandra/cql3/statements/UpdateStatement.java +++ b/src/java/org/apache/cassandra/cql3/statements/UpdateStatement.java @@ -371,9 +371,7 @@ public class UpdateStatement extends ModificationStatement public static void evaluateConstraint(ColumnMetadata columnMetadata, ByteBuffer cellData) { - for (ColumnConstraint constraint : columnMetadata.getColumnConstraints().getConstraints()) - { + for (ColumnConstraint constraint : columnMetadata.getColumnConstraints().getConstraints()) constraint.evaluate(columnMetadata.type, cellData); - } } } diff --git a/src/java/org/apache/cassandra/cql3/statements/schema/AlterTableStatement.java b/src/java/org/apache/cassandra/cql3/statements/schema/AlterTableStatement.java index fc2ab582f7..94120ac63c 100644 --- a/src/java/org/apache/cassandra/cql3/statements/schema/AlterTableStatement.java +++ b/src/java/org/apache/cassandra/cql3/statements/schema/AlterTableStatement.java @@ -257,13 +257,16 @@ public abstract class AlterTableStatement extends AlterSchemaStatement private final boolean isStatic; @Nullable private final ColumnMask.Raw mask; + @Nullable + private final ColumnConstraints.Raw constraints; - Column(ColumnIdentifier name, CQL3Type.Raw type, boolean isStatic, @Nullable ColumnMask.Raw mask) + Column(ColumnIdentifier name, CQL3Type.Raw type, boolean isStatic, @Nullable ColumnMask.Raw mask, @Nullable ColumnConstraints.Raw constraints) { this.name = name; this.type = type; this.isStatic = isStatic; this.mask = mask; + this.constraints = constraints; } } @@ -311,6 +314,7 @@ public abstract class AlterTableStatement extends AlterSchemaStatement AbstractType type = column.type.prepare(keyspaceName, keyspace.types).getType(); boolean isStatic = column.isStatic; ColumnMask mask = column.mask == null ? null : column.mask.prepare(keyspaceName, tableName, name, type, keyspace.userFunctions); + ColumnConstraints columnConstraints = column.constraints == null ? ColumnConstraints.NO_OP : column.constraints.prepare(name); if (null != tableBuilder.getColumn(name)) { if (!ifColumnNotExists) @@ -361,9 +365,9 @@ public abstract class AlterTableStatement extends AlterSchemaStatement } if (isStatic) - tableBuilder.addStaticColumn(name, type, mask); + tableBuilder.addStaticColumn(name, type, mask, columnConstraints); else - tableBuilder.addRegularColumn(name, type, mask); + tableBuilder.addRegularColumn(name, type, mask, columnConstraints); if (!isStatic) { @@ -372,7 +376,8 @@ public abstract class AlterTableStatement extends AlterSchemaStatement if (view.includeAllColumns) { ColumnMetadata viewColumn = ColumnMetadata.regularColumn(view.metadata, name.bytes, type) - .withNewMask(mask); + .withNewMask(mask) + .withNewColumnConstraints(columnConstraints); viewsBuilder.put(viewsBuilder.get(view.name()).withAddedRegularColumn(viewColumn)); } } @@ -732,7 +737,7 @@ public abstract class AlterTableStatement extends AlterSchemaStatement if (column != null) { ColumnConstraints oldConstraints = column.getColumnConstraints(); - ColumnConstraints newConstraints = constraints == null ? ColumnConstraints.NO_OP : constraints.prepare(); + ColumnConstraints newConstraints = constraints == null ? ColumnConstraints.NO_OP : constraints.prepare(columnName); if (Objects.equals(oldConstraints, newConstraints)) return keyspace; newConstraints.validate(column); @@ -837,10 +842,10 @@ public abstract class AlterTableStatement extends AlterSchemaStatement rawMask = mask; } - public void add(ColumnIdentifier name, CQL3Type.Raw type, boolean isStatic, @Nullable ColumnMask.Raw mask) + public void add(ColumnIdentifier name, CQL3Type.Raw type, boolean isStatic, @Nullable ColumnMask.Raw mask, @Nullable ColumnConstraints.Raw constraints) { kind = Kind.ADD_COLUMNS; - addedColumns.add(new AddColumns.Column(name, type, isStatic, mask)); + addedColumns.add(new AddColumns.Column(name, type, isStatic, mask, constraints)); } public void drop(ColumnIdentifier name) diff --git a/src/java/org/apache/cassandra/cql3/statements/schema/CreateTableStatement.java b/src/java/org/apache/cassandra/cql3/statements/schema/CreateTableStatement.java index 997a402000..b8e51d1286 100644 --- a/src/java/org/apache/cassandra/cql3/statements/schema/CreateTableStatement.java +++ b/src/java/org/apache/cassandra/cql3/statements/schema/CreateTableStatement.java @@ -578,7 +578,7 @@ public final class CreateTableStatement extends AlterSchemaStatement if (null == constraints) columnConstraints.put(column, ColumnConstraints.NO_OP); else - columnConstraints.put(column, constraints.prepare()); + columnConstraints.put(column, constraints.prepare(column)); } public void setCompactStorage() diff --git a/src/java/org/apache/cassandra/db/marshal/AbstractType.java b/src/java/org/apache/cassandra/db/marshal/AbstractType.java index 5378a4cd3f..b5156c4fdb 100644 --- a/src/java/org/apache/cassandra/db/marshal/AbstractType.java +++ b/src/java/org/apache/cassandra/db/marshal/AbstractType.java @@ -211,20 +211,12 @@ public abstract class AbstractType implements Comparator, Assignm public void checkConstraints(ByteBuffer bytes, ColumnConstraints constraints) throws ConstraintViolationException { - if (constraints.isEmpty()) - return; - - T value = getSerializer().deserialize(bytes); - constraints.evaluate(this, bytes); + checkConstraints(bytes, constraints.getConstraints()); } - public void checkConstraints(ByteBuffer bytes, List constraints) throws ConstraintViolationException + public void checkConstraints(ByteBuffer bytes, List> constraints) throws ConstraintViolationException { - if (constraints.isEmpty()) - return; - - T value = getSerializer().deserialize(bytes); - for (ColumnConstraint constraint : constraints) + for (ColumnConstraint constraint : constraints) constraint.evaluate(this, bytes); } diff --git a/src/java/org/apache/cassandra/db/marshal/CollectionType.java b/src/java/org/apache/cassandra/db/marshal/CollectionType.java index 8c39dbab4f..3952ee137e 100644 --- a/src/java/org/apache/cassandra/db/marshal/CollectionType.java +++ b/src/java/org/apache/cassandra/db/marshal/CollectionType.java @@ -171,6 +171,12 @@ public abstract class CollectionType extends MultiElementType return true; } + @Override + public boolean isConstrainable() + { + return isFrozenCollection(); + } + public ByteBuffer serializeForNativeProtocol(Iterator> cells) { assert isMultiCell(); diff --git a/src/java/org/apache/cassandra/db/marshal/MapType.java b/src/java/org/apache/cassandra/db/marshal/MapType.java index f8ac6c0068..69ea6d17e1 100644 --- a/src/java/org/apache/cassandra/db/marshal/MapType.java +++ b/src/java/org/apache/cassandra/db/marshal/MapType.java @@ -456,10 +456,4 @@ public class MapType extends CollectionType> return getSerializer().getSerializedValue(((Cell) columnData).buffer(), keyOrIndex, getValuesType()); } - - @Override - public boolean isConstrainable() - { - return false; - } } diff --git a/src/java/org/apache/cassandra/schema/TableMetadata.java b/src/java/org/apache/cassandra/schema/TableMetadata.java index 9fdf5e8215..268111abd6 100644 --- a/src/java/org/apache/cassandra/schema/TableMetadata.java +++ b/src/java/org/apache/cassandra/schema/TableMetadata.java @@ -204,7 +204,7 @@ public class TableMetadata implements SchemaElement // We cache the columns with constraints to avoid iterations over columns // Partition keys columns are evaluated separately, so we keep the two of them in // two different variables. - public final List partitionKeyConstraints; + public final List> partitionKeyConstraints; public final List columnsWithConstraints; public final List notNullColumns; @@ -248,7 +248,7 @@ public class TableMetadata implements SchemaElement else ref = TableMetadataRef.withInitialReference(new TableMetadataRef(Schema.instance, keyspace, name, id), this); - List pkConstraints = new ArrayList<>(this.partitionKeyColumns.size()); + List> pkConstraints = new ArrayList<>(this.partitionKeyColumns.size()); for (ColumnMetadata column : this.partitionKeyColumns) { if (column.hasConstraint()) diff --git a/test/unit/org/apache/cassandra/constraints/AlterTableWithTableConstraintValidationTest.java b/test/unit/org/apache/cassandra/constraints/AlterTableWithTableConstraintValidationTest.java index d5f3f4a32c..91f21f8b09 100644 --- a/test/unit/org/apache/cassandra/constraints/AlterTableWithTableConstraintValidationTest.java +++ b/test/unit/org/apache/cassandra/constraints/AlterTableWithTableConstraintValidationTest.java @@ -22,10 +22,8 @@ import org.junit.Test; import org.apache.cassandra.exceptions.InvalidRequestException; - public class AlterTableWithTableConstraintValidationTest extends CqlConstraintValidationTester { - @Test public void testCreateTableWithColumnNamedConstraintDescribeTableNonFunction() throws Throwable { @@ -239,4 +237,44 @@ public class AlterTableWithTableConstraintValidationTest extends CqlConstraintVa String expectedErrorMessage = "Column 'foo' doesn't exist"; assertInvalidThrowMessage(expectedErrorMessage, InvalidRequestException.class, "ALTER TABLE %s ALTER foo CHECK foo < 100"); } + + @Test + public void testAlterTableAlterExistingColumnWithCheckOnNonExistingColumn() throws Throwable + { + createTable("CREATE TABLE %s (pk int, ck1 text, ck2 text, v int, PRIMARY KEY ((pk),ck1, ck2));"); + assertInvalidThrowMessage("Constraint ck3 < 100 was not specified on a column it operates on: ck1 but on: ck3", + InvalidRequestException.class, + "ALTER TABLE %s ALTER ck1 CHECK ck3 < 100"); + assertInvalidThrowMessage("Constraint NOT_NULL(ck3) was not specified on a column it operates on: ck1 but on: ck3", + InvalidRequestException.class, + "ALTER TABLE %s ALTER ck1 CHECK NOT_NULL(ck3)"); + assertInvalidThrowMessage("Constraint LENGTH(ck3) > 10 was not specified on a column it operates on: ck1 but on: ck3", + InvalidRequestException.class, + "ALTER TABLE %s ALTER ck1 CHECK LENGTH(ck3) > 10"); + } + + @Test + public void testAlterTableAddNewColumnWithCheckOnNonExistingColumn() throws Throwable + { + createTable("CREATE TABLE %s (pk int, ck1 text, ck2 text, v int, PRIMARY KEY ((pk),ck1, ck2));"); + + assertInvalidThrowMessage("Constraint v3 < 100 was not specified on a column it operates on: v2 but on: v3", + InvalidRequestException.class, + "ALTER TABLE %s ADD v2 int CHECK v3 < 100"); + + assertInvalidThrowMessage("Constraint NOT_NULL(v3) was not specified on a column it operates on: v2 but on: v3", + InvalidRequestException.class, + "ALTER TABLE %s ADD v2 int CHECK NOT_NULL(v3)"); + + assertInvalidThrowMessage("Constraint LENGTH(v3) > 10 was not specified on a column it operates on: v2 but on: v3", + InvalidRequestException.class, + "ALTER TABLE %s ADD v2 int CHECK LENGTH(v3) > 10"); + } + + @Test + public void testAlterTableAddColumnWithCheck() + { + createTable("CREATE TABLE %s (pk text, col1 int, primary key (pk));"); + execute("ALTER TABLE %s ADD col2 int CHECK col2 > 0"); + } } diff --git a/test/unit/org/apache/cassandra/constraints/ConstraintsSatisfiabilityTest.java b/test/unit/org/apache/cassandra/constraints/ConstraintsSatisfiabilityTest.java index 6f087c5850..148aaa48a6 100644 --- a/test/unit/org/apache/cassandra/constraints/ConstraintsSatisfiabilityTest.java +++ b/test/unit/org/apache/cassandra/constraints/ConstraintsSatisfiabilityTest.java @@ -71,13 +71,13 @@ public class ConstraintsSatisfiabilityTest if (op1 == NEQ) { // a_column != 0 and a_column != 10 -> valid - check(op1, 0, op2, 100, quadFunction, null, columnMetadata); + check(op1, 50, op2, 100, quadFunction, null, columnMetadata); // does not make sense to check twice // check a_column != 0 and a_column != 0 check(op1, 0, op2, 0, quadFunction, "There are duplicate constraint definitions on column", columnMetadata); } else - check(op1, 0, op2, 100, quadFunction, "There are duplicate constraint definitions on column", columnMetadata); + check(op1, 50, op2, 100, quadFunction, "There are duplicate constraint definitions on column", columnMetadata); } else if ((op1 == GT && op2 == GTE) || (op1 == GTE && op2 == GT) || @@ -85,18 +85,25 @@ public class ConstraintsSatisfiabilityTest (op1 == LTE && op2 == LT) || (op1 == EQ || op2 == EQ)) { - check(op1, 0, op2, 100, quadFunction, "not supported", columnMetadata); + check(op1, 50, op2, 100, quadFunction, "not supported", columnMetadata); } else if ((op1 == LTE && op2 == GT) || (op1 == LT && op2 == GT) || (op1 == LTE && op2 == GTE) || (op1 == LT && op2 == GTE)) { - check(op1, 0, op2, 100, quadFunction, "are not satisfiable", columnMetadata); + check(op1, 50, op2, 100, quadFunction, "are not satisfiable", columnMetadata); + } + else if ((op1 == GT && op2 == LTE) || + (op1 == GT && op2 == LT) || + (op1 == GTE && op2 == LTE) || + (op1 == GTE && op2 == LT)) + { + check(op1, 50, op2, 100, quadFunction, null, columnMetadata); } else if (!(op1 == NEQ || op2 == NEQ)) { - check(op1, 0, op2, 100, quadFunction, null, columnMetadata); + check(op1, 50, op2, 100, quadFunction, null, columnMetadata); } else { diff --git a/test/unit/org/apache/cassandra/constraints/CreateTableWithColumnCqlConstraintValidationTest.java b/test/unit/org/apache/cassandra/constraints/CreateTableWithColumnCqlConstraintValidationTest.java index 6dc1604686..857ec85f40 100644 --- a/test/unit/org/apache/cassandra/constraints/CreateTableWithColumnCqlConstraintValidationTest.java +++ b/test/unit/org/apache/cassandra/constraints/CreateTableWithColumnCqlConstraintValidationTest.java @@ -26,9 +26,11 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.Parameterized; +import org.apache.cassandra.cql3.constraints.InvalidConstraintDefinitionException; import org.apache.cassandra.exceptions.InvalidRequestException; import org.apache.cassandra.utils.Generators; +import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; import static accord.utils.Property.qt; @@ -1438,4 +1440,20 @@ public class CreateTableWithColumnCqlConstraintValidationTest extends CqlConstra } }); } + + @Test + public void testCreateTableAddConstraintWithCheckOnNonExistingColumn() throws Throwable + { + assertThatThrownBy(() -> createTable("CREATE TABLE %s (pk int, ck1 text CHECK NOT_NULL(ck3), ck2 text, v int, PRIMARY KEY ((pk),ck1, ck2));")) + .hasRootCauseMessage("Constraint NOT_NULL(ck3) was not specified on a column it operates on: ck1 but on: ck3") + .rootCause().isInstanceOf(InvalidConstraintDefinitionException.class); + + assertThatThrownBy(() -> createTable("CREATE TABLE %s (pk int, ck1 int CHECK ck3 > 5, ck2 text, v int, PRIMARY KEY ((pk),ck1, ck2));")) + .hasRootCauseMessage("Constraint ck3 > 5 was not specified on a column it operates on: ck1 but on: ck3") + .rootCause().isInstanceOf(InvalidConstraintDefinitionException.class); + + assertThatThrownBy(() -> createTable("CREATE TABLE %s (pk int, ck1 text CHECK LENGTH(ck3) > 10, ck2 text, v int, PRIMARY KEY ((pk),ck1, ck2));")) + .hasRootCauseMessage("Constraint LENGTH(ck3) > 10 was not specified on a column it operates on: ck1 but on: ck3") + .rootCause().isInstanceOf(InvalidConstraintDefinitionException.class); + } } diff --git a/test/unit/org/apache/cassandra/constraints/JsonConstraintTest.java b/test/unit/org/apache/cassandra/constraints/JsonConstraintTest.java index 95db5b7604..adf86093bc 100644 --- a/test/unit/org/apache/cassandra/constraints/JsonConstraintTest.java +++ b/test/unit/org/apache/cassandra/constraints/JsonConstraintTest.java @@ -50,7 +50,7 @@ public class JsonConstraintTest run("{}"); run("{\"a\": 5, \"b\": \"1\", \"c\": [1,2,3]}"); run("nonsense", "Value for column 'a_column' violated JSON constraint as it is not a valid JSON."); - run("", "Column value does not satisfy value constraint for column 'a_column' as it is null."); + run("", "Value for column 'a_column' violated JSON constraint as it is not a valid JSON."); } @Test diff --git a/test/unit/org/apache/cassandra/constraints/NotNullConstraintTest.java b/test/unit/org/apache/cassandra/constraints/NotNullConstraintTest.java new file mode 100644 index 0000000000..61d24d8506 --- /dev/null +++ b/test/unit/org/apache/cassandra/constraints/NotNullConstraintTest.java @@ -0,0 +1,165 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 org.apache.cassandra.constraints; + +import java.nio.ByteBuffer; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.junit.Test; + +import org.apache.cassandra.cql3.ColumnIdentifier; +import org.apache.cassandra.cql3.constraints.ColumnConstraints; +import org.apache.cassandra.cql3.constraints.ConstraintViolationException; +import org.apache.cassandra.cql3.constraints.FunctionColumnConstraint; +import org.apache.cassandra.cql3.constraints.InvalidConstraintDefinitionException; +import org.apache.cassandra.cql3.constraints.NotNullConstraint; +import org.apache.cassandra.cql3.constraints.ScalarColumnConstraint; +import org.apache.cassandra.cql3.constraints.UnaryFunctionColumnConstraint; +import org.apache.cassandra.db.marshal.AbstractType; +import org.apache.cassandra.db.marshal.Int32Type; +import org.apache.cassandra.db.marshal.ListType; +import org.apache.cassandra.db.marshal.MapType; +import org.apache.cassandra.db.marshal.SetType; +import org.apache.cassandra.db.marshal.UTF8Type; +import org.apache.cassandra.db.marshal.UUIDType; +import org.apache.cassandra.schema.ColumnMetadata; + +import static java.util.List.of; +import static org.apache.cassandra.cql3.Operator.GT; +import static org.apache.cassandra.schema.ColumnMetadata.Kind.REGULAR; +import static org.apache.cassandra.utils.ByteBufferUtil.EMPTY_BYTE_BUFFER; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +/** + * TODO - UDTs are not supported yet in constraints as such + */ +public class NotNullConstraintTest +{ + private static final ColumnIdentifier columnIdentifier = new ColumnIdentifier("a_column", false); + private static final ColumnConstraints unaryConstraint = new ColumnConstraints(of(new UnaryFunctionColumnConstraint.Raw(new ColumnIdentifier(NotNullConstraint.FUNCTION_NAME, false), columnIdentifier).prepare())); + private static final ColumnConstraints scalarConstraint = new ColumnConstraints(of(new ScalarColumnConstraint.Raw(columnIdentifier, GT, "5").prepare())); + private static final ColumnConstraints functionConstraint = new ColumnConstraints(of(new FunctionColumnConstraint.Raw(new ColumnIdentifier("LENGTH", false), columnIdentifier, GT, "5").prepare())); + + @Test + public void testNotNullConstraintValidation() + { + // unary + unaryConstraint.validate(getColumnOfType(UTF8Type.instance)); + assertThatThrownBy(() -> unaryConstraint.evaluate(UTF8Type.instance, EMPTY_BYTE_BUFFER)) + .hasMessage("Column value does not satisfy value constraint for column 'a_column' as it is null.") + .isInstanceOf(ConstraintViolationException.class); + + // not null / empty + unaryConstraint.evaluate(UTF8Type.instance, UTF8Type.instance.fromString("a value")); + + // scalar + scalarConstraint.validate(getColumnOfType(Int32Type.instance)); + assertThatThrownBy(() -> scalarConstraint.evaluate(Int32Type.instance, EMPTY_BYTE_BUFFER)) + .hasMessage("Column value does not satisfy value constraint for column 'a_column' as it is null.") + .isInstanceOf(ConstraintViolationException.class); + + // function, e.g. length + functionConstraint.validate(getColumnOfType(UTF8Type.instance)); + assertThatThrownBy(() -> functionConstraint.evaluate(UTF8Type.instance, EMPTY_BYTE_BUFFER)) + .hasMessage("Column value does not satisfy value constraint for column 'a_column' as it is null.") + .isInstanceOf(ConstraintViolationException.class); + + // empty string is not _null_ string so this passes + unaryConstraint.evaluate(UTF8Type.instance, UTF8Type.instance.fromString("")); + + // test a type for which empty value is meaningless + + assertThatThrownBy(() -> unaryConstraint.evaluate(UUIDType.instance, ByteBuffer.allocate(0))) + .hasMessage("Column value does not satisfy value constraint for column 'a_column' as it is empty.") + .isInstanceOf(ConstraintViolationException.class); + } + + @Test + public void testCollections() + { + checkList(false); + checkSet(false); + checkMap(false); + + checkList(true); + checkSet(true); + checkMap(true); + } + + private static ColumnMetadata getColumnOfType(AbstractType type) + { + return new ColumnMetadata("a", "b", columnIdentifier, type, -1, REGULAR, null); + } + + private void checkList(boolean frozen) + { + if (frozen) + { + ListType listType = ListType.getInstance(Int32Type.instance, false); + ByteBuffer payload = listType.getSerializer().serialize(List.of(1, 2, 3)); + checkFrozenCollection(listType, payload); + } + else + checkUnfrozenCollection(ListType.getInstance(Int32Type.instance, true)); + } + + private void checkMap(boolean frozen) + { + if (frozen) + { + MapType mapType = MapType.getInstance(Int32Type.instance, Int32Type.instance, false); + ByteBuffer payload = mapType.getSerializer().serialize(Map.of(1, 1, 2, 2, 3, 3)); + checkFrozenCollection(mapType, payload); + } + else + checkUnfrozenCollection(MapType.getInstance(Int32Type.instance, Int32Type.instance, true)); + } + + private void checkSet(boolean frozen) + { + if (frozen) + { + SetType setType = SetType.getInstance(Int32Type.instance, false); + ByteBuffer payload = setType.getSerializer().serialize(Set.of(1, 2, 3)); + checkFrozenCollection(setType, payload); + } + else + checkUnfrozenCollection(SetType.getInstance(Int32Type.instance, true)); + } + + private void checkFrozenCollection(AbstractType type, ByteBuffer payload) + { + unaryConstraint.validate(getColumnOfType(type)); + unaryConstraint.evaluate(type, payload); + + assertThatThrownBy(() -> unaryConstraint.evaluate(type, EMPTY_BYTE_BUFFER)) + .hasMessage("Column value does not satisfy value constraint for column 'a_column' as it is null.") + .isInstanceOf(ConstraintViolationException.class); + } + + private void checkUnfrozenCollection(AbstractType type) + { + assertThatThrownBy(() -> unaryConstraint.validate(getColumnOfType(type))) + .hasMessageContaining("Constraint cannot be defined on the column") + .hasMessageContaining("When using collections, constraints can be used only of frozen collections") + .isInstanceOf(InvalidConstraintDefinitionException.class); + } +} diff --git a/test/unit/org/apache/cassandra/cql3/ColumnSpecificationTest.java b/test/unit/org/apache/cassandra/cql3/ColumnSpecificationTest.java new file mode 100644 index 0000000000..8513a54400 --- /dev/null +++ b/test/unit/org/apache/cassandra/cql3/ColumnSpecificationTest.java @@ -0,0 +1,206 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 org.apache.cassandra.cql3; + +import java.util.Map; + +import org.junit.Before; +import org.junit.Test; + +import org.apache.cassandra.config.DatabaseDescriptor; +import org.apache.cassandra.schema.CompactionParams; + +/** + * Test various "extensions" to a column spec when altering / creating a table + */ +public class ColumnSpecificationTest extends CQLTester +{ + @Before + public void before() + { + DatabaseDescriptor.setDynamicDataMaskingEnabled(true); + } + + @Test + public void testCreateTableWithColumnHavingMaskBeforeCheck() + { + createTable("CREATE TABLE %s (pk text primary key, name text MASKED WITH system.mask_default() CHECK NOT_NULL(name) AND LENGTH(name) > 1);"); + verifyColumnSpec("name text MASKED WITH system.mask_default() CHECK NOT_NULL(name) AND LENGTH(name) > 1"); + } + + @Test + public void testAlterTableAlterColumnWithMaskAndCheckStandalone() + { + createTable("CREATE TABLE %s (pk text, name text, primary key (pk));"); + execute("ALTER TABLE %s ALTER name MASKED WITH system.mask_default()"); + execute("ALTER TABLE %s ALTER name CHECK NOT_NULL(name) AND LENGTH(name) > 1;"); + verifyColumnSpec("name text MASKED WITH system.mask_default() CHECK NOT_NULL(name) AND LENGTH(name) > 1"); + } + + @Test + public void testAlterTableAlterColumnWithMask() + { + createTable("CREATE TABLE %s (pk text, name text, primary key (pk));"); + execute("ALTER TABLE %s ALTER name MASKED WITH system.mask_default()"); + verifyColumnSpec("name text MASKED WITH system.mask_default()"); + } + + @Test + public void testAlterTableAlterColumnWithCheck() + { + createTable("CREATE TABLE %s (pk text, name text, primary key (pk));"); + execute("ALTER TABLE %s ALTER name CHECK NOT_NULL(name) AND LENGTH(name) > 1;"); + verifyColumnSpec("name text CHECK NOT_NULL(name) AND LENGTH(name) > 1"); + } + + @Test + public void testAddingCheckToColumnWithMask() + { + createTable("CREATE TABLE %s (pk text primary key, name text MASKED WITH system.mask_default());"); + execute("ALTER TABLE %s ALTER name CHECK NOT_NULL(name) AND LENGTH(name) > 1"); + verifyColumnSpec("name text MASKED WITH system.mask_default() CHECK NOT_NULL(name) AND LENGTH(name) > 1"); + } + + @Test + public void testAddingMaskToColumnWithCheck() + { + createTable("CREATE TABLE %s (pk text primary key, name text CHECK NOT_NULL(name) AND LENGTH(name) > 1);"); + execute("ALTER TABLE %s ALTER name MASKED WITH system.mask_default()"); + verifyColumnSpec("name text MASKED WITH system.mask_default() CHECK NOT_NULL(name) AND LENGTH(name) > 1"); + } + + @Test + public void testDroppingCheckKeepsMask() + { + createTable("CREATE TABLE %s (pk text primary key, name text MASKED WITH system.mask_default() CHECK NOT_NULL(name) AND LENGTH(name) > 1);"); + execute("ALTER TABLE %s ALTER name DROP CHECK"); + verifyColumnSpec("name text MASKED WITH system.mask_default()"); + } + + @Test + public void droppingMaskKeepsCheck() + { + createTable("CREATE TABLE %s (pk text primary key, name text MASKED WITH system.mask_default() CHECK NOT_NULL(name) AND LENGTH(name) > 1);"); + execute("ALTER TABLE %s ALTER name DROP MASKED"); + verifyColumnSpec("name text CHECK NOT_NULL(name) AND LENGTH(name) > 1"); + } + + @Test + public void testAlterTableAddColumnWithCheck() + { + createTable("CREATE TABLE %s (pk text primary key);"); + execute("ALTER TABLE %s ADD name text CHECK NOT_NULL(name) AND LENGTH(name) > 1"); + verifyColumnSpec("name text CHECK NOT_NULL(name) AND LENGTH(name) > 1"); + } + + @Test + public void testAlterTableAddColumnWithMask() + { + createTable("CREATE TABLE %s (pk text primary key);"); + execute("ALTER TABLE %s ADD name text MASKED WITH system.mask_default()"); + verifyColumnSpec("name text MASKED WITH system.mask_default()"); + } + + @Test + public void testAlterTableAddColumnWithMaskAndCheck() + { + createTable("CREATE TABLE %s (pk text primary key);"); + execute("ALTER TABLE %s ADD name text MASKED WITH system.mask_default() CHECK NOT_NULL(name)"); + verifyColumnSpec("name text MASKED WITH system.mask_default() CHECK NOT_NULL(name)"); + } + + @Test + public void testAlterTableAddColumnWithMaskAndMultipleChecks() + { + createTable("CREATE TABLE %s (pk text primary key);"); + execute("ALTER TABLE %s ADD name text MASKED WITH system.mask_default() CHECK NOT_NULL(name) AND LENGTH(name) > 1"); + verifyColumnSpec("name text MASKED WITH system.mask_default() CHECK NOT_NULL(name) AND LENGTH(name) > 1"); + } + + /** + * TODO - investigate if it is possible to specify checks before mask when creating a table + */ + @Test(expected = RuntimeException.class) + public void testFailingCreateTableWithColumnHavingMaskAfterCheck() + { + createTable("CREATE TABLE %s (pk text primary key, name text CHECK NOT_NULL(name) AND LENGTH(name) > 1 MASKED WITH system.mask_default());"); + } + + /** + * TODO - investigate if it is possible to specify both check and mask, check being first + */ + @Test(expected = RuntimeException.class) + public void testFailingAlterTableAlterColumnWithCheckAndMask() + { + createTable("CREATE TABLE %s (pk text, name text, primary key (pk));"); + execute("ALTER TABLE %s ALTER name CHECK NOT_NULL(name) AND LENGTH(name) > 1 MASKED WITH system.mask_default();"); + verifyColumnSpec("name text MASKED WITH system.mask_default() CHECK NOT_NULL(name) AND LENGTH(name) > 1"); + } + + /** + * TODO - investigate if it is possible to specify both check and mask, mask being first + */ + @Test(expected = RuntimeException.class) + public void testFailingAlterTableAlterColumnWithMaskAndCheck() + { + createTable("CREATE TABLE %s (pk text, name text, primary key (pk));"); + execute("ALTER TABLE %s ALTER name MASKED WITH system.mask_default() CHECK NOT_NULL(name) AND LENGTH(name) > 1"); + verifyColumnSpec("name text MASKED WITH system.mask_default() CHECK NOT_NULL(name) AND LENGTH(name) > 1"); + } + + private void verifyColumnSpec(String modifiedColumn) + { + assertRowsContains(executeNetWithoutPaging("DESCRIBE TABLE " + KEYSPACE + '.' + currentTable()), + row(KEYSPACE, + "table", + currentTable(), + "CREATE TABLE " + KEYSPACE + '.' + currentTable() + " (\n" + + " pk text PRIMARY KEY,\n" + + " " + modifiedColumn + '\n' + + ") WITH " + tableParametersCql())); + } + + static String tableParametersCql() + { + return "additional_write_policy = '99p'\n" + + " AND allow_auto_snapshot = true\n" + + " AND bloom_filter_fp_chance = 0.01\n" + + " AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'}\n" + + " AND cdc = false\n" + + " AND comment = ''\n" + + " AND compaction = " + cqlQuoted(CompactionParams.DEFAULT.asMap()) + "\n" + + " AND compression = {'chunk_length_in_kb': '16', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'}\n" + + " AND memtable = 'default'\n" + + " AND crc_check_chance = 1.0\n" + + " AND default_time_to_live = 0\n" + + " AND extensions = {}\n" + + " AND gc_grace_seconds = 864000\n" + + " AND incremental_backups = true\n" + + " AND max_index_interval = 2048\n" + + " AND memtable_flush_period_in_ms = 0\n" + + " AND min_index_interval = 128\n" + + " AND read_repair = 'BLOCKING'\n" + + " AND speculative_retry = '99p';"; + } + + private static String cqlQuoted(Map map) + { + return new CqlBuilder().append(map).toString(); + } +} diff --git a/test/unit/org/apache/cassandra/db/marshal/AbstractTypeTest.java b/test/unit/org/apache/cassandra/db/marshal/AbstractTypeTest.java index 5cb051b35e..630c322fce 100644 --- a/test/unit/org/apache/cassandra/db/marshal/AbstractTypeTest.java +++ b/test/unit/org/apache/cassandra/db/marshal/AbstractTypeTest.java @@ -263,10 +263,15 @@ public class AbstractTypeTest public void isConstrainedTest() { qt().forAll(genBuilder().build()).checkAssert(type -> { - if (type instanceof MapType || type instanceof TupleType || type instanceof AbstractCompositeType) + if (type instanceof TupleType || type instanceof AbstractCompositeType) assertThat(type.isConstrainable()).isEqualTo(false); else - assertThat(type.isConstrainable()).isEqualTo(true); + { + if (type.isCollection() && !type.isFrozenCollection()) + assertThat(type.isConstrainable()).isEqualTo(false); + else + assertThat(type.isConstrainable()).isEqualTo(true); + } }); }