Skip to content

Commit a1807e4

Browse files
committed
feat(core,isthmus): add DynamicParameter expression support
Implement full support for Substrait DynamicParameter expressions, enabling parameterized placeholders in plan bodies instead of embedded literals. This maps bidirectionally to Calcite's RexDynamicParam (JDBC ? bind parameters). Changes: - Add Expression.DynamicParameter POJO with type and parameterReference - Wire visitor pattern across all expression visitors - Add proto conversion (POJO<->Proto) in both directions - Add Calcite conversion (RexDynamicParam<->DynamicParameter) - Replace UnsupportedOperationException in visitDynamicParam - Add debug stringification in ExpressionStringify - Add 20 tests covering proto roundtrips, Calcite conversions, and full end-to-end roundtrips
1 parent 2370661 commit a1807e4

12 files changed

Lines changed: 510 additions & 1 deletion

File tree

core/src/main/java/io/substrait/expression/AbstractExpressionVisitor.java

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -603,4 +603,17 @@ public O visit(Expression.ScalarSubquery expr, C context) throws E {
603603
public O visit(Expression.InPredicate expr, C context) throws E {
604604
return visitFallback(expr, context);
605605
}
606+
607+
/**
608+
* Visits a dynamic parameter expression.
609+
*
610+
* @param expr the dynamic parameter
611+
* @param context the visitation context
612+
* @return the visit result
613+
* @throws E if visitation fails
614+
*/
615+
@Override
616+
public O visit(Expression.DynamicParameter expr, C context) throws E {
617+
return visitFallback(expr, context);
618+
}
606619
}

core/src/main/java/io/substrait/expression/Expression.java

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1250,6 +1250,28 @@ public <R, C extends VisitationContext, E extends Throwable> R accept(
12501250
}
12511251
}
12521252

1253+
@Value.Immutable
1254+
abstract class DynamicParameter implements Expression {
1255+
public abstract Type type();
1256+
1257+
public abstract int parameterReference();
1258+
1259+
@Override
1260+
public Type getType() {
1261+
return type();
1262+
}
1263+
1264+
public static ImmutableExpression.DynamicParameter.Builder builder() {
1265+
return ImmutableExpression.DynamicParameter.builder();
1266+
}
1267+
1268+
@Override
1269+
public <R, C extends VisitationContext, E extends Throwable> R accept(
1270+
ExpressionVisitor<R, C, E> visitor, C context) throws E {
1271+
return visitor.visit(this, context);
1272+
}
1273+
}
1274+
12531275
enum PredicateOp {
12541276
PREDICATE_OP_UNSPECIFIED(
12551277
io.substrait.proto.Expression.Subquery.SetPredicate.PredicateOp.PREDICATE_OP_UNSPECIFIED),

core/src/main/java/io/substrait/expression/ExpressionVisitor.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -470,4 +470,14 @@ public interface ExpressionVisitor<R, C extends VisitationContext, E extends Thr
470470
* @throws E on visit failure
471471
*/
472472
R visit(Expression.InPredicate expr, C context) throws E;
473+
474+
/**
475+
* Visit a dynamic parameter expression.
476+
*
477+
* @param expr the dynamic parameter expression
478+
* @param context visitation context
479+
* @return visit result
480+
* @throws E on visit failure
481+
*/
482+
R visit(Expression.DynamicParameter expr, C context) throws E;
473483
}

core/src/main/java/io/substrait/expression/proto/ExpressionProtoConverter.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -733,6 +733,18 @@ public Expression visit(
733733
.build();
734734
}
735735

736+
@Override
737+
public Expression visit(
738+
io.substrait.expression.Expression.DynamicParameter expr, EmptyVisitationContext context)
739+
throws RuntimeException {
740+
return Expression.newBuilder()
741+
.setDynamicParameter(
742+
io.substrait.proto.DynamicParameter.newBuilder()
743+
.setType(toProto(expr.type()))
744+
.setParameterReference(expr.parameterReference()))
745+
.build();
746+
}
747+
736748
public static class BoundConverter
737749
implements WindowBound.WindowBoundVisitor<Expression.WindowFunction.Bound, RuntimeException> {
738750
private static final BoundConverter TO_BOUND_VISITOR = new BoundConverter();

core/src/main/java/io/substrait/expression/proto/ProtoExpressionConverter.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -289,6 +289,14 @@ public Type visit(Type.Struct type) throws RuntimeException {
289289

290290
return lambdaBuilder.lambdaFromStruct(parameters, () -> from(protoLambda.getBody()));
291291
}
292+
case DYNAMIC_PARAMETER:
293+
{
294+
io.substrait.proto.DynamicParameter dp = expr.getDynamicParameter();
295+
return Expression.DynamicParameter.builder()
296+
.type(protoTypeConverter.from(dp.getType()))
297+
.parameterReference(dp.getParameterReference())
298+
.build();
299+
}
292300
// TODO enum.
293301
case ENUM:
294302
throw new UnsupportedOperationException("Unsupported type: " + expr.getRexTypeCase());

core/src/main/java/io/substrait/relation/ExpressionCopyOnWriteVisitor.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -455,6 +455,12 @@ public Optional<Expression> visit(Expression.Lambda lambda, EmptyVisitationConte
455455
.build());
456456
}
457457

458+
@Override
459+
public Optional<Expression> visit(
460+
Expression.DynamicParameter expr, EmptyVisitationContext context) throws E {
461+
return Optional.empty();
462+
}
463+
458464
// utilities
459465

