Allow custom constraints to be loaded via SPI

patch by Stefan Miklosovic; reviewed by Bernardo Botella, Saranya Krishnakumar for CASSANDRA-20824
This commit is contained in:
Stefan Miklosovic 2025-08-08 11:23:43 +02:00
parent 9ed9c7cacd
commit 9d5cef7f8c
No known key found for this signature in database
GPG Key ID: 32F35CB2F546D93E
16 changed files with 639 additions and 64 deletions

View File

@ -1,4 +1,5 @@
5.1
* Allow custom constraints to be loaded via SPI (CASSANDRA-20824)
* Optimize DataPlacement lookup by ReplicationParams (CASSANDRA-20804)
* Fix ShortPaxosSimulationTest and AccordSimulationRunner do not execute from the cli (CASSANDRA-20805)
* Allow overriding arbitrary settings via environment variables (CASSANDRA-20749)

View File

@ -47,6 +47,21 @@ ALTER TABLE [IF EXISTS] <table> ALTER [IF EXISTS] <column> DROP CHECK;
There is no way how to drop individual check when multiple checks are specified on a column. After dropping checks, you
are required to re-define all necessary checks again.
== Constraints are pluggable
On top of in-built constraints enumerated below, since https://issues.apache.org/jira/browse/CASSANDRA-20824[CASSANDRA-20824],
it is possible to provide your own implementation of a constraint and use it in Cassandra. This is possible
thanks to Java SPI, where you need to implement `ConstraintProvider` interface. There is an example of a custom constraint implementation in `examples/constraints` in the source distribution you can follow to implement your own constraints.
The advantage of this approach is that you can use an official release while you can just "patch" Cassandra by
providing your additional constraints specific to your business / needs. Feel free to reach us if you want
to make your constraint in-built!
If you start to use custom constraints, your nodes will fail to start if there isn't appropriate JAR these
constraints are implemented in. If constraints specified in JAR via SPI were not present, we skipped them and
a user didn't notice, then it might be possible to insert invalid data (per constraint) which we consider
is not desired behavior.
== AVAILABLE CONSTRAINTS
=== SCALAR CONSTRAINT

View File

@ -0,0 +1,46 @@
== Cassandra Constraint Example
The `CustomConstraintsProvider` class will create custom constraints. For the purposes of this example,
there will be two constraints: `MY_CUSTOM_CONSTRAINT` and `MY_CUSTOM_UNARY_CONSTRAINT`.
If you want to code your own constraints without patching Cassandra yourself, you need to do the following:
1. Code against interface `org.apache.cassandra.cql3.constraints.ConstraintProvider`.
2. Put the class implementing this interface to `META-INF/services/org.apache.cassandra.cql3.constraints.ConstraintProvider`
3. Build a JAR both with the implementation and `META-INF` resources, as show in this example, and put this JAR onto
Cassandra's classpath.
4. When Cassandra starts, it will auto-detect new constraints by loading `CustomConstraintsProvider` in your JAR.
Custom constraints are resolved first, in-built ones when constraint can not be resolved by given provider. This
means that if you have a constraint in Cassandra which is buggy, you can create your own provider, and return constraint
which "fixes" the in-built one, without any need to patch Cassandra itself. Of course, you can just implement any
other constraint you want so you can start to use your custom constraints without depending on Cassandra release etc.
There is only one `ConstraintProvider` loaded via `ServiceLoader`.
=== Installation
----
$ cd <cassandra_src_dir>/examples/constraints
$ ant install
----
It will build the constraints and will copy it to `lib` as well as to `build/lib/jars`.
You remove it from everywhere by
----
$ cd <cassandra_src_dir>/examples/constraints
$ ant clean
----
=== Usage
----
cqlsh> CREATE KEYSPACE ks WITH replication = {'class': 'SimpleStrategy', 'replication_factor': 1};
cqlsh> CREATE TABLE ks.tb (id int primary key, col text CHECK MY_CUSTOM_CONSTRAINT() > 10);
cqlsh> ALTER TABLE ks.tb ALTER col CHECK MY_CUSTOM_CONSTRAINT() > 20;
----
`MY_CUSTOM_CONSTRAINT` is functionally same as `LENGTH` constraint and `MY_CUSTOM_UNARY_CONSTRAINT` is functionally
same as `JSON` constraint.

