From 5cbf993d96749fa8994e48f3ff2cbd9843a0a0ef Mon Sep 17 00:00:00 2001 From: Bernardo Botella Corbi Date: Mon, 3 Jun 2024 15:41:20 -0700 Subject: [PATCH] CEP-42 - Add Constraints Framework patch by Bernardo Botella; reviewed by Yifan Cai, Stefan Miklosovic, Josh McKenzie, Maxwell Guo, Dmitry Konstantinov, Sam Tunnicliffe for CASSANDRA-19947 --- CHANGES.txt | 1 + NEWS.txt | 3 + doc/modules/cassandra/nav.adoc | 1 + .../pages/developing/cql/constraints.adoc | 93 ++ .../cassandra/pages/developing/cql/index.adoc | 1 + doc/modules/cassandra/pages/new/index.adoc | 1 + pylib/cqlshlib/cql3handling.py | 11 +- pylib/cqlshlib/test/test_cqlsh_completion.py | 11 +- src/antlr/Cql.g | 1 + src/antlr/Lexer.g | 1 + src/antlr/Parser.g | 20 +- .../org/apache/cassandra/cql3/Validation.java | 13 + .../cql3/constraints/ColumnConstraint.java | 85 ++ .../cql3/constraints/ColumnConstraints.java | 207 +++ .../cql3/constraints/ConstraintFunction.java | 48 + .../ConstraintViolationException.java | 32 + .../constraints/FunctionColumnConstraint.java | 199 +++ .../InvalidConstraintDefinitionException.java | 32 + .../cql3/constraints/LengthConstraint.java | 115 ++ .../constraints/ScalarColumnConstraint.java | 160 +++ .../statements/ModificationStatement.java | 22 + .../cql3/statements/UpdateStatement.java | 30 +- .../schema/AlterTableStatement.java | 89 +- .../schema/CreateTableStatement.java | 22 +- .../apache/cassandra/db/ClusteringPrefix.java | 6 + src/java/org/apache/cassandra/db/Columns.java | 7 +- .../db/marshal/AbstractCompositeType.java | 62 +- .../cassandra/db/marshal/AbstractType.java | 32 + .../apache/cassandra/db/marshal/MapType.java | 6 + .../cassandra/db/marshal/ReversedType.java | 6 + .../cassandra/db/marshal/TupleType.java | 6 + .../apache/cassandra/db/marshal/UserType.java | 6 + .../cassandra/schema/ColumnMetadata.java | 126 +- .../cassandra/schema/TableMetadata.java | 78 +- .../cassandra/tcm/membership/NodeVersion.java | 2 +- .../cassandra/tcm/serialization/Version.java | 4 + .../test/ColumnConstraintsTest.java | 302 ++++ .../test/cdc/ToggleCDCOnRepair.java | 98 ++ .../cdc/ToggleCDCOnRepairEnabledTest.java | 65 +- ...CDCWithConstraintsOnRepairEnabledTest.java | 38 + .../distributed/test/log/SnapshotTest.java | 1 + ...ableWithTableConstraintValidationTest.java | 208 +++ .../contraints/ColumnConstraintsTest.java | 50 + .../CqlConstraintValidationTester.java | 81 ++ ...WithColumnCqlConstraintValidationTest.java | 1264 +++++++++++++++++ .../db/marshal/AbstractTypeTest.java | 14 +- 46 files changed, 3550 insertions(+), 110 deletions(-) create mode 100644 doc/modules/cassandra/pages/developing/cql/constraints.adoc create mode 100644 src/java/org/apache/cassandra/cql3/constraints/ColumnConstraint.java create mode 100644 src/java/org/apache/cassandra/cql3/constraints/ColumnConstraints.java create mode 100644 src/java/org/apache/cassandra/cql3/constraints/ConstraintFunction.java create mode 100644 src/java/org/apache/cassandra/cql3/constraints/ConstraintViolationException.java create mode 100644 src/java/org/apache/cassandra/cql3/constraints/FunctionColumnConstraint.java create mode 100644 src/java/org/apache/cassandra/cql3/constraints/InvalidConstraintDefinitionException.java create mode 100644 src/java/org/apache/cassandra/cql3/constraints/LengthConstraint.java create mode 100644 src/java/org/apache/cassandra/cql3/constraints/ScalarColumnConstraint.java create mode 100644 test/distributed/org/apache/cassandra/distributed/test/ColumnConstraintsTest.java create mode 100644 test/distributed/org/apache/cassandra/distributed/test/cdc/ToggleCDCOnRepair.java create mode 100644 test/distributed/org/apache/cassandra/distributed/test/cdc/ToggleCDCWithConstraintsOnRepairEnabledTest.java create mode 100644 test/unit/org/apache/cassandra/contraints/AlterTableWithTableConstraintValidationTest.java create mode 100644 test/unit/org/apache/cassandra/contraints/ColumnConstraintsTest.java create mode 100644 test/unit/org/apache/cassandra/contraints/CqlConstraintValidationTester.java create mode 100644 test/unit/org/apache/cassandra/contraints/CreateTableWithColumnCqlConstraintValidationTest.java diff --git a/CHANGES.txt b/CHANGES.txt index 59d478d713..6f09b0e0a3 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,4 +1,5 @@ 5.1 + * CEP-42 - Add Constraints Framework (CASSANDRA-19947) * Add table metric PurgeableTombstoneScannedHistogram and a tracing event for scanned purgeable tombstones (CASSANDRA-20132) * Make sure we can parse the expanded CQL before writing it to the log or sending it to replicas (CASSANDRA-20218) * Add format_bytes and format_time functions (CASSANDRA-19546) diff --git a/NEWS.txt b/NEWS.txt index dc4c766e9f..bb8898d53b 100644 --- a/NEWS.txt +++ b/NEWS.txt @@ -76,6 +76,9 @@ New features metadata. In the first instance, this encompasses cluster membership, token ownership and schema metadata. See https://cwiki.apache.org/confluence/display/CASSANDRA/CEP-21%3A+Transactional+Cluster+Metadata for more detail on the motivation and design, and see "Upgrading" below for specific instructions on migrating clusters to this system. + - CEP-42 Constraints Framework provides flexibility to Cassandra users and operators by providing a set of + usable constraints at table level, that will ease validations at application level and protect + the Database from misconfigured clients. More updates and documentation to follow. - New Guardrails added: - Whether bulk loading of SSTables is allowed. diff --git a/doc/modules/cassandra/nav.adoc b/doc/modules/cassandra/nav.adoc index 6a4c7a7a2f..c549ba4ae5 100644 --- a/doc/modules/cassandra/nav.adoc +++ b/doc/modules/cassandra/nav.adoc @@ -60,6 +60,7 @@ *** xref:cassandra:developing/cql/json.adoc[JSON] *** xref:cassandra:developing/cql/security.adoc[Security] *** xref:cassandra:developing/cql/triggers.adoc[Triggers] +*** xref:cassandra:developing/cql/constraints.adoc[Constraints] *** xref:cassandra:developing/cql/appendices.adoc[Appendices] *** xref:cassandra:developing/cql/changes.adoc[Changes] *** xref:cassandra:developing/cql/SASI.adoc[SASI] diff --git a/doc/modules/cassandra/pages/developing/cql/constraints.adoc b/doc/modules/cassandra/pages/developing/cql/constraints.adoc new file mode 100644 index 0000000000..e6a76dbe7d --- /dev/null +++ b/doc/modules/cassandra/pages/developing/cql/constraints.adoc @@ -0,0 +1,93 @@ += Constraints + +Constraints provide a way of specifying and enforcing conditions at a +column level in a table schema definition and enforcing them at write time. + +== CREATE CONSTRAINT + +Constraints can be created within the column definition, or as part +of the table properties. + +The main syntax to define a constraint is as follows: + +[source,bnf] +---- +CREATE TABLE keyspace.table ( + name text, + i int CHECK (condition) (AND (condition))* + ..., + +); +---- + +As shown in this syntax, more than one constraint can be defined for a given column using the AND keyword. + +== ALTER CONSTRAINT + +Altering a constraint is done by following the alter column CQL syntax: +[source,bnf] +---- +ALTER TABLE [IF EXISTS] ALTER [IF EXISTS] CHECK ; +---- + +== DROP CONSTRAINT +And DROP can be used to drop constraints for a column as well. +[source,bnf] +---- +ALTER TABLE [IF EXISTS]
ALTER [IF EXISTS] DROP CHECK; +---- + +== AVAILABLE CONSTRAINTS + +=== SCALAR CONSTRAINT + +Defines a comparator against a numeric type. It support all numeric types supported in Cassandra, with all the regular +comparators. + +For example, we can define constraints that ensure that i is bigger or equal than 100 but smaller than 1000. + +[source,bnf] +---- +CREATE TABLE keyspace.table ( + name text, + i int CHECK i < 1000 AND i > 100 + ..., +); +---- + +Altering that constraint can be done with: + +---- +ALTER TABLE keyspace.table ALTER i CHECK i >= 500; +---- + +Finally, the constraint can be removed: + +---- +ALTER TABLE keyspace.table ALTER i DROP CHECK; +---- + +=== LENGTH CONSTRAINT + +Defines a condition that checks the length of text or binary type. + +For example, we can create a constraint that checks that name can't be longer than 256 characters: + +---- +CREATE TABLE keyspace.table ( + name text CHECK LENGTH(name) < 256 + ..., +); +---- + +Altering that constraint can be done with: + +---- +ALTER TABLE keyspace.table ALTER name LENGTH(name) < 512; +---- + +Finally, the constraint can be removed: + +---- +ALTER TABLE keyspace.table ALTER name DROP CHECK; +---- diff --git a/doc/modules/cassandra/pages/developing/cql/index.adoc b/doc/modules/cassandra/pages/developing/cql/index.adoc index 3b31a4d381..97204ce568 100644 --- a/doc/modules/cassandra/pages/developing/cql/index.adoc +++ b/doc/modules/cassandra/pages/developing/cql/index.adoc @@ -21,5 +21,6 @@ For that reason, when used in this document, these terms (tables, rows and colum * xref:developing/cql/json.adoc[JSON] * xref:developing/cql/security.adoc[CQL security] * xref:developing/cql/triggers.adoc[Triggers] +* xref:developing/cql/constraints.adoc[Constraints] * xref:developing/cql/appendices.adoc[Appendices] * xref:developing/cql/changes.adoc[Changes] \ No newline at end of file diff --git a/doc/modules/cassandra/pages/new/index.adoc b/doc/modules/cassandra/pages/new/index.adoc index 4371cebd6b..015f484f57 100644 --- a/doc/modules/cassandra/pages/new/index.adoc +++ b/doc/modules/cassandra/pages/new/index.adoc @@ -7,6 +7,7 @@ This section covers the new features in Apache Cassandra 5.1. * https://cwiki.apache.org/confluence/x/FQRACw[ACID Transactions (Accord)] * https://cwiki.apache.org/confluence/x/YyD1D[Transactional Cluster Metadata] +* https://cwiki.apache.org/confluence/x/8IpyEg[Constraints] == New Features in Apache Cassandra 5.0 diff --git a/pylib/cqlshlib/cql3handling.py b/pylib/cqlshlib/cql3handling.py index 4d2f4dffcf..9883543ab5 100644 --- a/pylib/cqlshlib/cql3handling.py +++ b/pylib/cqlshlib/cql3handling.py @@ -322,7 +322,14 @@ JUNK ::= /([ \t\r\f\v]+|(--|[/][/])[^\n\r]*([\n\r]|$)|[/][*].*?[*][/])/ ; ::= utname= ; - ::= ( | | | | ) ( )? ; + ::= ( | | | | ) ( )? ( )? ; + + ::= "CHECK" ( "AND" )* + ; + + ::= + | + ; ::= "MASKED" "WITH" ( "DEFAULT" | ); @@ -1463,7 +1470,7 @@ syntax_rules += r''' | "WITH" ( "AND" )* | "RENAME" ("IF" "EXISTS")? existcol= "TO" newcol= ( "AND" existcol= "TO" newcol= )* - | "ALTER" ("IF" "EXISTS")? existcol= ( | "DROP" "MASKED" ) + | "ALTER" ("IF" "EXISTS")? existcol= ( | | "DROP" ( "CHECK" | "MASKED" ) ) ; ::= "ALTER" "TYPE" ("IF" "EXISTS")? ut= diff --git a/pylib/cqlshlib/test/test_cqlsh_completion.py b/pylib/cqlshlib/test/test_cqlsh_completion.py index 2a02fcf464..b8c0fb1499 100644 --- a/pylib/cqlshlib/test/test_cqlsh_completion.py +++ b/pylib/cqlshlib/test/test_cqlsh_completion.py @@ -636,7 +636,7 @@ class TestCqlshCompletion(CqlshCompletionCase): self.trycompletions(prefix + ' new_table (col_a ine', immediate='t ') self.trycompletions(prefix + ' new_table (col_a int ', - choices=[',', 'MASKED', 'PRIMARY']) + choices=[',', 'CHECK', 'MASKED', 'PRIMARY']) self.trycompletions(prefix + ' new_table (col_a int M', immediate='ASKED WITH ') self.trycompletions(prefix + ' new_table (col_a int MASKED WITH ', @@ -1106,7 +1106,7 @@ class TestCqlshCompletion(CqlshCompletionCase): self.trycompletions('ALTER TABLE IF EXISTS new_table ADD ', choices=['', 'IF']) self.trycompletions('ALTER TABLE IF EXISTS new_table ADD IF NOT EXISTS ', choices=['']) self.trycompletions('ALTER TABLE new_table ADD IF NOT EXISTS ', choices=['']) - self.trycompletions('ALTER TABLE new_table ADD col int ', choices=[';', 'MASKED', 'static']) + self.trycompletions('ALTER TABLE new_table ADD col int ', choices=[';', 'MASKED', 'CHECK', 'static']) self.trycompletions('ALTER TABLE new_table ADD col int M', immediate='ASKED WITH ') self.trycompletions('ALTER TABLE new_table ADD col int MASKED WITH ', choices=['DEFAULT', self.cqlsh.keyspace + '.', 'system.'], @@ -1116,12 +1116,15 @@ class TestCqlshCompletion(CqlshCompletionCase): self.trycompletions('ALTER TABLE IF EXISTS new_table DROP ', choices=['IF', '', '']) self.trycompletions('ALTER TABLE IF EXISTS new_table ALTER ', choices=['IF', '', '']) self.trycompletions('ALTER TABLE IF EXISTS new_table ALTER IF E', immediate='XISTS ') - self.trycompletions('ALTER TABLE IF EXISTS new_table ALTER IF EXISTS col ', choices=['MASKED', 'DROP']) + self.trycompletions('ALTER TABLE IF EXISTS new_table ALTER IF EXISTS col ', choices=['CHECK', 'MASKED', 'DROP']) + self.trycompletions('ALTER TABLE IF EXISTS new_table ALTER IF EXISTS col C', immediate='HECK ') self.trycompletions('ALTER TABLE IF EXISTS new_table ALTER IF EXISTS col M', immediate='ASKED WITH ') self.trycompletions('ALTER TABLE IF EXISTS new_table ALTER IF EXISTS col MASKED WITH ', choices=['DEFAULT', self.cqlsh.keyspace + '.', 'system.'], other_choices_ok=True) - self.trycompletions('ALTER TABLE IF EXISTS new_table ALTER IF EXISTS col D', immediate='ROP MASKED ;') + self.trycompletions('ALTER TABLE IF EXISTS new_table ALTER IF EXISTS col D', immediate='ROP ') + self.trycompletions('ALTER TABLE IF EXISTS new_table ALTER IF EXISTS col DROP ', choices=['CHECK', 'MASKED']) + self.trycompletions('ALTER TABLE IF EXISTS new_table ALTER IF EXISTS col DROP C', immediate='HECK ;') self.trycompletions('ALTER TABLE IF EXISTS new_table ALTER IF EXISTS col DROP M', immediate='ASKED ;') def test_complete_in_alter_type(self): diff --git a/src/antlr/Cql.g b/src/antlr/Cql.g index 5cf8c88a31..fe068f4b5a 100644 --- a/src/antlr/Cql.g +++ b/src/antlr/Cql.g @@ -38,6 +38,7 @@ import Parser,Lexer; import org.apache.cassandra.auth.*; import org.apache.cassandra.cql3.conditions.*; + import org.apache.cassandra.cql3.constraints.*; import org.apache.cassandra.cql3.functions.*; import org.apache.cassandra.cql3.functions.masking.*; import org.apache.cassandra.cql3.restrictions.CustomIndexExpression; diff --git a/src/antlr/Lexer.g b/src/antlr/Lexer.g index 34f5a80ccd..96a51e23c8 100644 --- a/src/antlr/Lexer.g +++ b/src/antlr/Lexer.g @@ -129,6 +129,7 @@ K_GROUP: G R O U P; K_CLUSTER: C L U S T E R; K_INTERNALS: I N T E R N A L S; K_ONLY: O N L Y; +K_CHECK: C H E C K; K_GRANT: G R A N T; K_ALL: A L L; diff --git a/src/antlr/Parser.g b/src/antlr/Parser.g index 9400395733..5f5908f6b0 100644 --- a/src/antlr/Parser.g +++ b/src/antlr/Parser.g @@ -781,11 +781,24 @@ tableDefinition[CreateTableStatement.Raw stmt] tableColumns[CreateTableStatement.Raw stmt] @init { boolean isStatic = false; } - : k=ident v=comparatorType (K_STATIC { isStatic = true; })? (mask=columnMask)? { $stmt.addColumn(k, v, isStatic, mask); } + : k=ident v=comparatorType (K_STATIC { isStatic = true; })? (mask=columnMask)? (constraints=columnConstraints)? { $stmt.addColumn(k, v, isStatic, mask, constraints); } (K_PRIMARY K_KEY { $stmt.setPartitionKeyColumn(k); })? | K_PRIMARY K_KEY '(' tablePartitionKey[stmt] (',' c=ident { $stmt.markClusteringColumn(c); } )* ')' ; +columnConstraints returns [ColumnConstraints.Raw constraints] + @init { + boolean isStatic = false; + List constraintsList = new ArrayList(); + } + : K_CHECK cc=columnConstraint { constraintsList.add(cc); } (K_AND cc=columnConstraint { constraintsList.add(cc); })* { $constraints = new ColumnConstraints.Raw(constraintsList); } + ; + +columnConstraint returns [ColumnConstraint columnConstraint] + : funcName=ident '(' k=ident ')' op=relationType t=value { $columnConstraint = new FunctionColumnConstraint.Raw(funcName, k, op, t.getText()).prepare(); } + | k=ident op=relationType t=value { $columnConstraint = new ScalarColumnConstraint.Raw(k, op, t.getText()).prepare(); } + ; + columnMask returns [ColumnMask.Raw mask] @init { List arguments = new ArrayList<>(); } : K_MASKED K_WITH name=functionName columnMaskArguments[arguments] { $mask = new ColumnMask.Raw(name, arguments); } @@ -965,7 +978,9 @@ alterTableStatement returns [AlterTableStatement.Raw stmt] | K_ALTER ( K_IF K_EXISTS { $stmt.ifColumnExists(true); } )? id=cident ( mask=columnMask { $stmt.mask(id, mask); } - | K_DROP K_MASKED { $stmt.mask(id, null); } ) + | K_DROP K_MASKED { $stmt.mask(id, null); } + | K_DROP K_CHECK { $stmt.dropConstraints(id); } + | (constraints=columnConstraints) { $stmt.alterConstraints(id, constraints); }) | 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); } @@ -2067,5 +2082,6 @@ basic_unreserved_keyword returns [String str] | K_VECTOR | K_ANN | K_BETWEEN + | K_CHECK ) { $str = $k.text; } ; diff --git a/src/java/org/apache/cassandra/cql3/Validation.java b/src/java/org/apache/cassandra/cql3/Validation.java index 27a1b4e667..cbffa5a52e 100644 --- a/src/java/org/apache/cassandra/cql3/Validation.java +++ b/src/java/org/apache/cassandra/cql3/Validation.java @@ -19,6 +19,7 @@ package org.apache.cassandra.cql3; import java.nio.ByteBuffer; +import org.apache.cassandra.cql3.constraints.ConstraintViolationException; import org.apache.cassandra.exceptions.InvalidRequestException; import org.apache.cassandra.schema.TableMetadata; import org.apache.cassandra.serializers.MarshalException; @@ -64,4 +65,16 @@ public abstract class Validation throw new InvalidRequestException(e.getMessage()); } } + + public static void checkConstraints(TableMetadata metadata, ByteBuffer key) + { + try + { + metadata.partitionKeyType.checkConstraints(key, metadata.partitionKeyConstraints); + } + catch (ConstraintViolationException e) + { + throw new InvalidRequestException(e.getMessage(), e); + } + } } diff --git a/src/java/org/apache/cassandra/cql3/constraints/ColumnConstraint.java b/src/java/org/apache/cassandra/cql3/constraints/ColumnConstraint.java new file mode 100644 index 0000000000..ccbcdef48a --- /dev/null +++ b/src/java/org/apache/cassandra/cql3/constraints/ColumnConstraint.java @@ -0,0 +1,85 @@ +/* + * 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.nio.ByteBuffer; + +import org.apache.cassandra.cql3.CqlBuilder; +import org.apache.cassandra.db.marshal.AbstractType; +import org.apache.cassandra.tcm.serialization.MetadataSerializer; +import org.apache.cassandra.schema.ColumnMetadata; + +/** + * Common class for the conditions that a CQL Constraint needs to implement to be integrated in the + * CQL Constraints framework, with T as a constraint serializer. + */ +public interface ColumnConstraint +{ + + // Enum containing all the possible constraint serializers to help with serialization/deserialization + // of constraints. + enum ConstraintType + { + // The order of that enum matters!! + // We are serializing its enum position instead of its name. + // Changing this enum would affect how that int is interpreted when deserializing. + COMPOSED(ColumnConstraints.serializer), + FUNCTION(FunctionColumnConstraint.serializer), + SCALAR(ScalarColumnConstraint.serializer); + + private final MetadataSerializer serializer; + + ConstraintType(MetadataSerializer serializer) + { + this.serializer = serializer; + } + + public static MetadataSerializer getSerializer(int i) + { + return ConstraintType.values()[i].serializer; + } + } + + MetadataSerializer serializer(); + + void appendCqlTo(CqlBuilder builder); + + /** + * Method that evaluates the condition. It can either succeed or throw a {@link ConstraintViolationException}. + * + * @param valueType value type of the column value under test + * @param columnValue Column value to be evaluated at write time + */ + void evaluate(AbstractType valueType, ByteBuffer columnValue) throws ConstraintViolationException; + + /** + * Method to validate the condition. This method is called when creating constraint via CQL. + * A {@link InvalidConstraintDefinitionException} is thrown for invalid consrtaint definition. + * + * @param columnMetadata Metadata of the column in which the constraint is defined. + */ + void validate(ColumnMetadata columnMetadata) throws InvalidConstraintDefinitionException; + + /** + * Method to get the Constraint serializer + * + * @return the Constraint type serializer + */ + ConstraintType getConstraintType(); +} diff --git a/src/java/org/apache/cassandra/cql3/constraints/ColumnConstraints.java b/src/java/org/apache/cassandra/cql3/constraints/ColumnConstraints.java new file mode 100644 index 0000000000..3b6646aec3 --- /dev/null +++ b/src/java/org/apache/cassandra/cql3/constraints/ColumnConstraints.java @@ -0,0 +1,207 @@ +/* + * 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.io.IOException; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Objects; + +import org.apache.cassandra.cql3.CqlBuilder; +import org.apache.cassandra.db.TypeSizes; +import org.apache.cassandra.db.marshal.AbstractType; +import org.apache.cassandra.io.util.DataInputPlus; +import org.apache.cassandra.io.util.DataOutputPlus; +import org.apache.cassandra.schema.ColumnMetadata; +import org.apache.cassandra.tcm.serialization.MetadataSerializer; +import org.apache.cassandra.tcm.serialization.Version; + + +// group of constraints for the column +public class ColumnConstraints implements ColumnConstraint +{ + public static final Serializer serializer = new Serializer(); + public static final ColumnConstraints NO_OP = new Noop(); + + private final List> constraints; + + public ColumnConstraints(List> constraints) + { + this.constraints = constraints; + } + + @Override + public MetadataSerializer serializer() + { + return serializer; + } + + @Override + public void appendCqlTo(CqlBuilder builder) + { + for (ColumnConstraint constraint : constraints) + constraint.appendCqlTo(builder); + } + + @Override + public void evaluate(AbstractType valueType, ByteBuffer columnValue) throws ConstraintViolationException + { + for (ColumnConstraint constraint : constraints) + constraint.evaluate(valueType, columnValue); + } + + public List> getConstraints() + { + return constraints; + } + + public boolean isEmpty() + { + return constraints.isEmpty(); + } + + public int getSize() + { + return constraints.size(); + } + + // Checks if there is at least one constraint that will perform checks + public boolean hasRelevantConstraints() + { + for (ColumnConstraint c : constraints) + { + if (c != ColumnConstraints.NO_OP) + return true; + } + return false; + } + + @Override + 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 (ColumnConstraint constraint : constraints) + constraint.validate(columnMetadata); + } + + @Override + public ConstraintType getConstraintType() + { + return ConstraintType.COMPOSED; + } + + private static class Noop extends ColumnConstraints + { + private Noop() + { + super(Collections.emptyList()); + } + + @Override + public void validate(ColumnMetadata columnMetadata) + { + // Do nothing. It is always valid + } + } + + public final static class Raw + { + private final List> constraints; + + public Raw(List> constraints) + { + this.constraints = constraints; + } + + public Raw() + { + this.constraints = Collections.emptyList(); + } + + public ColumnConstraints prepare() + { + if (constraints.isEmpty()) + return NO_OP; + return new ColumnConstraints(constraints); + } + } + + public static class Serializer implements MetadataSerializer + { + + @Override + public void serialize(ColumnConstraints columnConstraint, DataOutputPlus out, Version version) throws IOException + { + out.writeInt(columnConstraint.getSize()); + for (ColumnConstraint constraint : columnConstraint.getConstraints()) + { + // We serialize the serializer ordinal in the enum to save space + out.writeShort(constraint.getConstraintType().ordinal()); + constraint.serializer().serialize(constraint, out, version); + } + } + + @Override + public ColumnConstraints deserialize(DataInputPlus in, Version version) throws IOException + { + List> columnConstraints = new ArrayList<>(); + int numberOfConstraints = in.readInt(); + for (int i = 0; i < numberOfConstraints; i++) + { + int serializerPosition = in.readShort(); + ColumnConstraint constraint = (ColumnConstraint) ConstraintType + .getSerializer(serializerPosition) + .deserialize(in, version); + columnConstraints.add(constraint); + } + return new ColumnConstraints(columnConstraints); + } + + @Override + public long serializedSize(ColumnConstraints columnConstraint, Version version) + { + long constraintsSize = TypeSizes.INT_SIZE; + for (ColumnConstraint constraint : columnConstraint.getConstraints()) + { + constraintsSize += TypeSizes.SHORT_SIZE; + constraintsSize += constraint.serializer().serializedSize(constraint, version); + } + return constraintsSize; + } + } + + @Override + public boolean equals(Object o) + { + if (this == o) + return true; + + if (!(o instanceof ColumnConstraints)) + return false; + + ColumnConstraints other = (ColumnConstraints) o; + return Objects.equals(constraints, other.constraints); + } +} diff --git a/src/java/org/apache/cassandra/cql3/constraints/ConstraintFunction.java b/src/java/org/apache/cassandra/cql3/constraints/ConstraintFunction.java new file mode 100644 index 0000000000..aa3ea2303b --- /dev/null +++ b/src/java/org/apache/cassandra/cql3/constraints/ConstraintFunction.java @@ -0,0 +1,48 @@ +/* + * 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.nio.ByteBuffer; + +import org.apache.cassandra.cql3.Operator; +import org.apache.cassandra.db.marshal.AbstractType; +import org.apache.cassandra.schema.ColumnMetadata; + +/** + * Interface to be implemented by functions that are executed as part of CQL constraints. + */ +public interface ConstraintFunction +{ + /** + * @return the function name to be executed. + */ + String getName(); + + /** + * Method that performs the actual condition test, executed during the write path. + * It the test is not successful, it throws a {@link ConstraintViolationException}. + */ + void evaluate(AbstractType valueType, Operator relationType, String term, ByteBuffer columnValue) throws ConstraintViolationException; + + /** + * Method that validates that a condition is valid. This method is called when the CQL constraint is created to determine + * if the CQL statement is valid or needs to be rejected as invalid throwing a {@link InvalidConstraintDefinitionException} + */ + void validate(ColumnMetadata columnMetadata) throws InvalidConstraintDefinitionException; +} diff --git a/src/java/org/apache/cassandra/cql3/constraints/ConstraintViolationException.java b/src/java/org/apache/cassandra/cql3/constraints/ConstraintViolationException.java new file mode 100644 index 0000000000..ea7742ac18 --- /dev/null +++ b/src/java/org/apache/cassandra/cql3/constraints/ConstraintViolationException.java @@ -0,0 +1,32 @@ +/* + * 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 org.apache.cassandra.exceptions.InvalidRequestException; + +/** + * Thrown to indicate that the CQL operation did not comply with the defined Constraints + */ +public class ConstraintViolationException extends InvalidRequestException +{ + public ConstraintViolationException(String msg) + { + super(msg); + } +} diff --git a/src/java/org/apache/cassandra/cql3/constraints/FunctionColumnConstraint.java b/src/java/org/apache/cassandra/cql3/constraints/FunctionColumnConstraint.java new file mode 100644 index 0000000000..ad42e1e129 --- /dev/null +++ b/src/java/org/apache/cassandra/cql3/constraints/FunctionColumnConstraint.java @@ -0,0 +1,199 @@ +/* + * 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.io.IOException; +import java.nio.ByteBuffer; +import java.util.function.Function; + +import org.apache.cassandra.cql3.ColumnIdentifier; +import org.apache.cassandra.cql3.CqlBuilder; +import org.apache.cassandra.cql3.Operator; +import org.apache.cassandra.db.TypeSizes; +import org.apache.cassandra.db.marshal.AbstractType; +import org.apache.cassandra.io.util.DataInputPlus; +import org.apache.cassandra.io.util.DataOutputPlus; +import org.apache.cassandra.schema.ColumnMetadata; +import org.apache.cassandra.tcm.serialization.MetadataSerializer; +import org.apache.cassandra.tcm.serialization.Version; +import org.apache.cassandra.utils.LocalizeString; + +public class FunctionColumnConstraint implements ColumnConstraint +{ + public static final Serializer serializer = new Serializer(); + + public final ConstraintFunction function; + public final ColumnIdentifier columnName; + public final Operator relationType; + public final String term; + + public final static class Raw + { + public final ConstraintFunction function; + public final ColumnIdentifier columnName; + public final Operator relationType; + public final String term; + + public Raw(ColumnIdentifier functionName, ColumnIdentifier columnName, Operator relationType, String term) + { + this.relationType = relationType; + this.columnName = columnName; + this.term = term; + function = createConstraintFunction(functionName.toCQLString(), columnName); + } + + public FunctionColumnConstraint prepare() + { + return new FunctionColumnConstraint(function, columnName, relationType, term); + } + } + + private enum Functions + { + LENGTH(LengthConstraint::new); + + private final Function functionCreator; + + Functions(Function functionCreator) + { + this.functionCreator = functionCreator; + } + } + + private static ConstraintFunction createConstraintFunction(String functionName, ColumnIdentifier columnName) + { + try + { + return Functions.valueOf(LocalizeString.toUpperCaseLocalized(functionName)).functionCreator.apply(columnName); + } + catch (IllegalArgumentException ex) + { + throw new InvalidConstraintDefinitionException("Unrecognized constraint function: " + functionName); + } + } + + private FunctionColumnConstraint(ConstraintFunction function, ColumnIdentifier columnName, Operator relationType, String term) + { + this.function = function; + this.columnName = columnName; + this.relationType = relationType; + this.term = term; + } + + @Override + public void appendCqlTo(CqlBuilder builder) + { + builder.append(toString()); + } + + @Override + public MetadataSerializer serializer() + { + return serializer; + } + + @Override + public void evaluate(AbstractType valueType, ByteBuffer columnValue) + { + function.evaluate(valueType, relationType, term, columnValue); + } + + @Override + public void validate(ColumnMetadata columnMetadata) + { + validateArgs(columnMetadata); + function.validate(columnMetadata); + } + + @Override + public ConstraintType getConstraintType() + { + return ConstraintType.FUNCTION; + } + + void validateArgs(ColumnMetadata columnMetadata) + { + if (!columnMetadata.name.equals(columnName)) + throw new InvalidConstraintDefinitionException("Function parameter should be the column name"); + } + + @Override + public String toString() + { + return function.getName() + "(" + columnName + ") " + relationType + " " + term; + } + + public static class Serializer implements MetadataSerializer + { + @Override + public void serialize(FunctionColumnConstraint columnConstraint, DataOutputPlus out, Version version) throws IOException + { + out.writeUTF(columnConstraint.function.getName()); + out.writeUTF(columnConstraint.columnName.toCQLString()); + columnConstraint.relationType.writeTo(out); + out.writeUTF(columnConstraint.term); + } + + @Override + public FunctionColumnConstraint deserialize(DataInputPlus in, Version version) throws IOException + { + String functionName = in.readUTF(); + ConstraintFunction function; + String columnNameString = in.readUTF(); + ColumnIdentifier columnName = new ColumnIdentifier(columnNameString, true); + try + { + function = createConstraintFunction(functionName, columnName); + } + catch (Exception e) + { + throw new IOException(e); + } + Operator relationType = Operator.readFrom(in); + final String term = in.readUTF(); + return new FunctionColumnConstraint(function, columnName, relationType, term); + } + + @Override + public long serializedSize(FunctionColumnConstraint columnConstraint, Version version) + { + return TypeSizes.sizeof(columnConstraint.function.getClass().getName()) + + TypeSizes.sizeof(columnConstraint.columnName.toCQLString()) + + TypeSizes.sizeof(columnConstraint.term) + + Operator.serializedSize(); + } + } + + @Override + public boolean equals(Object o) + { + if (this == o) + return true; + + if (!(o instanceof FunctionColumnConstraint)) + return false; + + FunctionColumnConstraint other = (FunctionColumnConstraint) o; + + return function.equals(other.function) + && columnName.equals(other.columnName) + && relationType == other.relationType + && term.equals(other.term); + } +} diff --git a/src/java/org/apache/cassandra/cql3/constraints/InvalidConstraintDefinitionException.java b/src/java/org/apache/cassandra/cql3/constraints/InvalidConstraintDefinitionException.java new file mode 100644 index 0000000000..7ef32f0ca1 --- /dev/null +++ b/src/java/org/apache/cassandra/cql3/constraints/InvalidConstraintDefinitionException.java @@ -0,0 +1,32 @@ +/* + * 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 org.apache.cassandra.exceptions.InvalidRequestException; + +/** + * Thrown to indicate that the CQL constraint is not valid + */ +public class InvalidConstraintDefinitionException extends InvalidRequestException +{ + public InvalidConstraintDefinitionException(String msg) + { + super(msg); + } +} diff --git a/src/java/org/apache/cassandra/cql3/constraints/LengthConstraint.java b/src/java/org/apache/cassandra/cql3/constraints/LengthConstraint.java new file mode 100644 index 0000000000..9d6f99ca24 --- /dev/null +++ b/src/java/org/apache/cassandra/cql3/constraints/LengthConstraint.java @@ -0,0 +1,115 @@ +/* + * 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.nio.ByteBuffer; + +import org.apache.cassandra.cql3.ColumnIdentifier; +import org.apache.cassandra.cql3.Operator; +import org.apache.cassandra.db.marshal.AbstractType; +import org.apache.cassandra.db.marshal.AsciiType; +import org.apache.cassandra.db.marshal.BytesType; +import org.apache.cassandra.db.marshal.Int32Type; +import org.apache.cassandra.db.marshal.UTF8Type; +import org.apache.cassandra.schema.ColumnMetadata; +import org.apache.cassandra.utils.ByteBufferUtil; + +public class LengthConstraint implements ConstraintFunction +{ + private static final AbstractType[] SUPPORTED_TYPES = new AbstractType[] { BytesType.instance, UTF8Type.instance, AsciiType.instance }; + + public static final String FUNCTION_NAME = "LENGTH"; + + private final ColumnIdentifier columnName; + + public LengthConstraint(ColumnIdentifier columnName) + { + this.columnName = columnName; + } + + @Override + public String getName() + { + return FUNCTION_NAME; + } + + @Override + public void evaluate(AbstractType valueType, Operator relationType, String term, ByteBuffer columnValue) + { + int valueLength = getValueLength(columnValue, valueType); + int sizeConstraint = Integer.parseInt(term); + + ByteBuffer leftOperand = ByteBufferUtil.bytes(valueLength); + ByteBuffer rightOperand = ByteBufferUtil.bytes(sizeConstraint); + + if (!relationType.isSatisfiedBy(Int32Type.instance, leftOperand, rightOperand)) + throw new ConstraintViolationException(columnName + " does not satisfy length constraint. " + + valueLength + " should be " + relationType + ' ' + term); + } + + @Override + public void validate(ColumnMetadata columnMetadata) + { + boolean supported = false; + AbstractType unwrapped = columnMetadata.type.unwrap(); + for (AbstractType supportedType : SUPPORTED_TYPES) + { + if (supportedType == unwrapped) + { + supported = true; + break; + } + } + + if (!supported) + throw invalidConstraintDefinitionException(columnMetadata.type); + } + + private int getValueLength(ByteBuffer value, AbstractType valueType) + { + if (valueType.getClass() == BytesType.class) + { + return value.remaining(); + } + + if (valueType.getClass() == AsciiType.class || valueType.getClass() == UTF8Type.class) + return ((String) valueType.compose(value)).length(); + + throw invalidConstraintDefinitionException(valueType); + } + + private InvalidConstraintDefinitionException invalidConstraintDefinitionException(AbstractType valueType) + { + throw new InvalidConstraintDefinitionException("Column type " + valueType.getClass() + " is not supported."); + } + + @Override + public boolean equals(Object o) + { + if (this == o) + return true; + + if (!(o instanceof LengthConstraint)) + return false; + + LengthConstraint other = (LengthConstraint) o; + + return columnName.equals(other.columnName); + } +} diff --git a/src/java/org/apache/cassandra/cql3/constraints/ScalarColumnConstraint.java b/src/java/org/apache/cassandra/cql3/constraints/ScalarColumnConstraint.java new file mode 100644 index 0000000000..14be7d689d --- /dev/null +++ b/src/java/org/apache/cassandra/cql3/constraints/ScalarColumnConstraint.java @@ -0,0 +1,160 @@ +/* + * 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.io.IOException; +import java.nio.ByteBuffer; + +import org.apache.cassandra.cql3.ColumnIdentifier; +import org.apache.cassandra.cql3.CqlBuilder; +import org.apache.cassandra.cql3.Operator; +import org.apache.cassandra.db.TypeSizes; +import org.apache.cassandra.db.marshal.AbstractType; +import org.apache.cassandra.io.util.DataInputPlus; +import org.apache.cassandra.io.util.DataOutputPlus; +import org.apache.cassandra.schema.ColumnMetadata; +import org.apache.cassandra.tcm.serialization.MetadataSerializer; +import org.apache.cassandra.tcm.serialization.Version; + +public class ScalarColumnConstraint implements ColumnConstraint +{ + public final ColumnIdentifier param; + public final Operator relationType; + public final String term; + + public final static Serializer serializer = new Serializer(); + + public final static class Raw + { + public final ColumnIdentifier param; + public final Operator relationType; + public final String term; + + public Raw(ColumnIdentifier param, Operator relationType, String term) + { + this.param = param; + this.relationType = relationType; + this.term = term; + } + + public ScalarColumnConstraint prepare() + { + return new ScalarColumnConstraint(param, relationType, term); + } + } + + private ScalarColumnConstraint(ColumnIdentifier param, Operator relationType, String term) + { + this.param = param; + this.relationType = relationType; + this.term = term; + } + + @Override + public void evaluate(AbstractType valueType, ByteBuffer columnValue) + { + ByteBuffer value; + try + { + value = valueType.fromString(term); + } + catch (NumberFormatException exception) + { + throw new ConstraintViolationException(param + " and " + term + " need to be numbers."); + } + + if (!relationType.isSatisfiedBy(valueType, columnValue, value)) + throw new ConstraintViolationException("Column value does not satisfy value constraint. " + + " It should be " + relationType + " " + term); + } + + @Override + public void validate(ColumnMetadata columnMetadata) throws InvalidConstraintDefinitionException + { + if (!columnMetadata.type.isNumber()) + throw new InvalidConstraintDefinitionException(param + " is not a number"); + } + + @Override + public ConstraintType getConstraintType() + { + return ConstraintType.SCALAR; + } + + @Override + public String toString() + { + return param + " " + relationType + " " + term; + } + + @Override + public MetadataSerializer serializer() + { + return serializer; + } + + @Override + public void appendCqlTo(CqlBuilder builder) + { + builder.append(toString()); + } + + private static class Serializer implements MetadataSerializer + { + @Override + public void serialize(ScalarColumnConstraint columnConstraint, DataOutputPlus out, Version version) throws IOException + { + out.writeUTF(columnConstraint.param.toCQLString()); + columnConstraint.relationType.writeTo(out); + out.writeUTF(columnConstraint.term); + } + + @Override + public ScalarColumnConstraint deserialize(DataInputPlus in, Version version) throws IOException + { + ColumnIdentifier param = new ColumnIdentifier(in.readUTF(), true); + Operator relationType = Operator.readFrom(in); + return new ScalarColumnConstraint(param, relationType, in.readUTF()); + } + + @Override + public long serializedSize(ScalarColumnConstraint columnConstraint, Version version) + { + return TypeSizes.sizeof(columnConstraint.term) + + Operator.serializedSize() + + TypeSizes.sizeof(columnConstraint.param.toString()); + } + } + + @Override + public boolean equals(Object o) + { + if (this == o) + return true; + + if (!(o instanceof ScalarColumnConstraint)) + return false; + + ScalarColumnConstraint other = (ScalarColumnConstraint) o; + + return param.equals(other.param) + && relationType == other.relationType + && term.equals(other.term); + } +} diff --git a/src/java/org/apache/cassandra/cql3/statements/ModificationStatement.java b/src/java/org/apache/cassandra/cql3/statements/ModificationStatement.java index 113135016d..b528133e43 100644 --- a/src/java/org/apache/cassandra/cql3/statements/ModificationStatement.java +++ b/src/java/org/apache/cassandra/cql3/statements/ModificationStatement.java @@ -26,6 +26,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.apache.cassandra.auth.Permission; +import org.apache.cassandra.cql3.constraints.ConstraintViolationException; import org.apache.cassandra.db.guardrails.Guardrails; import org.apache.cassandra.dht.Token; import org.apache.cassandra.locator.Replica; @@ -800,6 +801,7 @@ public abstract class ModificationStatement implements CQLStatement.SingleKeyspa for (ByteBuffer key : keys) { Validation.validateKey(metadata(), key); + Validation.checkConstraints(metadata(), key); DecoratedKey dk = metadata().partitioner.decorateKey(key); PartitionUpdate.Builder updateBuilder = collector.getPartitionUpdateBuilder(metadata(), dk, options.getConsistency()); @@ -810,9 +812,11 @@ public abstract class ModificationStatement implements CQLStatement.SingleKeyspa } else { + // Clustering keys need to be checked on their own for (Clustering clustering : clusterings) { clustering.validate(); + checkClusteringConstraints(clustering); addUpdateForKey(updateBuilder, clustering, params); } } @@ -820,6 +824,24 @@ public abstract class ModificationStatement implements CQLStatement.SingleKeyspa } } + private void checkClusteringConstraints(Clustering clustering) + { + for (ColumnMetadata column : metadata.clusteringColumns()) + { + if (column.hasConstraint()) + { + try + { + clustering.checkConstraints(column.position(), metadata.comparator, column.getColumnConstraints()); + } + catch (ConstraintViolationException e) + { + throw new InvalidRequestException(e.getMessage(), e); + } + } + } + } + public Slices createSlices(QueryOptions options) { return restrictions.getSlices(options); diff --git a/src/java/org/apache/cassandra/cql3/statements/UpdateStatement.java b/src/java/org/apache/cassandra/cql3/statements/UpdateStatement.java index 378b827a36..d8310cdd2f 100644 --- a/src/java/org/apache/cassandra/cql3/statements/UpdateStatement.java +++ b/src/java/org/apache/cassandra/cql3/statements/UpdateStatement.java @@ -17,6 +17,7 @@ */ package org.apache.cassandra.cql3.statements; +import java.nio.ByteBuffer; import java.util.Collection; import java.util.Collections; import java.util.List; @@ -26,12 +27,15 @@ import org.apache.cassandra.audit.AuditLogEntryType; import org.apache.cassandra.cql3.*; import org.apache.cassandra.cql3.conditions.ColumnCondition; import org.apache.cassandra.cql3.conditions.Conditions; +import org.apache.cassandra.cql3.constraints.ColumnConstraint; import org.apache.cassandra.cql3.restrictions.StatementRestrictions; import org.apache.cassandra.cql3.terms.Constants; import org.apache.cassandra.cql3.terms.Term; import org.apache.cassandra.db.Clustering; import org.apache.cassandra.db.Slice; import org.apache.cassandra.db.partitions.PartitionUpdate; +import org.apache.cassandra.db.rows.Cell; +import org.apache.cassandra.db.rows.Row; import org.apache.cassandra.schema.ColumnMetadata; import org.apache.cassandra.schema.TableMetadata; import org.apache.cassandra.service.ClientState; @@ -93,7 +97,9 @@ public class UpdateStatement extends ModificationStatement for (int i = 0, isize = updates.size(); i < isize; i++) updates.get(i).execute(updateBuilder.partitionKey(), params); - updateBuilder.add(params.buildRow()); + Row row = params.buildRow(); + evaluateConstraintsForRow(row, metadata); + updateBuilder.add(row); } if (updatesStaticRow()) @@ -348,4 +354,26 @@ public class UpdateStatement extends ModificationStatement { return new AuditLogContext(AuditLogEntryType.UPDATE, keyspace(), table()); } + + public static void evaluateConstraintsForRow(Row row, TableMetadata metadata) + { + for (ColumnMetadata column : metadata.columnsWithConstraints) + { + Cell cell = row.getCell(column); + if (cell != null) + { + ColumnMetadata columnMetadata = cell.column(); + ByteBuffer cellData = cell.buffer(); + evaluateConstraint(columnMetadata, cellData); + } + } + } + + public static void evaluateConstraint(ColumnMetadata columnMetadata, ByteBuffer cellData) + { + 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 4302919ca9..27d9961340 100644 --- a/src/java/org/apache/cassandra/cql3/statements/schema/AlterTableStatement.java +++ b/src/java/org/apache/cassandra/cql3/statements/schema/AlterTableStatement.java @@ -42,6 +42,7 @@ import org.apache.cassandra.auth.Permission; import org.apache.cassandra.config.DatabaseDescriptor; import org.apache.cassandra.cql3.CQL3Type; import org.apache.cassandra.cql3.CQLStatement; +import org.apache.cassandra.cql3.constraints.ColumnConstraints; import org.apache.cassandra.cql3.ColumnIdentifier; import org.apache.cassandra.cql3.QualifiedName; import org.apache.cassandra.cql3.functions.masking.ColumnMask; @@ -710,6 +711,67 @@ public abstract class AlterTableStatement extends AlterSchemaStatement } } + public static class DropConstraints extends AlterTableStatement + { + final ColumnIdentifier columnName; + + DropConstraints(String keyspaceName, String tableName, boolean ifTableExists, ColumnIdentifier columnName) + { + super(keyspaceName, tableName, ifTableExists); + this.columnName = columnName; + } + + @Override + public KeyspaceMetadata apply(Epoch epoch, KeyspaceMetadata keyspace, TableMetadata table, ClusterMetadata metadata) + { + ColumnMetadata columnMetadata = table.getColumn(columnName); + columnMetadata.removeColumnConstraints(); + + TableMetadata.Builder tableBuilder = table.unbuild().epoch(epoch); + Views.Builder viewsBuilder = keyspace.views.unbuild(); + TableMetadata tableMetadata = tableBuilder.build(); + tableMetadata.validate(); + + return keyspace.withSwapped(keyspace.tables.withSwapped(tableMetadata)) + .withSwapped(viewsBuilder.build()); + } + } + + public static class AlterConstraints extends AlterTableStatement + { + final ColumnIdentifier columnName; + final ColumnConstraints constraints; + + AlterConstraints(String keyspaceName, String tableName, boolean ifTableExists, ColumnIdentifier columnName, ColumnConstraints constraints) + { + super(keyspaceName, tableName, ifTableExists); + this.columnName = columnName; + this.constraints = constraints; + } + + @Override + public KeyspaceMetadata apply(Epoch epoch, KeyspaceMetadata keyspace, TableMetadata table, ClusterMetadata metadata) + { + TableMetadata.Builder tableBuilder = table.unbuild().epoch(epoch); + + for (ColumnMetadata column : tableBuilder.columns()) + { + if (column.name == columnName) + { + column.setColumnConstraints(constraints); + break; + } + } + + Views.Builder viewsBuilder = keyspace.views.unbuild(); + TableMetadata tableMetadata = tableBuilder.build(); + tableMetadata.validate(); + + return keyspace.withSwapped(keyspace.tables.withSwapped(tableMetadata)) + .withSwapped(viewsBuilder.build()); + } + } + public static final class Raw extends CQLStatement.Raw { private enum Kind @@ -720,13 +782,17 @@ public abstract class AlterTableStatement extends AlterSchemaStatement DROP_COLUMNS, RENAME_COLUMNS, ALTER_OPTIONS, - DROP_COMPACT_STORAGE + DROP_COMPACT_STORAGE, + DROP_CONSTRAINTS, + ALTER_CONSTRAINTS } private final QualifiedName name; private final boolean ifTableExists; private boolean ifColumnExists; private boolean ifColumnNotExists; + private ColumnIdentifier constraintName; + private ColumnConstraints constraints; private Kind kind; @@ -748,9 +814,15 @@ public abstract class AlterTableStatement extends AlterSchemaStatement public final TableAttributes attrs = new TableAttributes(); public Raw(QualifiedName name, boolean ifTableExists) + { + this(name, ifTableExists, null); + } + + public Raw(QualifiedName name, boolean ifTableExists, ColumnIdentifier constraintName) { this.name = name; this.ifTableExists = ifTableExists; + this.constraintName = constraintName; } public AlterTableStatement prepare(ClientState state) @@ -767,6 +839,8 @@ public abstract class AlterTableStatement extends AlterSchemaStatement case RENAME_COLUMNS: return new RenameColumns(keyspaceName, tableName, renamedColumns, ifTableExists, ifColumnExists); case ALTER_OPTIONS: return new AlterOptions(keyspaceName, tableName, attrs, ifTableExists); case DROP_COMPACT_STORAGE: return new DropCompactStorage(keyspaceName, tableName, ifTableExists); + case DROP_CONSTRAINTS: return new DropConstraints(keyspaceName, tableName, ifTableExists, constraintName); + case ALTER_CONSTRAINTS: return new AlterConstraints(keyspaceName, tableName, ifTableExists, constraintName, constraints); } throw new AssertionError(); @@ -811,6 +885,19 @@ public abstract class AlterTableStatement extends AlterSchemaStatement kind = Kind.DROP_COMPACT_STORAGE; } + public void dropConstraints(ColumnIdentifier name) + { + kind = Kind.DROP_CONSTRAINTS; + this.constraintName = name; + } + + public void alterConstraints(ColumnIdentifier name, ColumnConstraints.Raw rawConstraints) + { + kind = Kind.ALTER_CONSTRAINTS; + this.constraintName = name; + this.constraints = rawConstraints.prepare(); + } + public void timestamp(long timestamp) { this.timestamp = timestamp; 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 529054a1b7..8338b4006e 100644 --- a/src/java/org/apache/cassandra/cql3/statements/schema/CreateTableStatement.java +++ b/src/java/org/apache/cassandra/cql3/statements/schema/CreateTableStatement.java @@ -32,6 +32,7 @@ import org.apache.cassandra.auth.DataResource; import org.apache.cassandra.auth.IResource; import org.apache.cassandra.auth.Permission; import org.apache.cassandra.cql3.*; +import org.apache.cassandra.cql3.constraints.ColumnConstraints; import org.apache.cassandra.cql3.functions.masking.ColumnMask; import org.apache.cassandra.db.guardrails.Guardrails; import org.apache.cassandra.db.marshal.*; @@ -56,6 +57,7 @@ public final class CreateTableStatement extends AlterSchemaStatement private final Map rawColumns; private final Set staticColumns; private final List partitionKeyColumns; + private final Map columnConstraints; private final List clusteringColumns; private final LinkedHashMap clusteringOrder; @@ -72,6 +74,7 @@ public final class CreateTableStatement extends AlterSchemaStatement Set staticColumns, List partitionKeyColumns, List clusteringColumns, + Map columnConstraints, LinkedHashMap clusteringOrder, TableAttributes attrs, boolean ifNotExists, @@ -84,6 +87,7 @@ public final class CreateTableStatement extends AlterSchemaStatement this.staticColumns = staticColumns; this.partitionKeyColumns = partitionKeyColumns; this.clusteringColumns = clusteringColumns; + this.columnConstraints = columnConstraints; this.clusteringOrder = clusteringOrder; this.attrs = attrs; @@ -343,13 +347,13 @@ public final class CreateTableStatement extends AlterSchemaStatement for (int i = 0; i < partitionKeyColumns.size(); i++) { ColumnProperties properties = partitionKeyColumnProperties.get(i); - builder.addPartitionKeyColumn(partitionKeyColumns.get(i), properties.type, properties.mask); + builder.addPartitionKeyColumn(partitionKeyColumns.get(i), properties.type, properties.mask, columnConstraints.get(partitionKeyColumns.get(i))); } for (int i = 0; i < clusteringColumns.size(); i++) { ColumnProperties properties = clusteringColumnProperties.get(i); - builder.addClusteringColumn(clusteringColumns.get(i), properties.type, properties.mask); + builder.addClusteringColumn(clusteringColumns.get(i), properties.type, properties.mask, columnConstraints.get(clusteringColumns.get(i))); } if (useCompactStorage) @@ -360,11 +364,12 @@ public final class CreateTableStatement extends AlterSchemaStatement { columns.forEach((column, properties) -> { if (staticColumns.contains(column)) - builder.addStaticColumn(column, properties.type, properties.mask); + builder.addStaticColumn(column, properties.type, properties.mask, columnConstraints.get(column)); else - builder.addRegularColumn(column, properties.type, properties.mask); + builder.addRegularColumn(column, properties.type, properties.mask, columnConstraints.get(column)); }); } + return builder; } @@ -506,6 +511,7 @@ public final class CreateTableStatement extends AlterSchemaStatement private final Map rawColumns = new HashMap<>(); private final Set staticColumns = new HashSet<>(); private final List clusteringColumns = new ArrayList<>(); + private final Map columnConstraints = new HashMap<>(); private List partitionKeyColumns; @@ -531,6 +537,7 @@ public final class CreateTableStatement extends AlterSchemaStatement staticColumns, partitionKeyColumns, clusteringColumns, + columnConstraints, clusteringOrder, attrs, ifNotExists, @@ -559,14 +566,17 @@ public final class CreateTableStatement extends AlterSchemaStatement return name.getName(); } - public void addColumn(ColumnIdentifier column, CQL3Type.Raw type, boolean isStatic, ColumnMask.Raw mask) + public void addColumn(ColumnIdentifier column, CQL3Type.Raw type, boolean isStatic, ColumnMask.Raw mask, ColumnConstraints.Raw constraints) { - if (null != rawColumns.put(column, new ColumnProperties.Raw(type, mask))) throw ire("Duplicate column '%s' declaration for table '%s'", column, name); if (isStatic) staticColumns.add(column); + if (null == constraints) + columnConstraints.put(column, ColumnConstraints.NO_OP); + else + columnConstraints.put(column, constraints.prepare()); } public void setCompactStorage() diff --git a/src/java/org/apache/cassandra/db/ClusteringPrefix.java b/src/java/org/apache/cassandra/db/ClusteringPrefix.java index 02f9330b43..167d89c6a4 100644 --- a/src/java/org/apache/cassandra/db/ClusteringPrefix.java +++ b/src/java/org/apache/cassandra/db/ClusteringPrefix.java @@ -24,6 +24,7 @@ import java.util.function.ToIntFunction; import org.apache.cassandra.cache.IMeasurableMemory; import org.apache.cassandra.config.*; +import org.apache.cassandra.cql3.constraints.ColumnConstraints; import org.apache.cassandra.db.marshal.ByteArrayAccessor; import org.apache.cassandra.db.marshal.ByteBufferAccessor; import org.apache.cassandra.db.marshal.CompositeType; @@ -752,4 +753,9 @@ public interface ClusteringPrefix extends IMeasurableMemory, Clusterable return equals(prefix, (ClusteringPrefix) o); } + default void checkConstraints(int clusterIndex, ClusteringComparator comparator, ColumnConstraints constraints) + { + comparator.subtype(clusterIndex).checkConstraints(accessor().toBuffer(get(clusterIndex)), constraints); + } + } \ No newline at end of file diff --git a/src/java/org/apache/cassandra/db/Columns.java b/src/java/org/apache/cassandra/db/Columns.java index 275d000dd3..60852c5c56 100644 --- a/src/java/org/apache/cassandra/db/Columns.java +++ b/src/java/org/apache/cassandra/db/Columns.java @@ -28,6 +28,7 @@ import com.google.common.collect.Iterators; import net.nicoulaj.compilecommand.annotations.DontInline; import org.apache.cassandra.cql3.ColumnIdentifier; +import org.apache.cassandra.cql3.constraints.ColumnConstraints; import org.apache.cassandra.db.marshal.SetType; import org.apache.cassandra.db.marshal.UTF8Type; import org.apache.cassandra.db.rows.ColumnData; @@ -62,7 +63,8 @@ public class Columns extends AbstractCollection implements Colle SetType.getInstance(UTF8Type.instance, true), ColumnMetadata.NO_POSITION, ColumnMetadata.Kind.STATIC, - null); + null, + ColumnConstraints.NO_OP); public static final ColumnMetadata FIRST_COMPLEX_REGULAR = new ColumnMetadata("", @@ -71,7 +73,8 @@ public class Columns extends AbstractCollection implements Colle SetType.getInstance(UTF8Type.instance, true), ColumnMetadata.NO_POSITION, ColumnMetadata.Kind.REGULAR, - null); + null, + ColumnConstraints.NO_OP); private final Object[] columns; private final int complexIdx; // Index of the first complex column diff --git a/src/java/org/apache/cassandra/db/marshal/AbstractCompositeType.java b/src/java/org/apache/cassandra/db/marshal/AbstractCompositeType.java index dfa7496ea6..737954d09b 100644 --- a/src/java/org/apache/cassandra/db/marshal/AbstractCompositeType.java +++ b/src/java/org/apache/cassandra/db/marshal/AbstractCompositeType.java @@ -23,6 +23,9 @@ import java.util.Collections; import java.util.List; import java.util.regex.Pattern; +import org.apache.cassandra.cql3.constraints.ColumnConstraint; +import org.apache.cassandra.cql3.constraints.ColumnConstraints; +import org.apache.cassandra.cql3.constraints.ConstraintViolationException; import org.apache.cassandra.cql3.terms.Term; import org.apache.cassandra.serializers.MarshalException; import org.apache.cassandra.transport.ProtocolVersion; @@ -47,6 +50,13 @@ public abstract class AbstractCompositeType extends AbstractType return true; } + @Override + public boolean isConstrainable() + { + // Constraints are not supported for composite types + return false; + } + public int compareCustom(VL left, ValueAccessor accessorL, VR right, ValueAccessor accessorR) { if (accessorL.isEmpty(left) || accessorR.isEmpty(right)) @@ -281,7 +291,8 @@ public abstract class AbstractCompositeType extends AbstractType validate(bb, ByteBufferAccessor.instance); } - public void validate(V input, ValueAccessor accessor) + @Override + public void validate(V input, ValueAccessor accessor) { boolean isStatic = readIsStatic(input, accessor); int offset = startingOffset(isStatic); @@ -316,6 +327,55 @@ public abstract class AbstractCompositeType extends AbstractType } } + @Override + public void checkConstraints(ByteBuffer input, ColumnConstraints constraints) throws ConstraintViolationException + { + // no constraints defined for the partition keys + if (!constraints.hasRelevantConstraints()) + return; + + ValueAccessor accessor = ByteBufferAccessor.instance; + + boolean isStatic = readIsStatic(input, accessor); + int offset = startingOffset(isStatic); + + int i = 0; + List partitionKeyValues = new ArrayList<>(); + while (!accessor.isEmptyFromOffset(input, offset)) + { + AbstractType comparator = getComparator(i, input, accessor, offset); + offset += getComparatorSize(i, input, accessor, offset); + + int length = accessor.getUnsignedShort(input, offset); + offset += 2; + + ByteBuffer value = accessor.slice(input, offset, length); +// partitionKeyValues.add(comparator.compose(value)); + partitionKeyValues.add(value); + offset += length; + + accessor.getByte(input, offset++); + ++i; + } + + if (partitionKeyValues.size() != constraints.getConstraints().size()) + { + // contraints list should have the exact size of partition key values. + // Noop constraints are filled for the partition key columns w/o any constraints. + throw new IllegalStateException("The number of constraints (" + partitionKeyValues.size() + ") " + + "should be the same with the number of partition key columns (" + + constraints.getConstraints().size() + ")"); + } + + for (int k = 0; k < constraints.getConstraints().size(); k++) + { + AbstractType comparator = getComparator(i, input, accessor, offset); + ByteBuffer value = partitionKeyValues.get(k); + ColumnConstraint constraint = constraints.getConstraints().get(k); + constraint.evaluate(comparator, value); + } + } + public abstract ByteBuffer decompose(Object... objects); abstract protected int getComparatorSize(int i, V value, ValueAccessor accessor, int offset); diff --git a/src/java/org/apache/cassandra/db/marshal/AbstractType.java b/src/java/org/apache/cassandra/db/marshal/AbstractType.java index 3c382e781b..29f4468389 100644 --- a/src/java/org/apache/cassandra/db/marshal/AbstractType.java +++ b/src/java/org/apache/cassandra/db/marshal/AbstractType.java @@ -31,6 +31,9 @@ import java.util.Objects; import org.apache.cassandra.cql3.AssignmentTestable; import org.apache.cassandra.cql3.CQL3Type; import org.apache.cassandra.cql3.ColumnSpecification; +import org.apache.cassandra.cql3.constraints.ColumnConstraint; +import org.apache.cassandra.cql3.constraints.ColumnConstraints; +import org.apache.cassandra.cql3.constraints.ConstraintViolationException; import org.apache.cassandra.cql3.terms.Term; import org.apache.cassandra.cql3.functions.ArgumentDeserializer; import org.apache.cassandra.db.rows.Cell; @@ -204,6 +207,25 @@ public abstract class AbstractType implements Comparator, Assignm getSerializer().validate(value, accessor); } + public void checkConstraints(ByteBuffer bytes, ColumnConstraints constraints) throws ConstraintViolationException + { + if (constraints.isEmpty()) + return; + + T value = getSerializer().deserialize(bytes); + constraints.evaluate(this, bytes); + } + + public void checkConstraints(ByteBuffer bytes, List constraints) throws ConstraintViolationException + { + if (constraints.isEmpty()) + return; + + T value = getSerializer().deserialize(bytes); + for (ColumnConstraint constraint : constraints) + constraint.evaluate(this, bytes); + } + public final int compare(ByteBuffer left, ByteBuffer right) { return compare(left, ByteBufferAccessor.instance, right, ByteBufferAccessor.instance); @@ -525,6 +547,11 @@ public abstract class AbstractType implements Comparator, Assignm return getSerializer().isNull(buffer, accessor); } + public boolean isNumber() + { + return unwrap() instanceof org.apache.cassandra.db.marshal.NumberType; + } + // This assumes that no empty values are passed public void writeValue(ByteBuffer value, DataOutputPlus out) throws IOException { @@ -799,4 +826,9 @@ public abstract class AbstractType implements Comparator, Assignm return type.compose(buffer); } } + + public boolean isConstrainable() + { + return true; + } } diff --git a/src/java/org/apache/cassandra/db/marshal/MapType.java b/src/java/org/apache/cassandra/db/marshal/MapType.java index 69ea6d17e1..f8ac6c0068 100644 --- a/src/java/org/apache/cassandra/db/marshal/MapType.java +++ b/src/java/org/apache/cassandra/db/marshal/MapType.java @@ -456,4 +456,10 @@ 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/db/marshal/ReversedType.java b/src/java/org/apache/cassandra/db/marshal/ReversedType.java index b567fe3481..1bf60844cf 100644 --- a/src/java/org/apache/cassandra/db/marshal/ReversedType.java +++ b/src/java/org/apache/cassandra/db/marshal/ReversedType.java @@ -244,4 +244,10 @@ public class ReversedType extends AbstractType { return baseType.unwrap(); } + + @Override + public boolean isConstrainable() + { + return unwrap().isConstrainable(); + } } diff --git a/src/java/org/apache/cassandra/db/marshal/TupleType.java b/src/java/org/apache/cassandra/db/marshal/TupleType.java index feb9b405fc..d6ce2da0f4 100644 --- a/src/java/org/apache/cassandra/db/marshal/TupleType.java +++ b/src/java/org/apache/cassandra/db/marshal/TupleType.java @@ -616,4 +616,10 @@ public class TupleType extends MultiElementType { throw new UnsupportedOperationException("Multicell tuples are not supported"); } + + @Override + public boolean isConstrainable() + { + return false; + } } diff --git a/src/java/org/apache/cassandra/db/marshal/UserType.java b/src/java/org/apache/cassandra/db/marshal/UserType.java index 82e9aafcc6..8048914483 100644 --- a/src/java/org/apache/cassandra/db/marshal/UserType.java +++ b/src/java/org/apache/cassandra/db/marshal/UserType.java @@ -618,6 +618,12 @@ public class UserType extends TupleType implements SchemaElement return "field " + fieldName(i); } + @Override + public boolean isConstrainable() + { + return false; + } + private enum ConflictBehavior { LOG { diff --git a/src/java/org/apache/cassandra/schema/ColumnMetadata.java b/src/java/org/apache/cassandra/schema/ColumnMetadata.java index cb3c879a94..79fad3a80e 100644 --- a/src/java/org/apache/cassandra/schema/ColumnMetadata.java +++ b/src/java/org/apache/cassandra/schema/ColumnMetadata.java @@ -22,6 +22,7 @@ import java.nio.ByteBuffer; import java.util.*; import java.util.function.Predicate; +import javax.annotation.Nonnull; import javax.annotation.Nullable; import com.google.common.annotations.VisibleForTesting; @@ -30,6 +31,8 @@ import com.google.common.collect.Collections2; import com.google.common.collect.Lists; import org.apache.cassandra.cql3.*; +import org.apache.cassandra.cql3.constraints.ColumnConstraint; +import org.apache.cassandra.cql3.constraints.ColumnConstraints; import org.apache.cassandra.cql3.functions.masking.ColumnMask; import org.apache.cassandra.cql3.selection.Selectable; import org.apache.cassandra.cql3.selection.Selector; @@ -115,6 +118,9 @@ public final class ColumnMetadata extends ColumnSpecification implements Selecta @Nullable private final ColumnMask mask; + @Nonnull + private ColumnConstraints columnConstraints; + private static long comparisonOrder(Kind kind, boolean isComplex, long position, ColumnIdentifier name) { assert position >= 0 && position < 1 << 12; @@ -126,42 +132,60 @@ public final class ColumnMetadata extends ColumnSpecification implements Selecta public static ColumnMetadata partitionKeyColumn(TableMetadata table, ByteBuffer name, AbstractType type, int position) { - return new ColumnMetadata(table, name, type, position, Kind.PARTITION_KEY, null); + return new ColumnMetadata(table, name, type, position, Kind.PARTITION_KEY, null, ColumnConstraints.NO_OP); } public static ColumnMetadata partitionKeyColumn(String keyspace, String table, String name, AbstractType type, int position) { - return new ColumnMetadata(keyspace, table, ColumnIdentifier.getInterned(name, true), type, position, Kind.PARTITION_KEY, null); + return new ColumnMetadata(keyspace, table, ColumnIdentifier.getInterned(name, true), type, position, Kind.PARTITION_KEY, null, ColumnConstraints.NO_OP); } public static ColumnMetadata clusteringColumn(TableMetadata table, ByteBuffer name, AbstractType type, int position) { - return new ColumnMetadata(table, name, type, position, Kind.CLUSTERING, null); + return new ColumnMetadata(table, name, type, position, Kind.CLUSTERING, null, ColumnConstraints.NO_OP); } public static ColumnMetadata clusteringColumn(String keyspace, String table, String name, AbstractType type, int position) { - return new ColumnMetadata(keyspace, table, ColumnIdentifier.getInterned(name, true), type, position, Kind.CLUSTERING, null); + return new ColumnMetadata(keyspace, table, ColumnIdentifier.getInterned(name, true), type, position, Kind.CLUSTERING, null, ColumnConstraints.NO_OP); } public static ColumnMetadata regularColumn(TableMetadata table, ByteBuffer name, AbstractType type) { - return new ColumnMetadata(table, name, type, NO_POSITION, Kind.REGULAR, null); + return new ColumnMetadata(table, name, type, NO_POSITION, Kind.REGULAR, null, ColumnConstraints.NO_OP); } public static ColumnMetadata regularColumn(String keyspace, String table, String name, AbstractType type) { - return new ColumnMetadata(keyspace, table, ColumnIdentifier.getInterned(name, true), type, NO_POSITION, Kind.REGULAR, null); + return new ColumnMetadata(keyspace, table, ColumnIdentifier.getInterned(name, true), type, NO_POSITION, Kind.REGULAR, null, ColumnConstraints.NO_OP); } public static ColumnMetadata staticColumn(TableMetadata table, ByteBuffer name, AbstractType type) { - return new ColumnMetadata(table, name, type, NO_POSITION, Kind.STATIC, null); + return new ColumnMetadata(table, name, type, NO_POSITION, Kind.STATIC, null, ColumnConstraints.NO_OP); } public static ColumnMetadata staticColumn(String keyspace, String table, String name, AbstractType type) { - return new ColumnMetadata(keyspace, table, ColumnIdentifier.getInterned(name, true), type, NO_POSITION, Kind.STATIC, null); + return new ColumnMetadata(keyspace, table, ColumnIdentifier.getInterned(name, true), type, NO_POSITION, Kind.STATIC, null, ColumnConstraints.NO_OP); + } + + public ColumnMetadata(TableMetadata table, + ByteBuffer name, + AbstractType type, + int position, + Kind kind, + @Nullable ColumnMask mask, + @Nonnull ColumnConstraints columnConstraints) + { + this(table.keyspace, + table.name, + ColumnIdentifier.getInterned(name, UTF8Type.instance), + type, + position, + kind, + mask, + columnConstraints); } public ColumnMetadata(TableMetadata table, @@ -177,7 +201,8 @@ public final class ColumnMetadata extends ColumnSpecification implements Selecta type, position, kind, - mask); + mask, + ColumnConstraints.NO_OP); } @VisibleForTesting @@ -188,6 +213,19 @@ public final class ColumnMetadata extends ColumnSpecification implements Selecta int position, Kind kind, @Nullable ColumnMask mask) + { + this(ksName, cfName, name, type, position, kind, mask, ColumnConstraints.NO_OP); + } + + @VisibleForTesting + public ColumnMetadata(String ksName, + String cfName, + ColumnIdentifier name, + AbstractType type, + int position, + Kind kind, + @Nullable ColumnMask mask, + @Nonnull ColumnConstraints columnConstraints) { super(ksName, cfName, name, type); assert name != null && type != null && kind != null; @@ -206,6 +244,7 @@ public final class ColumnMetadata extends ColumnSpecification implements Selecta this.asymmetricCellPathComparator = cellPathComparator == null ? null : (a, b) -> cellPathComparator.compare(((Cell)a).path(), (CellPath) b); this.comparisonOrder = comparisonOrder(kind, isComplex(), Math.max(0, position), name); this.mask = mask; + this.columnConstraints = columnConstraints; } private static Comparator makeCellPathComparator(Kind kind, AbstractType type) @@ -237,22 +276,22 @@ public final class ColumnMetadata extends ColumnSpecification implements Selecta public ColumnMetadata copy() { - return new ColumnMetadata(ksName, cfName, name, type, position, kind, mask); + return new ColumnMetadata(ksName, cfName, name, type, position, kind, mask, columnConstraints); } public ColumnMetadata withNewName(ColumnIdentifier newName) { - return new ColumnMetadata(ksName, cfName, newName, type, position, kind, mask); + return new ColumnMetadata(ksName, cfName, newName, type, position, kind, mask, columnConstraints); } public ColumnMetadata withNewType(AbstractType newType) { - return new ColumnMetadata(ksName, cfName, name, newType, position, kind, mask); + return new ColumnMetadata(ksName, cfName, name, newType, position, kind, mask, columnConstraints); } public ColumnMetadata withNewMask(@Nullable ColumnMask newMask) { - return new ColumnMetadata(ksName, cfName, name, type, position, kind, newMask); + return new ColumnMetadata(ksName, cfName, name, type, position, kind, newMask, columnConstraints); } public boolean isPartitionKey() @@ -275,6 +314,11 @@ public final class ColumnMetadata extends ColumnSpecification implements Selecta return mask != null; } + public boolean hasConstraint() + { + return columnConstraints.hasRelevantConstraints(); + } + public boolean isRegular() { return kind == Kind.REGULAR; @@ -299,6 +343,21 @@ public final class ColumnMetadata extends ColumnSpecification implements Selecta return mask; } + public ColumnConstraints getColumnConstraints() + { + return columnConstraints; + } + + public void setColumnConstraints(ColumnConstraints constraints) + { + this.columnConstraints = constraints; + } + + public void removeColumnConstraints() + { + columnConstraints = ColumnConstraints.NO_OP; + } + @Override public boolean equals(Object o) { @@ -320,7 +379,8 @@ public final class ColumnMetadata extends ColumnSpecification implements Selecta && position == other.position && ksName.equals(other.ksName) && cfName.equals(other.cfName) - && Objects.equals(mask, other.mask); + && Objects.equals(mask, other.mask) + && Objects.equals(columnConstraints, other.columnConstraints); } Optional compare(ColumnMetadata other) @@ -351,6 +411,7 @@ public final class ColumnMetadata extends ColumnSpecification implements Selecta result = 31 * result + (kind == null ? 0 : kind.hashCode()); result = 31 * result + position; result = 31 * result + (mask == null ? 0 : mask.hashCode()); + result = 31 * result + (columnConstraints == null ? 0 : columnConstraints.hashCode()); hash = result; } return result; @@ -516,6 +577,19 @@ public final class ColumnMetadata extends ColumnSpecification implements Selecta if (isMasked()) mask.appendCqlTo(builder); + + if (!columnConstraints.isEmpty()) + { + builder.append(" CHECK "); + Iterator> constraintIterator = columnConstraints.getConstraints().iterator(); + constraintIterator.next().appendCqlTo(builder); + + while (constraintIterator.hasNext()) + { + builder.append(" AND "); + constraintIterator.next().appendCqlTo(builder); + } + } } public static String toCQLString(Iterable defs) @@ -609,6 +683,13 @@ public final class ColumnMetadata extends ColumnSpecification implements Selecta out.writeBoolean(t.mask != null); if (t.mask != null) ColumnMask.serializer.serialize(t.mask, out, version); + if (version.isAtLeast(Version.V6)) + { + boolean hasConstraints = t.hasConstraint(); + out.writeBoolean(hasConstraints); + if (hasConstraints) + ColumnConstraints.serializer.serialize(t.columnConstraints, out, version); + } } public ColumnMetadata deserialize(DataInputPlus in, Types types, UserFunctions functions, Version version) throws IOException @@ -627,11 +708,23 @@ public final class ColumnMetadata extends ColumnSpecification implements Selecta boolean masked = in.readBoolean(); if (masked) mask = ColumnMask.serializer.deserialize(in, ksName, type, types, functions, version); - return new ColumnMetadata(ksName, tableName, new ColumnIdentifier(nameBB, name), type, position, kind, mask); + ColumnConstraints constraints; + if (version.isAtLeast(Version.V6) && in.readBoolean()) + constraints = ColumnConstraints.serializer.deserialize(in, version); + else + constraints = ColumnConstraints.NO_OP; + return new ColumnMetadata(ksName, tableName, new ColumnIdentifier(nameBB, name), type, position, kind, mask, constraints); } public long serializedSize(ColumnMetadata t, Version version) { + long constraintsSize = 0; + if (version.isAtLeast(Version.V6)) + { + constraintsSize += BOOL_SIZE; + if (t.hasConstraint()) + constraintsSize += t.getColumnConstraints().serializer().serializedSize(t.columnConstraints, version); + } return sizeof(t.ksName) + sizeof(t.cfName) + sizeof(t.kind.name()) + @@ -641,7 +734,8 @@ public final class ColumnMetadata extends ColumnSpecification implements Selecta sizeof(t.name.toString()) + ByteBufferUtil.serializedSizeWithShortLength(t.name.bytes) + BOOL_SIZE + - ((t.mask == null) ? 0 : ColumnMask.serializer.serializedSize(t.mask, version)); + ((t.mask == null) ? 0 : ColumnMask.serializer.serializedSize(t.mask, version)) + + constraintsSize; } } } diff --git a/src/java/org/apache/cassandra/schema/TableMetadata.java b/src/java/org/apache/cassandra/schema/TableMetadata.java index b97f8ef5a7..6f9672d3c1 100644 --- a/src/java/org/apache/cassandra/schema/TableMetadata.java +++ b/src/java/org/apache/cassandra/schema/TableMetadata.java @@ -32,6 +32,7 @@ import java.util.Map.Entry; import java.util.Objects; import java.util.Optional; import java.util.Set; +import javax.annotation.Nonnull; import javax.annotation.Nullable; import com.google.common.base.MoreObjects; @@ -46,9 +47,12 @@ import org.slf4j.LoggerFactory; import org.apache.cassandra.auth.DataResource; import org.apache.cassandra.config.DatabaseDescriptor; +import org.apache.cassandra.cql3.constraints.ColumnConstraint; +import org.apache.cassandra.cql3.constraints.ColumnConstraints; import org.apache.cassandra.cql3.ColumnIdentifier; import org.apache.cassandra.cql3.CqlBuilder; import org.apache.cassandra.cql3.SchemaElement; +import org.apache.cassandra.cql3.constraints.InvalidConstraintDefinitionException; import org.apache.cassandra.cql3.functions.Function; import org.apache.cassandra.cql3.functions.masking.ColumnMask; import org.apache.cassandra.db.Clustering; @@ -196,6 +200,12 @@ public class TableMetadata implements SchemaElement public final DataResource resource; public TableMetadataRef ref; + // 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 columnsWithConstraints; + protected TableMetadata(Builder builder) { flags = Sets.immutableEnumSet(builder.flags); @@ -235,6 +245,22 @@ public class TableMetadata implements SchemaElement ref = TableMetadataRef.forIndex(Schema.instance, this, keyspace, indexName, id); else ref = TableMetadataRef.withInitialReference(new TableMetadataRef(Schema.instance, keyspace, name, id), this); + + List pkConstraints = new ArrayList<>(this.partitionKeyColumns.size()); + for (ColumnMetadata column : this.partitionKeyColumns) + { + if (column.hasConstraint()) + pkConstraints.add(column.getColumnConstraints()); + } + this.partitionKeyConstraints = pkConstraints; + + List columnsWithConstraints = new ArrayList<>(); + for (ColumnMetadata column : this.columns()) + { + if (column.hasConstraint() && !column.isPartitionKey() && !column.isClusteringColumn()) + columnsWithConstraints.add(column); + } + this.columnsWithConstraints = columnsWithConstraints; } public static Builder builder(String keyspace, String table) @@ -527,6 +553,19 @@ public class TableMetadata implements SchemaElement except("Missing partition keys for table %s", toString()); indexes.validate(this); + + for (ColumnMetadata columnMetadata : columns()) + { + ColumnConstraints constraints = columnMetadata.getColumnConstraints(); + try + { + constraints.validate(columnMetadata); + } + catch (InvalidConstraintDefinitionException e) + { + throw new InvalidRequestException(e.getMessage(), e); + } + } } /** @@ -1019,7 +1058,12 @@ public class TableMetadata implements SchemaElement public Builder addPartitionKeyColumn(ColumnIdentifier name, AbstractType type, @Nullable ColumnMask mask) { - return addColumn(new ColumnMetadata(keyspace, this.name, name, type, partitionKeyColumns.size(), ColumnMetadata.Kind.PARTITION_KEY, mask)); + return addPartitionKeyColumn(name, type, mask, ColumnConstraints.NO_OP); + } + + public Builder addPartitionKeyColumn(ColumnIdentifier name, AbstractType type, @Nullable ColumnMask mask, @Nonnull ColumnConstraints cqlConstraints) + { + return addColumn(new ColumnMetadata(keyspace, this.name, name, type, partitionKeyColumns.size(), ColumnMetadata.Kind.PARTITION_KEY, mask, cqlConstraints)); } public Builder addClusteringColumn(String name, AbstractType type) @@ -1039,7 +1083,12 @@ public class TableMetadata implements SchemaElement public Builder addClusteringColumn(ColumnIdentifier name, AbstractType type, @Nullable ColumnMask mask) { - return addColumn(new ColumnMetadata(keyspace, this.name, name, type, clusteringColumns.size(), ColumnMetadata.Kind.CLUSTERING, mask)); + return addClusteringColumn(name, type, mask, ColumnConstraints.NO_OP); + } + + public Builder addClusteringColumn(ColumnIdentifier name, AbstractType type, @Nullable ColumnMask mask, @Nonnull ColumnConstraints cqlConstraints) + { + return addColumn(new ColumnMetadata(keyspace, this.name, name, type, clusteringColumns.size(), ColumnMetadata.Kind.CLUSTERING, mask, cqlConstraints)); } public Builder addRegularColumn(String name, AbstractType type) @@ -1059,7 +1108,12 @@ public class TableMetadata implements SchemaElement public Builder addRegularColumn(ColumnIdentifier name, AbstractType type, @Nullable ColumnMask mask) { - return addColumn(new ColumnMetadata(keyspace, this.name, name, type, ColumnMetadata.NO_POSITION, ColumnMetadata.Kind.REGULAR, mask)); + return addRegularColumn(name, type, mask, ColumnConstraints.NO_OP); + } + + public Builder addRegularColumn(ColumnIdentifier name, AbstractType type, @Nullable ColumnMask mask, @Nonnull ColumnConstraints cqlConstraints) + { + return addColumn(new ColumnMetadata(keyspace, this.name, name, type, ColumnMetadata.NO_POSITION, ColumnMetadata.Kind.REGULAR, mask, cqlConstraints)); } public Builder addStaticColumn(String name, AbstractType type) @@ -1079,7 +1133,12 @@ public class TableMetadata implements SchemaElement public Builder addStaticColumn(ColumnIdentifier name, AbstractType type, @Nullable ColumnMask mask) { - return addColumn(new ColumnMetadata(keyspace, this.name, name, type, ColumnMetadata.NO_POSITION, ColumnMetadata.Kind.STATIC, mask)); + return addStaticColumn(name, type, mask, ColumnConstraints.NO_OP); + } + + public Builder addStaticColumn(ColumnIdentifier name, AbstractType type, @Nullable ColumnMask mask, @Nonnull ColumnConstraints cqlConstraints) + { + return addColumn(new ColumnMetadata(keyspace, this.name, name, type, ColumnMetadata.NO_POSITION, ColumnMetadata.Kind.STATIC, mask, cqlConstraints)); } public Builder addColumn(ColumnMetadata column) @@ -1376,8 +1435,7 @@ public class TableMetadata implements SchemaElement if (!hasSingleColumnPrimaryKey) appendPrimaryKey(builder); - builder.decreaseIndent() - .append(')'); + builder.decreaseIndent().append(')'); builder.append(" WITH ") .increaseIndent(); @@ -1461,8 +1519,7 @@ public class TableMetadata implements SchemaElement builder.append(", ") .appendWithSeparators(clusteringColumns, (b, c) -> b.append(c.name), ", "); - builder.append(')') - .newLine(); + builder.append(')').newLine(); } void appendTableOptions(CqlBuilder builder, boolean withInternals) @@ -1682,7 +1739,7 @@ public class TableMetadata implements SchemaElement for (ColumnMetadata c : regularAndStaticColumns) { if (c.isStatic()) - columns.add(new ColumnMetadata(c.ksName, c.cfName, c.name, c.type, -1, ColumnMetadata.Kind.REGULAR, c.getMask())); + columns.add(new ColumnMetadata(c.ksName, c.cfName, c.name, c.type, -1, ColumnMetadata.Kind.REGULAR, c.getMask(), c.getColumnConstraints())); } otherColumns = columns.iterator(); } @@ -1900,7 +1957,6 @@ public class TableMetadata implements SchemaElement size += Triggers.serializer.serializedSize(t.triggers, version); return size; - } } -} \ No newline at end of file +} diff --git a/src/java/org/apache/cassandra/tcm/membership/NodeVersion.java b/src/java/org/apache/cassandra/tcm/membership/NodeVersion.java index 88d5296352..e5b608994a 100644 --- a/src/java/org/apache/cassandra/tcm/membership/NodeVersion.java +++ b/src/java/org/apache/cassandra/tcm/membership/NodeVersion.java @@ -34,7 +34,7 @@ import static org.apache.cassandra.db.TypeSizes.sizeofUnsignedVInt; public class NodeVersion implements Comparable { public static final Serializer serializer = new Serializer(); - public static final Version CURRENT_METADATA_VERSION = Version.V5; + public static final Version CURRENT_METADATA_VERSION = Version.V6; public static final NodeVersion CURRENT = new NodeVersion(new CassandraVersion(FBUtilities.getReleaseVersionString()), CURRENT_METADATA_VERSION); private static final CassandraVersion SINCE_VERSION = CassandraVersion.CASSANDRA_5_0; diff --git a/src/java/org/apache/cassandra/tcm/serialization/Version.java b/src/java/org/apache/cassandra/tcm/serialization/Version.java index 55bbbfb04e..50e1792e23 100644 --- a/src/java/org/apache/cassandra/tcm/serialization/Version.java +++ b/src/java/org/apache/cassandra/tcm/serialization/Version.java @@ -51,6 +51,10 @@ public enum Version * - PreInitialize includes datacenter (affects local serialization on first CMS node only) */ V5(5), + /** + * CEP-42 - Constraints framework. New version due to modifications in table metadata serialization. + */ + V6(6), UNKNOWN(Integer.MAX_VALUE); diff --git a/test/distributed/org/apache/cassandra/distributed/test/ColumnConstraintsTest.java b/test/distributed/org/apache/cassandra/distributed/test/ColumnConstraintsTest.java new file mode 100644 index 0000000000..7d2aeed896 --- /dev/null +++ b/test/distributed/org/apache/cassandra/distributed/test/ColumnConstraintsTest.java @@ -0,0 +1,302 @@ +/* + * 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.distributed.test; + +import java.io.IOException; +import java.util.Map; +import java.util.Set; + +import org.apache.cassandra.exceptions.InvalidRequestException; +import org.junit.Test; + +import org.apache.cassandra.distributed.Cluster; +import org.apache.cassandra.distributed.api.ConsistencyLevel; +import org.assertj.core.api.Assertions; +import org.assertj.core.api.Condition; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +public class ColumnConstraintsTest extends TestBaseImpl +{ + final static Map RELATIONS_MAP = Map.of("st", "<", + "set", "<=", + "et", "=", + "net", "!=", + "bt", ">", + "bet", ">="); + + @Test + public void testInvalidConstraintsExceptions() throws IOException + { + final String tableName = KEYSPACE + ".tbl1"; + + try (Cluster cluster = init(Cluster.build(3).start())) + { + assertThrowsInvalidConstraintException(cluster, String.format("CREATE TABLE %s (pk int, ck1 text CHECK ck1 < 100, ck2 int, v int, " + + "PRIMARY KEY ((pk), ck1, ck2));", tableName), + "ck1 is not a number"); + + assertThrowsInvalidConstraintException(cluster, String.format("CREATE TABLE %s (pk int, ck1 int CHECK LENGTH(ck1) < 100, ck2 int, v int, " + + "PRIMARY KEY ((pk), ck1, ck2));", tableName), + "Column should be of type class org.apache.cassandra.db.marshal.UTF8Type or " + + "class org.apache.cassandra.db.marshal.AsciiType but got class org.apache.cassandra.db.marshal.Int32Type"); + } + } + + @Test + public void testUpdateConstraint() throws IOException + { + final String tableName = KEYSPACE + ".tbl1"; + + try (Cluster cluster = init(Cluster.build(3).start())) + { + String createTableStatement = "CREATE TABLE %s (pk int, ck1 int CHECK ck1 < 100, ck2 int, v int, PRIMARY KEY ((pk), ck1, ck2));"; + cluster.schemaChange(String.format(createTableStatement, tableName)); + + String insertStatement = "INSERT INTO %s (pk, ck1, ck2, v) VALUES (1, 2, 200, 3)"; + + cluster.coordinator(1).execute(String.format("ALTER TABLE %s ALTER ck2 CHECK ck2 < 100", tableName), ConsistencyLevel.ALL); + + // Can't insert + assertThrowsConstraintViolationException(cluster, + String.format(insertStatement, tableName), + "ck1 value length should be smaller than 100"); + + cluster.coordinator(1).execute(String.format("ALTER TABLE %s ALTER ck2 DROP CHECK", tableName), ConsistencyLevel.ALL); + + // Can insert after droping the constraint + cluster.coordinator(1).execute(String.format(insertStatement, tableName), ConsistencyLevel.ALL); + } + } + + @Test + public void testConstraintWithJsonInsert() throws IOException + { + final String tableName = KEYSPACE + ".tbl1"; + + try (Cluster cluster = init(Cluster.build(3).start())) + { + String createTableStatement = "CREATE TABLE %s (pk int, ck1 int CHECK ck1 < 100, ck2 int, v uuid, PRIMARY KEY ((pk), ck1, ck2));"; + cluster.schemaChange(String.format(createTableStatement, tableName)); + + cluster.coordinator(1).execute(String.format("INSERT INTO %s JSON '{\"pk\" : 1, \"ck1\" : 2, \"ck2\" : 2, \"v\" : \"ac064e40-0417-4a4a-bf53-b7cf145afdc2\" }'", tableName), ConsistencyLevel.ALL); + + assertThrowsConstraintViolationException(cluster, + String.format("INSERT INTO %s JSON '{\"pk\" : 1, \"ck1\" : 200, \"ck2\" : 2, \"v\" : \"ac064e40-0417-4a4a-bf53-b7cf145afdc2\" }'", tableName), + "ck1 value length should be smaller than 100"); + + assertThrowsConstraintViolationException(cluster, + String.format("INSERT INTO %s JSON '{\"pk\" : 1, \"ck1\": 100, \"ck2\" : 2, \"v\" : \"ac064e40-0417-4a4a-bf53-b7cf145afdc2\" }'", tableName), + "ck1 value length should be smaller than 100"); + } + } + + @Test + public void testScalarTableLevelConstraint() throws IOException + { + Set typesSet = Set.of("int", "double", "float", "decimal"); + + try (Cluster cluster = init(Cluster.build(3).start())) + { + // Create tables + for (String type : typesSet) + { + for (Map.Entry relation : RELATIONS_MAP.entrySet()) + { + String tableName = String.format(KEYSPACE + ".%s_tbl1_%s", type, relation.getKey()); + String createTableStatementSmallerThan = "CREATE TABLE " + tableName + " (pk int, ck1 " + type + " CHECK ck1 " + relation.getValue() + " 100, ck2 int, v int, PRIMARY KEY ((pk), ck1, ck2));"; + cluster.schemaChange(createTableStatementSmallerThan); + } + } + + for (String type : typesSet) + { + // st + String tableName = String.format(KEYSPACE + ".%s_tbl1_%s", type, "st"); + + cluster.coordinator(1).execute(String.format("INSERT INTO %s (pk, ck1, ck2, v) VALUES (1, 2, 2, 3)", tableName), ConsistencyLevel.ALL); + + assertThrowsConstraintViolationException(cluster, + String.format("INSERT INTO %s (pk, ck1, ck2, v) VALUES (1, 200, 2, 3)", tableName), + "ck1 value should be smaller than 100.0"); + + assertThrowsConstraintViolationException(cluster, + String.format("INSERT INTO %s (pk, ck1, ck2, v) VALUES (1, 100, 2, 3)", tableName), + "ck1 value length be smaller than 100.0"); + + // set + tableName = String.format(KEYSPACE + ".%s_tbl1_%s", type, "set"); + + cluster.coordinator(1).execute(String.format("INSERT INTO %s (pk, ck1, ck2, v) VALUES (1, 2, 2, 3)", tableName), ConsistencyLevel.ALL); + cluster.coordinator(1).execute(String.format("INSERT INTO %s (pk, ck1, ck2, v) VALUES (1, 100, 2, 3)", tableName), ConsistencyLevel.ALL); + + assertThrowsConstraintViolationException(cluster, + String.format("INSERT INTO %s (pk, ck1, ck2, v) VALUES (1, 200, 2, 3)", tableName), + "ck1 value should be smaller than 100.0"); + + // et + tableName = String.format(KEYSPACE + ".%s_tbl1_%s", type, "et"); + + cluster.coordinator(1).execute(String.format("INSERT INTO %s (pk, ck1, ck2, v) VALUES (1, 100, 2, 3)", tableName), ConsistencyLevel.ALL); + + assertThrowsConstraintViolationException(cluster, + String.format("INSERT INTO %s (pk, ck1, ck2, v) VALUES (1, 200, 2, 3)", tableName), + "ck1 value should be smaller than 100.0"); + + // net + tableName = String.format(KEYSPACE + ".%s_tbl1_%s", type, "net"); + + cluster.coordinator(1).execute(String.format("INSERT INTO %s (pk, ck1, ck2, v) VALUES (1, 200, 2, 3)", tableName), ConsistencyLevel.ALL); + + assertThrowsConstraintViolationException(cluster, + String.format("INSERT INTO %s (pk, ck1, ck2, v) VALUES (1, 100, 2, 3)", tableName), + "ck1 value should be smaller than 100.0"); + + // bt + tableName = String.format(KEYSPACE + ".%s_tbl1_%s", type, "bt"); + + cluster.coordinator(1).execute(String.format("INSERT INTO %s (pk, ck1, ck2, v) VALUES (1, 200, 2, 3)", tableName), ConsistencyLevel.ALL); + + assertThrowsConstraintViolationException(cluster, + String.format("INSERT INTO %s (pk, ck1, ck2, v) VALUES (1, 1, 2, 3)", tableName), + "ck1 value should be smaller than 100.0"); + assertThrowsConstraintViolationException(cluster, + String.format("INSERT INTO %s (pk, ck1, ck2, v) VALUES (1, 100, 2, 3)", tableName), + "ck1 value should be smaller than 100.0"); + + // bet + tableName = String.format(KEYSPACE + ".%s_tbl1_%s", type, "bet"); + + cluster.coordinator(1).execute(String.format("INSERT INTO %s (pk, ck1, ck2, v) VALUES (1, 200, 2, 3)", tableName), ConsistencyLevel.ALL); + cluster.coordinator(1).execute(String.format("INSERT INTO %s (pk, ck1, ck2, v) VALUES (1, 100, 2, 3)", tableName), ConsistencyLevel.ALL); + + assertThrowsConstraintViolationException(cluster, + String.format("INSERT INTO %s (pk, ck1, ck2, v) VALUES (1, 1, 2, 3)", tableName), + "ck1 value should be smaller than 100.0"); + + } + } + } + + @Test + public void testLengthTableLevelConstraint() throws IOException + { + Set typesSet = Set.of("varchar", "text", "blob", "ascii"); + + try (Cluster cluster = init(Cluster.build(3).start())) + { + // Create tables + for (String type : typesSet) + { + for (Map.Entry relation : RELATIONS_MAP.entrySet()) + { + String tableName = String.format(KEYSPACE + ".%s_tbl1_%s", type, relation.getKey()); + String createTableStatementSmallerThan = "CREATE TABLE " + tableName + " (pk " + type + " CHECK LENGTH(pk) " + relation.getValue() + " 4, ck1 int, ck2 int, v int, PRIMARY KEY ((pk), ck1, ck2));"; + cluster.schemaChange(createTableStatementSmallerThan); + } + } + + for (String type : typesSet) + { + String value = "'%s'"; + if (type.equals("blob")) + value = "textAsBlob('%s')"; + // st + String tableName = String.format(KEYSPACE + ".%s_tbl1_%s", type, "st"); + cluster.coordinator(1).execute(String.format("INSERT INTO " + tableName + " (pk, ck1, ck2, v) VALUES (" + value + ", 2, 2, 3)", "foo"), ConsistencyLevel.ALL); + + assertThrowsConstraintViolationException(cluster, + String.format("INSERT INTO " + tableName + " (pk, ck1, ck2, v) VALUES (" + value + ", 200, 2, 3)", "fooo"), + "ck1 value length should be smaller than 100"); + + assertThrowsConstraintViolationException(cluster, + String.format("INSERT INTO " + tableName + " (pk, ck1, ck2, v) VALUES (" + value + ", 100, 2, 3)", "foooo"), + "ck1 value length should be smaller than 100"); + + // set + tableName = String.format(KEYSPACE + ".%s_tbl1_%s", type, "set"); + cluster.coordinator(1).execute(String.format("INSERT INTO " + tableName + " (pk, ck1, ck2, v) VALUES (" + value + ", 2, 2, 3)", "foo"), ConsistencyLevel.ALL); + cluster.coordinator(1).execute(String.format("INSERT INTO " + tableName + " (pk, ck1, ck2, v) VALUES (" + value + ", 2, 2, 3)", "fooo"), ConsistencyLevel.ALL); + + assertThrowsConstraintViolationException(cluster, + String.format("INSERT INTO " + tableName + " (pk, ck1, ck2, v) VALUES (" + value + ", 100, 2, 3)", "foooo"), + "ck1 value length should be smaller than 100"); + + // et + tableName = String.format(KEYSPACE + ".%s_tbl1_%s", type, "et"); + cluster.coordinator(1).execute(String.format("INSERT INTO " + tableName + " (pk, ck1, ck2, v) VALUES (" + value + ", 2, 2, 3)", "fooo"), ConsistencyLevel.ALL); + + assertThrowsConstraintViolationException(cluster, + String.format("INSERT INTO " + tableName + " (pk, ck1, ck2, v) VALUES (" + value + ", 100, 2, 3)", "foooo"), + "ck1 value length should be smaller than 100"); + + assertThrowsConstraintViolationException(cluster, + String.format("INSERT INTO " + tableName + " (pk, ck1, ck2, v) VALUES (" + value + ", 100, 2, 3)", "fo"), + "ck1 value length should be smaller than 100"); + + // net + tableName = String.format(KEYSPACE + ".%s_tbl1_%s", type, "net"); + cluster.coordinator(1).execute(String.format("INSERT INTO " + tableName + " (pk, ck1, ck2, v) VALUES (" + value + ", 2, 2, 3)", "foooo"), ConsistencyLevel.ALL); + cluster.coordinator(1).execute(String.format("INSERT INTO " + tableName + " (pk, ck1, ck2, v) VALUES (" + value + ", 2, 2, 3)", "foo"), ConsistencyLevel.ALL); + + assertThrowsConstraintViolationException(cluster, + String.format("INSERT INTO " + tableName + " (pk, ck1, ck2, v) VALUES (" + value + ", 100, 2, 3)", "fooo"), + "ck1 value length should be smaller than 100"); + + // bt + tableName = String.format(KEYSPACE + ".%s_tbl1_%s", type, "bt"); + cluster.coordinator(1).execute(String.format("INSERT INTO " + tableName + " (pk, ck1, ck2, v) VALUES (" + value + ", 2, 2, 3)", "foooo"), ConsistencyLevel.ALL); + + assertThrowsConstraintViolationException(cluster, + String.format("INSERT INTO " + tableName + " (pk, ck1, ck2, v) VALUES (" + value + ", 100, 2, 3)", "fooo"), + "ck1 value length should be smaller than 100"); + + assertThrowsConstraintViolationException(cluster, + String.format("INSERT INTO " + tableName + " (pk, ck1, ck2, v) VALUES (" + value + ", 100, 2, 3)", "foo"), + "ck1 value length should be smaller than 100"); + + // bet + tableName = String.format(KEYSPACE + ".%s_tbl1_%s", type, "bet"); + cluster.coordinator(1).execute(String.format("INSERT INTO " + tableName + " (pk, ck1, ck2, v) VALUES (" + value + ", 2, 2, 3)", "foooo"), ConsistencyLevel.ALL); + cluster.coordinator(1).execute(String.format("INSERT INTO " + tableName + " (pk, ck1, ck2, v) VALUES (" + value + ", 2, 2, 3)", "fooo"), ConsistencyLevel.ALL); + + assertThrowsConstraintViolationException(cluster, + String.format("INSERT INTO " + tableName + " (pk, ck1, ck2, v) VALUES (" + value + ", 100, 2, 3)", "foo"), + "ck1 value length should be smaller than 100"); + } + } + } + + private void assertThrowsConstraintViolationException(Cluster cluster, String statement, String description) + { + Assertions.assertThatThrownBy(() -> cluster.coordinator(1).execute(statement, ConsistencyLevel.ALL)) + .describedAs(description) + .has(new Condition(t -> t.getClass().getCanonicalName() + .equals(InvalidRequestException.class.getCanonicalName()), description)); + } + + private void assertThrowsInvalidConstraintException(Cluster cluster, String statement, String description) + { + Assertions.setMaxStackTraceElementsDisplayed(100); + assertThatThrownBy(() -> cluster.schemaChange(statement)) + .describedAs(description) + .has(new Condition(t -> t.getClass().getCanonicalName() + .equals(InvalidRequestException.class.getCanonicalName()), description)); + } +} diff --git a/test/distributed/org/apache/cassandra/distributed/test/cdc/ToggleCDCOnRepair.java b/test/distributed/org/apache/cassandra/distributed/test/cdc/ToggleCDCOnRepair.java new file mode 100644 index 0000000000..0fd09c6828 --- /dev/null +++ b/test/distributed/org/apache/cassandra/distributed/test/cdc/ToggleCDCOnRepair.java @@ -0,0 +1,98 @@ +/* + * 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.distributed.test.cdc; + +import java.util.function.Consumer; + + +import org.apache.cassandra.db.commitlog.CommitLog; +import org.apache.cassandra.db.commitlog.CommitLogSegment; +import org.apache.cassandra.distributed.Cluster; +import org.apache.cassandra.distributed.api.Feature; +import org.apache.cassandra.distributed.test.TestBaseImpl; + +import static org.apache.cassandra.distributed.shared.AssertUtils.assertRows; +import static org.apache.cassandra.distributed.shared.AssertUtils.assertTrue; +import static org.apache.cassandra.distributed.shared.AssertUtils.row; + +public abstract class ToggleCDCOnRepair extends TestBaseImpl +{ + Consumer getRepairEnabledRepairAssertion() + { + return cluster -> { + cluster.get(2).runOnInstance(() -> { + boolean containCDCInLog = CommitLog.instance.segmentManager + .getActiveSegments() + .stream() + .anyMatch(s -> s.getCDCState() == CommitLogSegment.CDCState.CONTAINS); + assertTrue("Mutation should be added to commit log when cdc_on_repair_enabled is true", + containCDCInLog); + }); + }; + } + + Consumer getRepairDisabledRepairAssertion() + { + return cluster -> { + cluster.get(2).runOnInstance(() -> { + boolean containCDCInLog = CommitLog.instance.segmentManager + .getActiveSegments() + .stream() + .allMatch(s -> s.getCDCState() != CommitLogSegment.CDCState.CONTAINS); + assertTrue("No mutation should be added to commit log when cdc_on_repair_enabled is false", + containCDCInLog); + }); + }; + } + + // test helper to repair data between nodes when cdc_on_repair_enabled is on or off. + void testCDCOnRepairEnabled(boolean enabled, Consumer assertion, boolean constraintsEnabled) throws Exception + { + try (Cluster cluster = init(Cluster.build(2) + .withConfig(c -> c.set("cdc_enabled", true) + .set("cdc_on_repair_enabled", enabled) + .with(Feature.NETWORK) + .with(Feature.GOSSIP)) + .start())) + { + cluster.schemaChange(withKeyspace("CREATE TABLE %s.tbl (k INT PRIMARY KEY, v INT) WITH cdc=true")); + + // Data only in node1 + cluster.get(1).executeInternal(withKeyspace("INSERT INTO %s.tbl (k, v) VALUES (1, 1)")); + + if (constraintsEnabled) + cluster.schemaChange(withKeyspace("ALTER TABLE %s.tbl ALTER v CHECK v != 1")); + + Object[][] result = cluster.get(1).executeInternal(withKeyspace("SELECT * FROM %s.tbl WHERE k = 1")); + assertRows(result, row(1, 1)); + result = cluster.get(2).executeInternal(withKeyspace("SELECT * FROM %s.tbl WHERE k = 1")); + assertRows(result); + + // repair + cluster.get(1).flush(KEYSPACE); + cluster.get(2).nodetool("repair", KEYSPACE, "tbl"); + + // verify node2 now have data + result = cluster.get(2).executeInternal(withKeyspace("SELECT * FROM %s.tbl WHERE k = 1")); + assertRows(result, row(1, 1)); + + assertion.accept(cluster); + } + } +} diff --git a/test/distributed/org/apache/cassandra/distributed/test/cdc/ToggleCDCOnRepairEnabledTest.java b/test/distributed/org/apache/cassandra/distributed/test/cdc/ToggleCDCOnRepairEnabledTest.java index 499cf076af..63aa4c8e25 100644 --- a/test/distributed/org/apache/cassandra/distributed/test/cdc/ToggleCDCOnRepairEnabledTest.java +++ b/test/distributed/org/apache/cassandra/distributed/test/cdc/ToggleCDCOnRepairEnabledTest.java @@ -18,80 +18,21 @@ package org.apache.cassandra.distributed.test.cdc; -import java.util.function.Consumer; import org.junit.Test; -import org.apache.cassandra.db.commitlog.CommitLog; -import org.apache.cassandra.db.commitlog.CommitLogSegment; -import org.apache.cassandra.distributed.Cluster; -import org.apache.cassandra.distributed.api.Feature; -import org.apache.cassandra.distributed.test.TestBaseImpl; -import static org.apache.cassandra.distributed.shared.AssertUtils.assertRows; -import static org.apache.cassandra.distributed.shared.AssertUtils.assertTrue; -import static org.apache.cassandra.distributed.shared.AssertUtils.row; - -public class ToggleCDCOnRepairEnabledTest extends TestBaseImpl +public class ToggleCDCOnRepairEnabledTest extends ToggleCDCOnRepair { @Test public void testCDCOnRepairIsEnabled() throws Exception { - testCDCOnRepairEnabled(true, cluster -> { - cluster.get(2).runOnInstance(() -> { - boolean containCDCInLog = CommitLog.instance.segmentManager - .getActiveSegments() - .stream() - .anyMatch(s -> s.getCDCState() == CommitLogSegment.CDCState.CONTAINS); - assertTrue("Mutation should be added to commit log when cdc_on_repair_enabled is true", - containCDCInLog); - }); - }); + testCDCOnRepairEnabled(true, getRepairEnabledRepairAssertion(), false); } @Test public void testCDCOnRepairIsDisabled() throws Exception { - testCDCOnRepairEnabled(false, cluster -> { - cluster.get(2).runOnInstance(() -> { - boolean containCDCInLog = CommitLog.instance.segmentManager - .getActiveSegments() - .stream() - .allMatch(s -> s.getCDCState() != CommitLogSegment.CDCState.CONTAINS); - assertTrue("No mutation should be added to commit log when cdc_on_repair_enabled is false", - containCDCInLog); - }); - }); - } - - // test helper to repair data between nodes when cdc_on_repair_enabled is on or off. - private void testCDCOnRepairEnabled(boolean enabled, Consumer assertion) throws Exception - { - try (Cluster cluster = init(Cluster.build(2) - .withConfig(c -> c.set("cdc_enabled", true) - .set("cdc_on_repair_enabled", enabled) - .with(Feature.NETWORK) - .with(Feature.GOSSIP)) - .start())) - { - cluster.schemaChange(withKeyspace("CREATE TABLE %s.tbl (k INT PRIMARY KEY, v INT) WITH cdc=true")); - - // Data only in node1 - cluster.get(1).executeInternal(withKeyspace("INSERT INTO %s.tbl (k, v) VALUES (1, 1)")); - Object[][] result = cluster.get(1).executeInternal(withKeyspace("SELECT * FROM %s.tbl WHERE k = 1")); - assertRows(result, row(1, 1)); - result = cluster.get(2).executeInternal(withKeyspace("SELECT * FROM %s.tbl WHERE k = 1")); - assertRows(result); - - // repair - cluster.get(1).flush(KEYSPACE); - cluster.get(2).nodetool("repair", KEYSPACE, "tbl"); - - // verify node2 now have data - result = cluster.get(2).executeInternal(withKeyspace("SELECT * FROM %s.tbl WHERE k = 1")); - assertRows(result, row(1, 1)); - - assertion.accept(cluster); - } + testCDCOnRepairEnabled(false, getRepairDisabledRepairAssertion(), false); } } diff --git a/test/distributed/org/apache/cassandra/distributed/test/cdc/ToggleCDCWithConstraintsOnRepairEnabledTest.java b/test/distributed/org/apache/cassandra/distributed/test/cdc/ToggleCDCWithConstraintsOnRepairEnabledTest.java new file mode 100644 index 0000000000..3697209df7 --- /dev/null +++ b/test/distributed/org/apache/cassandra/distributed/test/cdc/ToggleCDCWithConstraintsOnRepairEnabledTest.java @@ -0,0 +1,38 @@ +/* + * 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.distributed.test.cdc; + + +import org.junit.Test; + + +public class ToggleCDCWithConstraintsOnRepairEnabledTest extends ToggleCDCOnRepair +{ + @Test + public void testCDCWithConstraintsOnRepairIsEnabled() throws Exception + { + testCDCOnRepairEnabled(true, getRepairEnabledRepairAssertion(), true); + } + + @Test + public void testCDCWithConstraintsOnRepairIsDisabled() throws Exception + { + testCDCOnRepairEnabled(false, getRepairDisabledRepairAssertion(), true); + } +} diff --git a/test/distributed/org/apache/cassandra/distributed/test/log/SnapshotTest.java b/test/distributed/org/apache/cassandra/distributed/test/log/SnapshotTest.java index 8e424e0bc5..775e7ffe79 100644 --- a/test/distributed/org/apache/cassandra/distributed/test/log/SnapshotTest.java +++ b/test/distributed/org/apache/cassandra/distributed/test/log/SnapshotTest.java @@ -51,6 +51,7 @@ public class SnapshotTest extends TestBaseImpl .start())) { cluster.schemaChange(withKeyspace("create table %s.tbl (id int primary key, x int)")); + cluster.schemaChange(withKeyspace("create table %s.tblconstraints (id int primary key, x int check x > 100 and x < 200, v text check LENGTH(v) > 10)")); cluster.schemaChange(withKeyspace("CREATE OR REPLACE FUNCTION %s.fLog (input double) CALLED ON NULL INPUT RETURNS double LANGUAGE java AS 'return Double.valueOf(Math.log(input.doubleValue()));';")); cluster.schemaChange(withKeyspace("CREATE OR REPLACE FUNCTION %s.avgState ( state tuple, val int ) CALLED ON NULL INPUT RETURNS tuple LANGUAGE java AS \n" + " 'if (val !=null) { state.setInt(0, state.getInt(0)+1); state.setLong(1, state.getLong(1)+val.intValue()); } return state;'; ")); diff --git a/test/unit/org/apache/cassandra/contraints/AlterTableWithTableConstraintValidationTest.java b/test/unit/org/apache/cassandra/contraints/AlterTableWithTableConstraintValidationTest.java new file mode 100644 index 0000000000..0229d87e25 --- /dev/null +++ b/test/unit/org/apache/cassandra/contraints/AlterTableWithTableConstraintValidationTest.java @@ -0,0 +1,208 @@ +/* + * 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.contraints; + +import org.junit.Test; + + +public class AlterTableWithTableConstraintValidationTest extends CqlConstraintValidationTester +{ + + @Test + public void testCreateTableWithColumnNamedConstraintDescribeTableNonFunction() throws Throwable + { + String table = createTable("CREATE TABLE %s (pk int, ck1 int CHECK ck1 < 100, ck2 int, v int, PRIMARY KEY ((pk),ck1, ck2)) WITH CLUSTERING ORDER BY (ck1 ASC);"); + + execute("ALTER TABLE %s ALTER ck1 DROP CHECK"); + + String tableCreateStatement = "CREATE TABLE " + KEYSPACE + "." + table + " (\n" + + " pk int,\n" + + " ck1 int,\n" + + " ck2 int,\n" + + " v int,\n" + + " PRIMARY KEY (pk, ck1, ck2)\n" + + ") WITH CLUSTERING ORDER BY (ck1 ASC, ck2 ASC)\n" + + " AND " + tableParametersCql(); + + assertRowsNet(executeDescribeNet(KEYSPACE, "DESCRIBE TABLE " + KEYSPACE + "." + table), + row(KEYSPACE, + "table", + table, + tableCreateStatement)); + } + + @Test + public void testCreateTableAddConstraint() throws Throwable + { + String table = createTable("CREATE TABLE %s (pk int, ck1 int, ck2 int, v int, PRIMARY KEY ((pk),ck1, ck2)) WITH CLUSTERING ORDER BY (ck1 ASC);"); + + execute("ALTER TABLE %s ALTER ck1 CHECK ck1 < 100 AND ck1 > 10"); + + String tableCreateStatement = "CREATE TABLE " + KEYSPACE + "." + table + " (\n" + + " pk int,\n" + + " ck1 int CHECK ck1 < 100 AND ck1 > 10,\n" + + " ck2 int,\n" + + " v int,\n" + + " PRIMARY KEY (pk, ck1, ck2)\n" + + ") WITH CLUSTERING ORDER BY (ck1 ASC, ck2 ASC)\n" + + " AND " + tableParametersCql(); + + assertRowsNet(executeDescribeNet(KEYSPACE, "DESCRIBE TABLE " + KEYSPACE + "." + table), + row(KEYSPACE, + "table", + table, + tableCreateStatement)); + } + + @Test + public void testCreateTableAddMultipleConstraints() throws Throwable + { + String table = createTable("CREATE TABLE %s (pk int, ck1 int, ck2 int, v int, PRIMARY KEY ((pk),ck1, ck2)) WITH CLUSTERING ORDER BY (ck1 ASC);"); + + execute("ALTER TABLE %s ALTER ck1 CHECK ck1 < 100"); + execute("ALTER TABLE %s ALTER ck2 CHECK ck2 > 10"); + + String tableCreateStatement = "CREATE TABLE " + KEYSPACE + "." + table + " (\n" + + " pk int,\n" + + " ck1 int CHECK ck1 < 100,\n" + + " ck2 int CHECK ck2 > 10,\n" + + " v int,\n" + + " PRIMARY KEY (pk, ck1, ck2)\n" + + ") WITH CLUSTERING ORDER BY (ck1 ASC, ck2 ASC)\n" + + " AND " + tableParametersCql(); + + assertRowsNet(executeDescribeNet(KEYSPACE, "DESCRIBE TABLE " + KEYSPACE + "." + table), + row(KEYSPACE, + "table", + table, + tableCreateStatement)); + } + + @Test + public void testCreateTableAddMultipleMixedConstraints() throws Throwable + { + String table = createTable("CREATE TABLE %s (pk int, ck1 int, ck2 text, v int, PRIMARY KEY ((pk), ck1, ck2)) WITH CLUSTERING ORDER BY (ck1 ASC);"); + + execute("ALTER TABLE %s ALTER ck1 CHECK ck1 < 100"); + + String tableCreateStatement = "CREATE TABLE " + KEYSPACE + "." + table + " (\n" + + " pk int,\n" + + " ck1 int CHECK ck1 < 100,\n" + + " ck2 text,\n" + + " v int,\n" + + " PRIMARY KEY (pk, ck1, ck2)\n" + + ") WITH CLUSTERING ORDER BY (ck1 ASC, ck2 ASC)\n" + + " AND " + tableParametersCql(); + + assertRowsNet(executeDescribeNet(KEYSPACE, "DESCRIBE TABLE " + KEYSPACE + "." + table), + row(KEYSPACE, + "table", + table, + tableCreateStatement)); + + execute("ALTER TABLE %s ALTER ck2 CHECK LENGTH(ck2) = 4"); + + tableCreateStatement = "CREATE TABLE " + KEYSPACE + "." + table + " (\n" + + " pk int,\n" + + " ck1 int CHECK ck1 < 100,\n" + + " ck2 text CHECK LENGTH(ck2) = 4,\n" + + " v int,\n" + + " PRIMARY KEY (pk, ck1, ck2)\n" + + ") WITH CLUSTERING ORDER BY (ck1 ASC, ck2 ASC)\n" + + " AND " + tableParametersCql(); + + assertRowsNet(executeDescribeNet(KEYSPACE, "DESCRIBE TABLE " + KEYSPACE + "." + table), + row(KEYSPACE, + "table", + table, + tableCreateStatement)); + } + + @Test + public void testCreateTableAddAndRemoveConstraint() throws Throwable + { + String table = createTable("CREATE TABLE %s (pk int, ck1 int, ck2 text, v int, PRIMARY KEY ((pk),ck1, ck2)) WITH CLUSTERING ORDER BY (ck1 ASC);"); + + execute("ALTER TABLE %s ALTER ck1 CHECK ck1 < 100"); + + String tableCreateStatement = "CREATE TABLE " + KEYSPACE + "." + table + " (\n" + + " pk int,\n" + + " ck1 int CHECK ck1 < 100,\n" + + " ck2 text,\n" + + " v int,\n" + + " PRIMARY KEY (pk, ck1, ck2)\n" + + ") WITH CLUSTERING ORDER BY (ck1 ASC, ck2 ASC)\n" + + " AND " + tableParametersCql(); + + assertRowsNet(executeDescribeNet(KEYSPACE, "DESCRIBE TABLE " + KEYSPACE + "." + table), + row(KEYSPACE, + "table", + table, + tableCreateStatement)); + + execute("ALTER TABLE %s ALTER ck1 DROP CHECK"); + + String tableCreateStatement2 = "CREATE TABLE " + KEYSPACE + "." + table + " (\n" + + " pk int,\n" + + " ck1 int,\n" + + " ck2 text,\n" + + " v int,\n" + + " PRIMARY KEY (pk, ck1, ck2)\n" + + ") WITH CLUSTERING ORDER BY (ck1 ASC, ck2 ASC)\n" + + " AND " + tableParametersCql(); + + assertRowsNet(executeDescribeNet(KEYSPACE, "DESCRIBE TABLE " + KEYSPACE + "." + table), + row(KEYSPACE, + "table", + table, + tableCreateStatement2)); + } + + @Test + public void testAlterWithConstraintsAndCdcEnabled() throws Throwable + { + createTable("CREATE TABLE %s (pk text, ck1 int, ck2 int, PRIMARY KEY ((pk),ck1, ck2)) WITH cdc = true;"); + // It works + execute("ALTER TABLE %s ALTER ck1 CHECK ck1 < 100"); + } + + @Test + public void testAlterWithCdcAndPKConstraintsEnabled() throws Throwable + { + createTable("CREATE TABLE %s (pk text CHECK length(pk) = 100, ck1 int, ck2 int, PRIMARY KEY ((pk), ck1, ck2));"); + // It works + execute("ALTER TABLE %s WITH cdc = true"); + } + + @Test + public void testAlterWithCdcAndRegularConstraintsEnabled() throws Throwable + { + createTable("CREATE TABLE %s (pk text, ck1 int CHECK ck1 < 100, ck2 int, PRIMARY KEY (pk));"); + // It works + execute("ALTER TABLE %s WITH cdc = true"); + } + + @Test + public void testAlterWithCdcAndClusteringConstraintsEnabled() throws Throwable + { + createTable("CREATE TABLE %s (pk text, ck1 int CHECK ck1 < 100, ck2 int, PRIMARY KEY ((pk), ck1, ck2));"); + // It works + execute("ALTER TABLE %s WITH cdc = true"); + } +} diff --git a/test/unit/org/apache/cassandra/contraints/ColumnConstraintsTest.java b/test/unit/org/apache/cassandra/contraints/ColumnConstraintsTest.java new file mode 100644 index 0000000000..45a631cca6 --- /dev/null +++ b/test/unit/org/apache/cassandra/contraints/ColumnConstraintsTest.java @@ -0,0 +1,50 @@ +/* + * 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.contraints; + +import org.junit.Test; + +import org.apache.cassandra.cql3.constraints.ColumnConstraint.ConstraintType; + +import static org.apache.cassandra.cql3.constraints.ColumnConstraint.ConstraintType.COMPOSED; +import static org.apache.cassandra.cql3.constraints.ColumnConstraint.ConstraintType.FUNCTION; +import static org.apache.cassandra.cql3.constraints.ColumnConstraint.ConstraintType.SCALAR; +import static org.junit.Assert.assertEquals; + +public class ColumnConstraintsTest +{ + private static final ConstraintType[] EXPECTED_VALUES = { COMPOSED, FUNCTION, SCALAR }; + + @Test + public void testEnumCodesAndNames() + { + ConstraintType[] values = ConstraintType.values(); + + for (int i = 0; i < values.length; i++) + { + assertEquals("Column Constraint Serializer mismatch in the enum " + values[i], + EXPECTED_VALUES[i].name(), values[i].name()); + assertEquals("Column Constraint Serializer mismatch in the enum for value " + values[i], + ConstraintType.getSerializer(EXPECTED_VALUES[i].ordinal()), ConstraintType.getSerializer(i)); + } + + assertEquals("Column Constraint Serializer enum constants has changed. Update the test.", + EXPECTED_VALUES.length, values.length); + } +} diff --git a/test/unit/org/apache/cassandra/contraints/CqlConstraintValidationTester.java b/test/unit/org/apache/cassandra/contraints/CqlConstraintValidationTester.java new file mode 100644 index 0000000000..f2bc3dd1b4 --- /dev/null +++ b/test/unit/org/apache/cassandra/contraints/CqlConstraintValidationTester.java @@ -0,0 +1,81 @@ +/* + * 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.contraints; + +import java.util.Map; + +import com.datastax.driver.core.ResultSet; +import org.apache.cassandra.cql3.CQLTester; +import org.apache.cassandra.cql3.CqlBuilder; +import org.apache.cassandra.schema.CompactionParams; +import org.apache.cassandra.transport.ProtocolVersion; + +public abstract class CqlConstraintValidationTester extends CQLTester +{ + ResultSet executeDescribeNet(String cql) throws Throwable + { + return executeDescribeNet(null, cql); + } + + ResultSet executeDescribeNet(String useKs, String cql) throws Throwable + { + return executeNetWithPaging(getProtocolVersion(useKs), cql, useKs, 3); + } + + private ProtocolVersion getProtocolVersion(String useKs) throws Throwable + { + // We're using a trick here to distinguish driver sessions with a "USE keyspace" and without: + // As different ProtocolVersions use different driver instances, we use different ProtocolVersions + // for the with and without "USE keyspace" cases. + + ProtocolVersion v = useKs != null ? ProtocolVersion.CURRENT : ProtocolVersion.V6; + + if (useKs != null) + executeNet(v, "USE " + useKs); + return v; + } + + 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/contraints/CreateTableWithColumnCqlConstraintValidationTest.java b/test/unit/org/apache/cassandra/contraints/CreateTableWithColumnCqlConstraintValidationTest.java new file mode 100644 index 0000000000..97de503d1b --- /dev/null +++ b/test/unit/org/apache/cassandra/contraints/CreateTableWithColumnCqlConstraintValidationTest.java @@ -0,0 +1,1264 @@ +/* + * 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.contraints; + + +import java.util.Arrays; +import java.util.Collection; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; + +import org.apache.cassandra.exceptions.InvalidRequestException; +import org.apache.cassandra.utils.Generators; + +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; +import static accord.utils.Property.qt; +import static org.quicktheories.generators.SourceDSL.doubles; +import static org.quicktheories.generators.SourceDSL.integers; + +@RunWith(Parameterized.class) +public class CreateTableWithColumnCqlConstraintValidationTest extends CqlConstraintValidationTester +{ + + @Parameterized.Parameter + public String order; + + @Parameterized.Parameters() + public static Collection generateData() + { + return Arrays.asList(new Object[][]{ + { "ASC" }, + { "DESC" } + }); + } + + @Test + public void testCreateTableWithColumnNotNamedConstraintDescribeTableNonFunction() throws Throwable + { + String table = createTable(KEYSPACE_PER_TEST, "CREATE TABLE %s (pk int, ck1 int CHECK ck1 < 100, ck2 int, v int, PRIMARY KEY ((pk),ck1, ck2)) WITH CLUSTERING ORDER BY (ck1 ASC);"); + + String tableCreateStatement = "CREATE TABLE " + KEYSPACE_PER_TEST + "." + table + " (\n" + + " pk int,\n" + + " ck1 int CHECK ck1 < 100,\n" + + " ck2 int,\n" + + " v int,\n" + + " PRIMARY KEY (pk, ck1, ck2)\n" + + ") WITH CLUSTERING ORDER BY (ck1 ASC, ck2 ASC)\n" + + " AND " + tableParametersCql(); + + assertRowsNet(executeDescribeNet("DESCRIBE TABLE " + KEYSPACE_PER_TEST + "." + table), + row(KEYSPACE_PER_TEST, + "table", + table, + tableCreateStatement)); + } + + @Test + public void testCreateTableWithColumnMultipleConstraintsDescribeTableNonFunction() throws Throwable + { + String table = createTable(KEYSPACE_PER_TEST, "CREATE TABLE %s (pk int, ck1 int CHECK ck1 < 100 AND ck1 > 10, ck2 int, v int, PRIMARY KEY ((pk),ck1, ck2)) WITH CLUSTERING ORDER BY (ck1 ASC);"); + + String tableCreateStatement = "CREATE TABLE " + KEYSPACE_PER_TEST + "." + table + " (\n" + + " pk int,\n" + + " ck1 int CHECK ck1 < 100 AND ck1 > 10,\n" + + " ck2 int,\n" + + " v int,\n" + + " PRIMARY KEY (pk, ck1, ck2)\n" + + ") WITH CLUSTERING ORDER BY (ck1 ASC, ck2 ASC)\n" + + " AND " + tableParametersCql(); + + assertRowsNet(executeDescribeNet("DESCRIBE TABLE " + KEYSPACE_PER_TEST + "." + table), + row(KEYSPACE_PER_TEST, + "table", + table, + tableCreateStatement)); + } + + @Test + public void testCreateTableWithColumnNotNamedConstraintDescribeTableFunction() throws Throwable + { + String table = createTable(KEYSPACE_PER_TEST, "CREATE TABLE %s (pk int, ck1 text CHECK LENGTH(ck1) = 4, ck2 int, v int, PRIMARY KEY ((pk),ck1, ck2)) WITH CLUSTERING ORDER BY (ck1 ASC);"); + + String tableCreateStatement = "CREATE TABLE " + KEYSPACE_PER_TEST + "." + table + " (\n" + + " pk int,\n" + + " ck1 text CHECK LENGTH(ck1) = 4,\n" + + " ck2 int,\n" + + " v int,\n" + + " PRIMARY KEY (pk, ck1, ck2)\n" + + ") WITH CLUSTERING ORDER BY (ck1 ASC, ck2 ASC)\n" + + " AND " + tableParametersCql(); + + assertRowsNet(executeDescribeNet("DESCRIBE TABLE " + KEYSPACE_PER_TEST + "." + table), + row(KEYSPACE_PER_TEST, + "table", + table, + tableCreateStatement)); + } + + // SCALAR + @Test + public void testCreateTableWithColumnWithClusteringColumnLessThanScalarConstraintInteger() throws Throwable + { + createTable("CREATE TABLE %s (pk int, ck1 int CHECK ck1 < 4, ck2 int, v int, PRIMARY KEY ((pk),ck1, ck2)) WITH CLUSTERING ORDER BY (ck1 " + order + ");"); + + // Valid + qt().forAll(Generators.toGen(integers().between(0, 3))) + .check(d -> execute("INSERT INTO %s (pk, ck1, ck2, v) VALUES (1, " + d + ", 3, 4)")); + + // Invalid + qt().forAll(Generators.toGen(integers().between(4, 100))) + .check(d -> { + try + { + assertInvalidThrow(InvalidRequestException.class, "INSERT INTO %s (pk, ck1, ck2, v) VALUES (1, " + d + ", 3, 4)"); + } + catch (Throwable e) + { + throw new RuntimeException(e); + } + }); + } + + @Test + public void testCreateTableWithColumnWithClusteringColumnBiggerThanScalarConstraint() throws Throwable + { + createTable("CREATE TABLE %s (pk int, ck1 int CHECK ck1 > 4, ck2 int, v int, PRIMARY KEY ((pk),ck1, ck2)) WITH CLUSTERING ORDER BY (ck1 " + order + ");"); + + // Valid + qt().forAll(Generators.toGen(integers().between(5, 100))) + .check(d -> execute("INSERT INTO %s (pk, ck1, ck2, v) VALUES (1, " + d + ", 3, 4)")); + + // Invalid + qt().forAll(Generators.toGen(integers().between(0, 4))) + .check(d -> { + try + { + assertInvalidThrow(InvalidRequestException.class, "INSERT INTO %s (pk, ck1, ck2, v) VALUES (1, " + d + ", 3, 4)"); + } + catch (Throwable e) + { + throw new RuntimeException(e); + } + }); + } + + @Test + public void testCreateTableWithColumnWithClusteringColumnBiggerOrEqualThanScalarConstraint() throws Throwable + { + createTable("CREATE TABLE %s (pk int, ck1 int CHECK ck1 >= 4, ck2 int, v int, PRIMARY KEY ((pk),ck1, ck2)) WITH CLUSTERING ORDER BY (ck1 " + order + ");"); + + // Valid + qt().forAll(Generators.toGen(integers().between(4, 100))) + .check(d -> execute("INSERT INTO %s (pk, ck1, ck2, v) VALUES (1, " + d + ", 3, 4)")); + + // Invalid + qt().forAll(Generators.toGen(integers().between(0, 3))) + .check(d -> { + try + { + assertInvalidThrow(InvalidRequestException.class, "INSERT INTO %s (pk, ck1, ck2, v) VALUES (1, " + d + ", 3, 4)"); + } + catch (Throwable e) + { + throw new RuntimeException(e); + } + }); + } + + @Test + public void testCreateTableWithColumnWithClusteringColumnLessOrEqualThanScalarConstraint() throws Throwable + { + createTable("CREATE TABLE %s (pk int, ck1 int CHECK ck1 <= 4, ck2 int, v int, PRIMARY KEY ((pk),ck1, ck2)) WITH CLUSTERING ORDER BY (ck1 " + order + ");"); + + // Valid + qt().forAll(Generators.toGen(integers().between(0, 4))) + .check(d -> execute("INSERT INTO %s (pk, ck1, ck2, v) VALUES (1, " + d + ", 3, 4)")); + + // Invalid + qt().forAll(Generators.toGen(integers().between(5, 100))) + .check(d -> { + try + { + assertInvalidThrow(InvalidRequestException.class, "INSERT INTO %s (pk, ck1, ck2, v) VALUES (1, " + d + ", 3, 4)"); + } + catch (Throwable e) + { + throw new RuntimeException(e); + } + }); + } + + @Test + public void testCreateTableWithColumnWithClusteringColumnDifferentThanScalarConstraint() throws Throwable + { + createTable("CREATE TABLE %s (pk int, ck1 int CHECK ck1 != 4, ck2 int, v int, PRIMARY KEY ((pk),ck1, ck2)) WITH CLUSTERING ORDER BY (ck1 " + order + ");"); + + // Valid + qt().forAll(Generators.toGen(integers().between(0, 3))) + .check(d -> execute("INSERT INTO %s (pk, ck1, ck2, v) VALUES (1, " + d + ", 3, 4)")); + qt().forAll(Generators.toGen(integers().between(5, 100))) + .check(d -> execute("INSERT INTO %s (pk, ck1, ck2, v) VALUES (1, " + d + ", 3, 4)")); + + // Invalid + assertInvalidThrow(InvalidRequestException.class, "INSERT INTO %s (pk, ck1, ck2, v) VALUES (1, 4, 2, 3)"); + } + + @Test + public void testCreateTableWithColumnWithClusteringColumnMultipleScalarConstraints() throws Throwable + { + createTable("CREATE TABLE %s (pk int, ck1 int CHECK ck1 < 4 AND ck1 >= 2, ck2 int, v int, PRIMARY KEY ((pk),ck1, ck2)) WITH CLUSTERING ORDER BY (ck1 " + order + ");"); + + // Valid + qt().forAll(Generators.toGen(integers().between(2, 3))) + .check(d -> execute("INSERT INTO %s (pk, ck1, ck2, v) VALUES (1, " + d + ", 3, 4)")); + + // Invalid + qt().forAll(Generators.toGen(integers().between(-100, 1))) + .check(d -> { + try + { + assertInvalidThrow(InvalidRequestException.class, "INSERT INTO %s (pk, ck1, ck2, v) VALUES (1, " + d + ", 3, 4)"); + } + catch (Throwable e) + { + throw new RuntimeException(e); + } + }); + qt().forAll(Generators.toGen(integers().between(4, 100))) + .check(d -> { + try + { + assertInvalidThrow(InvalidRequestException.class, "INSERT INTO %s (pk, ck1, ck2, v) VALUES (1, " + d + ", 3, 4)"); + } + catch (Throwable e) + { + throw new RuntimeException(e); + } + }); + } + + @Test + public void testCreateTableWithColumnWithClusteringColumnLessThanScalarSmallIntConstraint() throws Throwable + { + createTable("CREATE TABLE %s (pk int, ck1 smallint CHECK ck1 < 4, ck2 int, v int, PRIMARY KEY ((pk),ck1, ck2)) WITH CLUSTERING ORDER BY (ck1 " + order + ");"); + + // Valid + qt().forAll(Generators.toGen(integers().between(0, 3))) + .check(d -> execute("INSERT INTO %s (pk, ck1, ck2, v) VALUES (1, " + d + ", 3, 4)")); + + // Invalid + qt().forAll(Generators.toGen(integers().between(4, 100))) + .check(d -> { + try + { + assertInvalidThrow(InvalidRequestException.class, "INSERT INTO %s (pk, ck1, ck2, v) VALUES (1, " + d + ", 3, 4)"); + } + catch (Throwable e) + { + throw new RuntimeException(e); + } + }); + } + + @Test + public void testCreateTableWithColumnWithClusteringColumnBiggerThanScalarSmallIntConstraint() throws Throwable + { + createTable("CREATE TABLE %s (pk int, ck1 smallint CHECK ck1 > 4, ck2 int, v int, PRIMARY KEY ((pk),ck1, ck2)) WITH CLUSTERING ORDER BY (ck1 " + order + ");"); + + // Valid + qt().forAll(Generators.toGen(integers().between(5, 100))) + .check(d -> execute("INSERT INTO %s (pk, ck1, ck2, v) VALUES (1, " + d + ", 3, 4)")); + + // Invalid + qt().forAll(Generators.toGen(integers().between(0, 4))) + .check(d -> { + try + { + assertInvalidThrow(InvalidRequestException.class, "INSERT INTO %s (pk, ck1, ck2, v) VALUES (1, " + d + ", 3, 4)"); + } + catch (Throwable e) + { + throw new RuntimeException(e); + } + }); + } + + @Test + public void testCreateTableWithColumnWithClusteringColumnBiggerOrEqualThanScalarSmallIntConstraint() throws Throwable + { + createTable("CREATE TABLE %s (pk int, ck1 smallint CHECK ck1 >= 4, ck2 int, v int, PRIMARY KEY ((pk),ck1, ck2)) WITH CLUSTERING ORDER BY (ck1 " + order + ");"); + + // Valid + qt().forAll(Generators.toGen(integers().between(4, 100))) + .check(d -> execute("INSERT INTO %s (pk, ck1, ck2, v) VALUES (1, " + d + ", 3, 4)")); + + // Invalid + qt().forAll(Generators.toGen(integers().between(0, 3))) + .check(d -> { + try + { + assertInvalidThrow(InvalidRequestException.class, "INSERT INTO %s (pk, ck1, ck2, v) VALUES (1, " + d + ", 3, 4)"); + } + catch (Throwable e) + { + throw new RuntimeException(e); + } + }); + } + + @Test + public void testCreateTableWithColumnWithClusteringColumnLessOrEqualThanScalarSmallIntConstraint() throws Throwable + { + createTable("CREATE TABLE %s (pk int, ck1 smallint CHECK ck1 <= 4, ck2 int, v int, PRIMARY KEY ((pk),ck1, ck2)) WITH CLUSTERING ORDER BY (ck1 " + order + ");"); + + // Valid + qt().forAll(Generators.toGen(integers().between(0, 4))) + .check(d -> execute("INSERT INTO %s (pk, ck1, ck2, v) VALUES (1, " + d + ", 3, 4)")); + + // Invalid + qt().forAll(Generators.toGen(integers().between(5, 100))) + .check(d -> { + try + { + assertInvalidThrow(InvalidRequestException.class, "INSERT INTO %s (pk, ck1, ck2, v) VALUES (1, " + d + ", 3, 4)"); + } + catch (Throwable e) + { + throw new RuntimeException(e); + } + }); + } + + @Test + public void testCreateTableWithColumnWithClusteringColumnDifferentThanScalarSmallIntConstraint() throws Throwable + { + createTable("CREATE TABLE %s (pk int, ck1 smallint CHECK ck1 != 4, ck2 int, v int, PRIMARY KEY ((pk),ck1, ck2)) WITH CLUSTERING ORDER BY (ck1 " + order + ");"); + + // Valid + qt().forAll(Generators.toGen(integers().between(0, 3))) + .check(d -> execute("INSERT INTO %s (pk, ck1, ck2, v) VALUES (1, " + d + ", 3, 4)")); + qt().forAll(Generators.toGen(integers().between(5, 100))) + .check(d -> execute("INSERT INTO %s (pk, ck1, ck2, v) VALUES (1, " + d + ", 3, 4)")); + + // Invalid + assertInvalidThrow(InvalidRequestException.class, "INSERT INTO %s (pk, ck1, ck2, v) VALUES (1, 4, 2, 3)"); + } + + @Test + public void testCreateTableWithColumnWithClusteringColumnMultipleScalarSmallIntConstraints() throws Throwable + { + createTable("CREATE TABLE %s (pk int, ck1 smallint CHECK ck1 < 4 AND ck1 >= 2, ck2 int, v int, PRIMARY KEY ((pk),ck1, ck2)) WITH CLUSTERING ORDER BY (ck1 " + order + ");"); + + // Valid + qt().forAll(Generators.toGen(integers().between(2, 3))) + .check(d -> execute("INSERT INTO %s (pk, ck1, ck2, v) VALUES (1, " + d + ", 3, 4)")); + + // Invalid + qt().forAll(Generators.toGen(integers().between(-100, 1))) + .check(d -> { + try + { + assertInvalidThrow(InvalidRequestException.class, "INSERT INTO %s (pk, ck1, ck2, v) VALUES (1, " + d + ", 3, 4)"); + } + catch (Throwable e) + { + throw new RuntimeException(e); + } + }); + qt().forAll(Generators.toGen(integers().between(4, 100))) + .check(d -> { + try + { + assertInvalidThrow(InvalidRequestException.class, "INSERT INTO %s (pk, ck1, ck2, v) VALUES (1, " + d + ", 3, 4)"); + } + catch (Throwable e) + { + throw new RuntimeException(e); + } + }); + } + + @Test + public void testCreateTableWithColumnWithClusteringColumnLessThanScalarDecimalConstraint() throws Throwable + { + createTable("CREATE TABLE %s (pk int, ck1 decimal CHECK ck1 < 4.2, ck2 int, v int, PRIMARY KEY ((pk),ck1, ck2)) WITH CLUSTERING ORDER BY (ck1 " + order + ");"); + + // Valid + execute("INSERT INTO %s (pk, ck1, ck2, v) VALUES (1, 2, 2, 3)"); + qt().forAll(Generators.toGen(doubles().between(0, 4.1))) + .check(d -> execute("INSERT INTO %s (pk, ck1, ck2, v) VALUES (1, " + d + ", 3, 4)")); + + // Invalid + qt().forAll(Generators.toGen(doubles().between(4.3, 100))) + .check(d -> { + try + { + assertInvalidThrow(InvalidRequestException.class, "INSERT INTO %s (pk, ck1, ck2, v) VALUES (1, " + d + ", 3, 4)"); + } + catch (Throwable e) + { + throw new RuntimeException(e); + } + }); + } + + @Test + public void testCreateTableWithColumnWithClusteringColumnBiggerThanScalarDecimalConstraint() throws Throwable + { + createTable("CREATE TABLE %s (pk int, ck1 decimal CHECK ck1 > 4.2, ck2 int, v int, PRIMARY KEY ((pk),ck1, ck2)) WITH CLUSTERING ORDER BY (ck1 " + order + ");"); + + // Valid + qt().forAll(Generators.toGen(doubles().between(4.3, 100))) + .check(d -> execute("INSERT INTO %s (pk, ck1, ck2, v) VALUES (1, " + d + ", 3, 4)")); + + // Invalid + qt().forAll(Generators.toGen(doubles().between(0, 4.2))) + .check(d -> { + try + { + assertInvalidThrow(InvalidRequestException.class, "INSERT INTO %s (pk, ck1, ck2, v) VALUES (1, " + d + ", 3, 4)"); + } + catch (Throwable e) + { + throw new RuntimeException(e); + } + }); + } + + @Test + public void testCreateTableWithColumnWithClusteringColumnBiggerOrEqualThanScalarDecimalConstraint() throws Throwable + { + createTable("CREATE TABLE %s (pk int, ck1 decimal CHECK ck1 >= 4.2, ck2 int, v int, PRIMARY KEY ((pk),ck1, ck2)) WITH CLUSTERING ORDER BY (ck1 " + order + ");"); + + // Valid + qt().forAll(Generators.toGen(doubles().between(4.2, 100))) + .check(d -> execute("INSERT INTO %s (pk, ck1, ck2, v) VALUES (1, " + d + ", 3, 4)")); + + // Invalid + qt().forAll(Generators.toGen(doubles().between(0, 4.1))) + .check(d -> { + try + { + assertInvalidThrow(InvalidRequestException.class, "INSERT INTO %s (pk, ck1, ck2, v) VALUES (1, " + d + ", 3, 4)"); + } + catch (Throwable e) + { + throw new RuntimeException(e); + } + }); + } + + @Test + public void testCreateTableWithColumnWithClusteringColumnLessOrEqualThanScalarDecimalConstraint() throws Throwable + { + createTable("CREATE TABLE %s (pk int, ck1 decimal CHECK ck1 <= 4.2, ck2 int, v int, PRIMARY KEY ((pk),ck1, ck2)) WITH CLUSTERING ORDER BY (ck1 " + order + ");"); + + // Valid + qt().forAll(Generators.toGen(doubles().between(0, 4.2))) + .check(d -> execute("INSERT INTO %s (pk, ck1, ck2, v) VALUES (1, " + d + ", 3, 4)")); + + // Invalid + qt().forAll(Generators.toGen(doubles().between(4.3, 100))) + .check(d -> { + try + { + assertInvalidThrow(InvalidRequestException.class, "INSERT INTO %s (pk, ck1, ck2, v) VALUES (1, " + d + ", 3, 4)"); + } + catch (Throwable e) + { + throw new RuntimeException(e); + } + }); + } + + @Test + public void testCreateTableWithColumnWithClusteringColumnDifferentThanScalarDecimalConstraint() throws Throwable + { + createTable("CREATE TABLE %s (pk int, ck1 decimal CHECK ck1 != 4.2, ck2 int, v int, PRIMARY KEY ((pk),ck1, ck2)) WITH CLUSTERING ORDER BY (ck1 " + order + ");"); + + // Valid + qt().forAll(Generators.toGen(doubles().between(0, 4.1))) + .check(d -> execute("INSERT INTO %s (pk, ck1, ck2, v) VALUES (1, " + d + ", 3, 4)")); + qt().forAll(Generators.toGen(doubles().between(4.3, 100))) + .check(d -> execute("INSERT INTO %s (pk, ck1, ck2, v) VALUES (1, " + d + ", 3, 4)")); + + // Invalid + assertInvalidThrow(InvalidRequestException.class, "INSERT INTO %s (pk, ck1, ck2, v) VALUES (1, 4.2, 2, 3)"); + } + + @Test + public void testCreateTableWithColumnWithClusteringColumnMultipleScalarDecimalConstraints() throws Throwable + { + createTable("CREATE TABLE %s (pk int, ck1 decimal CHECK ck1 < 4.2 AND ck1 >= 2.1, ck2 int, v int, PRIMARY KEY ((pk),ck1, ck2)) WITH CLUSTERING ORDER BY (ck1 " + order + ");"); + + // Valid + qt().forAll(Generators.toGen(doubles().between(2.1, 4.1))) + .check(d -> execute("INSERT INTO %s (pk, ck1, ck2, v) VALUES (1, " + d + ", 3, 4)")); + + // Invalid + qt().forAll(Generators.toGen(doubles().between(-100, 2))) + .check(d -> { + try + { + assertInvalidThrow(InvalidRequestException.class, "INSERT INTO %s (pk, ck1, ck2, v) VALUES (1, " + d + ", 3, 4)"); + } + catch (Throwable e) + { + throw new RuntimeException(e); + } + }); + qt().forAll(Generators.toGen(doubles().between(4.2, 100))) + .check(d -> { + try + { + assertInvalidThrow(InvalidRequestException.class, "INSERT INTO %s (pk, ck1, ck2, v) VALUES (1, " + d + ", 3, 4)"); + } + catch (Throwable e) + { + throw new RuntimeException(e); + } + }); + } + + @Test + public void testCreateTableWithColumnWithClusteringColumnLessThanScalarDoubleConstraint() throws Throwable + { + createTable("CREATE TABLE %s (pk int, ck1 double CHECK ck1 < 4.2, ck2 int, v int, PRIMARY KEY ((pk),ck1, ck2)) WITH CLUSTERING ORDER BY (ck1 " + order + ");"); + + // Valid + execute("INSERT INTO %s (pk, ck1, ck2, v) VALUES (1, 2, 2, 3)"); + qt().forAll(Generators.toGen(doubles().between(0, 4.1))) + .check(d -> execute("INSERT INTO %s (pk, ck1, ck2, v) VALUES (1, " + d + ", 3, 4)")); + + // Invalid + qt().forAll(Generators.toGen(doubles().between(4.3, 100))) + .check(d -> { + try + { + assertInvalidThrow(InvalidRequestException.class, "INSERT INTO %s (pk, ck1, ck2, v) VALUES (1, " + d + ", 3, 4)"); + } + catch (Throwable e) + { + throw new RuntimeException(e); + } + }); + } + + @Test + public void testCreateTableWithColumnWithClusteringColumnBiggerThanScalarDoubleConstraint() throws Throwable + { + createTable("CREATE TABLE %s (pk int, ck1 double CHECK ck1 > 4.2, ck2 int, v int, PRIMARY KEY ((pk),ck1, ck2)) WITH CLUSTERING ORDER BY (ck1 " + order + ");"); + + // Valid + qt().forAll(Generators.toGen(doubles().between(4.3, 100))) + .check(d -> execute("INSERT INTO %s (pk, ck1, ck2, v) VALUES (1, " + d + ", 3, 4)")); + + // Invalid + qt().forAll(Generators.toGen(doubles().between(0, 4.2))) + .check(d -> { + try + { + assertInvalidThrow(InvalidRequestException.class, "INSERT INTO %s (pk, ck1, ck2, v) VALUES (1, " + d + ", 3, 4)"); + } + catch (Throwable e) + { + throw new RuntimeException(e); + } + }); + } + + @Test + public void testCreateTableWithColumnWithClusteringColumnBiggerOrEqualThanScalarDoubleConstraint() throws Throwable + { + createTable("CREATE TABLE %s (pk int, ck1 double CHECK ck1 >= 4.2, ck2 int, v int, PRIMARY KEY ((pk),ck1, ck2)) WITH CLUSTERING ORDER BY (ck1 " + order + ");"); + + // Valid + qt().forAll(Generators.toGen(doubles().between(4.2, 100))) + .check(d -> execute("INSERT INTO %s (pk, ck1, ck2, v) VALUES (1, " + d + ", 3, 4)")); + + // Invalid + qt().forAll(Generators.toGen(doubles().between(0, 4.1))) + .check(d -> { + try + { + assertInvalidThrow(InvalidRequestException.class, "INSERT INTO %s (pk, ck1, ck2, v) VALUES (1, " + d + ", 3, 4)"); + } + catch (Throwable e) + { + throw new RuntimeException(e); + } + }); + } + + @Test + public void testCreateTableWithColumnWithClusteringColumnLessOrEqualThanScalarDoubleConstraint() throws Throwable + { + createTable("CREATE TABLE %s (pk int, ck1 double CHECK ck1 <= 4.2, ck2 int, v int, PRIMARY KEY ((pk),ck1, ck2)) WITH CLUSTERING ORDER BY (ck1 " + order + ");"); + + // Valid + qt().forAll(Generators.toGen(doubles().between(0, 4.2))) + .check(d -> execute("INSERT INTO %s (pk, ck1, ck2, v) VALUES (1, " + d + ", 3, 4)")); + + // Invalid + qt().forAll(Generators.toGen(doubles().between(4.3, 100))) + .check(d -> { + try + { + assertInvalidThrow(InvalidRequestException.class, "INSERT INTO %s (pk, ck1, ck2, v) VALUES (1, " + d + ", 3, 4)"); + } + catch (Throwable e) + { + throw new RuntimeException(e); + } + }); + } + + @Test + public void testCreateTableWithColumnWithClusteringColumnDifferentThanScalarDoubleConstraint() throws Throwable + { + createTable("CREATE TABLE %s (pk int, ck1 double CHECK ck1 != 4.2, ck2 int, v int, PRIMARY KEY ((pk),ck1, ck2)) WITH CLUSTERING ORDER BY (ck1 " + order + ");"); + + // Valid + qt().forAll(Generators.toGen(doubles().between(0, 4.1))) + .check(d -> execute("INSERT INTO %s (pk, ck1, ck2, v) VALUES (1, " + d + ", 3, 4)")); + qt().forAll(Generators.toGen(doubles().between(4.3, 100))) + .check(d -> execute("INSERT INTO %s (pk, ck1, ck2, v) VALUES (1, " + d + ", 3, 4)")); + + // Invalid + assertInvalidThrow(InvalidRequestException.class, "INSERT INTO %s (pk, ck1, ck2, v) VALUES (1, 4.2, 2, 3)"); + } + + @Test + public void testCreateTableWithColumnWithClusteringColumnMultipleScalarDoubleConstraints() throws Throwable + { + createTable("CREATE TABLE %s (pk int, ck1 double CHECK ck1 < 4.2 AND ck1 >= 2.1, ck2 int, v int, PRIMARY KEY ((pk),ck1, ck2)) WITH CLUSTERING ORDER BY (ck1 " + order + ");"); + + // Valid + qt().forAll(Generators.toGen(doubles().between(2.1, 4.1))) + .check(d -> execute("INSERT INTO %s (pk, ck1, ck2, v) VALUES (1, " + d + ", 3, 4)")); + + // Invalid + qt().forAll(Generators.toGen(doubles().between(-100, 2))) + .check(d -> { + try + { + assertInvalidThrow(InvalidRequestException.class, "INSERT INTO %s (pk, ck1, ck2, v) VALUES (1, " + d + ", 3, 4)"); + } + catch (Throwable e) + { + throw new RuntimeException(e); + } + }); + qt().forAll(Generators.toGen(doubles().between(4.2, 100))) + .check(d -> { + try + { + assertInvalidThrow(InvalidRequestException.class, "INSERT INTO %s (pk, ck1, ck2, v) VALUES (1, " + d + ", 3, 4)"); + } + catch (Throwable e) + { + throw new RuntimeException(e); + } + }); + } + + @Test + public void testCreateTableWithColumnWithClusteringColumnLessThanScalarFloatConstraint() throws Throwable + { + createTable("CREATE TABLE %s (pk int, ck1 float CHECK ck1 < 4.2, ck2 int, v int, PRIMARY KEY ((pk),ck1, ck2)) WITH CLUSTERING ORDER BY (ck1 " + order + ");"); + + // Valid + execute("INSERT INTO %s (pk, ck1, ck2, v) VALUES (1, 2, 2, 3)"); + qt().forAll(Generators.toGen(doubles().between(0, 4.1))) + .check(d -> execute("INSERT INTO %s (pk, ck1, ck2, v) VALUES (1, " + d + ", 3, 4)")); + + // Invalid + qt().forAll(Generators.toGen(doubles().between(4.3, 100))) + .check(d -> { + try + { + assertInvalidThrow(InvalidRequestException.class, "INSERT INTO %s (pk, ck1, ck2, v) VALUES (1, " + d + ", 3, 4)"); + } + catch (Throwable e) + { + throw new RuntimeException(e); + } + }); + } + + @Test + public void testCreateTableWithColumnWithClusteringColumnBiggerThanScalarFloatConstraint() throws Throwable + { + createTable("CREATE TABLE %s (pk int, ck1 float CHECK ck1 > 4.2, ck2 int, v int, PRIMARY KEY ((pk),ck1, ck2)) WITH CLUSTERING ORDER BY (ck1 " + order + ");"); + + // Valid + qt().forAll(Generators.toGen(doubles().between(4.3, 100))) + .check(d -> execute("INSERT INTO %s (pk, ck1, ck2, v) VALUES (1, " + d + ", 3, 4)")); + + // Invalid + qt().forAll(Generators.toGen(doubles().between(0, 4.2))) + .check(d -> { + try + { + assertInvalidThrow(InvalidRequestException.class, "INSERT INTO %s (pk, ck1, ck2, v) VALUES (1, " + d + ", 3, 4)"); + } + catch (Throwable e) + { + throw new RuntimeException(e); + } + }); + } + + @Test + public void testCreateTableWithColumnWithClusteringColumnBiggerOrEqualThanScalarFloatConstraint() throws Throwable + { + createTable("CREATE TABLE %s (pk int, ck1 float CHECK ck1 >= 4.2, ck2 int, v int, PRIMARY KEY ((pk),ck1, ck2)) WITH CLUSTERING ORDER BY (ck1 " + order + ");"); + + // Valid + qt().forAll(Generators.toGen(doubles().between(4.2, 100))) + .check(d -> execute("INSERT INTO %s (pk, ck1, ck2, v) VALUES (1, " + d + ", 3, 4)")); + + // Invalid + qt().forAll(Generators.toGen(doubles().between(0, 4.1))) + .check(d -> { + try + { + assertInvalidThrow(InvalidRequestException.class, "INSERT INTO %s (pk, ck1, ck2, v) VALUES (1, " + d + ", 3, 4)"); + } + catch (Throwable e) + { + throw new RuntimeException(e); + } + }); + } + + @Test + public void testCreateTableWithColumnWithClusteringColumnLessOrEqualThanScalarFloatConstraint() throws Throwable + { + createTable("CREATE TABLE %s (pk int, ck1 float CHECK ck1 <= 4.2, ck2 int, v int, PRIMARY KEY ((pk),ck1, ck2)) WITH CLUSTERING ORDER BY (ck1 " + order + ");"); + + // Valid + qt().forAll(Generators.toGen(doubles().between(0, 4.2))) + .check(d -> execute("INSERT INTO %s (pk, ck1, ck2, v) VALUES (1, " + d + ", 3, 4)")); + + // Invalid + qt().forAll(Generators.toGen(doubles().between(4.3, 100))) + .check(d -> { + try + { + assertInvalidThrow(InvalidRequestException.class, "INSERT INTO %s (pk, ck1, ck2, v) VALUES (1, " + d + ", 3, 4)"); + } + catch (Throwable e) + { + throw new RuntimeException(e); + } + }); + } + + @Test + public void testCreateTableWithColumnWithClusteringColumnDifferentThanScalarFloatConstraint() throws Throwable + { + createTable("CREATE TABLE %s (pk int, ck1 float CHECK ck1 != 4.2, ck2 int, v int, PRIMARY KEY ((pk),ck1, ck2)) WITH CLUSTERING ORDER BY (ck1 " + order + ");"); + + // Valid + qt().forAll(Generators.toGen(doubles().between(0, 4.1))) + .check(d -> execute("INSERT INTO %s (pk, ck1, ck2, v) VALUES (1, " + d + ", 3, 4)")); + qt().forAll(Generators.toGen(doubles().between(4.3, 100))) + .check(d -> execute("INSERT INTO %s (pk, ck1, ck2, v) VALUES (1, " + d + ", 3, 4)")); + + // Invalid + assertInvalidThrow(InvalidRequestException.class, "INSERT INTO %s (pk, ck1, ck2, v) VALUES (1, 4.2, 2, 3)"); + } + + @Test + public void testCreateTableWithColumnWithClusteringColumnMultipleScalarFloatConstraints() throws Throwable + { + createTable("CREATE TABLE %s (pk int, ck1 float CHECK ck1 < 4.2 AND ck1 >= 2.1, ck2 int, v int, PRIMARY KEY ((pk),ck1, ck2)) WITH CLUSTERING ORDER BY (ck1 " + order + ");"); + + // Valid + qt().forAll(Generators.toGen(doubles().between(2.1, 4.1))) + .check(d -> execute("INSERT INTO %s (pk, ck1, ck2, v) VALUES (1, " + d + ", 3, 4)")); + + // Invalid + qt().forAll(Generators.toGen(doubles().between(-100, 2))) + .check(d -> { + try + { + assertInvalidThrow(InvalidRequestException.class, "INSERT INTO %s (pk, ck1, ck2, v) VALUES (1, " + d + ", 3, 4)"); + } + catch (Throwable e) + { + throw new RuntimeException(e); + } + }); + qt().forAll(Generators.toGen(doubles().between(4.2, 100))) + .check(d -> { + try + { + assertInvalidThrow(InvalidRequestException.class, "INSERT INTO %s (pk, ck1, ck2, v) VALUES (1, " + d + ", 3, 4)"); + } + catch (Throwable e) + { + throw new RuntimeException(e); + } + }); + } + + // FUNCTION + @Test + public void testCreateTableWithColumnWithClusteringColumnLengthEqualToConstraint() throws Throwable + { + createTable("CREATE TABLE %s (pk int, ck1 text CHECK LENGTH(ck1) = 4, ck2 int, v int, PRIMARY KEY ((pk), ck1, ck2)) WITH CLUSTERING ORDER BY (ck1 ASC);"); + + // Valid + execute("INSERT INTO %s (pk, ck1, ck2, v) VALUES (1, 'fooo', 2, 3)"); + + // Invalid + assertInvalidThrow(InvalidRequestException.class, "INSERT INTO %s (pk, ck1, ck2, v) VALUES (1, 'foo', 2, 3)"); + assertInvalidThrow(InvalidRequestException.class, "INSERT INTO %s (pk, ck1, ck2, v) VALUES (1, 'foooo', 2, 3)"); + } + + @Test + public void testCreateTableWithColumnWithClusteringColumnLengthDifferentThanConstraint() throws Throwable + { + createTable("CREATE TABLE %s (pk int, ck1 text CHECK LENGTH(ck1) != 4, ck2 int, v int, PRIMARY KEY ((pk),ck1, ck2)) WITH CLUSTERING ORDER BY (ck1 ASC);"); + + // Valid + execute("INSERT INTO %s (pk, ck1, ck2, v) VALUES (1, 'foo', 2, 3)"); + execute("INSERT INTO %s (pk, ck1, ck2, v) VALUES (1, 'foooo', 2, 3)"); + + // Invalid + assertInvalidThrow(InvalidRequestException.class, "INSERT INTO %s (pk, ck1, ck2, v) VALUES (1, 'fooo', 2, 3)"); + } + + @Test + public void testCreateTableWithColumnWithClusteringColumnLengthBiggerThanConstraint() throws Throwable + { + createTable("CREATE TABLE %s (pk int, ck1 text CHECK LENGTH(ck1) > 4, ck2 int, v int, PRIMARY KEY ((pk),ck1, ck2)) WITH CLUSTERING ORDER BY (ck1 ASC);"); + + // Valid + execute("INSERT INTO %s (pk, ck1, ck2, v) VALUES (1, 'foooo', 2, 3)"); + + // Invalid + assertInvalidThrow(InvalidRequestException.class, "INSERT INTO %s (pk, ck1, ck2, v) VALUES (1, 'foo', 2, 3)"); + assertInvalidThrow(InvalidRequestException.class, "INSERT INTO %s (pk, ck1, ck2, v) VALUES (1, 'fooo', 2, 3)"); + } + + @Test + public void testCreateTableWithColumnWithClusteringColumnLengthBiggerOrEqualThanConstraint() throws Throwable + { + createTable("CREATE TABLE %s (pk int, ck1 text CHECK LENGTH(ck1) >= 4, ck2 int, v int, PRIMARY KEY ((pk),ck1, ck2)) WITH CLUSTERING ORDER BY (ck1 ASC);"); + + // Valid + execute("INSERT INTO %s (pk, ck1, ck2, v) VALUES (1, 'foooo', 2, 3)"); + execute("INSERT INTO %s (pk, ck1, ck2, v) VALUES (1, 'fooo', 2, 3)"); + + // Invalid + assertInvalidThrow(InvalidRequestException.class, "INSERT INTO %s (pk, ck1, ck2, v) VALUES (1, 'foo', 2, 3)"); + } + + @Test + public void testCreateTableWithColumnWithClusteringColumnLengthSmallerThanConstraint() throws Throwable + { + createTable("CREATE TABLE %s (pk int, ck1 text CHECK LENGTH(ck1) < 4, ck2 int, v int, PRIMARY KEY ((pk),ck1, ck2)) WITH CLUSTERING ORDER BY (ck1 ASC);"); + + // Valid + execute("INSERT INTO %s (pk, ck1, ck2, v) VALUES (1, 'foo', 2, 3)"); + + // Invalid + assertInvalidThrow(InvalidRequestException.class, "INSERT INTO %s (pk, ck1, ck2, v) VALUES (1, 'fooo', 2, 3)"); + assertInvalidThrow(InvalidRequestException.class, "INSERT INTO %s (pk, ck1, ck2, v) VALUES (1, 'foooo', 2, 3)"); + } + + @Test + public void testCreateTableWithColumnWithClusteringColumnLengthSmallerOrEqualThanConstraint() throws Throwable + { + createTable("CREATE TABLE %s (pk int, ck1 text CHECK LENGTH(ck1) <= 4, ck2 int, v int, PRIMARY KEY ((pk),ck1, ck2)) WITH CLUSTERING ORDER BY (ck1 ASC);"); + + // Valid + execute("INSERT INTO %s (pk, ck1, ck2, v) VALUES (1, 'foo', 2, 3)"); + execute("INSERT INTO %s (pk, ck1, ck2, v) VALUES (1, 'fooo', 2, 3)"); + + // Invalid + assertInvalidThrow(InvalidRequestException.class, "INSERT INTO %s (pk, ck1, ck2, v) VALUES (1, 'foooo', 2, 3)"); + } + + @Test + public void testCreateTableWithColumnWithClusteringBlobColumnLengthEqualToConstraint() throws Throwable + { + createTable("CREATE TABLE %s (pk int, ck1 blob CHECK LENGTH(ck1) = 4, ck2 int, v int, PRIMARY KEY ((pk),ck1, ck2)) WITH CLUSTERING ORDER BY (ck1 ASC);"); + + // Valid + execute("INSERT INTO %s (pk, ck1, ck2, v) VALUES (1, textAsBlob('fooo'), 2, 3)"); + + // Invalid + assertInvalidThrow(InvalidRequestException.class, "INSERT INTO %s (pk, ck1, ck2, v) VALUES (1, textAsBlob('foo'), 2, 3)"); + assertInvalidThrow(InvalidRequestException.class, "INSERT INTO %s (pk, ck1, ck2, v) VALUES (1, textAsBlob('foooo'), 2, 3)"); + } + + @Test + public void testCreateTableWithColumnWithClusteringBlobColumnLengthDifferentThanConstraint() throws Throwable + { + createTable("CREATE TABLE %s (pk int, ck1 blob CHECK LENGTH(ck1) != 4, ck2 int, v int, PRIMARY KEY ((pk),ck1, ck2)) WITH CLUSTERING ORDER BY (ck1 ASC);"); + + // Valid + execute("INSERT INTO %s (pk, ck1, ck2, v) VALUES (1, textAsBlob('foo'), 2, 3)"); + execute("INSERT INTO %s (pk, ck1, ck2, v) VALUES (1, textAsBlob('foooo'), 2, 3)"); + + // Invalid + assertInvalidThrow(InvalidRequestException.class, "INSERT INTO %s (pk, ck1, ck2, v) VALUES (1, textAsBlob('fooo'), 2, 3)"); + } + + @Test + public void testCreateTableWithColumnWithClusteringBlobColumnLengthBiggerThanConstraint() throws Throwable + { + createTable("CREATE TABLE %s (pk int, ck1 blob CHECK LENGTH(ck1) > 4, ck2 int, v int, PRIMARY KEY ((pk),ck1, ck2)) WITH CLUSTERING ORDER BY (ck1 ASC);"); + + // Valid + execute("INSERT INTO %s (pk, ck1, ck2, v) VALUES (1, textAsBlob('foooo'), 2, 3)"); + + // Invalid + assertInvalidThrow(InvalidRequestException.class, "INSERT INTO %s (pk, ck1, ck2, v) VALUES (1, textAsBlob('foo'), 2, 3)"); + assertInvalidThrow(InvalidRequestException.class, "INSERT INTO %s (pk, ck1, ck2, v) VALUES (1, textAsBlob('fooo'), 2, 3)"); + } + + @Test + public void testCreateTableWithColumnWithClusteringBlobColumnLengthBiggerOrEqualThanConstraint() throws Throwable + { + createTable("CREATE TABLE %s (pk int, ck1 blob CHECK LENGTH(ck1) >= 4, ck2 int, v int, PRIMARY KEY ((pk),ck1, ck2)) WITH CLUSTERING ORDER BY (ck1 ASC);"); + + // Valid + execute("INSERT INTO %s (pk, ck1, ck2, v) VALUES (1, textAsBlob('foooo'), 2, 3)"); + execute("INSERT INTO %s (pk, ck1, ck2, v) VALUES (1, textAsBlob('fooo'), 2, 3)"); + + // Invalid + assertInvalidThrow(InvalidRequestException.class, "INSERT INTO %s (pk, ck1, ck2, v) VALUES (1, textAsBlob('foo'), 2, 3)"); + } + + @Test + public void testCreateTableWithColumnWithClusteringBlobColumnLengthSmallerThanConstraint() throws Throwable + { + createTable("CREATE TABLE %s (pk int, ck1 blob CHECK LENGTH(ck1) < 4, ck2 int, v int, PRIMARY KEY ((pk),ck1, ck2)) WITH CLUSTERING ORDER BY (ck1 ASC);"); + + // Valid + execute("INSERT INTO %s (pk, ck1, ck2, v) VALUES (1, textAsBlob('foo'), 2, 3)"); + + // Invalid + assertInvalidThrow(InvalidRequestException.class, "INSERT INTO %s (pk, ck1, ck2, v) VALUES (1, textAsBlob('fooo'), 2, 3)"); + assertInvalidThrow(InvalidRequestException.class, "INSERT INTO %s (pk, ck1, ck2, v) VALUES (1, textAsBlob('foooo'), 2, 3)"); + } + + @Test + public void testCreateTableWithColumnWithClusteringBlobColumnLengthSmallerOrEqualThanConstraint() throws Throwable + { + createTable("CREATE TABLE %s (pk int, ck1 blob CHECK LENGTH(ck1) <= 4, ck2 int, v int, PRIMARY KEY ((pk),ck1, ck2)) WITH CLUSTERING ORDER BY (ck1 ASC);"); + + // Valid + execute("INSERT INTO %s (pk, ck1, ck2, v) VALUES (1, textAsBlob('foo'), 2, 3)"); + execute("INSERT INTO %s (pk, ck1, ck2, v) VALUES (1, textAsBlob('fooo'), 2, 3)"); + + // Invalid + assertInvalidThrow(InvalidRequestException.class, "INSERT INTO %s (pk, ck1, ck2, v) VALUES (1, 'foooo', 2, 3)"); + } + + + @Test + public void testCreateTableWithColumnWithPkColumnLengthEqualToConstraint() throws Throwable + { + createTable("CREATE TABLE %s (pk text CHECK LENGTH(pk) = 4, ck1 int, ck2 int, v int, PRIMARY KEY ((pk),ck1, ck2)) WITH CLUSTERING ORDER BY (ck1 " + order + ");"); + + // Valid + execute("INSERT INTO %s (pk, ck1, ck2, v) VALUES ('fooo', 1, 2, 3)"); + + // Invalid + assertInvalidThrow(InvalidRequestException.class, "INSERT INTO %s (pk, ck1, ck2, v) VALUES ('foo', 1, 2, 3)"); + assertInvalidThrow(InvalidRequestException.class, "INSERT INTO %s (pk, ck1, ck2, v) VALUES ('foooo', 1, 2, 3)"); + } + + @Test + public void testCreateTableWithColumnWithPkColumnLengthDifferentThanConstraint() throws Throwable + { + createTable("CREATE TABLE %s (pk text CHECK LENGTH(pk) != 4, ck1 int, ck2 int, v int, PRIMARY KEY ((pk),ck1, ck2)) WITH CLUSTERING ORDER BY (ck1 " + order + ");"); + + // Valid + execute("INSERT INTO %s (pk, ck1, ck2, v) VALUES ('foo', 1, 2, 3)"); + execute("INSERT INTO %s (pk, ck1, ck2, v) VALUES ('foooo', 1, 2, 3)"); + + // Invalid + assertInvalidThrow(InvalidRequestException.class, "INSERT INTO %s (pk, ck1, ck2, v) VALUES ('fooo', 1, 2, 3)"); + } + + @Test + public void testCreateTableWithColumnWithPkColumnLengthBiggerThanConstraint() throws Throwable + { + createTable("CREATE TABLE %s (pk text CHECK LENGTH(pk) > 4, ck1 int, ck2 int, v int, PRIMARY KEY ((pk),ck1, ck2)) WITH CLUSTERING ORDER BY (ck1 " + order + ");"); + + // Valid + execute("INSERT INTO %s (pk, ck1, ck2, v) VALUES ('foooo', 1, 2, 3)"); + + // Invalid + assertInvalidThrow(InvalidRequestException.class, "INSERT INTO %s (pk, ck1, ck2, v) VALUES ('foo', 1, 2, 3)"); + assertInvalidThrow(InvalidRequestException.class, "INSERT INTO %s (pk, ck1, ck2, v) VALUES ('fooo', 1, 2, 3)"); + } + + @Test + public void testCreateTableWithColumnWithPkColumnLengthBiggerOrEqualThanConstraint() throws Throwable + { + createTable("CREATE TABLE %s (pk text CHECK LENGTH(pk) >= 4, ck1 int, ck2 int, v int, PRIMARY KEY ((pk),ck1, ck2)) WITH CLUSTERING ORDER BY (ck1 " + order + ");"); + + // Valid + execute("INSERT INTO %s (pk, ck1, ck2, v) VALUES ('foooo', 1, 2, 3)"); + execute("INSERT INTO %s (pk, ck1, ck2, v) VALUES ('fooo', 1, 2, 3)"); + + // Invalid + assertInvalidThrow(InvalidRequestException.class, "INSERT INTO %s (pk, ck1, ck2, v) VALUES ('foo', 1, 2, 3)"); + } + + @Test + public void testCreateTableWithColumnWithPkColumnLengthSmallerThanConstraint() throws Throwable + { + createTable("CREATE TABLE %s (pk text CHECK LENGTH(pk) < 4, ck1 int, ck2 int, v int, PRIMARY KEY ((pk),ck1, ck2)) WITH CLUSTERING ORDER BY (ck1 " + order + ");"); + + // Valid + execute("INSERT INTO %s (pk, ck1, ck2, v) VALUES ('foo', 1, 2, 3)"); + + // Invalid + assertInvalidThrow(InvalidRequestException.class, "INSERT INTO %s (pk, ck1, ck2, v) VALUES ('fooo', 1, 2, 3)"); + assertInvalidThrow(InvalidRequestException.class, "INSERT INTO %s (pk, ck1, ck2, v) VALUES ('foooo', 1, 2, 3)"); + } + + @Test + public void testCreateTableWithColumnWithPkColumnLengthSmallerOrEqualThanConstraint() throws Throwable + { + createTable("CREATE TABLE %s (pk text CHECK LENGTH(pk) <= 4, ck1 int, ck2 int, v int, PRIMARY KEY ((pk),ck1, ck2)) WITH CLUSTERING ORDER BY (ck1 " + order + ");"); + + // Valid + execute("INSERT INTO %s (pk, ck1, ck2, v) VALUES ('foo', 1, 2, 3)"); + execute("INSERT INTO %s (pk, ck1, ck2, v) VALUES ('fooo', 1, 2, 3)"); + + // Invalid + assertInvalidThrow(InvalidRequestException.class, "INSERT INTO %s (pk, ck1, ck2, v) VALUES ('foooo', 1, 2, 3)"); + } + + + @Test + public void testCreateTableWithColumnWithRegularColumnLengthEqualToConstraint() throws Throwable + { + createTable("CREATE TABLE %s (pk int, ck1 int, ck2 int, v text CHECK LENGTH(v) = 4, PRIMARY KEY ((pk),ck1, ck2)) WITH CLUSTERING ORDER BY (ck1 " + order + ");"); + + // Valid + execute("INSERT INTO %s (pk, ck1, ck2, v) VALUES (1, 2, 3, 'fooo')"); + + // Invalid + assertInvalidThrow(InvalidRequestException.class, "INSERT INTO %s (pk, ck1, ck2, v) VALUES (1, 2, 3, 'foo')"); + assertInvalidThrow(InvalidRequestException.class, "INSERT INTO %s (pk, ck1, ck2, v) VALUES (1, 2, 3, 'foooo')"); + } + + @Test + public void testCreateTableWithColumnWithRegularColumnLengthDifferentThanConstraint() throws Throwable + { + createTable("CREATE TABLE %s (pk int, ck1 int, ck2 int, v text CHECK LENGTH(v) != 4, PRIMARY KEY ((pk),ck1, ck2)) WITH CLUSTERING ORDER BY (ck1 " + order + ");"); + + // Valid + execute("INSERT INTO %s (pk, ck1, ck2, v) VALUES (1, 2, 3, 'foo')"); + execute("INSERT INTO %s (pk, ck1, ck2, v) VALUES (1, 2, 3, 'foooo')"); + + // Invalid + assertInvalidThrow(InvalidRequestException.class, "INSERT INTO %s (pk, ck1, ck2, v) VALUES (1, 2, 3, 'fooo')"); + } + + @Test + public void testCreateTableWithColumnWithRegularColumnLengthBiggerThanConstraint() throws Throwable + { + createTable("CREATE TABLE %s (pk int, ck1 int, ck2 int, v text CHECK LENGTH(v) > 4, PRIMARY KEY ((pk),ck1, ck2)) WITH CLUSTERING ORDER BY (ck1 " + order + ");"); + + // Valid + execute("INSERT INTO %s (pk, ck1, ck2, v) VALUES (1, 2, 3, 'foooo')"); + + // Invalid + assertInvalidThrow(InvalidRequestException.class, "INSERT INTO %s (pk, ck1, ck2, v) VALUES (1, 2, 3, 'foo')"); + assertInvalidThrow(InvalidRequestException.class, "INSERT INTO %s (pk, ck1, ck2, v) VALUES (1, 2, 3, 'fooo')"); + } + + @Test + public void testCreateTableWithColumnWithRegularColumnLengthBiggerOrEqualThanConstraint() throws Throwable + { + createTable("CREATE TABLE %s (pk int, ck1 int, ck2 int, v text CHECK LENGTH(v) >= 4, PRIMARY KEY ((pk),ck1, ck2)) WITH CLUSTERING ORDER BY (ck1 " + order + ");"); + + // Valid + execute("INSERT INTO %s (pk, ck1, ck2, v) VALUES (1, 2, 3, 'foooo')"); + execute("INSERT INTO %s (pk, ck1, ck2, v) VALUES (1, 2, 3, 'fooo')"); + + // Invalid + assertInvalidThrow(InvalidRequestException.class, "INSERT INTO %s (pk, ck1, ck2, v) VALUES (1, 2, 3, 'foo')"); + } + + @Test + public void testCreateTableWithColumnWithRegularColumnLengthSmallerThanConstraint() throws Throwable + { + createTable("CREATE TABLE %s (pk int, ck1 int, ck2 int, v text CHECK LENGTH(v) < 4, PRIMARY KEY ((pk),ck1, ck2)) WITH CLUSTERING ORDER BY (ck1 " + order + ");"); + + // Valid + execute("INSERT INTO %s (pk, ck1, ck2, v) VALUES (1, 2, 3, 'foo')"); + + // Invalid + assertInvalidThrow(InvalidRequestException.class, "INSERT INTO %s (pk, ck1, ck2, v) VALUES (1, 2, 3, 'fooo')"); + assertInvalidThrow(InvalidRequestException.class, "INSERT INTO %s (pk, ck1, ck2, v) VALUES (1, 2, 3, 'foooo')"); + } + + @Test + public void testCreateTableWithColumnWithRegularColumnLengthSmallerOrEqualThanConstraint() throws Throwable + { + createTable("CREATE TABLE %s (pk int, ck1 int, ck2 int, v text CHECK LENGTH(v) <= 4, PRIMARY KEY ((pk),ck1, ck2)) WITH CLUSTERING ORDER BY (ck1 " + order + ");"); + + // Valid + execute("INSERT INTO %s (pk, ck1, ck2, v) VALUES (1, 2, 3, 'foo')"); + execute("INSERT INTO %s (pk, ck1, ck2, v) VALUES (1, 2, 3, 'fooo')"); + + // Invalid + assertInvalidThrow(InvalidRequestException.class, "INSERT INTO %s (pk, ck1, ck2, v) VALUES (1, 2, 3, 'foooo')"); + } + + @Test + public void testCreateTableWithColumnMixedColumnsLengthConstraint() throws Throwable + { + createTable("CREATE TABLE %s (pk text CHECK LENGTH(pk) = 4, ck1 int, ck2 int, v text CHECK LENGTH(v) = 4, PRIMARY KEY ((pk),ck1, ck2)) WITH CLUSTERING ORDER BY (ck1 " + order + ");"); + + // Valid + execute("INSERT INTO %s (pk, ck1, ck2, v) VALUES ('fooo', 2, 3, 'fooo')"); + + // Invalid + assertInvalidThrow(InvalidRequestException.class, "INSERT INTO %s (pk, ck1, ck2, v) VALUES ('foo', 2, 3, 'foo')"); + assertInvalidThrow(InvalidRequestException.class, "INSERT INTO %s (pk, ck1, ck2, v) VALUES ('fooo', 2, 3, 'foo')"); + assertInvalidThrow(InvalidRequestException.class, "INSERT INTO %s (pk, ck1, ck2, v) VALUES ('foo', 2, 3, 'fooo')"); + assertInvalidThrow(InvalidRequestException.class, "INSERT INTO %s (pk, ck1, ck2, v) VALUES ('foooo', 2, 3, 'fooo')"); + assertInvalidThrow(InvalidRequestException.class, "INSERT INTO %s (pk, ck1, ck2, v) VALUES ('fooo', 2, 3, 'foooo')"); + assertInvalidThrow(InvalidRequestException.class, "INSERT INTO %s (pk, ck1, ck2, v) VALUES ('foooo', 2, 3, 'foooo')"); + } + + @Test + public void testCreateTableWithWrongColumnConstraint() throws Throwable + { + try + { + createTable("CREATE TABLE %s (pk text, ck1 int CHECK LENGTH(pk) = 4, ck2 int, v int, PRIMARY KEY ((pk),ck1, ck2)) WITH CLUSTERING ORDER BY (ck1 " + order + ");"); + fail(); + } + catch (InvalidRequestException e) + { + assertTrue(e.getCause() instanceof InvalidRequestException); + assertTrue(e.getMessage().contains("Error setting schema for test")); + } + } + + @Test + public void testCreateTableWithWrongColumnMultipleConstraint() throws Throwable + { + try + { + createTable("CREATE TABLE %s (pk text, ck1 int CHECK LENGTH(pk) = 4 AND ck1 < 4, ck2 int, v int, PRIMARY KEY ((pk),ck1, ck2)) WITH CLUSTERING ORDER BY (ck1 " + order + ");"); + fail(); + } + catch (InvalidRequestException e) + { + assertTrue(e.getCause() instanceof InvalidRequestException); + assertTrue(e.getMessage().contains("Error setting schema for test")); + } + } + + @Test + public void testCreateTableWithColumnWithClusteringColumnInvalidTypeConstraint() throws Throwable + { + try + { + createTable("CREATE TABLE %s (pk int, ck1 int CHECK LENGTH(ck1) = 4, ck2 int, v int, PRIMARY KEY ((pk),ck1, ck2)) WITH CLUSTERING ORDER BY (ck1 " + order + ");"); + fail(); + } + catch (InvalidRequestException e) + { + assertTrue(e.getCause() instanceof InvalidRequestException); + assertTrue(e.getMessage().contains("Error setting schema for test")); + } + } + + @Test + public void testCreateTableInvalidFunction() throws Throwable + { + try + { + createTable("CREATE TABLE %s (pk text CHECK not_a_function(pk) = 4, ck1 int, ck2 int, PRIMARY KEY ((pk),ck1, ck2)) WITH CLUSTERING ORDER BY (ck1 " + order + ");"); + fail(); + } + catch (InvalidRequestException e) + { + assertTrue(e.getCause() instanceof InvalidRequestException); + assertTrue(e.getMessage().contains("Error setting schema for test")); + } + } + + @Test + public void testCreateTableWithPKConstraintsAndCDCEnabled() throws Throwable + { + // It works + createTable("CREATE TABLE %s (pk text CHECK length(pk) = 4, ck1 int, ck2 int, PRIMARY KEY ((pk), ck1, ck2)) WITH cdc = true;"); + } + + @Test + public void testCreateTableWithClusteringConstraintsAndCDCEnabled() throws Throwable + { + // It works + createTable("CREATE TABLE %s (pk text, ck1 int CHECK ck1 < 100, ck2 int, PRIMARY KEY ((pk), ck1, ck2)) WITH cdc = true;"); + } + + @Test + public void testCreateTableWithRegularConstraintsAndCDCEnabled() throws Throwable + { + // It works + createTable("CREATE TABLE %s (pk text, ck1 int CHECK ck1 < 100, ck2 int, PRIMARY KEY (pk)) WITH cdc = true;"); + } + + // Copy table with like + @Test + public void testCreateTableWithColumnWithClusteringColumnLessThanScalarConstraintIntegerOnLikeTable() throws Throwable + { + createTable(KEYSPACE, "CREATE TABLE %s (pk int, ck1 int CHECK ck1 < 4, ck2 int, v int, PRIMARY KEY ((pk),ck1, ck2)) WITH CLUSTERING ORDER BY (ck1 " + order + ");", "liketabletame"); + + execute("create table " + KEYSPACE + ".tb_copy like %s"); + + // Valid + qt().forAll(Generators.toGen(integers().between(0, 3))) + .check(d -> execute("INSERT INTO " + KEYSPACE + ".tb_copy (pk, ck1, ck2, v) VALUES (1, " + d + ", 3, 4)")); + + // Invalid + qt().forAll(Generators.toGen(integers().between(4, 100))) + .check(d -> { + try + { + assertInvalidThrow(InvalidRequestException.class, "INSERT INTO " + KEYSPACE + ".tb_copy(pk, ck1, ck2, v) VALUES (1, " + d + ", 3, 4)"); + } + catch (Throwable e) + { + throw new RuntimeException(e); + } + }); + } +} diff --git a/test/unit/org/apache/cassandra/db/marshal/AbstractTypeTest.java b/test/unit/org/apache/cassandra/db/marshal/AbstractTypeTest.java index ed2046fe5d..5cb051b35e 100644 --- a/test/unit/org/apache/cassandra/db/marshal/AbstractTypeTest.java +++ b/test/unit/org/apache/cassandra/db/marshal/AbstractTypeTest.java @@ -259,6 +259,18 @@ public class AbstractTypeTest return "test".equals(new File(src.getLocation().getPath()).name()); } + @Test + public void isConstrainedTest() + { + qt().forAll(genBuilder().build()).checkAssert(type -> { + if (type instanceof MapType || type instanceof TupleType || type instanceof AbstractCompositeType) + assertThat(type.isConstrainable()).isEqualTo(false); + else + assertThat(type.isConstrainable()).isEqualTo(true); + }); + + } + @Test public void unsafeSharedSerializer() { @@ -950,7 +962,7 @@ public class AbstractTypeTest assertThat(leftDecomposed.hasRemaining()).describedAs(typeRelDesc(".decompose", left, right)).isEqualTo(rightDecomposed.hasRemaining()); // serialization compatibility means that we can read a cell written using right's type serializer with left's type serializer; - // this additinoally imposes the requirement for storing the buffer lenght in the serialized form if the value is of variable length + // this additinoally imposes the requirement for storing the buffer length in the serialized form if the value is of variable length // as well as, either both types serialize into a single or multiple cells if (left.isSerializationCompatibleWith(right)) {