Various fixes in constraint framework

- fix some edge cases for NOT_NULL
- ability to specify constraints when altering a column
- ensure constraint is specified on a column it is bound to
- fix nullity check on map type
- fix satistfiability check on function constraints

patch by Stefan Miklosovic; reviewed by Bernardo Botella for CASSANDRA-20481
This commit is contained in:
Stefan Miklosovic 2025-03-23 23:37:11 +01:00
parent 2e356413ee
commit bb66561142
No known key found for this signature in database
GPG Key ID: 32F35CB2F546D93E
23 changed files with 568 additions and 72 deletions

View File

@ -1,4 +1,5 @@
5.1
* Various fixes in constraint framework (CASSANDRA-20481)
* Add support in CAS for -= on numeric types, and fixed improper handling of empty bytes which lead to NPE (CASSANDRA-20477)
* Do not fail to start a node with materialized views after they are turned off in config (CASSANDRA-20452)
* Fix nodetool gcstats output, support human-readable units and more output formats (CASSANDRA-19022)

View File

@ -988,14 +988,14 @@ alterTableStatement returns [AlterTableStatement.Raw stmt]
| K_ALTER ( K_IF K_EXISTS { $stmt.ifColumnExists(true); } )? id=cident
( mask=columnMask { $stmt.mask(id, mask); }
| constraints=columnConstraints { $stmt.constraint(id, constraints); }
| K_DROP K_MASKED { $stmt.mask(id, null); }
| K_DROP K_CHECK { $stmt.constraint(id, null); }
| (constraints=columnConstraints) { $stmt.constraint(id, constraints); })
| K_DROP K_CHECK { $stmt.constraint(id, null); })
| K_ADD ( K_IF K_NOT K_EXISTS { $stmt.ifColumnNotExists(true); } )?
( id=ident v=comparatorType b=isStaticColumn (m=columnMask)? { $stmt.add(id, v, b, m); }
| ('(' id1=ident v1=comparatorType b1=isStaticColumn (m1=columnMask)? { $stmt.add(id1, v1, b1, m1); }
( ',' idn=ident vn=comparatorType bn=isStaticColumn (mn=columnMask)? { $stmt.add(idn, vn, bn, mn); mn=null; } )* ')') )
( id=ident v=comparatorType b=isStaticColumn (m=columnMask)? (c=columnConstraints)? { $stmt.add(id, v, b, m, c); }
| ('(' id1=ident v1=comparatorType b1=isStaticColumn (m1=columnMask)? (c=columnConstraints)? { $stmt.add(id1, v1, b1, m1, c); }
( ',' idn=ident vn=comparatorType bn=isStaticColumn (mn=columnMask)? (c=columnConstraints)? { $stmt.add(idn, vn, bn, mn, c); mn=null; c=null;} )* ')') )
| K_DROP ( K_IF K_EXISTS { $stmt.ifColumnExists(true); } )?
( id=ident { $stmt.drop(id); }

View File

@ -26,6 +26,8 @@ import java.util.TreeSet;
import org.apache.cassandra.cql3.Operator;
import org.apache.cassandra.cql3.functions.types.ParseUtils;
import org.apache.cassandra.db.marshal.AbstractType;
import org.apache.cassandra.db.marshal.Int32Type;
import org.apache.cassandra.schema.ColumnMetadata;
import org.apache.cassandra.utils.Pair;
@ -70,6 +72,8 @@ public abstract class AbstractFunctionSatisfiabilityChecker<CONSTRAINT_TYPE exte
*/
abstract Pair<List<CONSTRAINT_TYPE>, List<CONSTRAINT_TYPE>> categorizeConstraints(List<ColumnConstraint<?>> constraints, String functionName);
abstract AbstractType<?> returnType(ColumnMetadata columnMetadata);
private void checkSupportedOperators(List<CONSTRAINT_TYPE> allConstraints, String functionName)
{
for (CONSTRAINT_TYPE constraint : allConstraints)
@ -147,11 +151,12 @@ public abstract class AbstractFunctionSatisfiabilityChecker<CONSTRAINT_TYPE exte
}
else
{
ByteBuffer firstTermBuffer = columnMetadata.type.fromString(ParseUtils.unquote(firstTerm));
ByteBuffer secondTermBuffer = columnMetadata.type.fromString(ParseUtils.unquote(secondTerm));
AbstractType<?> returnType = returnType(columnMetadata);
ByteBuffer firstTermBuffer = returnType.fromString(ParseUtils.unquote(firstTerm));
ByteBuffer secondTermBuffer = returnType.fromString(ParseUtils.unquote(secondTerm));
boolean firstSatisfaction = firstRelation.isSatisfiedBy(columnMetadata.type, secondTermBuffer, firstTermBuffer);
boolean secondSatisfaction = secondRelation.isSatisfiedBy(columnMetadata.type, firstTermBuffer, secondTermBuffer);
boolean firstSatisfaction = firstRelation.isSatisfiedBy(returnType, secondTermBuffer, firstTermBuffer);
boolean secondSatisfaction = secondRelation.isSatisfiedBy(returnType, firstTermBuffer, secondTermBuffer);
if (!firstSatisfaction || !secondSatisfaction)
throw new InvalidConstraintDefinitionException(format("Constraints of %s are not satisfiable: %s %s %s, %s %s %s",
@ -186,6 +191,14 @@ public abstract class AbstractFunctionSatisfiabilityChecker<CONSTRAINT_TYPE exte
return Pair.create(scalars, notEqualScalars);
}
@Override
AbstractType<?> returnType(ColumnMetadata metadata)
{
// function constraints will always have terms of int32 type
// unlike scalar constraints where it will be a type of column
return metadata.type;
}
};
public static final AbstractFunctionSatisfiabilityChecker<FunctionColumnConstraint> FUNCTION_SATISFIABILITY_CHECKER = new AbstractFunctionSatisfiabilityChecker<>()
@ -215,5 +228,11 @@ public abstract class AbstractFunctionSatisfiabilityChecker<CONSTRAINT_TYPE exte
return Pair.create(funnctionColumnConstraints, notEqualConstraints);
}
@Override
AbstractType<?> returnType(ColumnMetadata columnMetadata)
{
return Int32Type.instance;
}
};
}

View File

@ -30,6 +30,7 @@ import org.apache.cassandra.cql3.constraints.ScalarColumnConstraint.ScalarColumn
import org.apache.cassandra.db.marshal.AbstractType;
import org.apache.cassandra.schema.ColumnMetadata;
import org.apache.cassandra.tcm.serialization.MetadataSerializer;
import org.apache.cassandra.utils.ByteBufferUtil;
import static java.lang.String.format;
@ -116,8 +117,10 @@ public abstract class ColumnConstraint<T>
*/
public void evaluate(AbstractType<?> valueType, ByteBuffer columnValue) throws ConstraintViolationException
{
if (columnValue.capacity() == 0)
if (columnValue == ByteBufferUtil.EMPTY_BYTE_BUFFER)
throw new ConstraintViolationException("Column value does not satisfy value constraint for column '" + columnName + "' as it is null.");
else if (valueType.isEmptyValueMeaningless() && columnValue.capacity() == 0)
throw new ConstraintViolationException("Column value does not satisfy value constraint for column '" + columnName + "' as it is empty.");
internalEvaluate(valueType, columnValue);
}

View File

@ -27,6 +27,7 @@ import java.util.Objects;
import java.util.Set;
import java.util.TreeSet;
import org.apache.cassandra.cql3.ColumnIdentifier;
import org.apache.cassandra.cql3.CqlBuilder;
import org.apache.cassandra.db.TypeSizes;
import org.apache.cassandra.db.marshal.AbstractType;
@ -108,7 +109,7 @@ public class ColumnConstraints extends ColumnConstraint<ColumnConstraints>
// Checks if there is at least one constraint that will perform checks
public boolean hasRelevantConstraints()
{
for (ColumnConstraint c : constraints)
for (ColumnConstraint<?> c : constraints)
{
if (c != ColumnConstraints.NO_OP)
return true;
@ -120,9 +121,12 @@ public class ColumnConstraints extends ColumnConstraint<ColumnConstraints>
public void validate(ColumnMetadata columnMetadata) throws InvalidConstraintDefinitionException
{
if (!columnMetadata.type.isConstrainable())
{
throw new InvalidConstraintDefinitionException("Constraint cannot be defined on the column "
+ columnMetadata.name + " of type " + columnMetadata.type.asCQL3Type()
+ " for the table " + columnMetadata.ksName + "." + columnMetadata.cfName);
+ " for the table " + columnMetadata.ksName + '.' + columnMetadata.cfName + '.' +
(columnMetadata.type.isCollection() ? " When using collections, constraints can be used only of frozen collections." : ""));
}
// this will look at constraints as a whole,
// checking if combinations of a particular constraint make sense (duplicities, satisfiability etc.).
@ -207,10 +211,18 @@ public class ColumnConstraints extends ColumnConstraint<ColumnConstraints>
this.constraints = Collections.emptyList();
}
public ColumnConstraints prepare()
public ColumnConstraints prepare(ColumnIdentifier column)
{
if (constraints.isEmpty())
return NO_OP;
for (ColumnConstraint<?> constraint : constraints)
{
if (constraint.columnName != null && !column.equals(constraint.columnName))
throw new InvalidConstraintDefinitionException(format("Constraint %s was not specified on a column it operates on: %s but on: %s",
constraint, column.toCQLString(), constraint.columnName));
}
return new ColumnConstraints(constraints);
}
}

View File

