mirror of https://github.com/apache/cassandra
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:
parent
12c061b3bb
commit
02d71cee45
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue