cloud-fan commented on code in PR #58831:
URL: https://github.com/apache/spark/pull/58831#discussion_r4385327709


##########
sql/catalyst/src/main/scala/org/apache/spark/sql/internal/SQLConf.scala:
##########
@@ -5903,6 +5903,17 @@ object SQLConf {
       .booleanConf
       .createWithDefault(true)
 
+  val INSERT_COLUMN_LIST_NESTED_FIELDS_RESOLVE_BY_POSITION =

Review Comment:
   **Nit (P3):** This entry lands between `INSERT_INTO_REPLACE_ON_ENABLED` and 
`INSERT_INTO_REPLACE_ON_BY_NAME_ENABLED`, splitting the REPLACE ON/USING/WHERE 
syntax flags, which have nothing to do with column-list nested resolution. 
Could it move after `INSERT_INTO_REPLACE_WHERE_COLUMN_LIST_ENABLED`, at the end 
of that group, with the definition unchanged?
   
   **Verification:**
   - **Inspection:** The REPLACE ON/USING/WHERE syntax-flag entries in SQLConf 
appear contiguously, and the column-list nested-resolution entry comes after 
them with an identical definition.
   - **Compile:** The catalyst module still compiles and the config is still 
registered under the same key.



##########
sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/analysis/ResolveInsertionBase.scala:
##########
@@ -64,25 +65,27 @@ abstract class ResolveInsertionBase extends 
Rule[LogicalPlan] {
   }
 
   private def renameFieldsInStruct(input: StructType, expected: StructType): 
StructType = {
-    if (input.length == expected.length) {
+    if (input.length == expected.length || resolveNestedFieldsByPosition) {
       val newFields = input.zip(expected).map { case (f1, f2) =>
         f1.copy(name = f2.name, dataType = renameFieldsInType(f1.dataType, 
f2.dataType))
       }
-      StructType(newFields)
+      StructType(newFields ++ input.drop(expected.length))

Review Comment:
   **Non-blocking (P2):** With the flag on, this appends 
`input.drop(expected.length)` under the source names without checking them 
against the target names the prefix was just renamed to. For `s STRUCT<x INT>` 
and `named_struct('y', 1, 'x', 2, 'z', 3)`, the cast type becomes `STRUCT<x, x, 
z>`.
   
   The PR relies on `TableOutputResolver.reorderColumnsByName` to reject that 
with `AMBIGUOUS_COLUMN_NAME`, but two consumers act before or instead of that 
check:
   
   - **Schema evolution runs first.** `ResolveSchemaEvolution` runs before 
`ResolveOutputRelation`, and `computeSchemaChangesByName` dedups the duplicate 
`x` last-wins, so it computes only `add s.z` and calls `catalog.alterTable`. 
The ambiguity error fires after that, so the `INSERT WITH SCHEMA EVOLUTION` 
fails but leaves `s.z` in the table schema. At the merge base the same 
statement succeeded.
   - **`ACCEPT_ANY_SCHEMA` tables skip the check.** `skipSchemaResolution` 
makes `outputResolved` true, so `ResolveOutputRelation` never runs and the 
connector receives `Cast(s, STRUCT<x, x, z>)` with duplicate names the user 
never wrote.
   
   The existing collision test (`named_struct('y', 2, 'x', 3)` into 
`STRUCT<x>`) has no extra non-colliding field and does not use an 
`ACCEPT_ANY_SCHEMA` table, so it observes neither effect. v1/Hive inserts are 
not affected because they cannot evolve and always go through output resolution.
   
   **Recommended change:** In the column-list INSERT projection, after renaming 
the overlapping prefix of a source struct to the target field names, check 
whether any retained surplus source field name matches one of those assigned 
names under the session resolver. If one does, fail analysis immediately with 
the existing INCOMPATIBLE_DATA_FOR_TABLE.AMBIGUOUS_COLUMN_NAME error for that 
nested column path, instead of emitting a cast to a struct with duplicate field 
names. Pass the table name and the nested column path through the struct and 
nested-type rename helpers, using the same element, key and value path segments 
that table output resolution uses for arrays and maps, so the error names the 
same column the current tests expect.
   
   **Why this works:** The duplicate-named struct is only ever created by the 
positional rename. Rejecting it there means no consumer ever sees it: schema 
evolution never computes or applies table changes from it, ACCEPT_ANY_SCHEMA 
writes are rejected before the write reaches the connector, and v1/Hive and v2 
output resolution report the same error earlier than they do today.
   
   **Scope:** Reject positional-rename name collisions where the renamed struct 
is built, and add regression coverage for the schema-evolution, connector and 
nested cases.
   
   **Compatibility:** Column-list inserts whose renamed struct has no 
collision, including the PR's {y, x, z} into STRUCT<x, y> example, and all 
behavior with the compatibility flag off, produce the same results as at the 
current head.
   
   **Risks:** The collision is now reported during the column-list projection, 
before table output resolution. A statement that has both a collision and 
another incompatibility will report the collision first.
   
   **Constraints:** Reject only collisions between a retained surplus source 
field and a name assigned by the positional rename. User-written duplicate 
names and unequal-arity inputs without such a collision must keep their current 
behavior. Compare names with the session resolver so that case sensitivity 
follows spark.sql.caseSensitive, matching how table output resolution detects 
the ambiguity today.
   
   **Exclusions:**
   - Introducing a new, more specific error condition for this collision is out 
of scope. The repair keeps INCOMPATIBLE_DATA_FOR_TABLE.AMBIGUOUS_COLUMN_NAME, 
which the PR's tests already assert.
   
   **Success:** For a schema-evolving v2 table with s STRUCT<x INT>, INSERT 
WITH SCHEMA EVOLUTION INTO t (s) SELECT named_struct('y', 1, 'x', 2, 'z', 3) 
fails with INCOMPATIBLE_DATA_FOR_TABLE.AMBIGUOUS_COLUMN_NAME on `s`.`x`, and 
the table schema afterwards still has no s.z. A column-list INSERT whose 
positional rename produces a colliding surplus name is rejected during analysis 
for every table kind, including tables with ACCEPT_ANY_SCHEMA, so no write 
receives a struct with duplicate field names it did not contain. A collision in 
a struct nested in an array element or a map key or value is rejected with the 
corresponding nested column path. Non-colliding inputs, including 
named_struct('y', 1, 'x', 2, 'z', 3) into STRUCT<x, y> writing {x: 1, y: 2, z: 
3}, and every flag-off behavior are unchanged.
   
   **Verification:**
   - **Regression:** A schema-evolving v2 table with s STRUCT<x INT>, given a 
column-list insert of named_struct('y', 1, 'x', 2, 'z', 3) WITH SCHEMA 
EVOLUTION, fails with AMBIGUOUS_COLUMN_NAME on `s`.`x`, and the table schema is 
unchanged afterwards.
   - **Regression:** A column-list insert with a colliding surplus name into a 
table that reports ACCEPT_ANY_SCHEMA is rejected during analysis instead of 
reaching the connector.
   - **Behavior:** A colliding surplus name in a struct nested inside an array 
element and inside a map value is rejected, and the error names the nested 
column path.
   - **Compatibility:** The existing collision cases still fail with 
AMBIGUOUS_COLUMN_NAME on `s`.`x` with and without schema evolution. 
Non-colliding wider and narrower sources keep their positional results with the 
flag on and their by-name results with it off.
   - **Compatibility:** With case-insensitive analysis, a surplus field 
differing from a renamed target name only by case is treated as a collision. 
With spark.sql.caseSensitive=true it is not, and it is evolved or rejected as 
an extra field as today.



##########
sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/analysis/ResolveInsertionBase.scala:
##########
@@ -64,25 +65,27 @@ abstract class ResolveInsertionBase extends 
Rule[LogicalPlan] {
   }
 
   private def renameFieldsInStruct(input: StructType, expected: StructType): 
StructType = {
-    if (input.length == expected.length) {
+    if (input.length == expected.length || resolveNestedFieldsByPosition) {

Review Comment:
   **Non-blocking (P2):** Since this condition now renames on any arity 
mismatch at every nesting level, a few behaviors it changes or keeps still have 
no test:
   
   - **Unequal arity inside a collection.** For example, 
`array(named_struct('y', 1, 'x', 2, 'z', 3))` into `ARRAY<STRUCT<x, y>>` with 
schema evolution. The PR description claims this case, but the SPARK-58816 
array/map tests are all equal-arity and the new tests only use a top-level 
struct, so if the relaxation stopped applying to collection elements, every 
current test would still pass.
   - **A narrower source.** With `spark.sql.insertNestedTypeCoercion.enabled` 
and schema evolution, `named_struct('y', 1)` into `STRUCT<x, y>` now writes 
`{x: 1, y: null}` instead of `{x: null, y: 1}`. The existing 
missing-nested-field tests use `INSERT ... SELECT *` without a column list, so 
they never reach this code.
   - **Equal arity with the flag off.** The flag-off tests use top-level 
array/map columns, which never reach this method, and an unequal-arity struct. 
Dropping the `input.length == expected.length ||` disjunct would switch 
equal-arity structs to by-name under flag=false without any test failing.
   
   The first item is also the array/map coverage asked for earlier in review.
   
   This extends an [existing 
discussion](https://github.com/apache/spark/pull/58831#discussion_r4047775291).
   
   **Verification:**
   - **Behavior:** Unequal-arity structs inside an array element and inside a 
map value resolve positionally with the flag on and by name with it off, under 
schema evolution.
   - **Behavior:** A narrower source struct under nested type coercion with 
schema evolution resolves positionally with the flag on and by name with it off.
   - **Compatibility:** With the flag off, an equal-arity struct with reordered 
field names keeps its positional result.



-- 
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