Skip to content

Commit ebbfe8a

Browse files
feat(isthmus): observe scalar function types
Assisted-by: gpt-5.6-sol (OpenAI Codex)
1 parent ca649b3 commit ebbfe8a

5 files changed

Lines changed: 447 additions & 2 deletions

File tree

isthmus/src/main/java/io/substrait/isthmus/ConverterProvider.java

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
import io.substrait.isthmus.expression.ScalarFunctionConverter;
1212
import io.substrait.isthmus.expression.SqlArrayValueConstructorCallConverter;
1313
import io.substrait.isthmus.expression.SqlMapValueConstructorCallConverter;
14+
import io.substrait.isthmus.expression.TypeObserver;
1415
import io.substrait.isthmus.expression.WindowFunctionConverter;
1516
import io.substrait.plan.ImmutableExecutionBehavior;
1617
import io.substrait.plan.Plan;
@@ -330,11 +331,23 @@ public ExpressionRexConverter getExpressionRexConverter(
330331
getTypeFactory(),
331332
getScalarFunctionConverter(),
332333
getWindowFunctionConverter(),
333-
getTypeConverter());
334+
getTypeConverter(),
335+
getTypeObserver());
334336
erc.setRelNodeConverter(relNodeConverter);
335337
return erc;
336338
}
337339

340+
/**
341+
* Returns the observer for supplied and independently inferred expression types.
342+
*
343+
* <p>Override to collect type observations during Substrait-to-Calcite conversion.
344+
*
345+
* @return a no-op observer by default
346+
*/
347+
public TypeObserver getTypeObserver() {
348+
return TypeObserver.NOOP;
349+
}
350+
338351
/**
339352
* A {@link RelBuilder} is a Calcite class used for creating {@link
340353
* org.apache.calcite.rel.RelNode}s.

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

Lines changed: 56 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,10 @@
2626
import java.math.BigDecimal;
2727
import java.util.Collections;
2828
import java.util.List;
29+
import java.util.Objects;
2930
import java.util.Set;
3031
import java.util.concurrent.TimeUnit;
32+
import java.util.function.Supplier;
3133
import java.util.stream.Collectors;
3234
import java.util.stream.IntStream;
3335
import java.util.stream.Stream;
@@ -95,6 +97,9 @@ public class ExpressionRexConverter
9597
/** Converter for Substrait window function invocations to Calcite {@link SqlOperator}s. */
9698
protected final WindowFunctionConverter windowFunctionConverter;
9799

100+
/** Observer for supplied and inferred expression types. */
101+
protected final TypeObserver typeObserver;
102+
98103
/** Converter for Substrait relational nodes to Calcite {@link RelNode}s, used for subqueries. */
99104
protected SubstraitRelNodeConverter relNodeConverter;
100105

@@ -113,11 +118,35 @@ public ExpressionRexConverter(
113118
ScalarFunctionConverter scalarFunctionConverter,
114119
WindowFunctionConverter windowFunctionConverter,
115120
TypeConverter typeConverter) {
121+
this(
122+
typeFactory,
123+
scalarFunctionConverter,
124+
windowFunctionConverter,
125+
typeConverter,
126+
TypeObserver.NOOP);
127+
}
128+
129+
/**
130+
* Creates an {@code ExpressionRexConverter} with type observation enabled.
131+
*
132+
* @param typeFactory Calcite type factory for type creation
133+
* @param scalarFunctionConverter converter for scalar function invocations
134+
* @param windowFunctionConverter converter for window function invocations
135+
* @param typeConverter converter for Substrait and Calcite type mappings
136+
* @param typeObserver observer for supplied and independently inferred expression types
137+
*/
138+
public ExpressionRexConverter(
139+
RelDataTypeFactory typeFactory,
140+
ScalarFunctionConverter scalarFunctionConverter,
141+
WindowFunctionConverter windowFunctionConverter,
142+
TypeConverter typeConverter,
143+
TypeObserver typeObserver) {
116144
this.typeFactory = typeFactory;
117145
this.typeConverter = typeConverter;
118146
this.rexBuilder = new RexBuilder(typeFactory);
119147
this.scalarFunctionConverter = scalarFunctionConverter;
120148
this.windowFunctionConverter = windowFunctionConverter;
149+
this.typeObserver = Objects.requireNonNull(typeObserver, "typeObserver");
121150
}
122151