View File

@ -0,0 +1,68 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
~ 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.
-->
<project default="jar" name="constraint-example">
<property name="cassandra.dir" value="../.." />
<property name="cassandra.dir.lib" value="${cassandra.dir}/lib" />
<property name="cassandra.classes" value="${cassandra.dir}/build/classes/main" />
<property name="build.src" value="${basedir}/src" />
<property name="build.dir" value="${basedir}/build" />
<property name="conf.dir" value="${basedir}/conf" />
<property name="build.classes" value="${build.dir}/classes" />
<property name="final.name" value="constraint-example" />
<path id="build.classpath">
<fileset dir="${cassandra.dir.lib}">
<include name="**/*.jar" />
</fileset>
<fileset dir="${cassandra.dir}/build/lib/jars">
<include name="**/*.jar" />
</fileset>
<pathelement location="${cassandra.classes}" />
</path>
<target name="init">
<mkdir dir="${build.classes}" />
</target>
<target name="build" depends="init">
<javac destdir="${build.classes}" debug="true" includeantruntime="false">
<src path="${build.src}" />
<classpath refid="build.classpath" />
</javac>
</target>
<target name="jar" depends="build">
<jar jarfile="${build.dir}/${final.name}.jar">
<fileset dir="${build.classes}" />
<fileset dir="${build.src}/resources"/>
</jar>
</target>
<target name="install" depends="jar">
<copy verbose="true" file="${build.dir}/${final.name}.jar" todir="${cassandra.dir}/lib" overwrite="true"/>
<copy verbose="true" file="${build.dir}/${final.name}.jar" todir="${cassandra.dir}/build/lib/jars" overwrite="true"/>
</target>
<target name="clean">
<delete dir="${build.dir}" />
<delete file="${cassandra.dir}/lib/${final.name}.jar"/>
<delete file="${cassandra.dir}/build/lib/jars/${final.name}.jar"/>
</target>
</project>

View File

@ -0,0 +1,100 @@
/**
* 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.util.List;
import java.util.Optional;
import org.apache.cassandra.cql3.constraints.ColumnConstraint;
import org.apache.cassandra.cql3.constraints.ConstraintFunction;
import org.apache.cassandra.cql3.constraints.ConstraintProvider;
import org.apache.cassandra.cql3.constraints.ConstraintResolver;
import org.apache.cassandra.cql3.constraints.InvalidConstraintDefinitionException;
import org.apache.cassandra.cql3.constraints.JsonConstraint;
import org.apache.cassandra.cql3.constraints.LengthConstraint;
import org.apache.cassandra.cql3.constraints.SatisfiabilityChecker;
import org.apache.cassandra.cql3.constraints.SatisfiabilityChecker.UnaryFunctionSatisfiabilityChecker;
import org.apache.cassandra.cql3.constraints.UnaryConstraintFunction;
import org.apache.cassandra.db.marshal.UTF8Type;
import org.apache.cassandra.schema.ColumnMetadata;
import static org.apache.cassandra.cql3.constraints.AbstractFunctionSatisfiabilityChecker.FUNCTION_SATISFIABILITY_CHECKER;
import static org.apache.cassandra.cql3.constraints.ConstraintResolver.getConstraintFunction;
import static org.apache.cassandra.cql3.constraints.ConstraintResolver.getUnaryConstraintFunction;
public class CustomConstraintsProvider implements ConstraintProvider
{
@Override
public Optional<UnaryConstraintFunction> getUnaryConstraint(String functionName, List<String> arguments)
{
if (!functionName.equalsIgnoreCase(MyCustomUnaryConstraint.NAME))
return Optional.empty();
return Optional.of(new MyCustomUnaryConstraint(arguments));
}
@Override
public Optional<ConstraintFunction> getConstraintFunction(String functionName, List<String> arguments)
{
if (!functionName.equalsIgnoreCase(MyCustomConstraint.NAME))
return Optional.empty();
return Optional.of(new MyCustomConstraint(arguments));
}
@Override
public List<? extends SatisfiabilityChecker> getUnaryConstraintSatisfiabilityCheckers()
{
return List.of(new MyCustomUnaryConstraint(List.of()));
}
@Override
public List<? extends SatisfiabilityChecker> getConstraintFunctionSatisfiabilityCheckers()
{
return List.of((constraints, columnMetadata) ->
FUNCTION_SATISFIABILITY_CHECKER.check(MyCustomConstraint.NAME,
constraints,
columnMetadata));
}
/**
* Same as length constraint, just under different name to prove the point
*/
public static class MyCustomConstraint extends LengthConstraint
{
public static final String NAME = "MY_CUSTOM_CONSTRAINT";
public MyCustomConstraint(List<String> arguments)
{
super(NAME, arguments);
}
}
/**
* Same as JSON constraint, just under different name to prove the point
*/
public static class MyCustomUnaryConstraint extends JsonConstraint
{
public static final String NAME = "MY_CUSTOM_UNARY_CONSTRAINT";
public MyCustomUnaryConstraint(List<String> arguments)
{
super(NAME, arguments);
}
}
}

