From 02d71cee45aa3b62a49a339b615124b1dabcf53a Mon Sep 17 00:00:00 2001 From: Stefan Miklosovic Date: Wed, 19 Mar 2025 15:57:46 +0100 Subject: [PATCH] Add support for time, date, timestamp types in scalar constraint patch by Stefan Miklosovic; reviewed by Bernardo Botella for CASSANDRA-20274 --- CHANGES.txt | 1 + .../pages/developing/cql/constraints.adoc | 54 +++-- ...AbstractFunctionSatisfiabilityChecker.java | 5 +- .../constraints/ScalarColumnConstraint.java | 27 +-- .../constraints/TimeConstraintsTest.java | 187 ++++++++++++++++++ 5 files changed, 249 insertions(+), 25 deletions(-) create mode 100644 test/unit/org/apache/cassandra/constraints/TimeConstraintsTest.java diff --git a/CHANGES.txt b/CHANGES.txt index b2a3004838..ba9c7aae3a 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -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) diff --git a/doc/modules/cassandra/pages/developing/cql/constraints.adoc b/doc/modules/cassandra/pages/developing/cql/constraints.adoc index 7e18938729..390d6c27a9 100644 --- a/doc/modules/cassandra/pages/developing/cql/constraints.adoc +++ b/doc/modules/cassandra/pages/developing/cql/constraints.adoc @@ -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: diff --git a/src/java/org/apache/cassandra/cql3/constraints/AbstractFunctionSatisfiabilityChecker.java b/src/java/org/apache/cassandra/cql3/constraints/AbstractFunctionSatisfiabilityChecker.java index 33f261535c..91d448da01 100644 --- a/src/java/org/apache/cassandra/cql3/constraints/AbstractFunctionSatisfiabilityChecker.java +++ b/src/java/org/apache/cassandra/cql3/constraints/AbstractFunctionSatisfiabilityChecker.java @@ -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> 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 SUPPORTED_OPERATORS = List.of(EQ, NEQ, GTE, GT, LTE, LT); @@ -91,6 +95,8 @@ public class ScalarColumnConstraint extends AbstractFunctionConstraint 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 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); + } +}