mirror of https://github.com/apache/cassandra
Add JSON constraint
patch by Stefan Miklosovic; reviewed by Bernardo Botella for CASSANDRA-20273
This commit is contained in:
parent
980047657d
commit
a51344c611
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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."
|
||||
|
||||
----
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue