Skip to content

Commit a3b55bf

Browse files
authored
Implement Compilable SQL functions (#3307)
This introduces compilable SQL functions, as defined, for the most part, in the SQL standard. The functions currently have the following characteristics: * non-temporary, defined, and serialized as part of the schema template. * lazily compiled, the deserialized form of a SQL function only captures its SQL definition, and compiled the first time it is referenced in a SQL query. * functions can nest, no support for recursive functions. * they support (named) parameters, with expression default values (no support for scalar queries). * functions can be invoked either with named arguments, or unnamed arguments, (no support for mixing named and unnamed arguments). * the body of the function is currently limited to a single SQL statement. **Example** ```sql create table t1(col1 bigint, col2 string, col3 integer, primary key(col1)) create function f1 ( in a bigint, in b string ) as select col1, col2 from t1 where col1 < a and col2 = b create function f2 ( in k bigint ) as select col1, col2, col3 from t1 where col3 = k create function f3 ( a bigint, b string, c bigint) as select A.col1, A.col2, B.col3 from f1(a, b) A, f2(c) B ``` When the above DDL is parsed, the functions are visited, and serialized to disk along with the rest of the metadata, the serialized form of compilable SQL functions comprises their name, and their description, i.e. their SQL definition. ```sql insert into t1 values (100, 'a', 1), (101, 'b', 2), (102, 'b', 2), (103, 'b', 2), (104, 'c', 2), (105, 'd', 4) ``` Invoking the functions can be done in two ways: 1. Using unnamed arguments ```sql select * from f3(103, 'b', 4) 101, 'b', 4 102, 'b', 4 ``` 2. Using named arguments ```sql select * from f3(a => 103, b => 'b', c => 4) 101, 'b', 4 102, 'b', 4 ``` It is possible to define a function with default parameter values, for example we could've defined the function `f3` as the following: ```sql create function f3 ( in a bigint default 42, in b string default 'b', in c bigint default '4') as select A.col1, A.col2, B.col3 from f1(a, b) A, f2(c) B ``` if we did so, then we could've invoked `f3` as the following: ```sql select * from f3() 101, 'b', 4 102, 'b', 4 ``` This fixes #3325
1 parent dcb0f30 commit a3b55bf

File tree

80 files changed

+4586
-526
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

80 files changed

+4586
-526
lines changed

fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/RecordMetaData.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -721,6 +721,11 @@ public Map<String, Descriptors.FieldDescriptor> getFieldDescriptorMapFromNames(@
721721
return getFieldDescriptorMap(recordTypeNames.stream().map(this::getRecordType));
722722
}
723723

724+
@Nonnull
725+
public Map<String, UserDefinedFunction> getUserDefinedFunctionMap() {
726+
return userDefinedFunctionMap;
727+
}
728+
724729
@Nonnull
725730
public static Map<String, Descriptors.FieldDescriptor> getFieldDescriptorMapFromTypes(@Nonnull final Collection<RecordType> recordTypes) {
726731
if (recordTypes.size() == 1) {

fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/RecordMetaDataBuilder.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -229,8 +229,9 @@ private void loadProtoExceptRecords(@Nonnull RecordMetaDataProto.MetaData metaDa
229229
}
230230
}
231231
for (RecordMetaDataProto.PUserDefinedFunction function: metaDataProto.getUserDefinedFunctionsList()) {
232-
UserDefinedFunction func = (UserDefinedFunction)PlanSerialization.dispatchFromProtoContainer(new PlanSerializationContext(DefaultPlanSerializationRegistry.INSTANCE,
233-
PlanHashable.CURRENT_FOR_CONTINUATION), function);
232+
final UserDefinedFunction func = (UserDefinedFunction)PlanSerialization.dispatchFromProtoContainer(
233+
new PlanSerializationContext(DefaultPlanSerializationRegistry.INSTANCE,
234+
PlanHashable.CURRENT_FOR_CONTINUATION), function);
234235
userDefinedFunctionMap.put(func.getFunctionName(), func);
235236
}
236237
if (metaDataProto.hasSplitLongRecords()) {

fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/metadata/expressions/FunctionKeyExpression.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@
3131
import com.apple.foundationdb.record.provider.foundationdb.FDBRecord;
3232
import com.apple.foundationdb.record.query.plan.cascades.BuiltInFunction;
3333
import com.apple.foundationdb.record.query.plan.cascades.KeyExpressionVisitor;
34-
import com.apple.foundationdb.record.query.plan.cascades.values.FunctionCatalog;
34+
import com.apple.foundationdb.record.query.plan.cascades.values.BuiltInFunctionCatalog;
3535
import com.apple.foundationdb.record.query.plan.cascades.values.Value;
3636
import com.apple.foundationdb.record.util.HashUtils;
3737
import com.apple.foundationdb.record.util.ServiceLoaderProvider;
@@ -276,7 +276,7 @@ public <S extends KeyExpressionVisitor.State, R> R expand(@Nonnull final KeyExpr
276276
protected Value resolveAndEncapsulateFunction(@Nonnull final String functionName,
277277
@Nonnull final List<? extends Value> argumentValues) {
278278
final BuiltInFunction<?> builtInFunction =
279-
FunctionCatalog.resolve(functionName, argumentValues.size())
279+
BuiltInFunctionCatalog.resolve(functionName, argumentValues.size())
280280
.orElseThrow(() -> new RecordCoreArgumentException("unknown function",
281281
LogMessageKeys.FUNCTION, getName()));
282282
return (Value)builtInFunction.encapsulate(argumentValues);

fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/BitmapAggregateIndexExpansionVisitor.java

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@
3131
import com.apple.foundationdb.record.query.plan.cascades.predicates.Placeholder;
3232
import com.apple.foundationdb.record.query.plan.cascades.values.ArithmeticValue;
3333
import com.apple.foundationdb.record.query.plan.cascades.values.FieldValue;
34-
import com.apple.foundationdb.record.query.plan.cascades.values.FunctionCatalog;
34+
import com.apple.foundationdb.record.query.plan.cascades.values.BuiltInFunctionCatalog;
3535
import com.apple.foundationdb.record.query.plan.cascades.values.LiteralValue;
3636
import com.apple.foundationdb.record.query.plan.cascades.values.NumericAggregationValue;
3737
import com.apple.foundationdb.record.query.plan.cascades.values.RecordConstructorValue;
@@ -86,8 +86,8 @@ protected NonnullPair<Quantifier, List<Placeholder>> constructGroupBy(@Nonnull f
8686
}
8787

8888

89-
final var bitmapConstructAggFunc = FunctionCatalog.getFunctionSingleton(NumericAggregationValue.BitmapConstructAggFn.class).orElseThrow();
90-
final var bitmapBitPositionFunc = FunctionCatalog.getFunctionSingleton(ArithmeticValue.BitmapBitPositionFn.class).orElseThrow();
89+
final var bitmapConstructAggFunc = BuiltInFunctionCatalog.getFunctionSingleton(NumericAggregationValue.BitmapConstructAggFn.class).orElseThrow();
90+
final var bitmapBitPositionFunc = BuiltInFunctionCatalog.getFunctionSingleton(ArithmeticValue.BitmapBitPositionFn.class).orElseThrow();
9191
final String sizeArgument = index.getOption(IndexOptions.BITMAP_VALUE_ENTRY_SIZE_OPTION);
9292
final int entrySize = sizeArgument != null ? Integer.parseInt(sizeArgument) : BitmapValueIndexMaintainer.DEFAULT_ENTRY_SIZE;
9393
final var entrySizeValue = LiteralValue.ofScalar(entrySize);
@@ -101,7 +101,7 @@ protected NonnullPair<Quantifier, List<Placeholder>> constructGroupBy(@Nonnull f
101101
.stream()
102102
.map(Column::getValue)
103103
.collect(ImmutableList.toImmutableList());
104-
final var bitmapBitPosition = FunctionCatalog.getFunctionSingleton(ArithmeticValue.BitmapBucketOffsetFn.class).orElseThrow();
104+
final var bitmapBitPosition = BuiltInFunctionCatalog.getFunctionSingleton(ArithmeticValue.BitmapBucketOffsetFn.class).orElseThrow();
105105
final var implicitGroupingValue = (Value)bitmapBitPosition.encapsulate(ImmutableList.of(argument, entrySizeValue));
106106
final var placeHolder = Placeholder.newInstanceWithoutRanges(implicitGroupingValue, newParameterAlias());
107107

fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/BuiltInFunction.java

Lines changed: 8 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -20,14 +20,14 @@
2020

2121
package com.apple.foundationdb.record.query.plan.cascades;
2222

23+
import com.apple.foundationdb.record.RecordCoreException;
2324
import com.apple.foundationdb.record.query.plan.cascades.typing.Type;
2425
import com.apple.foundationdb.record.query.plan.cascades.typing.Typed;
2526

2627
import javax.annotation.Nonnull;
2728
import javax.annotation.Nullable;
2829
import java.util.List;
29-
import java.util.Objects;
30-
import java.util.Optional;
30+
import java.util.Map;
3131

3232
/**
3333
* Main interface for defining a built-in function that can be evaluated against a number of arguments.
@@ -64,46 +64,6 @@ protected BuiltInFunction(@Nonnull final String functionName, @Nonnull final Lis
6464
this.encapsulationFunction = encapsulationFunction;
6565
}
6666

67-
/**
68-
* Checks whether the provided list of argument types matches the list of function's parameter types.
69-
*
70-
* @param argumentTypes The argument types list.
71-
* @return if the arguments type match, an {@link Optional} containing <code>this</code> instance, otherwise
72-
* and empty {@link Optional}.
73-
*/
74-
@SuppressWarnings("java:S3776")
75-
@Nonnull
76-
public Optional<BuiltInFunction<T>> validateCall(@Nonnull final List<Type> argumentTypes) {
77-
int numberOfArguments = argumentTypes.size();
78-
79-
final List<Type> functionParameterTypes = getParameterTypes();
80-
81-
if (numberOfArguments > functionParameterTypes.size() && !hasVariadicSuffix()) {
82-
return Optional.empty();
83-
}
84-
85-
// check the type codes of the fixed parameters
86-
for (int i = 0; i < functionParameterTypes.size(); i ++) {
87-
final Type typeI = functionParameterTypes.get(i);
88-
if (typeI.getTypeCode() != Type.TypeCode.ANY && typeI.getTypeCode() != argumentTypes.get(i).getTypeCode()) {
89-
return Optional.empty();
90-
}
91-
}
92-
93-
if (hasVariadicSuffix()) { // This is variadic function, match the rest of arguments, if any.
94-
final Type functionVariadicSuffixType = Objects.requireNonNull(getVariadicSuffixType());
95-
if (functionVariadicSuffixType.getTypeCode() != Type.TypeCode.ANY) {
96-
for (int i = getParameterTypes().size(); i < numberOfArguments; i++) {
97-
if (argumentTypes.get(i).getTypeCode() != functionVariadicSuffixType.getTypeCode()) {
98-
return Optional.empty();
99-
}
100-
}
101-
}
102-
}
103-
104-
return Optional.of(this);
105-
}
106-
10767
@Nonnull
10868
public EncapsulationFunction<T> getEncapsulationFunction() {
10969
return encapsulationFunction;
@@ -114,4 +74,10 @@ public EncapsulationFunction<T> getEncapsulationFunction() {
11474
public Typed encapsulate(@Nonnull final List<? extends Typed> arguments) {
11575
return encapsulationFunction.encapsulate(this, arguments);
11676
}
77+
78+
@Nonnull
79+
@Override
80+
public Typed encapsulate(@Nonnull final Map<String, ? extends Typed> namedArguments) {
81+
throw new RecordCoreException("built-in functions do not support named argument calling conventions");
82+
}
11783
}

fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/CatalogedFunction.java

Lines changed: 114 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -23,18 +23,29 @@
2323
import com.apple.foundationdb.annotation.API;
2424
import com.apple.foundationdb.record.query.plan.cascades.typing.Type;
2525
import com.apple.foundationdb.record.query.plan.cascades.typing.Typed;
26+
import com.google.common.base.Functions;
2627
import com.google.common.base.Verify;
28+
import com.google.common.collect.BiMap;
29+
import com.google.common.collect.ImmutableBiMap;
2730
import com.google.common.collect.ImmutableList;
31+
import com.google.common.collect.Sets;
32+
import com.google.common.collect.Streams;
2833

2934
import javax.annotation.Nonnull;
3035
import javax.annotation.Nullable;
36+
import java.util.Collection;
3137
import java.util.List;
38+
import java.util.Map;
39+
import java.util.Optional;
3240
import java.util.stream.Collectors;
41+
import java.util.stream.IntStream;
3342

3443
/**
3544
* Main interface for defining all functions that can be evaluated against a number of arguments.
3645
* Two major sub interfaces inherit this interface: {@link BuiltInFunction} and {@link UserDefinedFunction}
37-
* {@link BuiltInFunction} represents all functions that are built-in, and stored in code, while {@link UserDefinedFunction} represents all functions defined by users, and stored in {@link com.apple.foundationdb.record.RecordMetaDataProto.MetaData}
46+
* {@link BuiltInFunction} represents all functions that are built-in, and stored in code, while
47+
* {@link UserDefinedFunction} represents all functions defined by users, and stored in
48+
* {@link com.apple.foundationdb.record.RecordMetaDataProto.MetaData}
3849
*/
3950
@API(API.Status.EXPERIMENTAL)
4051
public abstract class CatalogedFunction {
@@ -44,18 +55,39 @@ public abstract class CatalogedFunction {
4455
@Nonnull
4556
protected final List<Type> parameterTypes;
4657

58+
@Nonnull
59+
protected final BiMap<String, Integer> parameterNamesMap;
60+
61+
@Nonnull
62+
protected final List<Optional<? extends Typed>> parameterDefaults;
63+
4764
/**
4865
* The type of the function's variadic parameters (if any).
4966
*/
5067
@Nullable
5168
private final Type variadicSuffixType;
5269

53-
protected CatalogedFunction(@Nonnull final String functionName, @Nonnull final List<Type> parameterTypes, @Nullable final Type variadicSuffixType) {
70+
protected CatalogedFunction(@Nonnull final String functionName, @Nonnull final List<Type> parameterTypes,
71+
@Nullable final Type variadicSuffixType) {
5472
this.functionName = functionName;
5573
this.parameterTypes = ImmutableList.copyOf(parameterTypes);
74+
this.parameterDefaults = ImmutableList.of();
75+
this.parameterNamesMap = ImmutableBiMap.of();
5676
this.variadicSuffixType = variadicSuffixType;
5777
}
5878

79+
protected CatalogedFunction(@Nonnull final String functionName, @Nonnull final List<String> parameterNames,
80+
@Nonnull final List<Type> parameterTypes,
81+
@Nonnull final List<Optional<? extends Typed>> parameterDefaults) {
82+
Verify.verify(parameterNames.size() == parameterTypes.size());
83+
this.functionName = functionName;
84+
this.parameterTypes = ImmutableList.copyOf(parameterTypes);
85+
this.parameterNamesMap = IntStream.range(0, parameterNames.size()).boxed().collect(ImmutableBiMap.toImmutableBiMap(parameterNames::get,
86+
Functions.identity()));
87+
this.parameterDefaults = ImmutableList.copyOf(parameterDefaults);
88+
this.variadicSuffixType = null; // no support yet with named parameters.
89+
}
90+
5991
@Nonnull
6092
public String getFunctionName() {
6193
return functionName;
@@ -66,6 +98,20 @@ public List<Type> getParameterTypes() {
6698
return parameterTypes;
6799
}
68100

101+
public boolean hasNamedParameters() {
102+
return !parameterNamesMap.isEmpty();
103+
}
104+
105+
@Nonnull
106+
public String getParameterName(int index) {
107+
return parameterNamesMap.inverse().get(index);
108+
}
109+
110+
@Nonnull
111+
public Collection<String> getParameterNames() {
112+
return parameterNamesMap.keySet();
113+
}
114+
69115
@Nullable
70116
public Type getVariadicSuffixType() {
71117
return variadicSuffixType;
@@ -76,7 +122,7 @@ public boolean hasVariadicSuffix() {
76122
}
77123

78124
@Nonnull
79-
public Type resolveParameterType(int index) {
125+
public Type conputeParameterType(int index) {
80126
Verify.verify(index >= 0, "unexpected negative parameter index");
81127
if (index < parameterTypes.size()) {
82128
return parameterTypes.get(index);
@@ -89,15 +135,69 @@ public Type resolveParameterType(int index) {
89135
}
90136

91137
@Nonnull
92-
public List<Type> resolveParameterTypes(int numberOfArguments) {
138+
public Type conputeParameterType(@Nonnull final String parameterName) {
139+
return conputeParameterType(getParamIndex(parameterName));
140+
}
141+
142+
@Nonnull
143+
public List<Type> getParameterTypes(int numberOfArguments) {
93144
Verify.verify(numberOfArguments > 0, "unexpected number of arguments");
94145
final ImmutableList.Builder<Type> resultBuilder = ImmutableList.builder();
95146
for (int i = 0; i < numberOfArguments; i ++) {
96-
resultBuilder.add(resolveParameterType(i));
147+
resultBuilder.add(conputeParameterType(i));
97148
}
98149
return resultBuilder.build();
99150
}
100151

152+
public int getParamIndex(@Nonnull final String parameter) {
153+
return parameterNamesMap.get(parameter);
154+
}
155+
156+
public Optional<? extends Typed> getDefaultValue(@Nonnull final String paramName) {
157+
return parameterDefaults.get(getParamIndex(paramName));
158+
}
159+
160+
public Optional<? extends Typed> getDefaultValue(int paramIndex) {
161+
return parameterDefaults.get(paramIndex);
162+
}
163+
164+
public boolean hasDefaultValue(@Nonnull final String paramName) {
165+
return getDefaultValue(paramName).isPresent();
166+
}
167+
168+
public boolean hasDefaultValue(int paramIndex) {
169+
return getDefaultValue(paramIndex).isPresent();
170+
}
171+
172+
/**
173+
* Validates the function invocation using named arguments. It checks whether the argument names match parameter
174+
* names, and that any missing parameter has a default value. It effectively leaves all the work related to handling
175+
* permisslbe implicit promotions to the actual function invocation.
176+
* @param namedArgumentsTypeMap a list of named arguments.
177+
* @return if the arguments type match, an {@link Optional} containing <code>this</code> instance, otherwise
178+
* and empty {@link Optional}.
179+
*/
180+
@Nonnull
181+
public Optional<? extends CatalogedFunction> validateCall(@Nonnull final Map<String, ? extends Typed> namedArgumentsTypeMap) {
182+
if (parameterNamesMap.isEmpty()) {
183+
return Optional.empty();
184+
}
185+
final var argumentNames = namedArgumentsTypeMap.keySet();
186+
final var parameterNames = parameterNamesMap.keySet();
187+
final var unknownArgs = Sets.difference(argumentNames, parameterNames);
188+
if (!unknownArgs.isEmpty()) {
189+
return Optional.empty();
190+
}
191+
final var missingParams = Sets.difference(parameterNames, argumentNames);
192+
for (final var missingParam : missingParams) {
193+
if (!hasDefaultValue(missingParam)) {
194+
return Optional.empty();
195+
}
196+
}
197+
return Optional.of(this);
198+
}
199+
200+
@SuppressWarnings("UnstableApiUsage")
101201
@Nonnull
102202
@Override
103203
public String toString() {
@@ -109,9 +209,18 @@ public String toString() {
109209
}
110210
}
111211

212+
final var parameterNames = parameterNamesMap.keySet();
213+
if (!parameterNamesMap.isEmpty()) {
214+
return functionName + "(" + Streams.zip(parameterNames.stream(), parameterTypes.stream(),
215+
(name, type) -> name + " -> " + type).collect(Collectors.joining(",")) + variadicSuffixString + ")";
216+
}
217+
112218
return functionName + "(" + parameterTypes.stream().map(Object::toString).collect(Collectors.joining(",")) + variadicSuffixString + ")";
113219
}
114220

115221
@Nonnull
116222
public abstract Typed encapsulate(@Nonnull List<? extends Typed> arguments);
223+
224+
@Nonnull
225+
public abstract Typed encapsulate(@Nonnull Map<String, ? extends Typed> namedArguments);
117226
}

fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/MacroFunction.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222

2323
import com.apple.foundationdb.record.PlanDeserializer;
2424
import com.apple.foundationdb.record.PlanSerializationContext;
25+
import com.apple.foundationdb.record.RecordCoreException;
2526
import com.apple.foundationdb.record.RecordMetaDataProto;
2627
import com.apple.foundationdb.record.planprotos.PMacroFunctionValue;
2728
import com.apple.foundationdb.record.query.plan.cascades.typing.Typed;
@@ -32,6 +33,7 @@
3233

3334
import javax.annotation.Nonnull;
3435
import java.util.List;
36+
import java.util.Map;
3537
import java.util.stream.Collectors;
3638

3739
/**
@@ -78,6 +80,13 @@ public RecordMetaDataProto.PUserDefinedFunction toProto(@Nonnull PlanSerializati
7880
.build();
7981
}
8082

83+
84+
@Nonnull
85+
@Override
86+
public Typed encapsulate(@Nonnull final Map<String, ? extends Typed> namedArguments) {
87+
throw new RecordCoreException("macro functions do not support named argument calling conventions");
88+
}
89+
8190
@Nonnull
8291
public static MacroFunction fromProto(@Nonnull final PlanSerializationContext serializationContext, @Nonnull final PMacroFunctionValue functionValue) {
8392
return new MacroFunction(

0 commit comments

Comments
 (0)