460466
protected Optional<List<Expression>> visitExprList(
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
package io.substrait.type.proto;
2+
3+
import static org.junit.jupiter.api.Assertions.assertEquals;
4+
5+
import io.substrait.TestBase;
6+
import io.substrait.expression.Expression;
7+
import io.substrait.type.TypeCreator;
8+
import org.junit.jupiter.api.Test;
9+
10+
class DynamicParameterRoundtripTest extends TestBase {
11+
12+
@Test
13+
void dynamicParameterI64() {
14+
Expression.DynamicParameter dp =
15+
Expression.DynamicParameter.builder()
16+
.type(TypeCreator.REQUIRED.I64)
17+
.parameterReference(0)
18+
.build();
19+
20+
assertEquals(TypeCreator.REQUIRED.I64, dp.getType());
21+
verifyRoundTrip(dp);
22+
}
23+
24+
@Test
25+
void dynamicParameterNullableString() {
26+
Expression.DynamicParameter dp =
27+
Expression.DynamicParameter.builder()
28+
.type(TypeCreator.NULLABLE.STRING)
29+
.parameterReference(1)
30+
.build();
31+
32+
assertEquals(TypeCreator.NULLABLE.STRING, dp.getType());
33+
verifyRoundTrip(dp);
34+
}
35+
36+
@Test
37+
void dynamicParameterFP64() {
38+
Expression.DynamicParameter dp =
39+
Expression.DynamicParameter.builder()
40+
.type(TypeCreator.REQUIRED.FP64)
41+
.parameterReference(2)
42+
.build();
43+
44+
assertEquals(TypeCreator.REQUIRED.FP64, dp.getType());
45+
verifyRoundTrip(dp);
46+
}
47+
48+
@Test
49+
void dynamicParameterI32Nullable() {
50+
Expression.DynamicParameter dp =
51+
Expression.DynamicParameter.builder()
52+
.type(TypeCreator.NULLABLE.I32)
53+
.parameterReference(42)
54+
.build();
55+
56+
assertEquals(42, dp.parameterReference());
57+
verifyRoundTrip(dp);
58+
}
59+
60+
@Test
61+
void dynamicParameterDate() {
62+
Expression.DynamicParameter dp =
63+
Expression.DynamicParameter.builder()
64+
.type(TypeCreator.REQUIRED.DATE)
65+
.parameterReference(3)
66+
.build();
67+
68+
assertEquals(TypeCreator.REQUIRED.DATE, dp.getType());
69+
verifyRoundTrip(dp);
70+
}
71+
72+
@Test
73+
void dynamicParameterBoolean() {
74+
Expression.DynamicParameter dp =
75+
Expression.DynamicParameter.builder()
76+
.type(TypeCreator.REQUIRED.BOOLEAN)
77+
.parameterReference(0)
78+
.build();
79+
80+
assertEquals(TypeCreator.REQUIRED.BOOLEAN, dp.getType());
81+
verifyRoundTrip(dp);
82+
}
83+
84+
@Test
85+
void dynamicParameterDecimal() {
86+
Expression.DynamicParameter dp =
87+
Expression.DynamicParameter.builder()
88+
.type(TypeCreator.REQUIRED.decimal(10, 2))
89+
.parameterReference(5)
90+
.build();
91+
92+
assertEquals(TypeCreator.REQUIRED.decimal(10, 2), dp.getType());
93+
verifyRoundTrip(dp);
94+
}
95+
96+
@Test
97+
void dynamicParameterTimestamp() {
98+
Expression.DynamicParameter dp =
99+
Expression.DynamicParameter.builder()
100+
.type(TypeCreator.NULLABLE.TIMESTAMP)
101+
.parameterReference(7)
102+
.build();
103+
104+
assertEquals(TypeCreator.NULLABLE.TIMESTAMP, dp.getType());
105+
verifyRoundTrip(dp);
106+
}
107+
}

examples/substrait-spark/src/main/java/io/substrait/examples/util/ExpressionStringify.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -363,4 +363,10 @@ public String visit(EmptyMapLiteral expr, EmptyVisitationContext context)
363363
throws RuntimeException {
364364
return "<EmptyMapLiteral>";
365365
}
366+
367+
@Override
368+
public String visit(Expression.DynamicParameter expr, EmptyVisitationContext context)
369+
throws RuntimeException {
370+
return "<DynamicParameter " + expr.parameterReference() + " " + expr.type() + ">";
371+
}
366372
}

isthmus/src/main/java/io/substrait/isthmus/expression/ExpressionRexConverter.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -824,6 +824,12 @@ public RexNode visit(SetPredicate expr, Context context) throws RuntimeException
824824
}
825825
}
826826

827+
@Override
828+
public RexNode visit(Expression.DynamicParameter expr, Context context) throws RuntimeException {
829+
RelDataType calciteType = typeConverter.toCalcite(typeFactory, expr.type());
830+
return rexBuilder.makeDynamicParam(calciteType, expr.parameterReference());
831+
}
832+
827833
/**
828834
* Helper method to create a Calcite ROW expression for encoding UDT struct literals.
829835
*

isthmus/src/main/java/io/substrait/isthmus/expression/RexExpressionConverter.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,10 @@ public Expression visitCorrelVariable(RexCorrelVariable correlVariable) {
118118

119119
@Override
120120
public Expression visitDynamicParam(RexDynamicParam dynamicParam) {
121-
throw new UnsupportedOperationException("RexDynamicParam not supported");
121+
return Expression.DynamicParameter.builder()
122+
.type(typeConverter.toSubstrait(dynamicParam.getType()))
123+
.parameterReference(dynamicParam.getIndex())
124+
.build();
122125
}
123126

124127
@Override

0 commit comments

Comments
 (0)