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 <[email protected]>
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: [email protected]
For additional commands, e-mail: [email protected]


Reply via email to