123152
/**
@@ -519,7 +548,33 @@ public RexNode visit(Expression.ScalarFunctionInvocation expr, Context context)
519548
.collect(Collectors.toList());
520549

521550
RelDataType returnType = typeConverter.toCalcite(typeFactory, expr.outputType());
522-
return rexBuilder.makeCall(returnType, operator, args);
551+
RexNode rexCall = rexBuilder.makeCall(returnType, operator, args);
552+
// If type observations are not needed, avoid recomputing the RexCall with Calcite's
553+
// independently inferred return type.
554+
if (typeObserver == TypeObserver.NOOP) {
555+
return rexCall;
556+
}
557+
observeScalarType(expr, () -> rexBuilder.makeCall(operator, args));
558+
return rexCall;
559+
}
560+
561+
private void observeScalarType(
562+
Expression.ScalarFunctionInvocation expression, Supplier<RexNode> inferredCallSupplier) {
563+
TypeObservation observation;
564+
RexNode inferredCall;
565+
try {
566+
inferredCall = inferredCallSupplier.get();
567+
} catch (RuntimeException inferenceFailure) {
568+
observation =
569+
TypeObservation.failure(
570+
TypeObservation.Source.SCALAR_FUNCTION, expression, inferenceFailure);
571+
typeObserver.observe(observation);
572+
return;
573+
}
574+
observation =
575+
TypeObservation.success(
576+
TypeObservation.Source.SCALAR_FUNCTION, expression, inferredCall.getType());
577+
typeObserver.observe(observation);
523578
}
524579

525580
private String callConversionFailureMessage(
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
package io.substrait.isthmus.expression;
2+
3+
import io.substrait.expression.Expression;
4+
import io.substrait.type.Type;
5+
import java.util.Objects;
6+
import java.util.Optional;
7+
import org.apache.calcite.rel.type.RelDataType;
8+
9+
/**
10+
* The result of attempting to independently infer a Calcite type during Substrait-to-Calcite
11+
* expression conversion. Exactly one of {@link #inferredType()} and {@link #inferenceFailure()} is
12+
* present.
13+
*/
14+
public final class TypeObservation {
15+
/** The expression category that produced an observation. */
16+
public enum Source {
17+
/** A scalar function invocation. */
18+
SCALAR_FUNCTION
19+
}
20+
21+
private final Source source;
22+
private final Expression expression;
23+
private final RelDataType inferredType;
24+
private final RuntimeException inferenceFailure;
25+
26+
/**
27+
* Creates a successful type observation.
28+
*
29+
* @param source expression category that produced the observation
30+
* @param expression expression that produced the observation
31+
* @param inferredType type independently inferred by Calcite
32+
* @return a successful type observation
33+
*/
34+
static TypeObservation success(Source source, Expression expression, RelDataType inferredType) {
35+
return new TypeObservation(source, expression, inferredType, null);
36+
}
37+
38+
/**
39+
* Creates a failed type observation.
40+
*
41+
* @param source expression category that produced the observation
42+
* @param expression expression that produced the observation
43+
* @param inferenceFailure failure to independently infer a Calcite type
44+
* @return a failed type observation
45+
*/
46+
static TypeObservation failure(
47+
Source source, Expression expression, RuntimeException inferenceFailure) {
48+
return new TypeObservation(source, expression, null, inferenceFailure);
49+
}
50+
51+
private TypeObservation(
52+
Source source,
53+
Expression expression,
54+
RelDataType inferredType,
55+
RuntimeException inferenceFailure) {
56+
this.source = Objects.requireNonNull(source, "source");
57+
this.expression = Objects.requireNonNull(expression, "expression");
58+
if ((inferredType == null) == (inferenceFailure == null)) {
59+
throw new IllegalArgumentException(
60+
"Exactly one of inferredType and inferenceFailure must be present");
61+
}
62+
this.inferredType = inferredType;
63+
this.inferenceFailure = inferenceFailure;
64+
}
65+
66+
/**
67+
* Returns the expression category that produced this observation.
68+
*
69+
* @return the expression category
70+
*/
71+
public Source source() {
72+
return source;
73+
}
74+
75+
/**
76+
* Returns the expression that produced this observation.
77+
*
78+
* @return the observed expression
79+
*/
80+
public Expression expression() {
81+
return expression;
82+
}
83+
84+
/**
85+
* Returns the type supplied by Substrait.
86+
*
87+
* @return the supplied type
88+
*/
89+
public Type suppliedType() {
90+
return expression.getType();
91+
}
92+
93+
/**
94+
* Returns the type independently inferred by Calcite.
95+
*
96+
* @return the inferred type, or empty if inference failed
97+
*/
98+
public Optional<RelDataType> inferredType() {
99+
return Optional.ofNullable(inferredType);
100+
}
101+
102+
/**
103+
* Returns the Calcite inference failure.
104+
*
105+
* @return the inference failure, or empty if inference succeeded
106+
*/
107+
public Optional<RuntimeException> inferenceFailure() {
108+
return Optional.ofNullable(inferenceFailure);
109+
}
110+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package io.substrait.isthmus.expression;
2+
3+
/** Receives type observations while converting Substrait expressions to Calcite. */
4+
@FunctionalInterface
5+
public interface TypeObserver {
6+
/** Observer that disables type inference and discards all observations. */
7+
TypeObserver NOOP = observation -> {};
8+
9+
/**
10+
* Receives the result of attempting to observe an expression's inferred type.
11+
*
12+
* <p>Exceptions thrown by an observer are propagated to the conversion caller.
13+
*
14+
* @param observation supplied type and either an inferred type or inference failure
15+
*/
16+
void observe(TypeObservation observation);
17+
}

0 commit comments

Comments
 (0)