gengliangwang commented on code in PR #58204:
URL: https://github.com/apache/spark/pull/58204#discussion_r3832876212


##########
sql/hive/src/main/scala/org/apache/spark/sql/hive/HiveStrategies.scala:
##########
@@ -150,12 +150,6 @@ class DetermineTableStats(session: SparkSession) extends 
Rule[LogicalPlan] {
     case relation: HiveTableRelation
       if DDLUtils.isHiveTable(relation.tableMeta) && 
relation.tableMeta.stats.isEmpty =>
       hiveTableWithStats(relation)

Review Comment:
   `spark.sql.hive.convertInsertingPartitionedTable=false` no longer forces the 
Hive writer, now that the INSERT target is a child.
   
   `RelationConversions` runs top-down (`resolveOperators` delegates to 
`resolveOperatorsDownWithPruning`), and its write-path case is guarded by 
`convertInsertingPartitionedTable` / `convertInsertingUnpartitionedTable`. When 
that guard is false the `InsertIntoStatement` case no longer matches, so the 
traversal descends into the node's children — and `table` is now one of them, 
so the read-path case at line 258 matches the write target and replaces it via 
`metastoreCatalog.convert(r, isWrite = false)`. `HiveAnalysis` then never 
builds an `InsertIntoHiveTable`, so the built-in writer runs against a relation 
configured for reading and `hive.exec.max.dynamic.partitions` is never 
enforced. The rule's own doc comment at line 196 states the opposite contract.
   
   `HiveSQLInsertTestSuite` "SPARK-54853: SET hive.exec.max.dynamic.partitions 
takes effect in session conf" already fails on this head for exactly this 
reason: "Expected exception org.apache.spark.SparkException to be thrown, but 
no exception was thrown".
   
   Add an `InsertIntoStatement` case to this rule that recurses into `query` 
only, so the read path cannot reach a write target whatever the config says. 
Deleting `DetermineTableStats`' insert case here is fine by contrast — its 
generic case performs the same rewrite the deleted one did.



##########
sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/analysis/CTESubstitution.scala:
##########
@@ -411,6 +411,14 @@ object CTESubstitution extends Rule[LogicalPlan] {
       alwaysInline: Boolean,
       cteRelations: Seq[(String, CTERelationDef)],
       recursiveCTERelation: Option[(String, CTERelationDef)]): LogicalPlan = {
+    plan match {
+      case i: InsertIntoStatement =>
+        // CTE names are visible in the input query, but they do not shadow 
the INSERT target.
+        return i.copy(query = substituteCTE(

Review Comment:
   This guard only fires when the `InsertIntoStatement` is the root of the 
`substituteCTE` call, so a multi-insert statement still gets its target 
replaced by a same-named CTE.
   
   `substituteCTE` is called on the `UnresolvedWith`'s child (line 242). For a 
single insert that child *is* the `InsertIntoStatement` and this case matches. 
For a multi-insert, `visitMultiInsertQuery` builds `Union(inserts.toSeq)`, so 
the child is a `Union`, this case does not match, and 
`resolveOperatorsUpWithPruning` descends into each insert's `table` child — 
where `case u @ UnresolvedRelation(Seq(table), _, _)` at line 436 hands the 
target to `resolveWithCTERelations`.
   
   `WITH t1 AS (SELECT ...) FROM src INSERT INTO t1 SELECT ... INSERT INTO t2 
SELECT ...` is valid syntax (`ctes? dmlStatementNoWith`, with `fromClause 
multiInsertQueryBody+` as one of its forms). On master the target could not be 
substituted in either shape because it was not a child. Now it becomes a 
`SubqueryAlias` over the CTE plan, and `PreWriteCheck` rejects the statement 
with `UNSUPPORTED_INSERT.RDD_BASED` instead of writing to the persistent table.
   
   A root check can't be made to work here, because the traversal is bottom-up: 
by the time a nested `InsertIntoStatement` is visited, its target has already 
been rewritten. Handle the node inside the traversal and skip only its `table` 
subtree. That also restores this node's own expression pass, which the early 
`return` currently bypasses — `case other` is what substitutes CTEs inside 
subquery expressions, and `replaceCriteriaOpt` is an expression on this node. 
Worth adding a multi-insert test; nothing covers that shape today.



##########
sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/plans/logical/statements.scala:
##########
@@ -209,21 +208,15 @@ case class InsertIntoStatement(
   require(replaceCriteriaOpt.isEmpty || overwrite,
     "REPLACE USING/ON/WHERE requires overwrite to be true")
 
-  override def child: LogicalPlan = query
-  override protected def withNewChildInternal(newChild: LogicalPlan): 
InsertIntoStatement =
-    copy(query = newChild)
+  override def children: Seq[LogicalPlan] = Seq(table, query)

Review Comment:
   This trades a structural guarantee for a convention that every 
child-descending rule now has to know about.
   
   While `table` was not a child, "only the target-specific resolver rewrites 
the INSERT target" was enforced by the node shape: nothing could reach the 
slot, and the few rules that needed to look inside it did so explicitly. Now 
each rule that descends into children has to decide whether it is looking at a 
write target, and they disagree. `ResolveRelations` and `FindDataSourceTable` 
get it right because their `InsertIntoStatement` cases are unguarded and match 
before their generic relation cases. `RelationConversions` gets it wrong, 
because its write case is config-guarded and the generic read case picks up the 
target when the guard is false. `CTESubstitution` gets it half right, guarding 
only the case where the insert is the substitution root. Both are flagged 
separately.
   
   The peer commands this shape imitates aren't evidence that it's safe for 
INSERT: MERGE/UPDATE/DELETE reject Hive-serde and V1 file-source targets, so no 
read-conversion rule ever has to protect their target child. INSERT accepts 
them.
   
   My suggestion is to give the invariant one owner rather than a case per 
rule. Patching the two known gaps leaves the next rule that descends into 
children with the same unstated obligation — which is how both of these arose. 
Marking the target slot itself, so the generic read-side cases skip it by 
construction, makes the protection inheritable instead of remembered.



##########
sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/analysis/Analyzer.scala:
##########
@@ -1151,17 +1151,11 @@ class Analyzer(
     def apply(plan: LogicalPlan)
         : LogicalPlan = plan.resolveOperatorsUpWithPruning(AlwaysProcess.fn, 
ruleId) {
       case i @ InsertIntoStatement(table, _, _, _, _, _, _, _, _) =>
-        val relation = table match {
-          case u: UnresolvedRelation if !u.isStreaming =>
-            resolveRelation(u).getOrElse(u)
-          case other => other
-        }
-
         // Inserting into a file-based temporary view is allowed.
         // (e.g., spark.read.parquet("path").createOrReplaceTempView("t").
-        // Thus, we need to look at the raw plan if `relation` is a temporary 
view.
+        // Thus, we need to look at the raw plan if `table` is a temporary 
view.
         // unwrapRelationPlan also resolves V2TableReference nodes in temp 
view plans.
-        unwrapRelationPlan(relation) match {
+        unwrapRelationPlan(table) match {

Review Comment:
   Now that the target resolves through the generic relation case, an INSERT 
into a view analyzes the entire view body before this line rejects it.
   
   The generic case resolves the target with 
`resolveRelation(u).map(resolveViews(_, u.options))`, and `resolveViews` runs 
`ViewResolution.resolve` on a `View` whose child is unresolved. Only afterwards 
does `unwrapRelationPlan` reach the `View` and raise 
`insertIntoViewNotAllowedError`. Previously the insert case resolved the target 
itself and never called `resolveViews`, so the body was never analyzed.
   
   For `CREATE TEMP VIEW v AS SELECT * FROM t; DROP TABLE t; INSERT INTO v 
VALUES (1);` the user now gets `TABLE_OR_VIEW_NOT_FOUND` for `t` instead of 
being told they cannot insert into a view, and a valid view target pays for a 
full body analysis — recursively, for nested views — that is then thrown away. 
Keeping the target off the generic view-resolution path avoids both, and the 
target-slot intercept that fixes the Hive case would cover it.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to