// 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.
+ *
+ * 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 getUnaryConstraint(String functionName, List arguments);
+
+ /**
+ * Tries to instantiate {@link ConstraintFunction} with given arguments.
+ *
+ * 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 getConstraintFunction(String functionName, List 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, ConstraintFunction> functionCreator;
+
+ UnaryFunctions(Function, ConstraintFunction> functionCreator)
+ {
+ this.functionCreator = functionCreator;
+ }
+ }
+
+ public enum Functions
+ {
+ LENGTH(LengthConstraint::new),
+ OCTET_LENGTH(OctetLengthConstraint::new),
+ REGEXP(RegexpConstraint::new);
+
+ public final Function, ConstraintFunction> functionCreator;
+
+ Functions(Function, 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 arguments)
+ {
+ if (customConstraintProvider != null)
+ {
+ Optional 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 arguments)
+ {
+ if (customConstraintProvider != null)
+ {
+ Optional 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 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 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 > Optional getEnum(Class 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();
- function = createConstraintFunction(functionName.toCQLString(), arguments);
+ function = ConstraintResolver.getConstraintFunction(functionName.toCQLString(), arguments);
}
public FunctionColumnConstraint prepare()
@@ -76,25 +76,6 @@ public class FunctionColumnConstraint extends AbstractFunctionConstraint, ConstraintFunction> functionCreator;
-
- Functions(Function, ConstraintFunction> functionCreator)
- {
- this.functionCreator = functionCreator;
- }
- }
-
- private static ConstraintFunction createConstraintFunction(String functionName, List 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 args)
+ {
+ super(name, args);
+ }
+
public JsonConstraint(List 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> SUPPORTED_TYPES = List.of(BytesType.instance, UTF8Type.instance, AsciiType.instance);
+ public LengthConstraint(String name, List args)
+ {
+ super(name, args);
+ }
+
public LengthConstraint(List 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 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 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, ConstraintFunction> functionCreator;
-
- Functions(Function, ConstraintFunction> functionCreator)
- {
- this.functionCreator = functionCreator;
- }
- }
-
- private static ConstraintFunction createConstraintFunction(String functionName, List 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 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 getUnaryConstraint(String functionName, List arguments)
+ {
+ if (!functionName.equalsIgnoreCase(MyCustomUnaryConstraint.NAME))
+ return Optional.empty();
+
+ return Optional.of(new MyCustomUnaryConstraint(arguments));
+ }
+
+ @Override
+ public Optional getConstraintFunction(String functionName, List 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 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 arguments)
+ {
+ super(NAME, arguments);
+ }
+ }
+ }
+}