Skip to content

[SPARK-58941][SDP] Sort schema inference flows by identifier parts to avoid dotted-name collisions - #58223

Open
anew wants to merge 3 commits into
apache:masterfrom
anew:sdp-sort-flows-quoted-identifier
Open

[SPARK-58941][SDP] Sort schema inference flows by identifier parts to avoid dotted-name collisions#58223
anew wants to merge 3 commits into
apache:masterfrom
anew:sdp-sort-flows-quoted-identifier

Conversation

@anew

@anew anew commented Aug 22, 2026

Copy link
Copy Markdown
Contributor

What changes were proposed in this pull request?

SchemaInferenceUtils.inferSchemaFromFlows merges the flows that write to a table in a
deterministic order so that, when two flows emit a column whose names differ only in case, the
surviving spelling is well-defined and every caller agrees on it. It established that order by
sorting on flow.identifier.unquotedString.

TableIdentifier.unquotedString joins the identifier's name parts with an unescaped .. That
mapping is not one-to-one: two structurally distinct identifiers whose parts contain dots (a dot is
legal inside a back-tick-quoted schema or flow name) can render to the same string -- e.g.
`c`.`a.b`.`x` and `c`.`a`.`b.x` both render to c.a.b.x. Because sortBy is stable,
colliding keys fall back to the incoming flows order, which is the nondeterministic
flow-resolution completion order the sort was meant to remove, so the surviving column casing could
flip between runs.

This PR sorts on the identifier's parts (catalog, database, table) instead. Comparing the parts
tuple is injective -- two distinct identifiers are two distinct tuples, so they can never collide.
It also preserves the existing order for dotless identifiers (a shorter part sorts first, exactly
as the . separator, 0x2E, ordered a joined string), unlike TableIdentifier.quotedString, which
would additionally reorder some ordinary pairs because ` (0x60) sorts after name characters.
Flow identifiers are always fully qualified (assertIsFullyQualifiedForCreate), so all three parts
are present and the ordering never depends on an absent catalog/database.

This is a follow-up to SPARK-58517, which added the sort.

Why are the changes needed?

The sort exists to guarantee a deterministic surviving column spelling. unquotedString is a lossy
key, so under identifiers that contain dots the guarantee silently breaks: the merge order reverts
to the nondeterministic completion order of concurrent flow resolution, and a case-only column
spelling can flip between runs of an unchanged pipeline. On the non-merging evolution paths
diffSchemas keys column identity on the exact name, so a run-to-run flip surfaces as a
deleteColumn + addColumn for a column that only changed case. Sorting on the identifier parts is
injective and removes the collision.

Does this PR introduce any user-facing change?

No. The merge order changes only for identifiers with dotted (back-tick-quoted) name parts, and the
change is confined to the unreleased master / branch-4.x. For ordinary (dotless) identifiers the
order is unchanged.

How was this patch tested?

Added a unit test to SchemaInferenceUtilsSuite that builds two resolved flows whose identifiers
(`c`.`a.b`.`x` and `c`.`a`.`b.x`) would collide under a dot-joined key but stay distinct
when sorted on their parts, each carrying a case-only-differing column, and asserts the inferred
schema is identical regardless of the order the flows are passed in. The test fails with the
previous unquotedString key and passes with the parts key.

Ran build/sbt 'pipelines/testOnly *SchemaInferenceUtilsSuite' (13 tests, all passed).

Was this patch authored or co-authored using generative AI tooling?

Generated-by: Claude Opus 4.8

…o avoid dotted-name collisions

`SchemaInferenceUtils.inferSchemaFromFlows` sorts the flows it merges by
`identifier.unquotedString` to make the surviving spelling of a case-only-differing
column deterministic. `unquotedString` joins the identifier's name parts with an
unescaped `.`, so two distinct identifiers whose parts contain dots (e.g. a
back-tick-quoted schema or flow name) can render to the same string. A stable
`sortBy` then falls back to the incoming flow order -- the nondeterministic
flow-resolution completion order -- so the surviving column casing could flip
between runs.

Sort on `quotedString` instead: it back-tick-quotes each part (escaping embedded
back-ticks), so the ordering stays one-to-one with the identifier and the two names
can no longer collide.

Follow-up to SPARK-58517.
@uros-b

uros-b commented Aug 22, 2026

Copy link
Copy Markdown
Member

LGTM

// The merge order decides which spelling of a case-only-differing column survives, so it has to
// be deterministic and independent of the incoming flow order (which is the nondeterministic
// flow-resolution completion order). inferSchemaFromFlows sorts on the identifier's
// `quotedString`. Sorting on `unquotedString` would collapse these two DISTINCT identifiers to

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: there's a lot of comment about unquoted string in this test, can we remove them as they are a bit repetitive and not that relevant as we now use 'quotedString'

// the same key ("c.a.b.x"), and a stable sort would then fall back to the incoming order --
// flipping the surviving column casing when the flows happen to arrive swapped.
val destination = TableIdentifier("t", Some("d"), Some("c"))
// Both identifiers render to "c.a.b.x" under unquotedString, but differ under quotedString.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

like this one (we can maybe keep one version of them?)

sqlConf = Map.empty))
}

test("inferSchemaFromFlows merges deterministically for identifiers that collide under " +

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

optional: maybe dotted-name collision describes it better? again unquoted string is a bit specific to be persisted as comment, as its not used anymore.

import org.apache.spark.sql.types.{IntegerType, StringType, StructType}

/** Tests for the flow ordering used by [[SchemaInferenceUtils.inferSchemaFromFlows]]. */
class InferSchemaFromFlowsSuite extends QueryTest with SharedSparkSession {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we put this in an existing suite rather than a suite just for one test?

@szehon-ho szehon-ho left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The fix looks right to me: quotedString is injective (back-tick doubling makes the token stream uniquely decodable, and TableIdentifier's assert(catalog.isEmpty || database.isDefined) rules out the one shape both keys would flatten), so the collision is gone. I also checked that the described collision is reachable through the flow registration path, and that the new test does fail on the old key.

One correction to the description, no action needed: the ordering also changes for ordinary dotless identifiers, not only dotted ones. ` (0x60) sorts after every character normally used in names while . (0x2E) sorts before them, so any pair where one name is a prefix of the other flips:

A B unquotedString quotedString
c.d.events c.d.events_us A first B first
c.d.f1 c.d.f10 A first B first

Roughly 2% of random identifier pairs reorder. The "no user-facing change" answer still holds since the sort is unreleased; it is just the stated reason that is off.

One more nit for existing test: MaterializeTablesSuite:1281 derives its expected surviving spelling by re-sorting the flows itself with .sortBy(_.identifier.unquotedString), so it now mirrors a key the code no longer uses. It passes today because f1/f2 order the same under both keys, but with names like f1/f10 it would assert the opposite of the correct behavior. Switching it to quotedString, or asserting the literal field names the way the sibling test at line 1292 does, would keep it honest.

@anew

anew commented Aug 23, 2026

Copy link
Copy Markdown
Contributor Author

the ordering also changes for ordinary dotless identifiers, not only dotted ones.

Ah I missed that, and I don't really like it, it is not that intuitive. Let me sort by (catalog, database, table) instead.

…into SchemaInferenceUtilsSuite

Address review feedback on the schema-inference flow sort:

- Sort on the identifier's parts `(catalog, database, table)` instead of `quotedString`.
  Both are injective (no dotted-name collision), but the parts tuple also preserves the
  existing order for dotless identifiers, whereas `quotedString` reordered some pairs
  (back-tick 0x60 sorts after name characters, dot 0x2E before them). Flow identifiers are
  always fully qualified, so all three parts are present and the ordering never depends on
  an absent catalog/database.
- Move the `inferSchemaFromFlows` test into the existing `SchemaInferenceUtilsSuite`
  (now `QueryTest with SharedSparkSession`) and delete the single-test suite; reword it
  around dotted-name folding and trim the comments.
@anew anew changed the title [SPARK-58941][SDP] Sort schema inference flows by quoted identifier to avoid dotted-name collisions [SPARK-58941][SDP] Sort schema inference flows by identifier parts to avoid dotted-name collisions Aug 23, 2026
…eTablesSuite

The multi-flow case-only fold test derived its expected surviving spelling by re-sorting the
flows with `.sortBy(_.identifier.unquotedString)` -- a key inferSchemaFromFlows no longer uses.
Assert the literal field names instead, matching the sibling test, so the expectation is not
coupled to a stale sort key.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants