Add support for time, date, timestamp types in scalar constraint

patch by Stefan Miklosovic; reviewed by Bernardo Botella for CASSANDRA-20274
This commit is contained in:
Stefan Miklosovic 2025-03-19 15:57:46 +01:00
parent 12c061b3bb
commit 02d71cee45
No known key found for this signature in database
GPG Key ID: 32F35CB2F546D93E
5 changed files with 249 additions and 25 deletions

View File

@ -1,4 +1,5 @@
5.1
* Add support for time, date, timestamp types in scalar constraint (CASSANDRA-20274)
* Add regular expression constraint (CASSANDRA-20275)
* Improve constraints autocompletion (CASSANDRA-20341)
* Add JVM version and Cassandra build date to nodetool version -v (CASSANDRA-19721)

View File

@ -12,7 +12,7 @@ The main syntax to define a constraint is as follows:
[source,bnf]
----
CREATE TABLE keyspace.table (
CREATE TABLE ks.tb (
name text,
i int CHECK (condition) (AND (condition))*
...,
@ -48,7 +48,7 @@ For example, we can define constraints that ensure that i is bigger or equal tha
[source,bnf]
----
CREATE TABLE keyspace.table (
CREATE TABLE ks.tb (
name text,
i int CHECK i < 1000 AND i > 100
...,
@ -58,13 +58,43 @@ CREATE TABLE keyspace.table (
Altering that constraint can be done with:
----
ALTER TABLE keyspace.table ALTER i CHECK i >= 500;
ALTER TABLE ks.tb ALTER i CHECK i >= 500;
----
Finally, the constraint can be removed:
----
ALTER TABLE keyspace.table ALTER i DROP CHECK;
ALTER TABLE ks.tb ALTER i DROP CHECK;
----
This constraint also works for `time`, `date` and `timestamp` types. For example, a user
can express constraints like this:
----
CREATE TABLE ks.tb
name text,
dob date CHECK dob > '1900-01-01'
...
)
----
Hence, we can enforce that date of birth is later than January 1st, 1900.
----
INSERT INTO ks.tb (name, dob) VALUES ( 'Joe Doe', '1899-08-06');
... [Invalid query] message="Column value does not satisfy value constraint for column 'dob'. It should be dob > '1900-01-01'"
-- this passes as it is > January 1st, 1900
INSERT INTO ks.tb (name, dob) VALUES ( 'Joe Doe', '1976-12-06');
----
A user can also use ranges on time-related columns with `>` and `<` operators, e.g.
----
CREATE TABLE ks.tb
name text,
afternoon time CHECK afternoon >= '12:00:00' AND afternoon <= '23:59:59';
...
)
----
=== LENGTH CONSTRAINT
@ -74,7 +104,7 @@ 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 (
CREATE TABLE ks.tb (
name text CHECK LENGTH(name) < 256
...,
);
@ -83,13 +113,13 @@ CREATE TABLE keyspace.table (
Altering that constraint can be done with:
----
ALTER TABLE keyspace.table ALTER name LENGTH(name) < 512;
ALTER TABLE ks.tb ALTER name LENGTH(name) < 512;
----
Finally, the constraint can be removed:
----
ALTER TABLE keyspace.table ALTER name DROP CHECK;
ALTER TABLE ks.tb ALTER name DROP CHECK;
----
=== OCTET_LENGTH CONSTRAINT
@ -99,7 +129,7 @@ Defines a condition that checks the size in bytes of text or binary type.
For example, we can create a constraint that checks that name can't be bigger than 256 characters:
----
CREATE TABLE keyspace.table (
CREATE TABLE ks.tb (
name text CHECK OCTET_LENGTH(name) < 2
...,
);
@ -107,12 +137,12 @@ CREATE TABLE keyspace.table (
Inserting a valid row:
----
INSERT INTO keyspace.table (name) VALUES ("f")
INSERT INTO ks.tb (name) VALUES ("f")
----
Inserting an invalid row:
----
INSERT INTO keyspace.table (name) VALUES ("fooooooo")
INSERT INTO ks.tb (name) VALUES ("fooooooo")
ERROR: Column value does not satisfy value constraint for column 'name'. It has a length of 8 and
and it should be should be < 2
@ -152,8 +182,8 @@ A column which has `NOT_NULL` constraint has to be specified in every modificati
The constraint can be removed:
----
ALTER TABLE keyspace.table ALTER col1 DROP CHECK;
ALTER TABLE keyspace.table ALTER col2 DROP CHECK;
ALTER TABLE ks.tb ALTER col1 DROP CHECK;
ALTER TABLE ks.tb ALTER col2 DROP CHECK;
----
We can not remove the value of a column where `NOT_NULL` constraint is present:

View File

@ -25,6 +25,7 @@ import java.util.Set;
import java.util.TreeSet;
import org.apache.cassandra.cql3.Operator;
import org.apache.cassandra.cql3.functions.types.ParseUtils;
import org.apache.cassandra.schema.ColumnMetadata;
import org.apache.cassandra.utils.Pair;
@ -146,8 +147,8 @@ public abstract class AbstractFunctionSatisfiabilityChecker<CONSTRAINT_TYPE exte
}
else
{
ByteBuffer firstTermBuffer = columnMetadata.type.fromString(firstTerm);
ByteBuffer secondTermBuffer = columnMetadata.type.fromString(secondTerm);
ByteBuffer firstTermBuffer = columnMetadata.type.fromString(ParseUtils.unquote(firstTerm));
ByteBuffer secondTermBuffer = columnMetadata.type.fromString(ParseUtils.unquote(secondTerm));
boolean firstSatisfaction = firstRelation.isSatisfiedBy(columnMetadata.type, secondTermBuffer, firstTermBuffer);
boolean secondSatisfaction = secondRelation.isSatisfiedBy(columnMetadata.type, firstTermBuffer, secondTermBuffer);

View File

@ -26,6 +26,7 @@ import com.google.common.annotations.VisibleForTesting;
import org.apache.cassandra.cql3.ColumnIdentifier;
import org.apache.cassandra.cql3.Operator;
import org.apache.cassandra.cql3.functions.types.ParseUtils;
import org.apache.cassandra.db.TypeSizes;
import org.apache.cassandra.db.marshal.AbstractType;
import org.apache.cassandra.db.marshal.ByteType;
@ -37,6 +38,9 @@ import org.apache.cassandra.db.marshal.Int32Type;
import org.apache.cassandra.db.marshal.IntegerType;
import org.apache.cassandra.db.marshal.LongType;
import org.apache.cassandra.db.marshal.ShortType;
import org.apache.cassandra.db.marshal.SimpleDateType;
import org.apache.cassandra.db.marshal.TimeType;
import org.apache.cassandra.db.marshal.TimestampType;
import org.apache.cassandra.io.util.DataInputPlus;
import org.apache.cassandra.io.util.DataOutputPlus;
import org.apache.cassandra.schema.ColumnMetadata;
@ -56,7 +60,7 @@ public class ScalarColumnConstraint extends AbstractFunctionConstraint<ScalarCol
private static final List<AbstractType<?>> SUPPORTED_TYPES =
List.of(ByteType.instance, CounterColumnType.instance, DecimalType.instance, DoubleType.instance,
FloatType.instance, Int32Type.instance, IntegerType.instance, LongType.instance,
ShortType.instance);
ShortType.instance, TimeType.instance, SimpleDateType.instance, TimestampType.instance);
@VisibleForTesting
public static final List<Operator> SUPPORTED_OPERATORS = List.of(EQ, NEQ, GTE, GT, LTE, LT);
@ -91,6 +95,8 @@ public class ScalarColumnConstraint extends AbstractFunctionConstraint<ScalarCol
}
}
private ByteBuffer value;
private ScalarColumnConstraint(ColumnIdentifier param, Operator relationType, String term)
{
super(param, relationType, term);
@ -111,16 +117,6 @@ public class ScalarColumnConstraint extends AbstractFunctionConstraint<ScalarCol
@Override
protected void internalEvaluate(AbstractType<?> valueType, ByteBuffer columnValue)
{
ByteBuffer value;
try
{
value = valueType.fromString(term);
}
catch (NumberFormatException exception)
{
throw new ConstraintViolationException(columnName + " and " + term + " need to be numbers.");
}
if (!relationType.isSatisfiedBy(valueType, columnValue, value))
throw new ConstraintViolationException("Column value does not satisfy value constraint for column '" + columnName + "'. "
+ "It should be " + columnName + " " + relationType + " " + term);
@ -130,6 +126,15 @@ public class ScalarColumnConstraint extends AbstractFunctionConstraint<ScalarCol
public void validate(ColumnMetadata columnMetadata) throws InvalidConstraintDefinitionException
{
validateTypes(columnMetadata);
try
{
value = columnMetadata.type.fromString(ParseUtils.unquote(term));
}
catch (Throwable t)
{
throw new ConstraintViolationException("Cannot parse constraint value from " + term + " for column '" + columnName + '\'');
}
}
@Override

View File

@ -0,0 +1,187 @@
/*
* 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 org.junit.Test;
import org.apache.cassandra.cql3.ColumnIdentifier;
import org.apache.cassandra.cql3.Operator;
import org.apache.cassandra.cql3.constraints.ColumnConstraints;
import org.apache.cassandra.cql3.constraints.ConstraintViolationException;
import org.apache.cassandra.cql3.constraints.InvalidConstraintDefinitionException;
import org.apache.cassandra.cql3.constraints.ScalarColumnConstraint.Raw;
import org.apache.cassandra.db.marshal.AbstractType;
import org.apache.cassandra.db.marshal.SimpleDateType;
import org.apache.cassandra.db.marshal.TimeType;
import org.apache.cassandra.db.marshal.TimestampType;
import org.apache.cassandra.schema.ColumnMetadata;
import static java.util.List.of;
import static org.apache.cassandra.cql3.Operator.EQ;
import static org.apache.cassandra.cql3.Operator.GT;
import static org.apache.cassandra.cql3.Operator.GTE;
import static org.apache.cassandra.cql3.Operator.LT;
import static org.apache.cassandra.cql3.Operator.LTE;
import static org.apache.cassandra.cql3.Operator.NEQ;
import static org.apache.cassandra.cql3.functions.types.ParseUtils.quote;
import static org.apache.cassandra.schema.ColumnMetadata.Kind.REGULAR;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
public class TimeConstraintsTest
{
private static final ColumnIdentifier columnIdentifier = new ColumnIdentifier("a_column", false);
private static final ColumnMetadata timeColumn = getColumnOfType(TimeType.instance);
private static final ColumnMetadata dateColumn = getColumnOfType(SimpleDateType.instance);
private static final ColumnMetadata timestampColumn = getColumnOfType(TimestampType.instance);
@Test
public void testTimeConstraint()
{
evaluateTime(EQ, "12:00:00", "12:00:00");
evaluateTime(NEQ, "12:00:00", "11:00:00");
evaluateTime(LT, "12:00:00", "11:00:00");
evaluateTime(GT, "12:00:00", "13:00:00");
evaluateTime(GT, "12:00:00", "12:00:00.1234");
evaluateTime(GTE, "12:00:00", "12:00:00");
evaluateTime(LTE, "12:00:00", "12:00:00");
assertThatThrownBy(() -> evaluateTime(GT, "12:00:00", "01:00:00"))
.isInstanceOf(ConstraintViolationException.class);
assertThatThrownBy(() -> evaluateTime(LT, "12:00:00", "13:00:00"))
.isInstanceOf(ConstraintViolationException.class);
assertThatThrownBy(() -> evaluateTime(LT, "-3:00:00", "13:00:00"))
.isInstanceOf(ConstraintViolationException.class)
.hasMessage(cantParse("-3:00:00"));
evaluate(timeColumn, TimeType.instance, GT, "06:00:00", LT, "15:00:00", "12:00:00");
assertThatThrownBy(() -> evaluate(timeColumn, TimeType.instance, GT, "06:00:00", LT, "15:00:00", "18:00:00"))
.isInstanceOf(ConstraintViolationException.class)
.hasMessage("Column value does not satisfy value constraint for column 'a_column'. It should be a_column < '15:00:00'");
assertThatThrownBy(() -> evaluate(timeColumn, TimeType.instance, GT, "06:00:00", LT, "03:00:00", "18:00:00"))
.isInstanceOf(InvalidConstraintDefinitionException.class)
.hasMessage("Constraints of scalar are not satisfiable: a_column > '06:00:00', a_column < '03:00:00'");
}
@Test
public void testDateConstraint()
{
evaluateDate(EQ, "2000-01-01", "2000-01-01");
evaluateDate(NEQ, "2000-01-01", "1999-12-31");
evaluateDate(LT, "2000-01-01", "1999-12-31");
evaluateDate(GT, "2000-01-01", "2000-01-02");
evaluateDate(GTE, "2000-01-01", "2000-01-01");
evaluateDate(LTE, "2000-01-01", "2000-01-01");
assertThatThrownBy(() -> evaluateDate(GT, "2000-01-01", "1999-12-31"))
.isInstanceOf(ConstraintViolationException.class);
assertThatThrownBy(() -> evaluateDate(LT, "2000-01-01", "2000-01-02"))
.isInstanceOf(ConstraintViolationException.class);
assertThatThrownBy(() -> evaluateDate(LT, "2000-54-01", "13:00:00"))
.isInstanceOf(ConstraintViolationException.class)
.hasMessage(cantParse("2000-54-01"));
evaluate(dateColumn, SimpleDateType.instance, GT, "2000-01-01", LT, "2000-01-31", "2000-01-10");
assertThatThrownBy(() -> evaluate(dateColumn, SimpleDateType.instance, GT, "2000-01-01", LT, "2000-01-31", "2000-02-10"))
.isInstanceOf(ConstraintViolationException.class)
.hasMessage("Column value does not satisfy value constraint for column 'a_column'. It should be a_column < '2000-01-31'");
assertThatThrownBy(() -> evaluate(dateColumn, SimpleDateType.instance, GT, "2000-01-31", LT, "2000-01-01", "2000-01-10"))
.isInstanceOf(InvalidConstraintDefinitionException.class)
.hasMessage("Constraints of scalar are not satisfiable: a_column > '2000-01-31', a_column < '2000-01-01'");
}
@Test
public void testTimestampConstraint()
{
evaluateTimestamp(EQ, "2025-03-18 12:34:56", "2025-03-18 12:34:56");
evaluateTimestamp(NEQ, "2025-03-18 12:34:56", "2025-03-18 12:34:55");
evaluateTimestamp(LT, "2025-03-18 12:34:56", "2025-03-18 12:34:55");
evaluateTimestamp(GT, "2025-03-18 12:34:56", "2025-03-18 12:34:57");
evaluateTimestamp(GTE, "2025-03-18 12:34:56", "2025-03-18 12:34:56");
evaluateTimestamp(LTE, "2025-03-18 12:34:56", "2025-03-18 12:34:56");
assertThatThrownBy(() -> evaluateTimestamp(GT, "2025-03-18 12:34:56", "2025-03-18 12:34:55"))
.isInstanceOf(ConstraintViolationException.class);
assertThatThrownBy(() -> evaluateTimestamp(LT, "2025-03-18 12:34:56", "2025-03-18 12:34:57"))
.isInstanceOf(ConstraintViolationException.class);
assertThatThrownBy(() -> evaluateTimestamp(LT, "2025-55-18 12:34:56", "13:00:00"))
.isInstanceOf(ConstraintViolationException.class)
.hasMessage(cantParse("2025-55-18 12:34:56"));
evaluate(timestampColumn, TimestampType.instance, GT, "2025-03-18 12:34:56", LT, "2025-03-18 12:35:56", "2025-03-18 12:35:12");
assertThatThrownBy(() -> evaluate(timestampColumn, TimestampType.instance, GT, "2025-03-18 12:34:56", LT, "2025-03-18 12:35:56", "2025-03-18 13:35:12"))
.isInstanceOf(ConstraintViolationException.class)
.hasMessage("Column value does not satisfy value constraint for column 'a_column'. It should be a_column < '2025-03-18 12:35:56'");
assertThatThrownBy(() -> evaluate(timestampColumn, TimestampType.instance, GT, "2025-03-18 12:34:56", LT, "2025-03-18 12:33:56", "2025-03-18 12:35:12"))
.isInstanceOf(InvalidConstraintDefinitionException.class)
.hasMessage("Constraints of scalar are not satisfiable: a_column > '2025-03-18 12:34:56', a_column < '2025-03-18 12:33:56'");
}
private void evaluate(ColumnMetadata columnMetadata, AbstractType<?> type, Operator operator1, String term1, Operator operator2, String term2, String value)
{
ColumnConstraints constraint = new ColumnConstraints(of(new Raw(columnIdentifier, operator1, quote(term1)).prepare(),
new Raw(columnIdentifier, operator2, quote(term2)).prepare()));
constraint.validate(columnMetadata);
constraint.evaluate(type, type.fromString(value));
}
private void evaluateTime(Operator operator, String term, String value)
{
evaluate(TimeType.instance, timeColumn, operator, quote(term), value);
}
private void evaluateDate(Operator operator, String term, String value)
{
evaluate(SimpleDateType.instance, dateColumn, operator, quote(term), value);
}
private void evaluateTimestamp(Operator operator, String term, String value)
{
evaluate(TimestampType.instance, timestampColumn, operator, quote(term), value);
}
private void evaluate(AbstractType<?> type, ColumnMetadata columnMetadata, Operator operator, String term, String value)
{
ColumnConstraints constraint = new ColumnConstraints(of(new Raw(columnIdentifier, operator, term).prepare()));
constraint.validate(columnMetadata);
constraint.evaluate(type, type.fromString(value));
}
private String cantParse(String value)
{
return "Cannot parse constraint value from '" + value + "' for column '" + columnIdentifier + '\'';
}
private static ColumnMetadata getColumnOfType(AbstractType<?> type)
{
return new ColumnMetadata("a", "b", columnIdentifier, type, -1, REGULAR, null);
}
}