Add JSON constraint

patch by Stefan Miklosovic; reviewed by Bernardo Botella for CASSANDRA-20273
This commit is contained in:
Stefan Miklosovic 2025-02-20 13:53:07 +01:00
parent 980047657d
commit a51344c611
No known key found for this signature in database
GPG Key ID: 32F35CB2F546D93E
6 changed files with 205 additions and 1 deletions

View File

@ -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)

View File

@ -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."
----

View File

@ -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);
}
}

View File

@ -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;

View File

@ -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
{

View File

@ -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);
}
}