Skip to content

Commit 11fa472

Browse files
committed
feat: add support for unresolved expressions
1 parent 99ba9d1 commit 11fa472

33 files changed

Lines changed: 2238 additions & 1742 deletions

dialects/tests/expressions_test.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ supported_expressions:
77
- LITERAL
88
- SELECTION
99
- SCALAR_FUNCTION
10+
- expression: NAMED_EXPRESSION
1011
- expression: CAST
1112
metadata:
1213
truncates_on_overflow: true

dialects/tests/types_test.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ dependencies:
99
supported_types:
1010
- I8
1111
- I16
12+
- UNKNOWN
1213
- type: I32
1314
metadata:
1415
storage_bytes: 4

grammar/FuncTestCaseParser.g4

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -242,6 +242,7 @@ scalarType
242242
| dateType #date
243243
| intervalYearType #intervalYear
244244
| UUID isnull=QMark? #uuid
245+
| Unknown isnull=QMark? #unknown
245246
| UserDefined Identifier isnull=QMark? #userDefined
246247
;
247248

grammar/SubstraitLexer.g4

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,8 @@ Number
116116
: '-'? Int
117117
;
118118

119+
Unknown: 'UNKNOWN';
120+
119121
Identifier
120122
: ('A'..'Z' | '_' | '$') ('A'..'Z' | '_' | '$' | Digit)*
121123
;

grammar/SubstraitType.g4

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ scalarType
2323
| Date #date
2424
| Interval_Year #intervalYear
2525
| UUID #uuid
26+
| Unknown #unknown
2627
;
2728

2829
parameterizedType

