Skip to content

CASSANDRA-20276 implementation of NOT_NULL constraint #3867

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGES.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
5.1
* Implement NOT_NULL constraint (CASSANDRA-20276)
* Improve error messages for constraints (CASSANDRA-20266)
* Add system_views.partition_key_statistics for querying SSTable metadata (CASSANDRA-20161)
* CEP-42 - Add Constraints Framework (CASSANDRA-19947)
Expand Down
48 changes: 48 additions & 0 deletions doc/modules/cassandra/pages/developing/cql/constraints.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -91,3 +91,51 @@ Finally, the constraint can be removed:
----
ALTER TABLE keyspace.table ALTER name DROP CHECK;
----

=== NOT_NULL constraint

Defines a constraint that checks if a column is not null in every modification statement.

For example, let's have this table:

----
CREATE TABLE ks.tb (
id int,
cl int,
col1 int CHECK NOT_NULL(col1),
col2 int CHECK NOT_NULL(col2),
PRIMARY KEY (id, cl)
);
----

then this statement would fail:

----
INSERT INTO ks.tb (id, cl, col1) VALUES (1, 2, 3);
... [Invalid query] message="Column 'col2' has to be specified as part of this query."
----

as well as this statement:

----
INSERT INTO ks.tb (id, cl, col1, col2) VALUES (1, 2, 3, null);
----

A column which has `NOT_NULL` constraint has to be specified in every modification statement.

The constraint can be removed:

----
ALTER TABLE keyspace.table ALTER col1 DROP CHECK;
ALTER TABLE keyspace.table ALTER col2 DROP CHECK;
----

We can not remove the value of a column where `NOT_NULL` constraint is present:

----
DELETE col2 FROM ks.tb WHERE id = 1 AND cl = 2;
... [Invalid query] message="Column 'col2' can not be set to null."
----

Additionally, `NOT_NULL` can not be specified on any column of a primary key,
being it a partition key or a clustering column.
1 change: 1 addition & 0 deletions src/antlr/Parser.g
Original file line number Diff line number Diff line change
Expand Up @@ -796,6 +796,7 @@ columnConstraints returns [ColumnConstraints.Raw constraints]

columnConstraint returns [ColumnConstraint columnConstraint]
: funcName=ident '(' k=ident ')' op=relationType t=value { $columnConstraint = new FunctionColumnConstraint.Raw(funcName, k, op, t.getText()).prepare(); }
| funcName=ident '(' k=ident ')' { $columnConstraint = new UnaryFunctionColumnConstraint.Raw(funcName, k).prepare(); }
| k=ident op=relationType t=value { $columnConstraint = new ScalarColumnConstraint.Raw(k, op, t.getText()).prepare(); }
;

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/*
* 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.cql3.ColumnIdentifier;

public abstract class AbstractFunctionConstraint<T> extends ColumnConstraint<T>
{
public AbstractFunctionConstraint(ColumnIdentifier columnName)
{
super(columnName);
}

public abstract String name();
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@

import java.nio.ByteBuffer;

import org.apache.cassandra.cql3.ColumnIdentifier;
import org.apache.cassandra.cql3.CqlBuilder;
import org.apache.cassandra.db.marshal.AbstractType;
import org.apache.cassandra.tcm.serialization.MetadataSerializer;
Expand All @@ -29,19 +30,26 @@
* 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>
public abstract class ColumnConstraint<T>
{
protected final ColumnIdentifier columnName;

public ColumnConstraint(ColumnIdentifier columnName)
{
this.columnName = columnName;
}

// Enum containing all the possible constraint serializers to help with serialization/deserialization
// of constraints.
enum ConstraintType
public 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);
SCALAR(ScalarColumnConstraint.serializer),
UNARY_FUNCTION(UnaryFunctionColumnConstraint.serializer);

private final MetadataSerializer<?> serializer;

Expand All @@ -56,30 +64,42 @@ public static MetadataSerializer<?> getSerializer(int i)
}
}

MetadataSerializer<T> serializer();
public abstract MetadataSerializer<T> serializer();

void appendCqlTo(CqlBuilder builder);
public abstract 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;
public void evaluate(AbstractType<?> valueType, ByteBuffer columnValue) throws ConstraintViolationException
{
if (columnValue.capacity() == 0)
throw new ConstraintViolationException("Column value does not satisfy value constraint for column '" + columnName + "' as it is null.");

internalEvaluate(valueType, columnValue);
}

/**
* Internal evaluation method, by default called from {@link ColumnConstraint#evaluate(AbstractType, ByteBuffer)}.
* {@code columnValue} is by default guaranteed to not represent CQL value of 'null'.
*/
protected abstract void internalEvaluate(AbstractType<?> valueType, ByteBuffer columnValue);

