-
Notifications
You must be signed in to change notification settings - Fork 26.1k
ESQL: Support intra-row field references in ROW command #140217
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 4 commits
d437949
b498e21
63259e6
39ceedc
0e5aab2
8c52929
9c70b68
bb2ce94
4face4b
4a1c231
9ac08f8
2bb3168
a89a6f7
d55c704
98686ec
69cdb63
9cd3a3d
68c77d3
416ba05
48374a8
786e49a
d96d76f
ffc7947
8c51aa4
9031e82
c8f4db5
7b6646b
68c8739
6e120fe
6ca96f5
3007cc1
dff7662
3fc925d
9b5af75
f09e63f
cb66bc6
64759df
eb2fb92
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,6 @@ | ||
| pr: 140217 | ||
| summary: "ESQL: Support intra-row field references in ROW command" | ||
| area: ES|QL | ||
| type: feature | ||
| issues: | ||
| - 140119 |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -129,6 +129,7 @@ | |
| import org.elasticsearch.xpack.esql.plan.logical.MvExpand; | ||
| import org.elasticsearch.xpack.esql.plan.logical.Project; | ||
| import org.elasticsearch.xpack.esql.plan.logical.Rename; | ||
| import org.elasticsearch.xpack.esql.plan.logical.Row; | ||
| import org.elasticsearch.xpack.esql.plan.logical.TimeSeriesAggregate; | ||
| import org.elasticsearch.xpack.esql.plan.logical.UnionAll; | ||
| import org.elasticsearch.xpack.esql.plan.logical.UnresolvedRelation; | ||
|
|
@@ -499,6 +500,28 @@ private LocalRelation tableMapAsRelation(Source source, Map<String, Column> mapT | |
| } | ||
|
|
||
| public static class ResolveRefs extends ParameterizedAnalyzerRule<LogicalPlan, AnalyzerContext> { | ||
|
|
||
| /** | ||
| * Override skipResolved to always process Row nodes, even when they are already resolved. | ||
| * <p> | ||
| * When all Row fields are literals (e.g., {@code ROW a = 1, b = 2, a = 3}), the Row node | ||
| * is resolved immediately after parsing since all expressions are resolved literals. | ||
| * However, we still need the Analyzer to: | ||
| * <ul> | ||
| * <li>Handle duplicate field names (remove earlier definitions when later ones shadow them)</li> | ||
| * <li>Resolve field references in later fields that refer to earlier ones | ||
| * (e.g., {@code ROW x = 4, z = x + 1})</li> | ||
| * </ul> | ||
| * <p> | ||
| * In contrast, when Row contains field references (e.g., {@code ROW x = 4, z = x + y}), | ||
| * the Row is unresolved due to UnresolvedAttributes, so it would be processed anyway. | ||
| * This override ensures consistent handling for both cases. | ||
| */ | ||
| @Override | ||
| protected boolean skipResolved(LogicalPlan plan) { | ||
| return plan instanceof Row == false; | ||
| } | ||
|
|
||
| @Override | ||
| protected LogicalPlan rule(LogicalPlan plan, AnalyzerContext context) { | ||
| if (plan.childrenResolved() == false) { | ||
|
|
@@ -529,6 +552,7 @@ protected LogicalPlan rule(LogicalPlan plan, AnalyzerContext context) { | |
| case Fuse fuse -> resolveFuse(fuse, childrenOutput); | ||
| case Rerank r -> resolveRerank(r, childrenOutput); | ||
| case PromqlCommand promql -> resolvePromql(promql, childrenOutput); | ||
| case Row row -> resolveRow(row); | ||
| default -> plan.transformExpressionsOnly(UnresolvedAttribute.class, ua -> maybeResolveAttribute(ua, childrenOutput)); | ||
| }; | ||
| } | ||
|
|
@@ -1185,6 +1209,57 @@ private LogicalPlan resolveEval(Eval eval, List<Attribute> childOutput) { | |
| return changed ? new Eval(eval.source(), eval.child(), newFields) : eval; | ||
| } | ||
|
|
||
| /** | ||
| * Resolve Row fields, allowing later fields to reference earlier ones. | ||
| * Unlike resolveEval, Row fields are typically literals, so we directly substitute | ||
| * the literal expressions instead of creating attribute references. | ||
| * <p> | ||
| * For example: | ||
| * <pre> | ||
| * ROW x = 4, y = 2, z = x + y | ||
| * - x is resolved as literal 4 | ||
| * - y is resolved as literal 2 | ||
| * - z is resolved by substituting x with 4 and y with 2, resulting in 4 + 2 | ||
| * </pre> | ||
| * <p> | ||
| * Field shadowing is supported: | ||
| * <pre> | ||
| * ROW x = 5, y = x * 2, x = y + 1 | ||
| * - First x = 5 is defined | ||
| * - y = x * 2 resolves to y = 5 * 2 = 10 | ||
| * - Second x = y + 1 resolves to x = 10 + 1 = 11 | ||
| * - Final output: y = 10, x = 11 (first x is removed) | ||
| * </pre> | ||
| */ | ||
| private LogicalPlan resolveRow(Row row) { | ||
| // Build a mapping from field names to their expressions for substitution | ||
| Map<String, Expression> fieldExpressions = new HashMap<>(); | ||
| List<Alias> newFields = new ArrayList<>(); | ||
| boolean changed = false; | ||
|
|
||
| for (Alias field : row.fields()) { | ||
| // Resolve unresolved attributes by substituting them with previously defined field expressions. | ||
| // If a matching field is found, replace the attribute with its expression; otherwise, keep it unresolved. | ||
| Alias result = (Alias) field.transformUp(UnresolvedAttribute.class, ua -> fieldExpressions.getOrDefault(ua.name(), ua)); | ||
|
|
||
| changed |= result != field; | ||
|
|
||
| // Handle field shadowing: if a field with the same name already exists, remove it | ||
| // This ensures only the last definition of a field appears in the output | ||
| if (result.resolved()) { | ||
| boolean removed = newFields.removeIf(existing -> existing.name().equals(result.name())); | ||
| changed |= removed; | ||
|
|
||
| // Store the field's child expression for future substitutions | ||
| // If there's a duplicate name, the later one overrides | ||
| fieldExpressions.put(result.name(), result.child()); | ||
|
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. In the current implementation, for I'm not aware of existing public non-deterministic functions, but for example we have the Now, I'm not sure if this is worth worrying now, or if we already have this problem in EVAL or in other planning steps. We could fold the values here before inserting them in the map. I guess that would add quite some complexity though. WDYT @astefan? If we decide to go with it, we could reuse the |
||
| } | ||
|
|
||
| newFields.add(result); | ||
| } | ||
| return changed ? new Row(row.source(), newFields) : row; | ||
| } | ||
|
|
||
| /** | ||
| * resolve each item manually. | ||
| * | ||
|
|
||
|
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. Also, can we have a failing test like: |
Uh oh!
There was an error while loading. Please reload this page.