View File

@ -0,0 +1,16 @@
# 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.
org.apache.cassandra.cql3.constraints.CustomConstraintsProvider

View File

@ -22,7 +22,6 @@ import java.util.List;
import org.apache.cassandra.cql3.CqlBuilder;
import org.apache.cassandra.cql3.Operator;
import org.apache.cassandra.utils.LocalizeString;
public abstract class AbstractFunctionConstraint<T> extends ColumnConstraint<T>
{
@ -52,16 +51,4 @@ public abstract class AbstractFunctionConstraint<T> extends ColumnConstraint<T>
{
builder.append(toString());
}
public static <T extends Enum<T>> T getEnum(Class<T> enumClass, String functionName)
{
try
{
return Enum.valueOf(enumClass, LocalizeString.toUpperCaseLocalized(functionName));
}
catch (IllegalArgumentException e)
{
throw new InvalidConstraintDefinitionException("Unrecognized constraint function: " + functionName);
}
}
}

View File

@ -55,9 +55,9 @@ public abstract class ColumnConstraint<T>
// We are serializing its enum position instead of its name.
// Changing this enum would affect how that int is interpreted when deserializing.
COMPOSED(ColumnConstraints.serializer, new DuplicatesChecker()),
FUNCTION(FunctionColumnConstraint.serializer, FunctionColumnConstraint.getSatisfiabilityCheckers()),
FUNCTION(FunctionColumnConstraint.serializer, ConstraintResolver.getConstraintFunctionSatisfiabilityCheckers()),
SCALAR(ScalarColumnConstraint.serializer, new ScalarColumnConstraintSatisfiabilityChecker()),
UNARY_FUNCTION(UnaryFunctionColumnConstraint.serializer, UnaryFunctionColumnConstraint.Functions.values());
UNARY_FUNCTION(UnaryFunctionColumnConstraint.serializer, ConstraintResolver.getUnarySatisfiabilityCheckers());
private final MetadataSerializer<?> serializer;
private final SatisfiabilityChecker[] satisfiabilityCheckers;

View File

@ -0,0 +1,63 @@
/*
* 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.util.List;
import java.util.Optional;
/**
* Users implementing this interface and integrating it with SPI (putting JAR on a class path and
* adding it to META-INF/services/ConstraintProvider) will enrich Cassandra with their custom constraints.
*/
public interface ConstraintProvider
{
/**
* Tries to instantiate {@link UnaryConstraintFunction} with given arguments.
* <p>
* An implementation of this method should always return new object for each method call. Do not
* cache constraint instances and do not return them! Create a new instance every time. Do not re-use it.
*
* @param functionName name of function
* @param arguments arguments to the function
* @return unary constraint function when possible to create with this provider, empty optional otherwise.
*/
Optional<UnaryConstraintFunction> getUnaryConstraint(String functionName, List<String> arguments);
/**
* Tries to instantiate {@link ConstraintFunction} with given arguments.
* <p>
* An implementation of this method should always return new object for each method call. Do not
* cache constraint instances and do not return them! Create a new instance every time. Do not re-use it.
*
* @param functionName name of function
* @param arguments arguments to the function
* @return constraint function when possible to create with this provider, empty optional otherwise.
*/
Optional<ConstraintFunction> getConstraintFunction(String functionName, List<String> arguments);
/**
* @return list of satisfiability checkers for all unary constraints this provider is responsible for
*/
List<? extends SatisfiabilityChecker> getUnaryConstraintSatisfiabilityCheckers();
/**
* @return list of satisfiability checkers for all function constraints this provider is responsible for
*/
List<? extends SatisfiabilityChecker> getConstraintFunctionSatisfiabilityCheckers();
}

