Skip to content

Commit 0ab85ce

Browse files
authored
Allow CASE alternative implicit type promotions when feasible. (#3379)
Previously, the `CASE` runtime used to reject alternatives with different types even if they have a common max-type. This PR address this by implementing an encapsulator that is capable of generating a `CASE` expression callsite that with implicit promotions applied to the alternatives when necessary. As a result a query like this now works: ```sql create table A(A1 bigint, A2 bigint, A3 bigint, primary key(A1)); insert into A values (1, 10, 10), (2, 11, 20), (3, 12, 30); select a3, case when a3 > 15 then 10 else 3.14 end from A; 10, 3.14 20, 10.0 30, 10.0 ``` (note how `10` is promoted to `10.0` in the result set). This fixes #3378.
1 parent 929fcd7 commit 0ab85ce

File tree

5 files changed

+51
-7
lines changed

5 files changed

+51
-7
lines changed

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

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,6 @@ public JavaCallFunction() {
4444
super("java_call", List.of(Type.primitiveType(Type.TypeCode.STRING)), new Type.Any(), JavaCallFunction::findFunction);
4545
}
4646

47-
@SuppressWarnings({"DataFlowIssue"})
4847
@Nonnull
4948
private static Value findFunction(@Nonnull final BuiltInFunction<Value> ignored, final List<? extends Typed> arguments) {
5049
Verify.verify(!arguments.isEmpty());

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

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@
3131
import com.apple.foundationdb.record.planprotos.PValue;
3232
import com.apple.foundationdb.record.provider.foundationdb.FDBRecordStoreBase;
3333
import com.apple.foundationdb.record.query.plan.cascades.AliasMap;
34+
import com.apple.foundationdb.record.query.plan.cascades.BuiltInFunction;
35+
import com.apple.foundationdb.record.query.plan.cascades.typing.Typed;
3436
import com.apple.foundationdb.record.query.plan.explain.ExplainTokens;
3537
import com.apple.foundationdb.record.query.plan.explain.ExplainTokensWithPrecedence;
3638
import com.apple.foundationdb.record.query.plan.cascades.SemanticException;
@@ -218,4 +220,37 @@ public PickValue fromProto(@Nonnull final PlanSerializationContext serialization
218220
return PickValue.fromProto(serializationContext, pickValueProto);
219221
}
220222
}
223+
224+
/**
225+
* The {@code pick} function.
226+
*/
227+
@AutoService(BuiltInFunction.class)
228+
public static class PickValueFn extends BuiltInFunction<Value> {
229+
public PickValueFn() {
230+
super("pick", List.of(Type.primitiveType(Type.TypeCode.INT), Type.any()), Type.any(), PickValueFn::encapsulate);
231+
}
232+
233+
@SuppressWarnings("PMD.UnusedFormalParameter")
234+
private static Value encapsulate(@Nonnull BuiltInFunction<Value> ignored,
235+
@Nonnull final List<? extends Typed> arguments) {
236+
Verify.verify(arguments.size() > 1);
237+
var selectorValue = (Value)arguments.get(0);
238+
final var selectorMaxType = Type.maximumType(selectorValue.getResultType(), Type.primitiveType(Type.TypeCode.INT));
239+
SemanticException.check(selectorMaxType != null, SemanticException.ErrorCode.INCOMPATIBLE_TYPE);
240+
selectorValue = PromoteValue.inject(selectorValue, selectorMaxType);
241+
242+
final var firstAlternative = (Value)arguments.get(1);
243+
var alternativesMaxType = firstAlternative.getResultType();
244+
for (int i = 2; i < arguments.size(); i++) {
245+
alternativesMaxType = Type.maximumType(alternativesMaxType, arguments.get(i).getResultType());
246+
SemanticException.check(alternativesMaxType != null, SemanticException.ErrorCode.INCOMPATIBLE_TYPE);
247+
}
248+
249+
final var alternativesList = ImmutableList.<Value>builder();
250+
for (int i = 1; i < arguments.size(); i++) {
251+
alternativesList.add(PromoteValue.inject((Value)arguments.get(i), alternativesMaxType));
252+
}
253+
return new PickValue(selectorValue, alternativesList.build());
254+
}
255+
}
221256
}

fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/query/functions/SqlFunctionCatalogImpl.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,7 @@ private static ImmutableMap<String, Function<Integer, Optional<BuiltInFunction<?
148148
.put("range", argumentsCount -> BuiltInFunctionCatalog.resolve("range", argumentsCount))
149149
.put("__pattern_for_like", argumentsCount -> BuiltInFunctionCatalog.resolve("patternForLike", argumentsCount))
150150
.put("__internal_array", argumentsCount -> BuiltInFunctionCatalog.resolve("array", argumentsCount))
151+
.put("__pick_value", argumentsCount -> BuiltInFunctionCatalog.resolve("pick", argumentsCount))
151152
.build();
152153
}
153154

fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/query/visitors/ExpressionVisitor.java

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,6 @@
3030
import com.apple.foundationdb.record.query.plan.cascades.values.FieldValue;
3131
import com.apple.foundationdb.record.query.plan.cascades.values.LiteralValue;
3232
import com.apple.foundationdb.record.query.plan.cascades.values.NullValue;
33-
import com.apple.foundationdb.record.query.plan.cascades.values.PickValue;
3433
import com.apple.foundationdb.record.query.plan.cascades.values.PromoteValue;
3534
import com.apple.foundationdb.record.query.plan.cascades.values.RecordConstructorValue;
3635
import com.apple.foundationdb.record.query.plan.cascades.values.Value;
@@ -311,20 +310,24 @@ public Expression visitScalarFunctionCall(@Nonnull RelationalParser.ScalarFuncti
311310
@Override
312311
public Expression visitCaseFunctionCall(@Nonnull RelationalParser.CaseFunctionCallContext ctx) {
313312
final ImmutableList.Builder<Value> implications = ImmutableList.builder();
314-
final ImmutableList.Builder<Value> pickerValues = ImmutableList.builder();
313+
final ImmutableList.Builder<Expression> pickerValues = ImmutableList.builder();
315314
for (final var caseAlternative : ctx.caseFuncAlternative()) {
316315
final var condition = visitFunctionArg(caseAlternative.condition);
317-
Assert.thatUnchecked(condition.getDataType().getCode().equals(DataType.Code.BOOLEAN));
316+
Assert.thatUnchecked(condition.getDataType().getCode().equals(DataType.Code.BOOLEAN), ErrorCode.DATATYPE_MISMATCH,
317+
"argument of case when must be of boolean type");
318318
final var consequent = visitFunctionArg(caseAlternative.consequent);
319319
implications.add(condition.getUnderlying());
320-
pickerValues.add(consequent.getUnderlying());
320+
pickerValues.add(consequent);
321321
}
322322
if (ctx.ELSE() != null) {
323323
implications.add(TautologicalValue.getInstance());
324324
final var defaultConsequent = visitFunctionArg(ctx.functionArg());
325-
pickerValues.add(defaultConsequent.getUnderlying());
325+
pickerValues.add(defaultConsequent);
326326
}
327-
return Expression.ofUnnamed(new PickValue(new ConditionSelectorValue(implications.build()), pickerValues.build()));
327+
final var arguments = ImmutableList.<Expression>builder();
328+
arguments.add(Expression.ofUnnamed(new ConditionSelectorValue(implications.build())));
329+
arguments.addAll(pickerValues.build());
330+
return getDelegate().resolveFunction("__pick_value", arguments.build().toArray(new Expression[0]));
328331
}
329332

330333
@Nonnull

yaml-tests/src/test/resources/case-when.yamsql

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,12 @@ test_block:
5353
- supported_version: !current_version
5454
- result: [ { B2: true, 'foo' },
5555
{ B2: false, 'bar' } ]
56+
-
57+
- query: select a3, case when a3 > 15 then 10 else 3.14 end from A;
58+
- supported_version: !current_version
59+
- result: [ { A3: 10, 3.14 },
60+
{ A3: 20, 10.0 },
61+
{ A3: 30, 10.0 } ]
5662
---
5763
setup:
5864
steps:

0 commit comments

Comments
 (0)