proto/substrait/algebra.proto

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1019,6 +1019,7 @@ message Expression {
10191019
Lambda lambda = 15;
10201020
LambdaInvocation lambda_invocation = 16;
10211021
ExecutionContextVariable execution_context_variable = 17;
1022+
NamedExpression named_expression = 18;
10221023

10231024
// deprecated: enum literals are only sensible in the context of
10241025
// function arguments, for which FunctionArgument should now be
@@ -1254,6 +1255,17 @@ message Expression {
12541255
Nested.Struct arguments = 2;
12551256
}
12561257

1258+
// A named expression that is not yet resolved to a positional reference or
1259+
// concrete expression. The names field is used to represent namespacing
1260+
// (e.g. qualifier and field name). Producers and consumers must share enough
1261+
// external context to resolve this expression before execution unless the
1262+
// consumer defines semantics for unresolved named expressions. Until
1263+
// resolved, a NamedExpression's type is Type.Unknown.
1264+
message NamedExpression {
1265+
repeated string names = 1;
1266+
substrait.extensions.AdvancedExtension advanced_extension = 10;
1267+
}
1268+
12571269
// A scalar function call.
12581270
message ScalarFunction {
12591271
// Points to a function_anchor defined in this plan, which must refer
@@ -1271,6 +1283,9 @@ message Expression {
12711283
// - Enum arguments must be bound using FunctionArgument.enum
12721284
// followed by Enum.specified, with a string that case-insensitively
12731285
// matches one of the allowed options.
1286+
//
1287+
// In a partially bound expression, Type.Unknown may stand in for any
1288+
// concrete type while binding value or type arguments.
12741289
repeated FunctionArgument arguments = 4;
12751290

12761291
// Options to specify behavior for corner cases, or leave behavior
@@ -1279,7 +1294,8 @@ message Expression {
12791294
repeated FunctionOption options = 5;
12801295

12811296
// Must be set to the return type of the function, exactly as derived
1282-
// using the declaration in the extension.
1297+
// using the declaration in the extension. In a partially bound expression,
1298+
// this may be Type.Unknown if the concrete return type is unresolved.
12831299
Type output_type = 3;
12841300

12851301
// Deprecated; use arguments instead.
@@ -1310,6 +1326,9 @@ message Expression {
13101326
// - Enum arguments must be bound using FunctionArgument.enum
13111327
// followed by Enum.specified, with a string that case-insensitively
13121328
// matches one of the allowed options.
1329+
//
1330+
// In a partially bound expression, Type.Unknown may stand in for any
1331+
// concrete type while binding value or type arguments.
13131332
repeated FunctionArgument arguments = 9;
13141333

13151334
// Options to specify behavior for corner cases, or leave behavior
@@ -1318,7 +1337,8 @@ message Expression {
13181337
repeated FunctionOption options = 11;
13191338

13201339
// Must be set to the return type of the function, exactly as derived
1321-
// using the declaration in the extension.
1340+
// using the declaration in the extension. In a partially bound expression,
1341+
// this may be Type.Unknown if the concrete return type is unresolved.
13221342
Type output_type = 7;
13231343

13241344
// Describes which part of the window function to perform within the
@@ -1839,6 +1859,9 @@ message AggregateFunction {
18391859
// - Optional enum arguments must be bound using FunctionArgument.enum
18401860
// followed by either Enum.specified or Enum.unspecified. If specified,
18411861
// the string must case-insensitively match one of the allowed options.
1862+
//
1863+
// In a partially bound expression, Type.Unknown may stand in for any
1864+
// concrete type while binding value or type arguments.
18421865
repeated FunctionArgument arguments = 7;
18431866

18441867
// Options to specify behavior for corner cases, or leave behavior
@@ -1847,7 +1870,8 @@ message AggregateFunction {
18471870
repeated FunctionOption options = 8;
18481871

18491872
// Must be set to the return type of the function, exactly as derived
1850-
// using the declaration in the extension.
1873+
// using the declaration in the extension. In a partially bound expression,
1874+
// this may be Type.Unknown if the concrete return type is unresolved.
18511875
Type output_type = 5;
18521876

18531877
// Describes which part of the aggregation to perform within the context of

proto/substrait/type.proto

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,13 @@ message Type {
4646
Map map = 28;
4747
Func func = 38;
4848

49+
// A placeholder type whose concrete type has not yet been resolved.
50+
// Unknown may be used where a concrete type would normally appear in
51+
// partially bound plans and expressions, and must be resolved to a
52+
// concrete type before execution unless the consumer defines semantics for
53+
// unknown-typed values.
54+
Unknown unknown = 39;
55+
4956
UserDefined user_defined = 30;
5057

5158
// Deprecated in favor of user_defined, which allows nullability and
@@ -225,6 +232,12 @@ message Type {
225232
Nullability nullability = 3;
226233
}
227234

235+
message Unknown {
236+
// Optional nullability constraint for this unknown type. If unspecified,
237+
// both the concrete type and its nullability are unresolved.
238+
Nullability nullability = 1;
239+
}
240+
228241
message UserDefined {
229242
// References a type_anchor defined in the plan's extension declarations.
230243
uint32 type_reference = 1;

site/docs/expressions/_config

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
arrange:
22
- field_references.md
3+
- unbound_expressions.md
34
- scalar_functions.md
45
- aggregate_functions.md
56
- specialized_record_expressions.md

site/docs/expressions/extended_expression.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,3 +23,11 @@ For a message with multiple expressions, users may produce each Extended Express
2323
## Function extensions
2424

2525
Function extensions work the same for both Extended Expression and the original Expression defined in the Substrait protocol.
26+
27+
## Partially Bound Expressions
28+
29+
Extended Expression can also carry partially bound expressions for producers that do not yet know the full input schema. In this form, field-like references can be represented as `NamedExpression`, and any known-but-unresolved schema or expression types can use `unknown`. A consumer must bind these names and types before execution unless it defines its own unresolved-expression semantics.
30+
31+
```protobuf
32+
--8<-- "examples/proto-textformat/extended_expression/unbound_named_projection.textproto"
33+
```
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
# Unbound Expressions
2+
3+
Substrait normally represents bound relational expressions: field references are positional, value types are known, and function invocations identify implementations whose argument and return types have been derived. Some producers need to serialize expressions earlier in planning, before names and types have been resolved.
4+
5+
An expression tree is partially bound when it contains either an [`unknown`](../types/type_classes.md#unknown-type) type or a `NamedExpression`. Consumers may validate and transform partially bound expressions, but must resolve them before execution unless they define their own semantics for unresolved names or unknown-typed values.
6+
7+
This applies anywhere Substrait uses `Expression`, including both full `Plan` messages and `ExtendedExpression`. The examples below use `ExtendedExpression` because expression-only filter / projection APIs are a common motivating use case, not because partially bound expressions are limited to that entry point.
8+
9+
## Detecting Partial Binding
10+
11+
There is no separate "unbound expression" message. Instead, partially bound state is detected structurally:
12+
13+
- If any expression, function argument, or schema field type is `unknown`, the expression is partially bound.
14+
- If any expression contains `NamedExpression`, the expression is partially bound.
15+
16+
This is the canonical way to distinguish fully bound expressions from partially bound expressions in Substrait.
17+
18+
## Unknown Type
19+
20+
The `unknown` type marks an expression whose concrete type is not known yet. It may be used anywhere a concrete type would normally be expected in a partially bound function call. If only the nullability is known, set the nullability field; otherwise leave it unspecified.
21+
22+
## Named Expression
23+
24+
`NamedExpression` represents a reference by name instead of ordinal position. The `names` field stores one or more namespace components, such as `["foo"]` for an unqualified name or `["orders", "amount"]` for a qualified name. Until resolved, a named expression's type is `unknown`. Resolution of these components is intentionally external to Substrait and must be understood by both producer and consumer.
25+
26+
```protobuf
27+
--8<-- "examples/proto-textformat/unbound_expression/named_expression.textproto"
28+
```
29+
30+
## Function Example
31+
32+
Partially bound function calls can use named expressions as value arguments and `unknown` as the output type when the return type cannot be derived yet.
33+
34+
```protobuf
35+
--8<-- "examples/proto-textformat/unbound_expression/scalar_function_unknown.textproto"
36+
```
37+
38+
## Extended Expression Protocols
39+
40+
Expression-level APIs, such as filters and projections exchanged outside a full `Plan`, should use `ExtendedExpression`. This lets the producer include output names, any known input names, and extension declarations next to the expression tree.
41+
42+
If the input names are known but their types are not, `base_schema` can contain fields with `unknown` types. If the function overload is also unresolved, the function can refer to the `extension:io.substrait:unknown` extension until a downstream binder replaces it with a concrete function reference and concrete output type. In that case, the referenced function name should use the normal Substrait function-signature form with `unknown` short names, such as `add:unknown_unknown`.
43+
44+
```protobuf
45+
--8<-- "examples/proto-textformat/extended_expression/unbound_named_projection.textproto"
46+
```
47+
48+
Consumers that execute expressions must reject or bind away all `NamedExpression` and `unknown` types before execution unless they explicitly support unresolved semantics. A typical binder resolves `NamedExpression` values to `FieldReference`, replaces `unknown` input and output types with concrete types, and updates unresolved function references to concrete overloads.
49+
50+
## Plans, ReadRel, and Dynamic Parameters
51+
52+
Partially bound expressions may also appear inside a `Plan`. In that case, relational operators such as `ReadRel` still provide the surrounding schema context. If field names are known, `ReadRel.base_schema` remains the source of those names even before ordinal binding has happened. A downstream binder can use that schema to resolve `NamedExpression` values into positional `FieldReference`s. If names are known but types are not, `base_schema` can use `unknown` types until type resolution completes.
53+
54+
`NamedExpression` is intentionally distinct from `DynamicParameter`. A `DynamicParameter` represents an externally supplied literal value, identified by `parameter_anchor` and resolved through `DynamicParameterBinding`. A `NamedExpression` represents an unresolved field-like or catalog-like reference that is expected to bind against schema or catalog context. Because these are different binding problems, this proposal keeps `DynamicParameterBinding` and `NamedExpression` separate.

0 commit comments

Comments
 (0)