View File

@ -0,0 +1,162 @@
/*
* 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.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
import java.util.ServiceLoader;
import java.util.function.Function;
import com.google.common.annotations.VisibleForTesting;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.apache.cassandra.cql3.constraints.SatisfiabilityChecker.UnaryFunctionSatisfiabilityChecker;
import org.apache.cassandra.utils.LocalizeString;
public class ConstraintResolver
{
private static final Logger logger = LoggerFactory.getLogger(ConstraintResolver.class);
@VisibleForTesting
public static ConstraintProvider customConstraintProvider = ServiceLoader.load(ConstraintProvider.class).findFirst().orElse(null);
static
{
if (customConstraintProvider != null)
logger.info("Found custom constraint provider {}", customConstraintProvider.getClass().getName());
}
public enum UnaryFunctions implements UnaryFunctionSatisfiabilityChecker
{
NOT_NULL(NotNullConstraint::new),
JSON(JsonConstraint::new);
public final Function<List<String>, ConstraintFunction> functionCreator;
UnaryFunctions(Function<List<String>, ConstraintFunction> functionCreator)
{
this.functionCreator = functionCreator;
}
}
public enum Functions
{
LENGTH(LengthConstraint::new),
OCTET_LENGTH(OctetLengthConstraint::new),
REGEXP(RegexpConstraint::new);
public final Function<List<String>, ConstraintFunction> functionCreator;
Functions(Function<List<String>, ConstraintFunction> functionCreator)
{
this.functionCreator = functionCreator;
}
}
/**
* Returns implementation of function constraint. First, it iterates over custom functions, when not found,
* then it will look into built-in ones. If it is not found in either, throws an exception.
*
* @param functionName name of function of get an instance of a constraint of
* @param arguments arguments for constraint
* @return new instance of constraint for given name
* @throws InvalidConstraintDefinitionException in case constraint can not be resolved.
*/
public static ConstraintFunction getConstraintFunction(String functionName, List<String> arguments)
{
if (customConstraintProvider != null)
{
Optional<ConstraintFunction> maybeConstraint = customConstraintProvider.getConstraintFunction(functionName, arguments);
if (maybeConstraint.isPresent())
return maybeConstraint.get();
}
return ConstraintResolver.getEnum(Functions.class, functionName)
.map(c -> c.functionCreator.apply(arguments))
.orElseThrow(() -> new InvalidConstraintDefinitionException("Unrecognized constraint function: " + functionName));
}
/**
* Returns implementation of unary function constraint. First, it iterates over built-in functions, when not found,
* then it will look into custom constraint provider, if any. If custom provider is not set or if it is not found
* there either, throws an exception.
*
* @param functionName name of function of get an instance of a constraint of
* @param arguments arguments for constraint
* @return new instance of constraint for given name
* @throws InvalidConstraintDefinitionException in case constraint can not be resolved.
*/
public static ConstraintFunction getUnaryConstraintFunction(String functionName, List<String> arguments)
{
if (customConstraintProvider != null)
{
Optional<UnaryConstraintFunction> maybeConstraint = customConstraintProvider.getUnaryConstraint(functionName, arguments);
if (maybeConstraint.isPresent())
return maybeConstraint.get();
}
return ConstraintResolver.getEnum(UnaryFunctions.class, functionName)
.map(c -> c.functionCreator.apply(arguments))
.orElseThrow(() -> new InvalidConstraintDefinitionException("Unrecognized constraint function: " + functionName));
}
public static SatisfiabilityChecker[] getConstraintFunctionSatisfiabilityCheckers()
{
List<SatisfiabilityChecker> checkers = new ArrayList<>(Arrays.asList(FunctionColumnConstraint.getSatisfiabilityCheckers()));
if (customConstraintProvider != null)
{
List<? extends SatisfiabilityChecker> checkersCustom = customConstraintProvider.getConstraintFunctionSatisfiabilityCheckers();
if (checkersCustom != null)
checkers.addAll(checkersCustom);
}
return checkers.toArray(new SatisfiabilityChecker[checkers.size()]);
}
public static SatisfiabilityChecker[] getUnarySatisfiabilityCheckers()
{
List<SatisfiabilityChecker> checkers = new ArrayList<>(Arrays.asList(UnaryFunctions.values()));
if (customConstraintProvider != null)
{
List<? extends SatisfiabilityChecker> checkersCustom = customConstraintProvider.getUnaryConstraintSatisfiabilityCheckers();
if (checkersCustom != null)
checkers.addAll(checkersCustom);
}
return checkers.toArray(new SatisfiabilityChecker[checkers.size()]);
}
public static <T extends Enum<T>> Optional<T> getEnum(Class<T> enumClass, String functionName)
{
try
{
return Optional.of(Enum.valueOf(enumClass, LocalizeString.toUpperCaseLocalized(functionName)));
}
catch (IllegalArgumentException e)
{
return Optional.empty();
}
}
}

