-
Notifications
You must be signed in to change notification settings - Fork 192
feat: introduce GenerateRel for lateral view and unnest operations #917
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
44ad7bf
afdef63
28d923c
4d5ab68
1d8b067
e53b43c
b250ed0
4152410
c640f8b
bac2fd6
12e7143
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,109 @@ | ||||||
| %YAML 1.2 | ||||||
| --- | ||||||
| urn: extension:io.substrait:functions_table_generic | ||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Very minor nit, but IMO adding
Suggested change
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is mirroring the aggregate functions. I would go with table_functions over functions_table. One could functions is also devoid of information but table by itself fails to have enough information to make it clear what it is on its own. |
||||||
| table_functions: | ||||||
| - name: explode | ||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You have two Is it possible / sensible to make this one function with two |
||||||
| description: >- | ||||||
| Produces one row per array element. | ||||||
|
|
||||||
| For each input row, this function generates one output row for each element | ||||||
| in the input array. The output is a struct with a single field 'value' | ||||||
| containing the array element. | ||||||
|
|
||||||
| Empty arrays produce zero output rows. For outer semantics (producing a row | ||||||
| with NULL when the array is empty), use GenerateRel's preserve_on_empty option. | ||||||
| impls: | ||||||
| - args: | ||||||
| - name: input | ||||||
| value: list<any1> | ||||||
| description: The array to explode. | ||||||
| return: struct<any1> | ||||||
|
|
||||||
| - name: explode | ||||||
| description: >- | ||||||
| Produces one row per map entry. | ||||||
|
|
||||||
| For each input row, this function generates one output row for each key-value | ||||||
| pair in the input map. The output is a struct with two fields: 'key' and 'value'. | ||||||
| Note that map entries may be visited in any order. | ||||||
|
|
||||||
| Empty maps produce zero output rows. For outer semantics (producing a row | ||||||
| with NULLs when the map is empty), use GenerateRel's preserve_on_empty option. | ||||||
|
|
||||||
| Some systems call this unnest. | ||||||
|
Comment on lines
+32
to
+33
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This also applies to the other |
||||||
| impls: | ||||||
| - args: | ||||||
| - name: input | ||||||
| value: map<any1, any2> | ||||||
| description: The map to explode. | ||||||
| return: struct<any1, any2> | ||||||
|
|
||||||
| - name: posexplode | ||||||
| description: >- | ||||||
| Produces one row per array element with position. | ||||||
|
|
||||||
| For each input row, this function generates one output row for each element | ||||||
| in the input array. The output is a struct with two fields: | ||||||
| - 'pos': 0-indexed position of the element in the array (i64) | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. silly thing. some systems do use 1-indexed. do we want to have it as a function option?
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Could conversion from 0-indexed to 1-indexed is something that could be handled by their Substrait layer? If so, then we always canonicalize one way. |
||||||
| - 'value': the array element | ||||||
|
|
||||||
| Empty arrays produce zero output rows. For outer semantics (producing a row | ||||||
| with NULLs when the array is empty), use GenerateRel's preserve_on_empty option. | ||||||
| impls: | ||||||
| - args: | ||||||
| - name: input | ||||||
| value: list<any1> | ||||||
| description: The array to explode with positions. | ||||||
| return: struct<i64, any1> | ||||||
|
|
||||||
| - name: unnest | ||||||
| description: >- | ||||||
| Expands one or more lists into rows by zipping them in parallel (PostgreSQL semantics). | ||||||
|
|
||||||
| When multiple lists are provided, produces rows by expanding the lists in parallel, | ||||||
| similar to a zip operation. For example, unnest([1,2], [3,4]) produces rows: (1,3), (2,4). | ||||||
|
|
||||||
| If lists have different lengths, shorter lists are padded with NULL values to match | ||||||
| the length of the longest list. | ||||||
|
|
||||||
| Empty lists (when all inputs are empty) produce zero output rows. For outer semantics | ||||||
| (producing a row with NULLs when all lists are empty), use GenerateRel's preserve_on_empty option. | ||||||
| impls: | ||||||
| - args: | ||||||
| - name: input | ||||||
| value: list<any1> | ||||||
| description: The array to unnest. | ||||||
| return: struct<any1> | ||||||
| - args: | ||||||
| - name: input1 | ||||||
| value: list<any1> | ||||||
| description: First array to unnest. | ||||||
| - name: input2 | ||||||
| value: list<any2> | ||||||
| description: Second array to unnest. | ||||||
| return: struct<any1, any2> | ||||||
| - args: | ||||||
| - name: input1 | ||||||
| value: list<any1> | ||||||
| description: First array to unnest. | ||||||
| - name: input2 | ||||||
| value: list<any2> | ||||||
| description: Second array to unnest. | ||||||
| - name: input3 | ||||||
| value: list<any3> | ||||||
| description: Third array to unnest. | ||||||
| return: struct<any1, any2, any3> | ||||||
| - args: | ||||||
| - name: input1 | ||||||
| value: list<any1> | ||||||
| description: First array to unnest. | ||||||
| - name: input2 | ||||||
| value: list<any2> | ||||||
| description: Second array to unnest. | ||||||
| - name: input3 | ||||||
| value: list<any3> | ||||||
| description: Third array to unnest. | ||||||
| - name: input4 | ||||||
| value: list<any4> | ||||||
| description: Fourth array to unnest. | ||||||
| return: struct<any1, any2, any3, any4> | ||||||
|
Comment on lines
+71
to
+109
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. can this be expressed using inconsistent variadic arguments? or we need to extend the type grammar?
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We need to extend the type grammar. I left that to a future project. |
||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -556,6 +556,41 @@ message ExpandRel { | |
| } | ||
| } | ||
|
|
||
| // The Generate operator applies a table function to produce zero or more output | ||
| // rows for each input row. This operation is used to flatten or "explode" nested | ||
| // data structures like arrays and maps into separate rows, similar to SQL's | ||
| // LATERAL VIEW EXPLODE or UNNEST operations. | ||
| message GenerateRel { | ||
| RelCommon common = 1; | ||
|
|
||
| // The input relation to generate from. Each input row will produce zero or | ||
| // more output rows based on the generator table function. | ||
| Rel input = 2; | ||
|
|
||
| // The table expression to apply to each input row. This produces zero | ||
| // or more rows per input row. The output_type defines the schema | ||
| // of the generated columns, which are appended to the input schema. | ||
| // | ||
| // Required. | ||
| TableExpression generator = 3; | ||
|
|
||
| // Controls behavior when the generator produces zero rows for an input record. | ||
| // | ||
| // When false (default): | ||
| // Input rows that produce zero output rows are excluded from the result. | ||
| // | ||
| // When true: | ||
| // Input rows that produce zero output rows still generate one output row | ||
| // with the input fields populated and the generator output fields set to NULL. | ||
| // This is equivalent to Spark's "LATERAL VIEW OUTER" or can be achieved in | ||
| // standard SQL via LEFT JOIN LATERAL. | ||
| // | ||
| // Optional; defaults to false. | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. not: Optional |
||
| bool preserve_on_empty = 4; | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. An alternative would be to make this part of TableFunction but if there are other table expressions they might need this functionality.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Based on my other comment on |
||
|
|
||
| substrait.extensions.AdvancedExtension advanced_extension = 10; | ||
| } | ||
|
|
||
| // A relation with output field names. | ||
| // | ||
| // This is for use at the root of a `Rel` tree. | ||
|
|
@@ -585,6 +620,7 @@ message Rel { | |
| WriteRel write = 19; | ||
| DdlRel ddl = 20; | ||
| UpdateRel update = 22; | ||
| GenerateRel generate = 23; | ||
| // Physical relations | ||
| HashJoinRel hash_join = 13; | ||
| MergeJoinRel merge_join = 14; | ||
|
|
@@ -1690,6 +1726,54 @@ message Expression { | |
| } | ||
| } | ||
|
|
||
| // TableExpression represents expressions that produce zero or more rows, | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Table expressions are definitely possible elsewhere. I could see read relations being redesigned using table expressions. |
||
| // as opposed to Expression which produces a single scalar value. | ||
| // Table expressions are used in relational contexts where multi-row output | ||
| // is expected (e.g., GenerateRel for lateral views and unnest operations). | ||
| // | ||
| // Since they have different cardinality than Expression expressions, | ||
| // TableExpression expressions cannot be composed with other expressions. | ||
| message TableExpression { | ||
| oneof table_expr_type { | ||
| TableFunction table_function = 1; | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What are the other things here that you anticipate adding in the future?
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's possible that table expressions could have their own operators (perhaps implementing joins, projects, or fetches). I'm using Expression as a guide here.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I plan to send a proposal to add
@EpsilonPrime do you see we need a seperate
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. One needs preserve on empty, the other RelOp. That feels different enough to me. Fortunately there's enough room for ApplyRel in TableExpression. |
||
| } | ||
|
|
||
| // A table function call that produces zero or more rows per invocation. | ||
| // Table functions are similar to scalar functions but produce multiple output rows | ||
| // instead of a single scalar value. They are commonly used with the GenerateRel | ||
| // operator to flatten nested data structures like arrays and maps. | ||
| message TableFunction { | ||
| // Points to a function_anchor defined in this plan, which must refer | ||
| // to a table function in the associated YAML file. Table functions are | ||
| // distinguished from scalar functions by their ability to produce multiple | ||
| // output rows. Required; 0 is a valid anchor/reference. | ||
| uint32 function_reference = 1; | ||
|
|
||
| // The arguments to be bound to the table function. This must have exactly the | ||
| // number of arguments specified in the function definition, and the argument | ||
| // types must match: | ||
| // | ||
| // - Value arguments must be bound using FunctionArgument.value | ||
| // - Type arguments must be bound using FunctionArgument.type | ||
| // - Enum arguments must be bound using FunctionArgument.enum | ||
| // | ||
| // For table functions, arguments typically reference fields from the input | ||
| // relation that contain nested data (arrays, maps, etc.) to be exploded. | ||
| repeated FunctionArgument arguments = 2; | ||
|
|
||
| // Options to specify behavior for corner cases (e.g., handling malformed data, | ||
| // ordering within generated rows, deduplication). | ||
| repeated FunctionOption options = 3; | ||
|
|
||
| // Must be set to the output type of the table function. For table functions, | ||
| // this must be a Struct type where each field represents a column in the | ||
| // generated output rows. | ||
| // | ||
| // Required. | ||
| Type output_type = 4; | ||
| } | ||
| } | ||
|
|
||
| // Expression that represents a dynamic parameter. | ||
| // Dynamic parameters are identified by a surrogate key within a plan. | ||
| message DynamicParameter { | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.