/**
* 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;
public abstract void validate(ColumnMetadata columnMetadata) throws InvalidConstraintDefinitionException;

/**
* Method to get the Constraint serializer
*
* @return the Constraint type serializer
*/
ConstraintType getConstraintType();
public abstract ConstraintType getConstraintType();
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
import java.util.List;
import java.util.Objects;

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;
Expand All @@ -34,9 +35,8 @@
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 class ColumnConstraints extends ColumnConstraint<ColumnConstraints>
{
public static final Serializer serializer = new Serializer();
public static final ColumnConstraints NO_OP = new Noop();
Expand All @@ -45,6 +45,7 @@ public class ColumnConstraints implements ColumnConstraint<ColumnConstraints>

public ColumnConstraints(List<ColumnConstraint<?>> constraints)
{
super(null);
this.constraints = constraints;
}

Expand All @@ -68,6 +69,12 @@ public void evaluate(AbstractType<?> valueType, ByteBuffer columnValue) throws C
constraint.evaluate(valueType, columnValue);
}

@Override
protected void internalEvaluate(AbstractType<?> valueType, ByteBuffer columnValue)
{
// nothing to evaluate here
}

public List<ColumnConstraint<?>> getConstraints()
{
return constraints;
Expand All @@ -94,6 +101,11 @@ public boolean hasRelevantConstraints()
return false;
}

public void checkInvalidConstraintsCombinations(ColumnIdentifier columnName)
{
// TODO check duplicities etc CASSANDRA-20330
}

@Override
public void validate(ColumnMetadata columnMetadata) throws InvalidConstraintDefinitionException
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,29 +20,54 @@

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.schema.ColumnMetadata;

/**
* Interface to be implemented by functions that are executed as part of CQL constraints.
*/
public interface ConstraintFunction
public abstract class ConstraintFunction
{
/**
* @return the function name to be executed.
*/
String getName();
protected final ColumnIdentifier columnName;
protected final String name;

public ConstraintFunction(ColumnIdentifier columnName, String name)
{
this.columnName = columnName;
this.name = name;
}

/**
* 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;
public void evaluate(AbstractType<?> valueType, Operator relationType, String term, ByteBuffer columnValue) throws ConstraintViolationException
{
if (columnValue.capacity() == 0)
throw new ConstraintViolationException("Column value does not satisfy value constraint for column '" + columnName + "' as it is null.");

internalEvaluate(valueType, relationType, term, columnValue);
}

/**
* Internal evaluation method, by default called from {@link ConstraintFunction#evaluate(AbstractType, Operator, String, ByteBuffer)}.
* {@code columnValue} is by default guaranteed to not represent CQL value of 'null'.
*/
protected abstract void internalEvaluate(AbstractType<?> valueType, Operator relationType, String term, ByteBuffer columnValue);

/**
* Used mostly for unary functions which do not expect any relation type nor term.
*/
public void evaluate(AbstractType<?> valueType, ByteBuffer columnValue) throws ConstraintViolationException
{
evaluate(valueType, null, null, columnValue);
}

/**
* 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;
public abstract void validate(ColumnMetadata columnMetadata) throws InvalidConstraintDefinitionException;
}
Original file line number Diff line number Diff line change
Expand Up @@ -34,14 +34,13 @@
import org.apache.cassandra.tcm.serialization.Version;
import org.apache.cassandra.utils.LocalizeString;

public class FunctionColumnConstraint implements ColumnConstraint<FunctionColumnConstraint>
public class FunctionColumnConstraint extends AbstractFunctionConstraint<FunctionColumnConstraint>
{
public static final Serializer serializer = new Serializer();

public final ConstraintFunction function;
public final ColumnIdentifier columnName;
public final Operator relationType;
public final String term;
private final ConstraintFunction function;
private final Operator relationType;
private final String term;

public final static class Raw
{
Expand Down Expand Up @@ -90,12 +89,17 @@ private static ConstraintFunction createConstraintFunction(String functionName,

private FunctionColumnConstraint(ConstraintFunction function, ColumnIdentifier columnName, Operator relationType, String term)
{
super(columnName);
this.function = function;
this.columnName = columnName;
this.relationType = relationType;
this.term = term;
}

public String name()
{
return function.name;
}

@Override
public void appendCqlTo(CqlBuilder builder)
{
Expand All @@ -114,6 +118,12 @@ public void evaluate(AbstractType<?> valueType, ByteBuffer columnValue)
function.evaluate(valueType, relationType, term, columnValue);
}

@Override
protected void internalEvaluate(AbstractType<?> valueType, ByteBuffer columnValue)
{
// evaluation is done on function
}

@Override
public void validate(ColumnMetadata columnMetadata)
{
Expand All @@ -130,21 +140,23 @@ public ConstraintType getConstraintType()
void validateArgs(ColumnMetadata columnMetadata)
{
if (!columnMetadata.name.equals(columnName))
throw new InvalidConstraintDefinitionException("Function parameter should be the column name");
throw new InvalidConstraintDefinitionException(String.format("Parameter of %s constraint should be the column name (%s)",
name(),
columnMetadata.name));
}

@Override
public String toString()
{
return function.getName() + "(" + columnName + ") " + relationType + " " + term;
return function.name + "(" + 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.function.name);
out.writeUTF(columnConstraint.columnName.toCQLString());
columnConstraint.relationType.writeTo(out);
out.writeUTF(columnConstraint.term);
Expand Down
Loading