You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@cassandra.apache.org by sm...@apache.org on 2025/08/18 08:15:08 UTC
(cassandra) branch trunk updated: Allow custom constraints to be loaded via SPI
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 9d5cef7f8c Allow custom constraints to be loaded via SPI
9d5cef7f8c is described below
commit 9d5cef7f8c6c749d3f2b71fc1f6fae1ee086c2ae
Author: Stefan Miklosovic <sm...@apache.org>
AuthorDate: Fri Aug 8 11:23:43 2025 +0200
Allow custom constraints to be loaded via SPI
patch by Stefan Miklosovic; reviewed by Bernardo Botella, Saranya Krishnakumar for CASSANDRA-20824
---
CHANGES.txt | 1 +
.../pages/developing/cql/constraints.adoc | 15 ++
examples/constraints/README.adoc | 46 ++++++
examples/constraints/build.xml | 68 +++++++++
.../constraints/CustomConstraintsProvider.java | 100 +++++++++++++
...e.cassandra.cql3.constraints.ConstraintProvider | 16 ++
.../constraints/AbstractFunctionConstraint.java | 13 --
.../cql3/constraints/ColumnConstraint.java | 4 +-
.../cql3/constraints/ConstraintProvider.java | 63 ++++++++
.../cql3/constraints/ConstraintResolver.java | 162 +++++++++++++++++++++
.../cql3/constraints/FunctionColumnConstraint.java | 25 +---
.../cassandra/cql3/constraints/JsonConstraint.java | 7 +-
.../cql3/constraints/LengthConstraint.java | 7 +-
.../cql3/constraints/UnaryConstraintFunction.java | 3 +-
.../constraints/UnaryFunctionColumnConstraint.java | 29 +---
.../constraints/ConstraintsProviderTest.java | 144 ++++++++++++++++++
16 files changed, 639 insertions(+), 64 deletions(-)
diff --git a/CHANGES.txt b/CHANGES.txt
index 2a7162fb4c..8a17ee639a 100644
--- a/CHANGES.txt
+++ b/CHANGES.txt
@@ -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)
diff --git a/doc/modules/cassandra/pages/developing/cql/constraints.adoc b/doc/modules/cassandra/pages/developing/cql/constraints.adoc
index 2e729db01d..c352ce207f 100644
--- a/doc/modules/cassandra/pages/developing/cql/constraints.adoc
+++ b/doc/modules/cassandra/pages/developing/cql/constraints.adoc
@@ -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
diff --git a/examples/constraints/README.adoc b/examples/constraints/README.adoc
new file mode 100644
index 0000000000..9baafeccd3
--- /dev/null
+++ b/examples/constraints/README.adoc
@@ -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.
\ No newline at end of file
diff --git a/examples/constraints/build.xml b/examples/constraints/build.xml
new file mode 100644
index 0000000000..cd5db0fa44
--- /dev/null
+++ b/examples/constraints/build.xml
@@ -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>
diff --git a/examples/constraints/src/org/apache/cassandra/cql3/constraints/CustomConstraintsProvider.java b/examples/constraints/src/org/apache/cassandra/cql3/constraints/CustomConstraintsProvider.java
new file mode 100644
index 0000000000..4d4ced6158
--- /dev/null
+++ b/examples/constraints/src/org/apache/cassandra/cql3/constraints/CustomConstraintsProvider.java
@@ -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);
+ }
+ }
+}
\ No newline at end of file
diff --git a/examples/constraints/src/resources/META-INF/services/org.apache.cassandra.cql3.constraints.ConstraintProvider b/examples/constraints/src/resources/META-INF/services/org.apache.cassandra.cql3.constraints.ConstraintProvider
new file mode 100644
index 0000000000..a6f61e4087
--- /dev/null
+++ b/examples/constraints/src/resources/META-INF/services/org.apache.cassandra.cql3.constraints.ConstraintProvider
@@ -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
\ No newline at end of file
diff --git a/src/java/org/apache/cassandra/cql3/constraints/AbstractFunctionConstraint.java b/src/java/org/apache/cassandra/cql3/constraints/AbstractFunctionConstraint.java
index 2f6805bdef..a22453175b 100644
--- a/src/java/org/apache/cassandra/cql3/constraints/AbstractFunctionConstraint.java
+++ b/src/java/org/apache/cassandra/cql3/constraints/AbstractFunctionConstraint.java
@@ -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);
- }
- }
}
diff --git a/src/java/org/apache/cassandra/cql3/constraints/ColumnConstraint.java b/src/java/org/apache/cassandra/cql3/constraints/ColumnConstraint.java
index bddea571aa..727790f262 100644
--- a/src/java/org/apache/cassandra/cql3/constraints/ColumnConstraint.java
+++ b/src/java/org/apache/cassandra/cql3/constraints/ColumnConstraint.java
@@ -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;
diff --git a/src/java/org/apache/cassandra/cql3/constraints/ConstraintProvider.java b/src/java/org/apache/cassandra/cql3/constraints/ConstraintProvider.java
new file mode 100644
index 0000000000..3fa04e8995
--- /dev/null
+++ b/src/java/org/apache/cassandra/cql3/constraints/ConstraintProvider.java
@@ -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();
+}
diff --git a/src/java/org/apache/cassandra/cql3/constraints/ConstraintResolver.java b/src/java/org/apache/cassandra/cql3/constraints/ConstraintResolver.java
new file mode 100644
index 0000000000..6ac7cfcd28
--- /dev/null
+++ b/src/java/org/apache/cassandra/cql3/constraints/ConstraintResolver.java
@@ -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();
+ }
+ }
+}
diff --git a/src/java/org/apache/cassandra/cql3/constraints/FunctionColumnConstraint.java b/src/java/org/apache/cassandra/cql3/constraints/FunctionColumnConstraint.java
index a25553bd7b..0c60ed3dbe 100644
--- a/src/java/org/apache/cassandra/cql3/constraints/FunctionColumnConstraint.java
+++ b/src/java/org/apache/cassandra/cql3/constraints/FunctionColumnConstraint.java
@@ -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)
{
diff --git a/src/java/org/apache/cassandra/cql3/constraints/JsonConstraint.java b/src/java/org/apache/cassandra/cql3/constraints/JsonConstraint.java
index 15a19d7954..540d471696 100644
--- a/src/java/org/apache/cassandra/cql3/constraints/JsonConstraint.java
+++ b/src/java/org/apache/cassandra/cql3/constraints/JsonConstraint.java
@@ -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
diff --git a/src/java/org/apache/cassandra/cql3/constraints/LengthConstraint.java b/src/java/org/apache/cassandra/cql3/constraints/LengthConstraint.java
index 59d78afcaa..3304762713 100644
--- a/src/java/org/apache/cassandra/cql3/constraints/LengthConstraint.java
+++ b/src/java/org/apache/cassandra/cql3/constraints/LengthConstraint.java
@@ -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
diff --git a/src/java/org/apache/cassandra/cql3/constraints/UnaryConstraintFunction.java b/src/java/org/apache/cassandra/cql3/constraints/UnaryConstraintFunction.java
index 0e4b0ddd2d..d7c3941c6e 100644
--- a/src/java/org/apache/cassandra/cql3/constraints/UnaryConstraintFunction.java
+++ b/src/java/org/apache/cassandra/cql3/constraints/UnaryConstraintFunction.java
@@ -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)
{
diff --git a/src/java/org/apache/cassandra/cql3/constraints/UnaryFunctionColumnConstraint.java b/src/java/org/apache/cassandra/cql3/constraints/UnaryFunctionColumnConstraint.java
index c8edd1189e..6afeb35028 100644
--- a/src/java/org/apache/cassandra/cql3/constraints/UnaryFunctionColumnConstraint.java
+++ b/src/java/org/apache/cassandra/cql3/constraints/UnaryFunctionColumnConstraint.java
@@ -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
diff --git a/test/unit/org/apache/cassandra/constraints/ConstraintsProviderTest.java b/test/unit/org/apache/cassandra/constraints/ConstraintsProviderTest.java
new file mode 100644
index 0000000000..eb3e617cbc
--- /dev/null
+++ b/test/unit/org/apache/cassandra/constraints/ConstraintsProviderTest.java
@@ -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);
+ }
+ }
+ }
+}
---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@cassandra.apache.org
For additional commands, e-mail: commits-help@cassandra.apache.org