@ -25,6 +25,7 @@ import org.apache.cassandra.cql3.ColumnIdentifier;
import org.apache.cassandra.cql3.Operator;
import org.apache.cassandra.db.marshal.AbstractType;
import org.apache.cassandra.schema.ColumnMetadata;
import org.apache.cassandra.utils.ByteBufferUtil;
import static org.apache.cassandra.cql3.Operator.EQ;
import static org.apache.cassandra.cql3.Operator.GT;
@ -55,8 +56,10 @@ public abstract class ConstraintFunction
*/
public void evaluate(AbstractType<?> valueType, Operator relationType, String term, ByteBuffer columnValue) throws ConstraintViolationException
{
if (columnValue.capacity() == 0)
if (columnValue == ByteBufferUtil.EMPTY_BYTE_BUFFER)
throw new ConstraintViolationException("Column value does not satisfy value constraint for column '" + columnName + "' as it is null.");
else if (valueType.isEmptyValueMeaningless() && columnValue.capacity() == 0)
throw new ConstraintViolationException("Column value does not satisfy value constraint for column '" + columnName + "' as it is empty.");
internalEvaluate(valueType, relationType, term, columnValue);
}
@ -88,10 +91,7 @@ public abstract class ConstraintFunction
*
* @return list of operators this function is allowed to have.
*/
public List<Operator> getSupportedOperators()
{
return List.of();
}
public abstract List<Operator> getSupportedOperators();
/**
* Tells what types of columns are supported by this constraint.

View File

@ -31,7 +31,7 @@ import org.apache.cassandra.utils.JsonUtils;
import static java.lang.String.format;
public class JsonConstraint extends ConstraintFunction
public class JsonConstraint extends UnaryConstraintFunction
{
private static final List<AbstractType<?>> SUPPORTED_TYPES = List.of(UTF8Type.instance, AsciiType.instance);
@ -39,12 +39,7 @@ public class JsonConstraint extends ConstraintFunction
public JsonConstraint(ColumnIdentifier columnName)
{
this(columnName, FUNCTION_NAME);
}
public JsonConstraint(ColumnIdentifier columnName, String name)
{
super(columnName, name);
super(columnName, FUNCTION_NAME);
}
@Override

View File

@ -28,18 +28,13 @@ import org.apache.cassandra.schema.ColumnMetadata;
import static java.lang.String.format;
public class NotNullConstraint extends ConstraintFunction
public class NotNullConstraint extends UnaryConstraintFunction
{
public static final String FUNCTION_NAME = "NOT_NULL";
public NotNullConstraint(ColumnIdentifier columnName)
{
this(columnName, FUNCTION_NAME);
}
public NotNullConstraint(ColumnIdentifier columnName, String name)
{
super(columnName, name);
super(columnName, FUNCTION_NAME);
}
@Override

View File

@ -0,0 +1,37 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.cassandra.cql3.constraints;
import java.util.List;
import org.apache.cassandra.cql3.ColumnIdentifier;
import org.apache.cassandra.cql3.Operator;
public abstract class UnaryConstraintFunction extends ConstraintFunction
{
public UnaryConstraintFunction(ColumnIdentifier columnName, String name)
{
super(columnName, name);
}
public List<Operator> getSupportedOperators()
{
return List.of();
}
}

View File

@ -371,9 +371,7 @@ public class UpdateStatement extends ModificationStatement
public static void evaluateConstraint(ColumnMetadata columnMetadata, ByteBuffer cellData)
{
for (ColumnConstraint constraint : columnMetadata.getColumnConstraints().getConstraints())
{
for (ColumnConstraint<?> constraint : columnMetadata.getColumnConstraints().getConstraints())
constraint.evaluate(columnMetadata.type, cellData);
}
}
}

View File

@ -257,13 +257,16 @@ public abstract class AlterTableStatement extends AlterSchemaStatement
private final boolean isStatic;
@Nullable
private final ColumnMask.Raw mask;
@Nullable
private final ColumnConstraints.Raw constraints;
Column(ColumnIdentifier name, CQL3Type.Raw type, boolean isStatic, @Nullable ColumnMask.Raw mask)
Column(ColumnIdentifier name, CQL3Type.Raw type, boolean isStatic, @Nullable ColumnMask.Raw mask, @Nullable ColumnConstraints.Raw constraints)
{
this.name = name;
this.type = type;
this.isStatic = isStatic;
this.mask = mask;
this.constraints = constraints;
}
}
@ -311,6 +314,7 @@ public abstract class AlterTableStatement extends AlterSchemaStatement
AbstractType<?> type = column.type.prepare(keyspaceName, keyspace.types).getType();
boolean isStatic = column.isStatic;
ColumnMask mask = column.mask == null ? null : column.mask.prepare(keyspaceName, tableName, name, type, keyspace.userFunctions);
ColumnConstraints columnConstraints = column.constraints == null ? ColumnConstraints.NO_OP : column.constraints.prepare(name);
if (null != tableBuilder.getColumn(name)) {
if (!ifColumnNotExists)
@ -361,9 +365,9 @@ public abstract class AlterTableStatement extends AlterSchemaStatement
}
if (isStatic)
tableBuilder.addStaticColumn(name, type, mask);
tableBuilder.addStaticColumn(name, type, mask, columnConstraints);
else
tableBuilder.addRegularColumn(name, type, mask);
tableBuilder.addRegularColumn(name, type, mask, columnConstraints);
if (!isStatic)
{
@ -372,7 +376,8 @@ public abstract class AlterTableStatement extends AlterSchemaStatement
if (view.includeAllColumns)
{
ColumnMetadata viewColumn = ColumnMetadata.regularColumn(view.metadata, name.bytes, type)
.withNewMask(mask);
.withNewMask(mask)
.withNewColumnConstraints(columnConstraints);
viewsBuilder.put(viewsBuilder.get(view.name()).withAddedRegularColumn(viewColumn));
}
}
@ -732,7 +737,7 @@ public abstract class AlterTableStatement extends AlterSchemaStatement
if (column != null)
{
ColumnConstraints oldConstraints = column.getColumnConstraints();
ColumnConstraints newConstraints = constraints == null ? ColumnConstraints.NO_OP : constraints.prepare();
ColumnConstraints newConstraints = constraints == null ? ColumnConstraints.NO_OP : constraints.prepare(columnName);
if (Objects.equals(oldConstraints, newConstraints))
return keyspace;
newConstraints.validate(column);
@ -837,10 +842,10 @@ public abstract class AlterTableStatement extends AlterSchemaStatement
rawMask = mask;
}
public void add(ColumnIdentifier name, CQL3Type.Raw type, boolean isStatic, @Nullable ColumnMask.Raw mask)
public void add(ColumnIdentifier name, CQL3Type.Raw type, boolean isStatic, @Nullable ColumnMask.Raw mask, @Nullable ColumnConstraints.Raw constraints)
{
kind = Kind.ADD_COLUMNS;
addedColumns.add(new AddColumns.Column(name, type, isStatic, mask));
addedColumns.add(new AddColumns.Column(name, type, isStatic, mask, constraints));
}
public void drop(ColumnIdentifier name)

View File

@ -578,7 +578,7 @@ public final class CreateTableStatement extends AlterSchemaStatement
if (null == constraints)
columnConstraints.put(column, ColumnConstraints.NO_OP);
else
columnConstraints.put(column, constraints.prepare());
columnConstraints.put(column, constraints.prepare(column));
}
public void setCompactStorage()

View File

@ -211,20 +211,12 @@ public abstract class AbstractType<T> implements Comparator<ByteBuffer>, Assignm
public void checkConstraints(ByteBuffer bytes, ColumnConstraints constraints) throws ConstraintViolationException
{
if (constraints.isEmpty())
return;
T value = getSerializer().deserialize(bytes);
constraints.evaluate(this, bytes);
checkConstraints(bytes, constraints.getConstraints());
}
public void checkConstraints(ByteBuffer bytes, List<ColumnConstraint> constraints) throws ConstraintViolationException
public void checkConstraints(ByteBuffer bytes, List<ColumnConstraint<?>> constraints) throws ConstraintViolationException
{
if (constraints.isEmpty())
return;
T value = getSerializer().deserialize(bytes);
for (ColumnConstraint constraint : constraints)
for (ColumnConstraint<?> constraint : constraints)
constraint.evaluate(this, bytes);
}

View File

@ -171,6 +171,12 @@ public abstract class CollectionType<T> extends MultiElementType<T>
return true;
}
@Override
public boolean isConstrainable()
{
return isFrozenCollection();
}
public ByteBuffer serializeForNativeProtocol(Iterator<Cell<?>> cells)
{
assert isMultiCell();

View File

@ -456,10 +456,4 @@ 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

@ -204,7 +204,7 @@ public class TableMetadata implements SchemaElement
// We cache the columns with constraints to avoid iterations over columns
// Partition keys columns are evaluated separately, so we keep the two of them in
// two different variables.
public final List<ColumnConstraint> partitionKeyConstraints;
public final List<ColumnConstraint<?>> partitionKeyConstraints;
public final List<ColumnMetadata> columnsWithConstraints;
public final List<ColumnMetadata> notNullColumns;
@ -248,7 +248,7 @@ public class TableMetadata implements SchemaElement
else
ref = TableMetadataRef.withInitialReference(new TableMetadataRef(Schema.instance, keyspace, name, id), this);
List<ColumnConstraint> pkConstraints = new ArrayList<>(this.partitionKeyColumns.size());
List<ColumnConstraint<?>> pkConstraints = new ArrayList<>(this.partitionKeyColumns.size());
for (ColumnMetadata column : this.partitionKeyColumns)
{
if (column.hasConstraint())

View File

@ -22,10 +22,8 @@ import org.junit.Test;
import org.apache.cassandra.exceptions.InvalidRequestException;
public class AlterTableWithTableConstraintValidationTest extends CqlConstraintValidationTester
{
@Test
public void testCreateTableWithColumnNamedConstraintDescribeTableNonFunction() throws Throwable
{
@ -239,4 +237,44 @@ public class AlterTableWithTableConstraintValidationTest extends CqlConstraintVa
String expectedErrorMessage = "Column 'foo' doesn't exist";
assertInvalidThrowMessage(expectedErrorMessage, InvalidRequestException.class, "ALTER TABLE %s ALTER foo CHECK foo < 100");
}
@Test
public void testAlterTableAlterExistingColumnWithCheckOnNonExistingColumn() throws Throwable
{
createTable("CREATE TABLE %s (pk int, ck1 text, ck2 text, v int, PRIMARY KEY ((pk),ck1, ck2));");
assertInvalidThrowMessage("Constraint ck3 < 100 was not specified on a column it operates on: ck1 but on: ck3",
InvalidRequestException.class,
"ALTER TABLE %s ALTER ck1 CHECK ck3 < 100");
assertInvalidThrowMessage("Constraint NOT_NULL(ck3) was not specified on a column it operates on: ck1 but on: ck3",
InvalidRequestException.class,
"ALTER TABLE %s ALTER ck1 CHECK NOT_NULL(ck3)");
assertInvalidThrowMessage("Constraint LENGTH(ck3) > 10 was not specified on a column it operates on: ck1 but on: ck3",
InvalidRequestException.class,
"ALTER TABLE %s ALTER ck1 CHECK LENGTH(ck3) > 10");
}
@Test
public void testAlterTableAddNewColumnWithCheckOnNonExistingColumn() throws Throwable
{
createTable("CREATE TABLE %s (pk int, ck1 text, ck2 text, v int, PRIMARY KEY ((pk),ck1, ck2));");
assertInvalidThrowMessage("Constraint v3 < 100 was not specified on a column it operates on: v2 but on: v3",
InvalidRequestException.class,
"ALTER TABLE %s ADD v2 int CHECK v3 < 100");
assertInvalidThrowMessage("Constraint NOT_NULL(v3) was not specified on a column it operates on: v2 but on: v3",
InvalidRequestException.class,
"ALTER TABLE %s ADD v2 int CHECK NOT_NULL(v3)");
assertInvalidThrowMessage("Constraint LENGTH(v3) > 10 was not specified on a column it operates on: v2 but on: v3",
InvalidRequestException.class,
"ALTER TABLE %s ADD v2 int CHECK LENGTH(v3) > 10");
}
@Test
public void testAlterTableAddColumnWithCheck()
{
createTable("CREATE TABLE %s (pk text, col1 int, primary key (pk));");
execute("ALTER TABLE %s ADD col2 int CHECK col2 > 0");
}
}

View File

@ -71,13 +71,13 @@ public class ConstraintsSatisfiabilityTest
if (op1 == NEQ)
{
// a_column != 0 and a_column != 10 -> valid
check(op1, 0, op2, 100, quadFunction, null, columnMetadata);
check(op1, 50, op2, 100, quadFunction, null, columnMetadata);
// does not make sense to check twice
// check a_column != 0 and a_column != 0
check(op1, 0, op2, 0, quadFunction, "There are duplicate constraint definitions on column", columnMetadata);
}
else
check(op1, 0, op2, 100, quadFunction, "There are duplicate constraint definitions on column", columnMetadata);
check(op1, 50, op2, 100, quadFunction, "There are duplicate constraint definitions on column", columnMetadata);
}
else if ((op1 == GT && op2 == GTE) ||
(op1 == GTE && op2 == GT) ||
@ -85,18 +85,25 @@ public class ConstraintsSatisfiabilityTest
(op1 == LTE && op2 == LT) ||
(op1 == EQ || op2 == EQ))
{
check(op1, 0, op2, 100, quadFunction, "not supported", columnMetadata);
check(op1, 50, op2, 100, quadFunction, "not supported", columnMetadata);
}
else if ((op1 == LTE && op2 == GT) ||
(op1 == LT && op2 == GT) ||
(op1 == LTE && op2 == GTE) ||
(op1 == LT && op2 == GTE))
{
check(op1, 0, op2, 100, quadFunction, "are not satisfiable", columnMetadata);
check(op1, 50, op2, 100, quadFunction, "are not satisfiable", columnMetadata);
}
else if ((op1 == GT && op2 == LTE) ||
(op1 == GT && op2 == LT) ||
(op1 == GTE && op2 == LTE) ||
(op1 == GTE && op2 == LT))
{
check(op1, 50, op2, 100, quadFunction, null, columnMetadata);
}
else if (!(op1 == NEQ || op2 == NEQ))
{
check(op1, 0, op2, 100, quadFunction, null, columnMetadata);
check(op1, 50, op2, 100, quadFunction, null, columnMetadata);
}
else
{

View File

@ -26,9 +26,11 @@ import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import org.apache.cassandra.cql3.constraints.InvalidConstraintDefinitionException;
import org.apache.cassandra.exceptions.InvalidRequestException;
import org.apache.cassandra.utils.Generators;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import static accord.utils.Property.qt;
@ -1438,4 +1440,20 @@ public class CreateTableWithColumnCqlConstraintValidationTest extends CqlConstra
}
});
}
@Test
public void testCreateTableAddConstraintWithCheckOnNonExistingColumn() throws Throwable
{
assertThatThrownBy(() -> createTable("CREATE TABLE %s (pk int, ck1 text CHECK NOT_NULL(ck3), ck2 text, v int, PRIMARY KEY ((pk),ck1, ck2));"))
.hasRootCauseMessage("Constraint NOT_NULL(ck3) was not specified on a column it operates on: ck1 but on: ck3")
.rootCause().isInstanceOf(InvalidConstraintDefinitionException.class);
assertThatThrownBy(() -> createTable("CREATE TABLE %s (pk int, ck1 int CHECK ck3 > 5, ck2 text, v int, PRIMARY KEY ((pk),ck1, ck2));"))
.hasRootCauseMessage("Constraint ck3 > 5 was not specified on a column it operates on: ck1 but on: ck3")
.rootCause().isInstanceOf(InvalidConstraintDefinitionException.class);
assertThatThrownBy(() -> createTable("CREATE TABLE %s (pk int, ck1 text CHECK LENGTH(ck3) > 10, ck2 text, v int, PRIMARY KEY ((pk),ck1, ck2));"))
.hasRootCauseMessage("Constraint LENGTH(ck3) > 10 was not specified on a column it operates on: ck1 but on: ck3")
.rootCause().isInstanceOf(InvalidConstraintDefinitionException.class);
}
}

View File

@ -50,7 +50,7 @@ public class JsonConstraintTest
run("{}");
run("{\"a\": 5, \"b\": \"1\", \"c\": [1,2,3]}");
run("nonsense", "Value for column 'a_column' violated JSON constraint as it is not a valid JSON.");
run("", "Column value does not satisfy value constraint for column 'a_column' as it is null.");
run("", "Value for column 'a_column' violated JSON constraint as it is not a valid JSON.");
}
@Test

View File

@ -0,0 +1,165 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.cassandra.constraints;
import java.nio.ByteBuffer;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.junit.Test;
import org.apache.cassandra.cql3.ColumnIdentifier;
import org.apache.cassandra.cql3.constraints.ColumnConstraints;
import org.apache.cassandra.cql3.constraints.ConstraintViolationException;
import org.apache.cassandra.cql3.constraints.FunctionColumnConstraint;
import org.apache.cassandra.cql3.constraints.InvalidConstraintDefinitionException;
import org.apache.cassandra.cql3.constraints.NotNullConstraint;
import org.apache.cassandra.cql3.constraints.ScalarColumnConstraint;
import org.apache.cassandra.cql3.constraints.UnaryFunctionColumnConstraint;
import org.apache.cassandra.db.marshal.AbstractType;
import org.apache.cassandra.db.marshal.Int32Type;
import org.apache.cassandra.db.marshal.ListType;
import org.apache.cassandra.db.marshal.MapType;
import org.apache.cassandra.db.marshal.SetType;
import org.apache.cassandra.db.marshal.UTF8Type;
import org.apache.cassandra.db.marshal.UUIDType;
import org.apache.cassandra.schema.ColumnMetadata;
import static java.util.List.of;
import static org.apache.cassandra.cql3.Operator.GT;
import static org.apache.cassandra.schema.ColumnMetadata.Kind.REGULAR;
import static org.apache.cassandra.utils.ByteBufferUtil.EMPTY_BYTE_BUFFER;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
/**
* TODO - UDTs are not supported yet in constraints as such
*/
public class NotNullConstraintTest
{
private static final ColumnIdentifier columnIdentifier = new ColumnIdentifier("a_column", false);
private static final ColumnConstraints unaryConstraint = new ColumnConstraints(of(new UnaryFunctionColumnConstraint.Raw(new ColumnIdentifier(NotNullConstraint.FUNCTION_NAME, false), columnIdentifier).prepare()));
private static final ColumnConstraints scalarConstraint = new ColumnConstraints(of(new ScalarColumnConstraint.Raw(columnIdentifier, GT, "5").prepare()));
private static final ColumnConstraints functionConstraint = new ColumnConstraints(of(new FunctionColumnConstraint.Raw(new ColumnIdentifier("LENGTH", false), columnIdentifier, GT, "5").prepare()));
@Test
public void testNotNullConstraintValidation()
{
// unary
unaryConstraint.validate(getColumnOfType(UTF8Type.instance));
assertThatThrownBy(() -> unaryConstraint.evaluate(UTF8Type.instance, EMPTY_BYTE_BUFFER))
.hasMessage("Column value does not satisfy value constraint for column 'a_column' as it is null.")
.isInstanceOf(ConstraintViolationException.class);
// not null / empty
unaryConstraint.evaluate(UTF8Type.instance, UTF8Type.instance.fromString("a value"));
// scalar
scalarConstraint.validate(getColumnOfType(Int32Type.instance));
assertThatThrownBy(() -> scalarConstraint.evaluate(Int32Type.instance, EMPTY_BYTE_BUFFER))
.hasMessage("Column value does not satisfy value constraint for column 'a_column' as it is null.")
.isInstanceOf(ConstraintViolationException.class);
// function, e.g. length
functionConstraint.validate(getColumnOfType(UTF8Type.instance));
assertThatThrownBy(() -> functionConstraint.evaluate(UTF8Type.instance, EMPTY_BYTE_BUFFER))
.hasMessage("Column value does not satisfy value constraint for column 'a_column' as it is null.")
.isInstanceOf(ConstraintViolationException.class);
// empty string is not _null_ string so this passes
unaryConstraint.evaluate(UTF8Type.instance, UTF8Type.instance.fromString(""));
// test a type for which empty value is meaningless
assertThatThrownBy(() -> unaryConstraint.evaluate(UUIDType.instance, ByteBuffer.allocate(0)))
.hasMessage("Column value does not satisfy value constraint for column 'a_column' as it is empty.")
.isInstanceOf(ConstraintViolationException.class);
}
@Test
public void testCollections()
{
checkList(false);
checkSet(false);
checkMap(false);
checkList(true);
checkSet(true);
checkMap(true);
}
private static ColumnMetadata getColumnOfType(AbstractType<?> type)
{
return new ColumnMetadata("a", "b", columnIdentifier, type, -1, REGULAR, null);
}
private void checkList(boolean frozen)
{
if (frozen)
{
ListType<Integer> listType = ListType.getInstance(Int32Type.instance, false);
ByteBuffer payload = listType.getSerializer().serialize(List.of(1, 2, 3));
checkFrozenCollection(listType, payload);
}
else
checkUnfrozenCollection(ListType.getInstance(Int32Type.instance, true));
}
private void checkMap(boolean frozen)
{
if (frozen)
{
MapType<Integer, Integer> mapType = MapType.getInstance(Int32Type.instance, Int32Type.instance, false);
ByteBuffer payload = mapType.getSerializer().serialize(Map.of(1, 1, 2, 2, 3, 3));
checkFrozenCollection(mapType, payload);
}
else
checkUnfrozenCollection(MapType.getInstance(Int32Type.instance, Int32Type.instance, true));
}
private void checkSet(boolean frozen)
{
if (frozen)
{
SetType<Integer> setType = SetType.getInstance(Int32Type.instance, false);
ByteBuffer payload = setType.getSerializer().serialize(Set.of(1, 2, 3));
checkFrozenCollection(setType, payload);
}
else
checkUnfrozenCollection(SetType.getInstance(Int32Type.instance, true));
}
private void checkFrozenCollection(AbstractType<?> type, ByteBuffer payload)
{
unaryConstraint.validate(getColumnOfType(type));
unaryConstraint.evaluate(type, payload);
assertThatThrownBy(() -> unaryConstraint.evaluate(type, EMPTY_BYTE_BUFFER))
.hasMessage("Column value does not satisfy value constraint for column 'a_column' as it is null.")
.isInstanceOf(ConstraintViolationException.class);
}
private void checkUnfrozenCollection(AbstractType<?> type)
{
assertThatThrownBy(() -> unaryConstraint.validate(getColumnOfType(type)))
.hasMessageContaining("Constraint cannot be defined on the column")
.hasMessageContaining("When using collections, constraints can be used only of frozen collections")
.isInstanceOf(InvalidConstraintDefinitionException.class);
}
}

