-
Notifications
You must be signed in to change notification settings - Fork 29.2k
[SPARK-46625][SQL][FOLLOWUP] Resolve identifier expression in InsertIntoStatement/V2WriteCommand table slot #56024
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
base: master
Are you sure you want to change the base?
Changes from all commits
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 | ||||
|---|---|---|---|---|---|---|
|
|
@@ -1535,6 +1535,32 @@ class Analyzer( | |||||
| } | ||||||
|
|
||||||
| def doApply(plan: LogicalPlan): LogicalPlan = plan.resolveOperatorsUp { | ||||||
| // `InsertIntoStatement.table` and `V2WriteCommand.table` are non-child `LogicalPlan` | ||||||
| // slots (`child = query`), so the default `resolveOperatorsUp` + `mapExpressions` | ||||||
| // traversal never resolves expressions placed inside them. For a | ||||||
| // `PlanWithUnresolvedIdentifier`, `identifierExpr` (e.g. an `UnresolvedAttribute` | ||||||
| // referring to a SQL variable in `INSERT INTO IDENTIFIER(target_table) ...`) must | ||||||
| // be resolved here before `ResolveIdentifierClause` can materialize the relation. | ||||||
| // Mirror the recursion that `BindParameters` and `ResolveIdentifierClause` already | ||||||
| // do for the same shape (SPARK-46625). The `!identifierExpr.resolved` guard makes | ||||||
| // the case idempotent under bottom-up traversal. | ||||||
| case i: InsertIntoStatement | ||||||
| if i.table.isInstanceOf[PlanWithUnresolvedIdentifier] && | ||||||
| !i.table.asInstanceOf[PlanWithUnresolvedIdentifier].identifierExpr.resolved => | ||||||
| val p = i.table.asInstanceOf[PlanWithUnresolvedIdentifier] | ||||||
| val resolvedExpr = resolveExpressionByPlanChildren( | ||||||
| p.identifierExpr, i, includeLastResort = true) | ||||||
| resolveColumnDefaultInCommandInputQuery( | ||||||
|
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 wrap looks like a no-op in this iteration:
Suggested change
|
||||||
| i.copy(table = p.copy(identifierExpr = resolvedExpr))) | ||||||
|
|
||||||
| case w: V2WriteCommand | ||||||
| if w.table.isInstanceOf[PlanWithUnresolvedIdentifier] && | ||||||
| !w.table.asInstanceOf[PlanWithUnresolvedIdentifier].identifierExpr.resolved => | ||||||
| val p = w.table.asInstanceOf[PlanWithUnresolvedIdentifier] | ||||||
| val resolvedExpr = resolveExpressionByPlanChildren( | ||||||
| p.identifierExpr, w, includeLastResort = true) | ||||||
| w.withNewTable(p.copy(identifierExpr = resolvedExpr)) | ||||||
|
|
||||||
| // Don't wait other rules to resolve the child plans of `InsertIntoStatement` as we need | ||||||
| // to resolve column "DEFAULT" in the child plans so that they must be unresolved. | ||||||
| case i: InsertIntoStatement => resolveColumnDefaultInCommandInputQuery(i) | ||||||
|
|
||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2586,6 +2586,28 @@ class ParametersSuite extends SharedSparkSession { | |
| s"Expected :tname inside OverwriteByExpression.table to be bound, got:\n$boundOverwrite") | ||
| } | ||
|
|
||
| // SPARK-46625 followup: `INSERT INTO IDENTIFIER(<sql-variable>) ...` places a | ||
| // `PlanWithUnresolvedIdentifier` in `InsertIntoStatement.table`, whose `identifierExpr` | ||
| // holds an `UnresolvedAttribute` for the variable name. That slot is a non-child | ||
| // `LogicalPlan`, so the default `ResolveReferences` traversal never resolves the | ||
| // attribute, `ResolveIdentifierClause` cannot fire (it waits on `identifierExpr.resolved`), | ||
| // and analysis fails. Verify that the explicit `InsertIntoStatement` case added to | ||
| // `ResolveReferences` rewrites the attribute to a `VariableReference` and the insert | ||
| // completes end-to-end. | ||
| test("SPARK-46625: INSERT INTO IDENTIFIER(<sql-variable>) resolves variable in table slot") { | ||
| withTable("t_var_insert") { | ||
| sql("CREATE TABLE t_var_insert (a INT) USING PARQUET") | ||
| sql("DECLARE OR REPLACE VARIABLE target_table STRING") | ||
| try { | ||
| sql("SET VAR target_table = 't_var_insert'") | ||
| sql("INSERT INTO IDENTIFIER(target_table) SELECT 42 AS a") | ||
|
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. Worth adding a companion test where the SQL variable name matches a query output column (e.g. variable |
||
| checkAnswer(spark.table("t_var_insert"), Row(42)) | ||
| } finally { | ||
| sql("DROP TEMPORARY VARIABLE IF EXISTS target_table") | ||
| } | ||
| } | ||
| } | ||
|
|
||
| // SPARK-46625: `CacheTableAsSelect.tempViewName` is an `Expression` slot, so an | ||
| // `IDENTIFIER(<non-literal>)` produces an `ExpressionWithUnresolvedIdentifier` there instead of | ||
| // wrapping the entire command in a `PlanWithUnresolvedIdentifier`. Verify on the parsed plan | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should this resolve against
prather thani? Passingiwidens the resolution scope toi.children = Seq(query), so a nestedUnresolvedAttributeinidentifierExprcan match a query output column. With a colliding name likethe column wins and
IdentifierResolution.evalIdentifierExprthen throwsNOT_A_CONSTANT_STRING.NOT_CONSTANT(since anAttributeReferenceis not foldable). Resolving againstp(whosechildrenareNilfor the INSERT/OverwriteByExpression path built bybuildWriteTableSlot) mirrors howPlanWithUnresolvedIdentifieris resolved in regular child slots via the generic fallback at line 1865 — column resolution becomes a no-op and only the last-resort variable resolution fires.Same point applies to the
V2WriteCommandcase below (line 1561).