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] ALTER [IF EXISTS] 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 /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 /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 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 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 getUnaryConstraintSatisfiabilityCheckers() + { + return List.of(new MyCustomUnaryConstraint(List.of())); + } + + @Override + public List 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); + } + } +} \ 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 extends ColumnConstraint { @@ -52,16 +51,4 @@ public abstract class AbstractFunctionConstraint extends ColumnConstraint { builder.append(toString()); } - - public static > T getEnum(Class 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 // 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 getUnaryConstraintSatisfiabilityCheckers(); + + /** + * @return list of satisfiability checkers for all function constraints this provider is responsible for + */ + List 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 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 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 getUnaryConstraintSatisfiabilityCheckers() + { + return List.of(new MyCustomUnaryConstraint(List.of())); + } + + @Override + public List 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); + } + } + } +}