com.apple.foundationdb.record.metadata.expressions.FunctionKeyExpression Maven / Gradle / Ivy
Show all versions of fdb-record-layer-core Show documentation
/*
* FunctionKeyExpression.java
*
* This source file is part of the FoundationDB open source project
*
* Copyright 2015-2018 Apple Inc. and the FoundationDB project authors
*
* Licensed 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 com.apple.foundationdb.record.metadata.expressions;
import com.apple.foundationdb.annotation.API;
import com.apple.foundationdb.record.RecordCoreException;
import com.apple.foundationdb.record.RecordMetaDataProto;
import com.apple.foundationdb.record.logging.LogMessageKeys;
import com.apple.foundationdb.record.metadata.Key;
import com.apple.foundationdb.record.provider.foundationdb.FDBRecord;
import com.google.protobuf.Descriptors;
import com.google.protobuf.Message;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.ServiceLoader;
import java.util.function.BiFunction;
/**
* A FunctionKeyExpression
is a {@link KeyExpression} that is dynamically loaded and defined by a
* String
name and a Key.Expression
that produces sets of arguments to which the function
* is to be evaluated. FunctionKeyExpressions
provide a mechanism by which indexes can be defined on
* arbitrarily complex logic applied to a record being inserted. For example, assuming a function called
* "substr
" that functions much like the java String.substring()
method, you could
* create, say, a unique index with key of
*
*
* function("subsstr", concat(field("firstname"), value(0), value(2)))
*
*
* Which would prevent duplicate records in which the first two characters of the firstname
are identical.
*
* Similarly, the function can be made to apply in a fan-out fashion, simply by providing a argument an expression
* that itself fans out into a set of arguments to substr
. For example, given message definitions
* such as:
*
*
* message SubString {
* required string content = 1;
* required int32 start = 2;
* required int32 end = 3;
* }
*
* message SubStrings {
* repeated SubString substrings = 1;
* }
*
*
* In which we want to have the arguments to substr
driven by data stored in the records themselves,
* you could define the index key expression as:
*
*
* function("substr", field("substrings", FanType.FanOut).nest(concatenateFields("content", "start", "end")))
*
*
* This would produce the result of performing substr(content, start, end)
for each SubString
value
* in substrings.
*
* Actual implementations of FunctionKeyExpressions
are discovered by polling all available
* {@link Registry} implementations. A Registry
returns a list of {@link Builder}s which, given a set of
* arguments, are capable of creating an implementation of a function.
*/
@API(API.Status.EXPERIMENTAL)
public abstract class FunctionKeyExpression extends BaseKeyExpression implements AtomKeyExpression, KeyExpressionWithChild {
@Nonnull
protected final String name;
@Nonnull
protected final KeyExpression arguments;
protected FunctionKeyExpression(@Nonnull String name, @Nonnull KeyExpression arguments) {
this.name = name;
this.arguments = arguments;
}
/**
* Create a function.
*
* @param name the name of the function
* @param arguments an expression that produces the arguments to the function
* @return the key expression that implements the function
* @throws InvalidExpressionException if the function name provided does not have an available
* implementation, or the arguments provided are not suitable for the function
*/
public static FunctionKeyExpression create(@Nonnull String name, @Nonnull KeyExpression arguments) {
Optional funcBuilder = Registry.instance().getBuilder(name);
if (!funcBuilder.isPresent()) {
throw new InvalidExpressionException("Function not defined")
.addLogInfo(LogMessageKeys.FUNCTION, name);
}
final FunctionKeyExpression function = funcBuilder.get().build(arguments);
final int argumentCount = arguments.getColumnSize();
if (argumentCount < function.getMinArguments() || argumentCount > function.getMaxArguments()) {
throw new InvalidExpressionException("Invalid number of arguments provided to function",
LogMessageKeys.FUNCTION, name,
"args_provided", argumentCount,
"min_args_expected", function.getMinArguments(),
"max_args_expected", function.getMaxArguments());
}
return function;
}
@Nonnull
public final String getName() {
return name;
}
@Nonnull
@Override
public KeyExpression getChild() {
return getArguments();
}
@Nonnull
public final KeyExpression getArguments() {
return arguments;
}
/**
* Get the the minimum number of arguments supported by this function.
* @return the minimum number of arguments supported by this function
*/
public abstract int getMinArguments();
/**
* Get the maximum number of arguments supported by this function.
* @return the maximum number of arguments supported by this function
*/
public abstract int getMaxArguments();
@Nonnull
@Override
public List evaluateMessage(@Nullable FDBRecord record, @Nullable Message message) {
final List evaluatedArguments = getArguments().evaluateMessage(record, message);
final List results = new ArrayList<>(evaluatedArguments.size());
for (Key.Evaluated evaluatedArgument : evaluatedArguments) {
validateArgumentCount(evaluatedArgument);
results.addAll(evaluateFunction(record, message, evaluatedArgument));
}
validateColumnCounts(results);
return results;
}
/**
* The evaluateFunction
method implements the function execution. This method is invoked once per
* Key.Evaluated
that was produced by the evaluation of the function's argument. Put another way,
* the function's argument expression is evaluated and is expected to produce a set of arguments. This method
* is invoked once for each of these and, itself, may produce a set of Key.Evaluated
values that
* produce the final set of keys.
*
* Note that the record
parameter might be null
. Function implementors should treat
* this case the same way that they would treat a non-null
record that has all of its non-repeated
* fields unset and all of its repeated fields empty. If the function result depends only on the
* value of arguments
and not record
directly, then the implementor can ignore
* the nullity of record
.
*
*
* @param the type of the records
* @param record the record against which this function will produce a key
* @param message the Protobuf message against which this function will produce a key
* @param arguments the set of arguments to be applied by the function against the record
* @return the list of keys for the given record
*/
@Nonnull
public abstract List evaluateFunction(@Nullable FDBRecord record,
@Nullable Message message,
@Nonnull Key.Evaluated arguments);
private void validateArgumentCount(Key.Evaluated arguments) {
final int argumentCount = arguments.size();
if (argumentCount < getMinArguments() || argumentCount > getMaxArguments()) {
throw new InvalidResultException("Invalid number of arguments provided to function").addLogInfo(
LogMessageKeys.FUNCTION, getName(),
"args_provided", argumentCount,
"min_args_expected", getMinArguments(),
"max_args_expected", getMaxArguments());
}
}
@Override
public List validate(@Nonnull Descriptors.Descriptor descriptor) {
return getArguments().validate(descriptor);
}
@Override
public boolean equalsAtomic(AtomKeyExpression other) {
return equals(other);
}
/**
* Create a function from its protobuf serialized form.
* @param function the protobuf definition of the function
* @return the key expression that implements the function
* @throws InvalidExpressionException If the function name provided does not have an available
* implementation, or the arguments provided are not suitable for the function.
*/
@Nonnull
public static FunctionKeyExpression fromProto(RecordMetaDataProto.Function function) throws DeserializationException {
try {
return create(function.getName(), KeyExpression.fromProto(function.getArguments()));
} catch (RecordCoreException e) {
throw new DeserializationException(e.getMessage(), e);
}
}
@Nonnull
@Override
public final RecordMetaDataProto.Function toProto() throws SerializationException {
RecordMetaDataProto.Function.Builder builder = RecordMetaDataProto.Function.newBuilder()
.setName(getName());
builder.setArguments(getArguments().toKeyExpression());
return builder.build();
}
@Nonnull
@Override
public final RecordMetaDataProto.KeyExpression toKeyExpression() {
return RecordMetaDataProto.KeyExpression.newBuilder().setFunction(toProto()).build();
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (!(o instanceof FunctionKeyExpression)) {
return false;
}
FunctionKeyExpression that = (FunctionKeyExpression) o;
if (!getName().equals(that.getName())) {
return false;
}
return this.getArguments().equals(that.getArguments());
}
@Override
public int hashCode() {
return Objects.hash(getName(), getArguments());
}
@Override
public int planHash() {
return getName().hashCode() + getArguments().planHash();
}
@Override
public String toString() {
return getName() + "(" + getArguments() + ")";
}
/**
* A builder is capable of producing an instance of a FunctionKeyExpression
given the arguments
* to the function.
*/
public abstract static class Builder {
@Nonnull
protected final String functionName;
public Builder(@Nonnull String functionName) {
this.functionName = functionName;
}
@Nonnull
public String getName() {
return functionName;
}
@Nonnull
abstract FunctionKeyExpression build(@Nonnull KeyExpression arguments);
}
/**
* An implementation of a Builder
that can construct a KeyExpressionFunction
* via a provided generator.
*/
public static class BiFunctionBuilder extends Builder {
private final BiFunction generator;
public BiFunctionBuilder(@Nonnull String functionName,
@Nonnull BiFunction generator) {
super(functionName);
this.generator = generator;
}
@Nonnull
@Override
public FunctionKeyExpression build(@Nonnull KeyExpression expression) {
return generator.apply(super.getName(), expression);
}
}
/**
* Implementations of FunctionKeyExpression.Factory
are dynamically located by the {@link Registry}
* and are polled once to request a list of builders for functions that the factory is capable of producing.
*/
public interface Factory {
@Nonnull
List getBuilders();
}
/**
* The Registry
maintains a mapping from a function name to a Builder
capable of
* producing an instance of the function.
*/
public static class Registry {
private static final Registry INSTANCE = new Registry();
private final Map functions;
private Registry() {
functions = initRegistry();
}
@Nonnull
public static Registry instance() {
return INSTANCE;
}
public Optional getBuilder(String name) {
return Optional.ofNullable(functions.get(name));
}
private static Map initRegistry() {
Map functions = new HashMap<>();
for (Factory factory : ServiceLoader.load(Factory.class)) {
for (Builder function : factory.getBuilders()) {
if (functions.containsKey(function.getName())) {
throw new RecordCoreException("Function already defined").addLogInfo(
LogMessageKeys.FUNCTION, function.getName());
}
functions.put(function.getName(), function);
}
}
return functions;
}
}
}