View File

@ -22,10 +22,10 @@ import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Function;
import org.apache.cassandra.cql3.ColumnIdentifier;
import org.apache.cassandra.cql3.Operator;
import org.apache.cassandra.cql3.constraints.ConstraintResolver.Functions;
import org.apache.cassandra.db.TypeSizes;
import org.apache.cassandra.db.marshal.AbstractType;
import org.apache.cassandra.io.util.DataInputPlus;
@ -55,7 +55,7 @@ public class FunctionColumnConstraint extends AbstractFunctionConstraint<Functio
this.term = term;
if (arguments == null)
arguments = new ArrayList<>();
function = createConstraintFunction(functionName.toCQLString(), arguments);
function = ConstraintResolver.getConstraintFunction(functionName.toCQLString(), arguments);
}
public FunctionColumnConstraint prepare()
@ -76,25 +76,6 @@ public class FunctionColumnConstraint extends AbstractFunctionConstraint<Functio
return satisfiabilityCheckers;
}
public enum Functions
{
LENGTH(LengthConstraint::new),
OCTET_LENGTH(OctetLengthConstraint::new),
REGEXP(RegexpConstraint::new);
private final Function<List<String>, ConstraintFunction> functionCreator;
Functions(Function<List<String>, ConstraintFunction> functionCreator)
{
this.functionCreator = functionCreator;
}
}
private static ConstraintFunction createConstraintFunction(String functionName, List<String> args)
{
return getEnum(Functions.class, functionName).functionCreator.apply(args);
}
private FunctionColumnConstraint(ConstraintFunction function, Operator relationType, String term)
{
super(relationType, term);
@ -211,7 +192,7 @@ public class FunctionColumnConstraint extends AbstractFunctionConstraint<Functio
ConstraintFunction function;
try
{
function = createConstraintFunction(functionName, args);
function = ConstraintResolver.getConstraintFunction(functionName, args);
}
catch (Exception e)
{

View File

@ -36,9 +36,14 @@ public class JsonConstraint extends UnaryConstraintFunction
public static final String FUNCTION_NAME = "JSON";
public JsonConstraint(String name, List<String> args)
{
super(name, args);
}
public JsonConstraint(List<String> args)
{
super(FUNCTION_NAME, args);
this(FUNCTION_NAME, args);
}
@Override

View File

@ -34,9 +34,14 @@ public class LengthConstraint extends ConstraintFunction
private static final String NAME = "LENGTH";
private static final List<AbstractType<?>> SUPPORTED_TYPES = List.of(BytesType.instance, UTF8Type.instance, AsciiType.instance);
public LengthConstraint(String name, List<String> args)
{
super(name, args);
}
public LengthConstraint(List<String> args)
{
super(NAME, args);
this(NAME, args);
}
@Override

View File

@ -21,8 +21,9 @@ package org.apache.cassandra.cql3.constraints;
import java.util.List;
import org.apache.cassandra.cql3.Operator;
import org.apache.cassandra.cql3.constraints.SatisfiabilityChecker.UnaryFunctionSatisfiabilityChecker;
public abstract class UnaryConstraintFunction extends ConstraintFunction
public abstract class UnaryConstraintFunction extends ConstraintFunction implements UnaryFunctionSatisfiabilityChecker
{
public UnaryConstraintFunction(String name, List<String> args)
{

View File

@ -22,13 +22,12 @@ import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Function;
import com.google.common.annotations.VisibleForTesting;
import org.apache.cassandra.cql3.ColumnIdentifier;
import org.apache.cassandra.cql3.Operator;
import org.apache.cassandra.cql3.constraints.SatisfiabilityChecker.UnaryFunctionSatisfiabilityChecker;
import org.apache.cassandra.cql3.constraints.ConstraintResolver.UnaryFunctions;
import org.apache.cassandra.db.TypeSizes;
import org.apache.cassandra.db.marshal.AbstractType;
import org.apache.cassandra.io.util.DataInputPlus;
@ -56,12 +55,12 @@ public class UnaryFunctionColumnConstraint extends AbstractFunctionConstraint<Un
public Raw(ColumnIdentifier functionName, List<String> arguments)
{
function = createConstraintFunction(functionName.toString(), arguments);
function = ConstraintResolver.getUnaryConstraintFunction(functionName.toString(), arguments);
}
public Raw(ColumnIdentifier functionName)
{
function = createConstraintFunction(functionName.toString(), List.of());
function = ConstraintResolver.getUnaryConstraintFunction(functionName.toString(), List.of());
}
public UnaryFunctionColumnConstraint prepare()
@ -70,24 +69,6 @@ public class UnaryFunctionColumnConstraint extends AbstractFunctionConstraint<Un
}
}
public enum Functions implements UnaryFunctionSatisfiabilityChecker
{
NOT_NULL(NotNullConstraint::new),
JSON(JsonConstraint::new);
private final Function<List<String>, ConstraintFunction> functionCreator;
Functions(Function<List<String>, ConstraintFunction> functionCreator)
{
this.functionCreator = functionCreator;
}
}
private static ConstraintFunction createConstraintFunction(String functionName, List<String> arguments)
{
return getEnum(Functions.class, functionName).functionCreator.apply(arguments);
}
public UnaryFunctionColumnConstraint(ConstraintFunction function)
{
super(null, null);
@ -134,7 +115,7 @@ public class UnaryFunctionColumnConstraint extends AbstractFunctionConstraint<Un
@Override
public boolean enablesDuplicateDefinitions(String name)
{
return Functions.valueOf(name).enableDuplicateDefinitions();
return UnaryFunctions.valueOf(name).enableDuplicateDefinitions();
}
@Override
@ -209,7 +190,7 @@ public class UnaryFunctionColumnConstraint extends AbstractFunctionConstraint<Un
@VisibleForTesting
public ConstraintFunction getConstraintFunction(String functionName, List<String> args)
{
return createConstraintFunction(functionName, args);
return ConstraintResolver.getUnaryConstraintFunction(functionName, args);
}
@Override

View File

@ -0,0 +1,144 @@
/*
* 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 java.util.List;
import java.util.Optional;
import org.junit.Test;
import org.apache.cassandra.constraints.ConstraintsProviderTest.CustomConstraintProvider.MyCustomConstraint;
import org.apache.cassandra.constraints.ConstraintsProviderTest.CustomConstraintProvider.MyCustomUnaryConstraint;
import org.apache.cassandra.cql3.Operator;
import org.apache.cassandra.cql3.constraints.ConstraintFunction;
import org.apache.cassandra.cql3.constraints.ConstraintProvider;
import org.apache.cassandra.cql3.constraints.ConstraintResolver;
import org.apache.cassandra.cql3.constraints.ConstraintResolver.Functions;
import org.apache.cassandra.cql3.constraints.ConstraintResolver.UnaryFunctions;
import org.apache.cassandra.cql3.constraints.InvalidConstraintDefinitionException;
import org.apache.cassandra.cql3.constraints.JsonConstraint;
import org.apache.cassandra.cql3.constraints.LengthConstraint;
import org.apache.cassandra.cql3.constraints.SatisfiabilityChecker;
import org.apache.cassandra.cql3.constraints.UnaryConstraintFunction;
import org.apache.cassandra.db.marshal.UTF8Type;
import static org.apache.cassandra.cql3.constraints.AbstractFunctionSatisfiabilityChecker.FUNCTION_SATISFIABILITY_CHECKER;
import static org.apache.cassandra.cql3.constraints.ConstraintResolver.customConstraintProvider;
import static org.apache.cassandra.cql3.constraints.ConstraintResolver.getConstraintFunction;
import static org.apache.cassandra.cql3.constraints.ConstraintResolver.getUnaryConstraintFunction;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
public class ConstraintsProviderTest
{
@Test
public void testContraintProvider()
{
customConstraintProvider = new CustomConstraintProvider();
ConstraintFunction constraintFunction = getConstraintFunction(MyCustomConstraint.NAME, List.of());
assertNotNull(constraintFunction);
constraintFunction.evaluate(UTF8Type.instance,
Operator.EQ,
Integer.toString(MyCustomConstraint.NAME.length()),
UTF8Type.instance.fromString(MyCustomConstraint.NAME));
ConstraintFunction unaryConstraint = getUnaryConstraintFunction(MyCustomUnaryConstraint.NAME, List.of());
assertNotNull(unaryConstraint);
unaryConstraint.evaluate(UTF8Type.instance, UTF8Type.instance.fromString("{\"a\": 4, \"b\": 10}"));
assertThatThrownBy(() -> getConstraintFunction("not_existing", List.of()))
.isInstanceOf(InvalidConstraintDefinitionException.class);
SatisfiabilityChecker[] functionSatCheckers = ConstraintResolver.getConstraintFunctionSatisfiabilityCheckers();
assertNotNull(functionSatCheckers);
// in built + these in provider
assertEquals(functionSatCheckers.length,
Functions.values().length + customConstraintProvider.getConstraintFunctionSatisfiabilityCheckers().size());
SatisfiabilityChecker[] unarySatCheckers = ConstraintResolver.getUnarySatisfiabilityCheckers();
assertNotNull(unarySatCheckers);
// in built + these in provider
assertEquals(unarySatCheckers.length,
UnaryFunctions.values().length + customConstraintProvider.getUnaryConstraintSatisfiabilityCheckers().size());
}
public static class CustomConstraintProvider implements ConstraintProvider
{
@Override
public Optional<UnaryConstraintFunction> getUnaryConstraint(String functionName, List<String> arguments)
{
if (!functionName.equalsIgnoreCase(MyCustomUnaryConstraint.NAME))
return Optional.empty();
return Optional.of(new MyCustomUnaryConstraint(arguments));
}
@Override
public Optional<ConstraintFunction> getConstraintFunction(String functionName, List<String> arguments)
{
if (!functionName.equalsIgnoreCase(MyCustomConstraint.NAME))
return Optional.empty();
return Optional.of(new MyCustomConstraint(arguments));
}
@Override
public List<? extends SatisfiabilityChecker> getUnaryConstraintSatisfiabilityCheckers()
{
return List.of(new MyCustomUnaryConstraint(List.of()));
}
@Override
public List<? extends SatisfiabilityChecker> getConstraintFunctionSatisfiabilityCheckers()
{
return List.of((constraints, columnMetadata) ->
FUNCTION_SATISFIABILITY_CHECKER.check(MyCustomConstraint.NAME,
constraints,
columnMetadata));
}
/**
* Same as length constraint, just under different name to prove the point
*/
public static class MyCustomConstraint extends LengthConstraint
{
public static final String NAME = "MY_CUSTOM_CONSTRAINT";
public MyCustomConstraint(List<String> arguments)
{
super(NAME, arguments);
}
}
/**
* Same as JSON constraint, just under different name to prove the point
*/
public static class MyCustomUnaryConstraint extends JsonConstraint
{
public static final String NAME = "MY_CUSTOM_UNARY_CONSTRAINT";
public MyCustomUnaryConstraint(List<String> arguments)
{
super(NAME, arguments);
}
}
}
}