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
This commit is contained in:
Bernardo Botella Corbi 2024-06-03 15:41:20 -07:00 committed by Stefan Miklosovic
parent f74b1786e3
commit 5cbf993d96
No known key found for this signature in database
GPG Key ID: 32F35CB2F546D93E
46 changed files with 3550 additions and 110 deletions

View File

@ -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)

View File

@ -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.

View File

@ -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]

View File

@ -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] <table> ALTER [IF EXISTS] <column> CHECK <condition>;
----
== DROP CONSTRAINT
And DROP can be used to drop constraints for a column as well.
[source,bnf]
----
ALTER TABLE [IF EXISTS] <table> ALTER [IF EXISTS] <column> 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;
----

View File

@ -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]

View File

@ -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

View File

@ -322,7 +322,14 @@ JUNK ::= /([ \t\r\f\v]+|(--|[/][/])[^\n\r]*([\n\r]|$)|[/][*].*?[*][/])/ ;
<userType> ::= utname=<cfOrKsName> ;
<storageType> ::= ( <simpleStorageType> | <collectionType> | <frozenCollectionType> | <vectorType> | <userType> ) ( <column_mask> )? ;
<storageType> ::= ( <simpleStorageType> | <collectionType> | <frozenCollectionType> | <vectorType> | <userType> ) ( <constraintsExpr> )? ( <column_mask> )? ;
<constraintsExpr> ::= "CHECK" <constraint> ( "AND" <constraint> )*
;
<constraint> ::= <cident> <cmp> <term>
| <functionArguments> <cmp> <term>
;
<column_mask> ::= "MASKED" "WITH" ( "DEFAULT" | <functionName> <selectionFunctionArguments> );
@ -1463,7 +1470,7 @@ syntax_rules += r'''
| "WITH" <cfamProperty> ( "AND" <cfamProperty> )*
| "RENAME" ("IF" "EXISTS")? existcol=<cident> "TO" newcol=<cident>
( "AND" existcol=<cident> "TO" newcol=<cident> )*
| "ALTER" ("IF" "EXISTS")? existcol=<cident> ( <column_mask> | "DROP" "MASKED" )
| "ALTER" ("IF" "EXISTS")? existcol=<cident> ( <constraintsExpr> | <column_mask> | "DROP" ( "CHECK" | "MASKED" ) )
;
<alterUserTypeStatement> ::= "ALTER" "TYPE" ("IF" "EXISTS")? ut=<userTypeName>

View File

@ -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=['<new_column_name>', 'IF'])
self.trycompletions('ALTER TABLE IF EXISTS new_table ADD IF NOT EXISTS ', choices=['<new_column_name>'])
self.trycompletions('ALTER TABLE new_table ADD IF NOT EXISTS ', choices=['<new_column_name>'])
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', '<quotedName>', '<identifier>'])
self.trycompletions('ALTER TABLE IF EXISTS new_table ALTER ', choices=['IF', '<quotedName>', '<identifier>'])
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):

View File

@ -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;

View File

@ -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;

View File

@ -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<Term.Raw> 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; }
;

View File

@ -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);
}
}
}

View File

@ -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<T>
{
// 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<T> 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();
}

View File

@ -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<ColumnConstraints>
{
public static final Serializer serializer = new Serializer();
public static final ColumnConstraints NO_OP = new Noop();
private final List<ColumnConstraint<?>> constraints;
public ColumnConstraints(List<ColumnConstraint<?>> constraints)
{
this.constraints = constraints;
}
@Override
public MetadataSerializer<ColumnConstraints> 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<ColumnConstraint<?>> 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<ColumnConstraint<?>> constraints;
public Raw(List<ColumnConstraint<?>> 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<ColumnConstraints>
{
@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<ColumnConstraint<?>> 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);
}
}

View File

@ -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;
}

View File

@ -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);
}
}

View File

@ -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<FunctionColumnConstraint>
{
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<ColumnIdentifier, ConstraintFunction> functionCreator;
Functions(Function<ColumnIdentifier, ConstraintFunction> 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<FunctionColumnConstraint> 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<FunctionColumnConstraint>
{
@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);
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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<ScalarColumnConstraint>
{
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<ScalarColumnConstraint> serializer()
{
return serializer;
}
@Override
public void appendCqlTo(CqlBuilder builder)
{
builder.append(toString());
}
private static class Serializer implements MetadataSerializer<ScalarColumnConstraint>
{
@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);
}
}

View File

@ -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);

View File

@ -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);
}
}
}

View File

@ -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;

View File

@ -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<ColumnIdentifier, ColumnProperties.Raw> rawColumns;
private final Set<ColumnIdentifier> staticColumns;
private final List<ColumnIdentifier> partitionKeyColumns;
private final Map<ColumnIdentifier, ColumnConstraints> columnConstraints;
private final List<ColumnIdentifier> clusteringColumns;
private final LinkedHashMap<ColumnIdentifier, Boolean> clusteringOrder;
@ -72,6 +74,7 @@ public final class CreateTableStatement extends AlterSchemaStatement
Set<ColumnIdentifier> staticColumns,
List<ColumnIdentifier> partitionKeyColumns,
List<ColumnIdentifier> clusteringColumns,
Map<ColumnIdentifier, ColumnConstraints> columnConstraints,
LinkedHashMap<ColumnIdentifier, Boolean> 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<ColumnIdentifier, ColumnProperties.Raw> rawColumns = new HashMap<>();
private final Set<ColumnIdentifier> staticColumns = new HashSet<>();
private final List<ColumnIdentifier> clusteringColumns = new ArrayList<>();
private final Map<ColumnIdentifier, ColumnConstraints> columnConstraints = new HashMap<>();
private List<ColumnIdentifier> 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()

View File

@ -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<V> extends IMeasurableMemory, Clusterable<V>
return equals(prefix, (ClusteringPrefix<?>) o);
}
default void checkConstraints(int clusterIndex, ClusteringComparator comparator, ColumnConstraints constraints)
{
comparator.subtype(clusterIndex).checkConstraints(accessor().toBuffer(get(clusterIndex)), constraints);
}
}

View File

@ -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<ColumnMetadata> 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<ColumnMetadata> 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

View File

@ -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<ByteBuffer>
return true;
}
@Override
public boolean isConstrainable()
{
// Constraints are not supported for composite types
return false;
}
public <VL, VR> int compareCustom(VL left, ValueAccessor<VL> accessorL, VR right, ValueAccessor<VR> accessorR)
{
if (accessorL.isEmpty(left) || accessorR.isEmpty(right))
@ -281,7 +291,8 @@ public abstract class AbstractCompositeType extends AbstractType<ByteBuffer>
validate(bb, ByteBufferAccessor.instance);
}
public <V> void validate(V input, ValueAccessor<V> accessor)
@Override
public <V> void validate(V input, ValueAccessor<V> accessor)
{
boolean isStatic = readIsStatic(input, accessor);
int offset = startingOffset(isStatic);
@ -316,6 +327,55 @@ public abstract class AbstractCompositeType extends AbstractType<ByteBuffer>
}
}
@Override
public void checkConstraints(ByteBuffer input, ColumnConstraints constraints) throws ConstraintViolationException
{
// no constraints defined for the partition keys
if (!constraints.hasRelevantConstraints())
return;
ValueAccessor<ByteBuffer> accessor = ByteBufferAccessor.instance;
boolean isStatic = readIsStatic(input, accessor);
int offset = startingOffset(isStatic);
int i = 0;
List<ByteBuffer> 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 <V> int getComparatorSize(int i, V value, ValueAccessor<V> accessor, int offset);

View File

@ -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<T> implements Comparator<ByteBuffer>, 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<ColumnConstraint> 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<T> implements Comparator<ByteBuffer>, 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<T> implements Comparator<ByteBuffer>, Assignm
return type.compose(buffer);
}
}
public boolean isConstrainable()
{
return true;
}
}

View File

@ -456,4 +456,10 @@ public class MapType<K, V> extends CollectionType<Map<K, V>>
return getSerializer().getSerializedValue(((Cell<?>) columnData).buffer(), keyOrIndex, getValuesType());
}
@Override
public boolean isConstrainable()
{
return false;
}
}

View File

@ -244,4 +244,10 @@ public class ReversedType<T> extends AbstractType<T>
{
return baseType.unwrap();
}
@Override
public boolean isConstrainable()
{
return unwrap().isConstrainable();
}
}

View File

@ -616,4 +616,10 @@ public class TupleType extends MultiElementType<ByteBuffer>
{
throw new UnsupportedOperationException("Multicell tuples are not supported");
}
@Override
public boolean isConstrainable()
{
return false;
}
}

View File

@ -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 {

View File

@ -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<CellPath> 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<Difference> 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<ColumnConstraint<?>> constraintIterator = columnConstraints.getConstraints().iterator();
constraintIterator.next().appendCqlTo(builder);
while (constraintIterator.hasNext())
{
builder.append(" AND ");
constraintIterator.next().appendCqlTo(builder);
}
}
}
public static String toCQLString(Iterable<ColumnMetadata> 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;
}
}
}

View File

@ -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<ColumnConstraint> partitionKeyConstraints;
public final List<ColumnMetadata> 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<ColumnConstraint> pkConstraints = new ArrayList<>(this.partitionKeyColumns.size());
for (ColumnMetadata column : this.partitionKeyColumns)
{
if (column.hasConstraint())
pkConstraints.add(column.getColumnConstraints());
}
this.partitionKeyConstraints = pkConstraints;
List<ColumnMetadata> 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;
}
}
}
}

View File

@ -34,7 +34,7 @@ import static org.apache.cassandra.db.TypeSizes.sizeofUnsignedVInt;
public class NodeVersion implements Comparable<NodeVersion>
{
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;

View File

@ -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);

View File

@ -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<String, String> 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<String> typesSet = Set.of("int", "double", "float", "decimal");
try (Cluster cluster = init(Cluster.build(3).start()))
{
// Create tables
for (String type : typesSet)
{
for (Map.Entry<String, String> 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<String> typesSet = Set.of("varchar", "text", "blob", "ascii");
try (Cluster cluster = init(Cluster.build(3).start()))
{
// Create tables
for (String type : typesSet)
{
for (Map.Entry<String, String> 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<Throwable>(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<Throwable>(t -> t.getClass().getCanonicalName()
.equals(InvalidRequestException.class.getCanonicalName()), description));
}
}

View File

@ -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<Cluster> 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<Cluster> 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<Cluster> 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);
}
}
}

View File

@ -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<Cluster> 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);
}
}

View File

@ -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);
}
}

View File

@ -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<int,bigint>, val int ) CALLED ON NULL INPUT RETURNS tuple<int,bigint> LANGUAGE java AS \n" +
" 'if (val !=null) { state.setInt(0, state.getInt(0)+1); state.setLong(1, state.getLong(1)+val.intValue()); } return state;'; "));

View File

@ -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");
}
}

View File

@ -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);
}
}

View File

@ -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<String, String> map)
{
return new CqlBuilder().append(map).toString();
}
}

View File

@ -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))
{