View File

@ -0,0 +1,206 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.cassandra.cql3;
import java.util.Map;
import org.junit.Before;
import org.junit.Test;
import org.apache.cassandra.config.DatabaseDescriptor;
import org.apache.cassandra.schema.CompactionParams;
/**
* Test various "extensions" to a column spec when altering / creating a table
*/
public class ColumnSpecificationTest extends CQLTester
{
@Before
public void before()
{
DatabaseDescriptor.setDynamicDataMaskingEnabled(true);
}
@Test
public void testCreateTableWithColumnHavingMaskBeforeCheck()
{
createTable("CREATE TABLE %s (pk text primary key, name text MASKED WITH system.mask_default() CHECK NOT_NULL(name) AND LENGTH(name) > 1);");
verifyColumnSpec("name text MASKED WITH system.mask_default() CHECK NOT_NULL(name) AND LENGTH(name) > 1");
}
@Test
public void testAlterTableAlterColumnWithMaskAndCheckStandalone()
{
createTable("CREATE TABLE %s (pk text, name text, primary key (pk));");
execute("ALTER TABLE %s ALTER name MASKED WITH system.mask_default()");
execute("ALTER TABLE %s ALTER name CHECK NOT_NULL(name) AND LENGTH(name) > 1;");
verifyColumnSpec("name text MASKED WITH system.mask_default() CHECK NOT_NULL(name) AND LENGTH(name) > 1");
}
@Test
public void testAlterTableAlterColumnWithMask()
{
createTable("CREATE TABLE %s (pk text, name text, primary key (pk));");
execute("ALTER TABLE %s ALTER name MASKED WITH system.mask_default()");
verifyColumnSpec("name text MASKED WITH system.mask_default()");
}
@Test
public void testAlterTableAlterColumnWithCheck()
{
createTable("CREATE TABLE %s (pk text, name text, primary key (pk));");
execute("ALTER TABLE %s ALTER name CHECK NOT_NULL(name) AND LENGTH(name) > 1;");
verifyColumnSpec("name text CHECK NOT_NULL(name) AND LENGTH(name) > 1");
}
@Test
public void testAddingCheckToColumnWithMask()
{
createTable("CREATE TABLE %s (pk text primary key, name text MASKED WITH system.mask_default());");
execute("ALTER TABLE %s ALTER name CHECK NOT_NULL(name) AND LENGTH(name) > 1");
verifyColumnSpec("name text MASKED WITH system.mask_default() CHECK NOT_NULL(name) AND LENGTH(name) > 1");
}
@Test
public void testAddingMaskToColumnWithCheck()
{
createTable("CREATE TABLE %s (pk text primary key, name text CHECK NOT_NULL(name) AND LENGTH(name) > 1);");
execute("ALTER TABLE %s ALTER name MASKED WITH system.mask_default()");
verifyColumnSpec("name text MASKED WITH system.mask_default() CHECK NOT_NULL(name) AND LENGTH(name) > 1");
}
@Test
public void testDroppingCheckKeepsMask()
{
createTable("CREATE TABLE %s (pk text primary key, name text MASKED WITH system.mask_default() CHECK NOT_NULL(name) AND LENGTH(name) > 1);");
execute("ALTER TABLE %s ALTER name DROP CHECK");
verifyColumnSpec("name text MASKED WITH system.mask_default()");
}
@Test
public void droppingMaskKeepsCheck()
{
createTable("CREATE TABLE %s (pk text primary key, name text MASKED WITH system.mask_default() CHECK NOT_NULL(name) AND LENGTH(name) > 1);");
execute("ALTER TABLE %s ALTER name DROP MASKED");
verifyColumnSpec("name text CHECK NOT_NULL(name) AND LENGTH(name) > 1");
}
@Test
public void testAlterTableAddColumnWithCheck()
{
createTable("CREATE TABLE %s (pk text primary key);");
execute("ALTER TABLE %s ADD name text CHECK NOT_NULL(name) AND LENGTH(name) > 1");
verifyColumnSpec("name text CHECK NOT_NULL(name) AND LENGTH(name) > 1");
}
@Test
public void testAlterTableAddColumnWithMask()
{
createTable("CREATE TABLE %s (pk text primary key);");
execute("ALTER TABLE %s ADD name text MASKED WITH system.mask_default()");
verifyColumnSpec("name text MASKED WITH system.mask_default()");
}
@Test
public void testAlterTableAddColumnWithMaskAndCheck()
{
createTable("CREATE TABLE %s (pk text primary key);");
execute("ALTER TABLE %s ADD name text MASKED WITH system.mask_default() CHECK NOT_NULL(name)");
verifyColumnSpec("name text MASKED WITH system.mask_default() CHECK NOT_NULL(name)");
}
@Test
public void testAlterTableAddColumnWithMaskAndMultipleChecks()
{
createTable("CREATE TABLE %s (pk text primary key);");
execute("ALTER TABLE %s ADD name text MASKED WITH system.mask_default() CHECK NOT_NULL(name) AND LENGTH(name) > 1");
verifyColumnSpec("name text MASKED WITH system.mask_default() CHECK NOT_NULL(name) AND LENGTH(name) > 1");
}
/**
* TODO - investigate if it is possible to specify checks before mask when creating a table
*/
@Test(expected = RuntimeException.class)
public void testFailingCreateTableWithColumnHavingMaskAfterCheck()
{
createTable("CREATE TABLE %s (pk text primary key, name text CHECK NOT_NULL(name) AND LENGTH(name) > 1 MASKED WITH system.mask_default());");
}
/**
* TODO - investigate if it is possible to specify both check and mask, check being first
*/
@Test(expected = RuntimeException.class)
public void testFailingAlterTableAlterColumnWithCheckAndMask()
{
createTable("CREATE TABLE %s (pk text, name text, primary key (pk));");
execute("ALTER TABLE %s ALTER name CHECK NOT_NULL(name) AND LENGTH(name) > 1 MASKED WITH system.mask_default();");
verifyColumnSpec("name text MASKED WITH system.mask_default() CHECK NOT_NULL(name) AND LENGTH(name) > 1");
}
/**
* TODO - investigate if it is possible to specify both check and mask, mask being first
*/
@Test(expected = RuntimeException.class)
public void testFailingAlterTableAlterColumnWithMaskAndCheck()
{
createTable("CREATE TABLE %s (pk text, name text, primary key (pk));");
execute("ALTER TABLE %s ALTER name MASKED WITH system.mask_default() CHECK NOT_NULL(name) AND LENGTH(name) > 1");
verifyColumnSpec("name text MASKED WITH system.mask_default() CHECK NOT_NULL(name) AND LENGTH(name) > 1");
}
private void verifyColumnSpec(String modifiedColumn)
{
assertRowsContains(executeNetWithoutPaging("DESCRIBE TABLE " + KEYSPACE + '.' + currentTable()),
row(KEYSPACE,
"table",
currentTable(),
"CREATE TABLE " + KEYSPACE + '.' + currentTable() + " (\n" +
" pk text PRIMARY KEY,\n" +
" " + modifiedColumn + '\n' +
") WITH " + tableParametersCql()));
}
static String tableParametersCql()
{
return "additional_write_policy = '99p'\n" +
" AND allow_auto_snapshot = true\n" +
" AND bloom_filter_fp_chance = 0.01\n" +
" AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'}\n" +
" AND cdc = false\n" +
" AND comment = ''\n" +
" AND compaction = " + cqlQuoted(CompactionParams.DEFAULT.asMap()) + "\n" +
" AND compression = {'chunk_length_in_kb': '16', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'}\n" +
" AND memtable = 'default'\n" +
" AND crc_check_chance = 1.0\n" +
" AND default_time_to_live = 0\n" +
" AND extensions = {}\n" +
" AND gc_grace_seconds = 864000\n" +
" AND incremental_backups = true\n" +
" AND max_index_interval = 2048\n" +
" AND memtable_flush_period_in_ms = 0\n" +
" AND min_index_interval = 128\n" +
" AND read_repair = 'BLOCKING'\n" +
" AND speculative_retry = '99p';";
}
private static String cqlQuoted(Map<String, String> map)
{
return new CqlBuilder().append(map).toString();
}
}

View File

@ -263,10 +263,15 @@ public class AbstractTypeTest
public void isConstrainedTest()
{
qt().forAll(genBuilder().build()).checkAssert(type -> {
if (type instanceof MapType || type instanceof TupleType || type instanceof AbstractCompositeType)
if (type instanceof TupleType || type instanceof AbstractCompositeType)
assertThat(type.isConstrainable()).isEqualTo(false);
else
assertThat(type.isConstrainable()).isEqualTo(true);
{
if (type.isCollection() && !type.isFrozenCollection())
assertThat(type.isConstrainable()).isEqualTo(false);
else
assertThat(type.isConstrainable()).isEqualTo(true);
}
});
}