This is an automated email from the ASF dual-hosted git repository. smiklosovic pushed a commit to branch trunk in repository https://gitbox.apache.org/repos/asf/cassandra.git
The following commit(s) were added to refs/heads/trunk by this push: new a51344c611 Add JSON constraint a51344c611 is described below commit a51344c6115ca432a5e52a4f8aac69dbfa813c8c Author: Stefan Miklosovic <smikloso...@apache.org> AuthorDate: Thu Feb 20 13:53:07 2025 +0100 Add JSON constraint patch by Stefan Miklosovic; reviewed by Bernardo Botella for CASSANDRA-20273 --- CHANGES.txt | 1 + .../pages/developing/cql/constraints.adoc | 27 +++++++ .../cassandra/cql3/constraints/JsonConstraint.java | 81 ++++++++++++++++++++ .../constraints/UnaryFunctionColumnConstraint.java | 3 +- .../apache/cassandra/db/marshal/AbstractType.java | 5 ++ .../cassandra/contraints/JsonConstraintTest.java | 89 ++++++++++++++++++++++ 6 files changed, 205 insertions(+), 1 deletion(-) diff --git a/CHANGES.txt b/CHANGES.txt index 5827b80625..22149cd8de 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,4 +1,5 @@ 5.1 + * Add JSON constraint (CASSANDRA-20273) * Prevent invalid constraint combinations (CASSANDRA-20330) * Support CREATE TABLE LIKE WITH INDEXES (CASSANDRA-19965) * Invalidate relevant prepared statements on every change to TableMetadata (CASSANDRA-20318) diff --git a/doc/modules/cassandra/pages/developing/cql/constraints.adoc b/doc/modules/cassandra/pages/developing/cql/constraints.adoc index 82f454618a..0bd3120edc 100644 --- a/doc/modules/cassandra/pages/developing/cql/constraints.adoc +++ b/doc/modules/cassandra/pages/developing/cql/constraints.adoc @@ -139,3 +139,30 @@ DELETE col2 FROM ks.tb WHERE id = 1 AND cl = 2; Additionally, `NOT_NULL` can not be specified on any column of a primary key, being it a partition key or a clustering column. + +=== JSON constraint + +Defines a constraint which checks if a column contains a string which is a valid JSON. + +`JSON` constraint can be used only for columns of `text`, `varchar` or `ascii` types. + +---- +CREATE TABLE ks.tb ( + id int primary key, + val text CHECK JSON(val) +); + +-- valid JSON string + +INSERT INTO ks.tb (id, val) VALUES (1, '{"a": 5}'); +INSERT INTO ks.tb (id, val) VALUES (1, '{}'); + +-- invalid JSON string + +INSERT INTO ks.tb (id, val) VALUES (1, '{"a": 5'); +INSERT INTO ks.tb (id, val) VALUES (1, 'abc'); + +... [Invalid query] message="Value for column 'val' violated JSON +constraint as it is not a valid JSON." + +---- \ No newline at end of file diff --git a/src/java/org/apache/cassandra/cql3/constraints/JsonConstraint.java b/src/java/org/apache/cassandra/cql3/constraints/JsonConstraint.java new file mode 100644 index 0000000000..0abd1d511e --- /dev/null +++ b/src/java/org/apache/cassandra/cql3/constraints/JsonConstraint.java @@ -0,0 +1,81 @@ +/* + * 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.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; +import org.apache.cassandra.serializers.MarshalException; +import org.apache.cassandra.utils.JsonUtils; + +import static java.lang.String.format; + +public class JsonConstraint extends ConstraintFunction +{ + public static final String FUNCTION_NAME = "JSON"; + + public JsonConstraint(ColumnIdentifier columnName) + { + this(columnName, FUNCTION_NAME); + } + + public JsonConstraint(ColumnIdentifier columnName, String name) + { + super(columnName, name); + } + + @Override + public void internalEvaluate(AbstractType<?> valueType, Operator relationType, String term, ByteBuffer columnValue) + { + try + { + JsonUtils.decodeJson(valueType.getString(columnValue)); + } + catch (MarshalException ex) + { + throw new ConstraintViolationException(format("Value for column '%s' violated %s constraint as it is not a valid JSON.", + columnName.toCQLString(), + name)); + } + } + + @Override + public void validate(ColumnMetadata columnMetadata) throws InvalidConstraintDefinitionException + { + if (!columnMetadata.type.unwrap().isString()) + throw new InvalidConstraintDefinitionException(name + " can be used only for columns of 'text', 'varchar' or 'ascii' types."); + } + + @Override + public boolean equals(Object o) + { + if (this == o) + return true; + + if (!(o instanceof JsonConstraint)) + return false; + + JsonConstraint other = (JsonConstraint) o; + + return columnName.equals(other.columnName) && name.equals(other.name); + } +} diff --git a/src/java/org/apache/cassandra/cql3/constraints/UnaryFunctionColumnConstraint.java b/src/java/org/apache/cassandra/cql3/constraints/UnaryFunctionColumnConstraint.java index 8905bafd07..bb41f57c23 100644 --- a/src/java/org/apache/cassandra/cql3/constraints/UnaryFunctionColumnConstraint.java +++ b/src/java/org/apache/cassandra/cql3/constraints/UnaryFunctionColumnConstraint.java @@ -61,7 +61,8 @@ public class UnaryFunctionColumnConstraint extends AbstractFunctionConstraint<Un public enum Functions implements UnaryFunctionSatisfiabilityChecker { - NOT_NULL(NotNullConstraint::new); + NOT_NULL(NotNullConstraint::new), + JSON(JsonConstraint::new); private final Function<ColumnIdentifier, ConstraintFunction> functionCreator; diff --git a/src/java/org/apache/cassandra/db/marshal/AbstractType.java b/src/java/org/apache/cassandra/db/marshal/AbstractType.java index 29f4468389..c624ce505b 100644 --- a/src/java/org/apache/cassandra/db/marshal/AbstractType.java +++ b/src/java/org/apache/cassandra/db/marshal/AbstractType.java @@ -552,6 +552,11 @@ public abstract class AbstractType<T> implements Comparator<ByteBuffer>, Assignm return unwrap() instanceof org.apache.cassandra.db.marshal.NumberType; } + public boolean isString() + { + return unwrap() instanceof org.apache.cassandra.db.marshal.StringType; + } + // This assumes that no empty values are passed public void writeValue(ByteBuffer value, DataOutputPlus out) throws IOException { diff --git a/test/unit/org/apache/cassandra/contraints/JsonConstraintTest.java b/test/unit/org/apache/cassandra/contraints/JsonConstraintTest.java new file mode 100644 index 0000000000..ba92812230 --- /dev/null +++ b/test/unit/org/apache/cassandra/contraints/JsonConstraintTest.java @@ -0,0 +1,89 @@ +/* + * 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.contraints; + +import org.junit.Test; + +import org.apache.cassandra.cql3.ColumnIdentifier; +import org.apache.cassandra.cql3.constraints.ColumnConstraints; +import org.apache.cassandra.cql3.constraints.UnaryFunctionColumnConstraint.Raw; +import org.apache.cassandra.db.marshal.AbstractType; +import org.apache.cassandra.db.marshal.AsciiType; +import org.apache.cassandra.db.marshal.IntegerType; +import org.apache.cassandra.db.marshal.UTF8Type; +import org.apache.cassandra.schema.ColumnMetadata; +import org.assertj.core.api.ThrowableAssert.ThrowingCallable; + +import static java.util.List.of; +import static org.apache.cassandra.cql3.constraints.JsonConstraint.FUNCTION_NAME; +import static org.apache.cassandra.schema.ColumnMetadata.Kind.REGULAR; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +public class JsonConstraintTest +{ + private static final ColumnIdentifier columnIdentifier = new ColumnIdentifier("a_column", false); + private static final ColumnIdentifier jsonFunctionIdentifier = new ColumnIdentifier(FUNCTION_NAME, false); + private static final ColumnMetadata regularStringColumn = getColumnOfType(UTF8Type.instance); + private static final ColumnMetadata regularAsciiColumn = getColumnOfType(AsciiType.instance); + + private static final ColumnConstraints json = new ColumnConstraints(of(new Raw(jsonFunctionIdentifier, columnIdentifier).prepare())); + + @Test + public void testJsonConstraint() throws Throwable + { + 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."); + } + + @Test + public void testInvalidTypes() + { + assertThatThrownBy(() -> json.validate(getColumnOfType(IntegerType.instance))) + .hasMessageContaining("JSON can be used only for columns of 'text', 'varchar' or 'ascii' types."); + } + + private void run(String jsonToCheck) throws Throwable + { + run(jsonToCheck, null); + } + + private void run(String jsonToCheck, String exceptionMessage) throws Throwable + { + ThrowingCallable callable = () -> + { + json.validate(regularStringColumn); + json.evaluate(regularStringColumn.type, regularAsciiColumn.type.fromString(jsonToCheck)); + + json.validate(regularAsciiColumn); + json.evaluate(regularAsciiColumn.type, regularAsciiColumn.type.fromString(jsonToCheck)); + }; + + if (exceptionMessage == null) + callable.call(); + else + assertThatThrownBy(callable).hasMessageContaining(exceptionMessage); + } + + private static ColumnMetadata getColumnOfType(AbstractType<?> type) + { + return new ColumnMetadata("a", "b", columnIdentifier, type, -1, REGULAR, null); + } +} --------------------------------------------------------------------- To unsubscribe, e-mail: commits-unsubscr...@cassandra.apache.org For additional commands, e-mail: commits-h...@cassandra.apache.org