This is an automated email from the ASF dual-hosted git repository.

szehon-ho pushed a commit to branch branch-4.x
in repository https://gitbox.apache.org/repos/asf/spark.git


The following commit(s) were added to refs/heads/branch-4.x by this push:
     new 0ac43804a051 [SPARK-58517][SDP] Honor spark.sql.caseSensitive in table 
schema evolution
0ac43804a051 is described below

commit 0ac43804a0510092632ec0164f1e80f71e08ede8
Author: Andreas Neumann <[email protected]>
AuthorDate: Thu Aug 6 11:29:12 2026 -0700

    [SPARK-58517][SDP] Honor spark.sql.caseSensitive in table schema evolution
    
    ### What changes were proposed in this pull request?
    
      Declarative Pipelines' table schema evolution in 
`DatasetManager.evolveTable` compared schemas **case-sensitively regardless of 
the session's `spark.sql.caseSensitive`**. Under the default (case-insensitive) 
session, a target column `value` and an incoming column `Value` were treated as 
distinct, so the target was evolved to carry *both* columns -- a schema that is 
internally inconsistent under the resolver the rest of the engine uses.
    
      The root cause is on two utilities on the evolution path, each taking the 
case-sensitive default and never forwarding the session conf:
    
      - `SchemaMergingUtils.mergeSchemas` called `StructType.merge(tableSchema, 
dataSchema)` positionally, leaving `StructType.merge`'s `caseSensitive: Boolean 
= true` parameter at its default.
      - `SchemaInferenceUtils.diffSchemas` built `name -> field` maps and 
computed `targetFields.keySet.diff(currentFields.keySet)` -- an exact-string 
set difference, also case-sensitive.
    
      This PR threads case-sensitivity through both:
    
      - `mergeSchemas` and `diffSchemas` gain a `caseSensitive: Boolean = true` 
parameter. The default preserves existing behavior for the schema-*inference* 
callers (`inferSchemaFromFlows`, `DataflowGraph.inferredSchema`), which 
intentionally merge case-sensitively.
      - `DatasetManager.evolveTable` forwards 
`context.spark.sessionState.conf.caseSensitiveAnalysis` at both call sites (the 
target table and the AutoCDC auxiliary table).
      - With case-insensitivity, `StructType.merge` folds the incoming `Value` 
onto the existing `value` (the left field's name/position  wins), so no 
duplicate column is added; `diffSchemas` matches on the normalized name while 
emitting the column's already-persisted name, so a case-only difference is an 
in-place no-op rather than a spurious drop-then-add, and no rename is produced.
    
      This is not AutoCDC-specific -- `SchemaMergingUtils` / 
`SchemaInferenceUtils` are general pipeline-table utilities, so the fix covers 
any SDP table that evolves its schema under a case-insensitive session.
    
      ### Why are the changes needed?
    
      It is a correctness bug. Evolving a table under the default 
case-insensitive session with a column that differs only in case from an 
existing one produced a target schema with two columns (`value` and `Value`) 
that the engine's own case-insensitive resolver cannot tell apart. The corrupt 
schema then failed downstream where that resolver runs:
    
      - AutoCDC SCD2: `COLUMN_ALREADY_EXISTS`, raised by 
`ResolveUnion.checkColumnNames` over the `unionByName` in 
`Scd2ForeachBatchHandler.reconcileMicrobatch` (the affected-rows union reads 
the now-two-column target back).
      - AutoCDC SCD1: `AMBIGUOUS_REFERENCE`, deeper in the MERGE plan (no 
reconcile union).
    
      The same user mistake thus surfaced as two different, confusing error 
conditions -- both downstream symptoms of the corrupt evolved schema rather 
than the root cause.
    
      ### Does this PR introduce _any_ user-facing change?
    
      Yes, a bug fix (only within the unreleased `master` / `branch-4.x`; the 
buggy behavior was never in a release). Under case-insensitive resolution 
(Spark's default), a pipeline that emits a column differing only in case from 
an existing target column now evolves the table as a no-op (the incoming value 
is written to the existing column) instead of corrupting the schema and failing 
with  `COLUMN_ALREADY_EXISTS` (SCD2) or `AMBIGUOUS_REFERENCE` (SCD1). Under 
`spark.sql.caseSensitive=tru [...]
    
      ### How was this patch tested?
    
      Added and updated tests:
    
      - `SchemaInferenceUtilsSuite` -- new unit tests for `diffSchemas`: 
case-only difference is distinct under case-sensitive, a no-op under  
case-insensitive, and a case-insensitive type change addresses the current 
(persisted) column name.
      - `MaterializeTablesSuite` -- an end-to-end pair (non-AutoCDC path) 
asserting no `alterTable` is issued and the persisted column keeps  its 
original case under case-insensitive resolution, versus a column being added 
under case-sensitive.
      - `AutoCdcScd1SchemaEvolutionSuite` -- rewrote the case-only-difference 
test from asserting the old `AMBIGUOUS_REFERENCE` failure to asserting the 
fixed no-op-merge success; refreshed the suite header and a now-stale comment 
in `AutoCdcScd1KeyDriftSuite`.
    
      Ran the full `pipelines` module test suite. All pass except two failures 
(`TriggeredGraphExecutionSuite`: "stream failure on deletes and updates", 
"stopping a pipeline mid-execution") that I confirmed are pre-existing on clean 
`master` (they fail identically with this change stashed), so this PR 
introduces no regressions.
    
      ### Was this patch authored or co-authored using generative AI tooling?
    
      Generated-by: Claude Opus 4.8, Codex gpt-5.5
    
    Closes #57722 from anew/spark-58517-schema-evolution-case-sensitivity.
    
    Authored-by: Andreas Neumann <[email protected]>
    Signed-off-by: Szehon Ho <[email protected]>
    (cherry picked from commit ef128e33efb625d83f670fa607fb6626dc9d84ec)
    Signed-off-by: Szehon Ho <[email protected]>
---
 .../src/main/resources/error/error-conditions.json |   8 +
 .../connect/pipelines/PythonPipelineSuite.scala    |  55 +--
 .../SparkDeclarativePipelinesServerSuite.scala     |   7 +-
 .../spark/sql/pipelines/autocdc/ChangeArgs.scala   |   5 +-
 .../pipelines/graph/AutoCdcAuxiliaryTable.scala    |   8 +-
 .../graph/CoreDataflowNodeProcessor.scala          |  11 +-
 .../spark/sql/pipelines/graph/DataflowGraph.scala  |  62 ++--
 .../spark/sql/pipelines/graph/DatasetManager.scala |  82 ++++-
 .../apache/spark/sql/pipelines/graph/Flow.scala    |  21 +-
 .../spark/sql/pipelines/graph/FlowExecution.scala  |  14 +-
 .../spark/sql/pipelines/graph/GraphErrors.scala    |  29 ++
 .../sql/pipelines/graph/GraphValidations.scala     |   6 +-
 .../sql/pipelines/graph/PipelineExecution.scala    |   3 +-
 .../spark/sql/pipelines/graph/elements.scala       |   8 +-
 .../sql/pipelines/util/SchemaInferenceUtils.scala  | 151 +++++++-
 .../sql/pipelines/util/SchemaMergingUtils.scala    |  23 +-
 .../sql/pipelines/autocdc/AutoCdcFlowSuite.scala   |   5 +-
 .../pipelines/graph/AutoCdcConfigDriftSuite.scala  |  23 ++
 .../graph/AutoCdcScd1AuxiliaryTableSpecSuite.scala |   4 +-
 .../pipelines/graph/AutoCdcScd1KeyDriftSuite.scala |  50 ++-
 .../graph/AutoCdcScd1SchemaEvolutionSuite.scala    |  56 ++-
 .../graph/AutoCdcScd2AuxiliaryTableSpecSuite.scala |   7 +-
 .../graph/ConnectInvalidPipelineSuite.scala        |  53 +--
 .../graph/ConnectValidPipelineSuite.scala          |   4 +-
 .../pipelines/graph/MaterializeTablesSuite.scala   | 408 ++++++++++++++++++++-
 .../sql/pipelines/graph/SqlPipelineSuite.scala     |  32 +-
 .../graph/TriggeredGraphExecutionSuite.scala       |   4 +-
 .../graph/UserSpecifiedSchemaValidationSuite.scala |  68 +++-
 .../pipelines/util/SchemaInferenceUtilsSuite.scala | 130 ++++++-
 .../utils/TestGraphRegistrationContext.scala       |   8 +-
 30 files changed, 1133 insertions(+), 212 deletions(-)

diff --git a/common/utils/src/main/resources/error/error-conditions.json 
b/common/utils/src/main/resources/error/error-conditions.json
index 867e2d9dae30..a575551475be 100644
--- a/common/utils/src/main/resources/error/error-conditions.json
+++ b/common/utils/src/main/resources/error/error-conditions.json
@@ -1247,6 +1247,14 @@
     ],
     "sqlState" : "KD009"
   },
+  "CONFLICTING_PIPELINE_FLOW_CASE_SENSITIVITY" : {
+    "message" : [
+      "The flows writing to table <tableName> do not agree on '<configKey>': 
<flowConfigurations>.",
+      "The effective value determines whether column names that differ only in 
case identify the same column, so the table's schema would depend on the order 
the flows are evaluated in.",
+      "Set '<configKey>' to the same value for every flow writing to this 
table."
+    ],
+    "sqlState" : "42KD9"
+  },
   "CONNECT" : {
     "message" : [
       "Generic Spark Connect error."
diff --git 
a/sql/connect/server/src/test/scala/org/apache/spark/sql/connect/pipelines/PythonPipelineSuite.scala
 
b/sql/connect/server/src/test/scala/org/apache/spark/sql/connect/pipelines/PythonPipelineSuite.scala
index 17ce101bf181..ba44051ee692 100644
--- 
a/sql/connect/server/src/test/scala/org/apache/spark/sql/connect/pipelines/PythonPipelineSuite.scala
+++ 
b/sql/connect/server/src/test/scala/org/apache/spark/sql/connect/pipelines/PythonPipelineSuite.scala
@@ -137,14 +137,16 @@ class PythonPipelineSuite
     TableIdentifier(catalog = Option("spark_catalog"), database = 
Option("default"), table = name)
   }
 
+  private def sessionCaseSensitive: Boolean = 
spark.sessionState.conf.caseSensitiveAnalysis
+
   test("basic") {
     val graph = buildGraph("""
         |@dp.table
         |def table1():
         |    return spark.readStream.format("rate").load()
         |""".stripMargin)
-      .resolve()
-      .validate()
+      .resolve(sessionCaseSensitive)
+      .validate(sessionCaseSensitive)
     assert(graph.flows.size == 1)
     assert(graph.tables.size == 1)
   }
@@ -302,7 +304,7 @@ class PythonPipelineSuite
         |def a():
         |  return spark.range(5)
         |""".stripMargin)
-    val resolvedGraph = graph.resolve().validate()
+    val resolvedGraph = 
graph.resolve(sessionCaseSensitive).validate(sessionCaseSensitive)
     assert(resolvedGraph.tables.size == 4)
     assert(resolvedGraph.resolvedFlows.size == 4)
   }
@@ -316,7 +318,7 @@ class PythonPipelineSuite
       |@dp.append_flow(target = "a")
       |def supplement():
       |  return spark.readStream.format("rate").load()
-      |""".stripMargin).resolve().validate()
+      
|""".stripMargin).resolve(sessionCaseSensitive).validate(sessionCaseSensitive)
 
     assert(graph.tables.map(_.identifier.table).toSet == Set("a"))
     assert(graph.resolvedFlows.size == 2)
@@ -377,7 +379,7 @@ class PythonPipelineSuite
       |@dp.table
       |def d():
       |  return spark.sql("SELECT * FROM STREAM src")
-      |""".stripMargin).resolve().validate()
+      
|""".stripMargin).resolve(sessionCaseSensitive).validate(sessionCaseSensitive)
 
     assert(
       graph.table.keySet == Set(
@@ -424,7 +426,7 @@ class PythonPipelineSuite
         |@dp.table
         |def e():
         |  return spark.sql("SELECT * FROM STREAM spark_catalog.default.src")
-        |""".stripMargin).resolve().validate()
+        
|""".stripMargin).resolve(sessionCaseSensitive).validate(sessionCaseSensitive)
 
     assert(
       graph.tables.map(_.identifier).toSet == Set(
@@ -466,7 +468,7 @@ class PythonPipelineSuite
         |@dp.table
         |def e():
         |  return spark.sql("SELECT * FROM STREAM src")
-        |""".stripMargin).resolve()
+        |""".stripMargin).resolve(sessionCaseSensitive)
 
     assert(graph.resolutionFailedFlows.size == 5)
     graph.resolutionFailedFlows.foreach { flow =>
@@ -496,7 +498,7 @@ class PythonPipelineSuite
         |@dp.table
         |def e():
         |  return spark.sql("SELECT * FROM STREAM spark_catalog.default.src")
-        |""".stripMargin).resolve()
+        |""".stripMargin).resolve(sessionCaseSensitive)
     assert(graph.resolutionFailedFlows.size == 5)
     graph.resolutionFailedFlows.foreach { flow =>
       
assert(flow.failure.head.getMessage.contains("[TABLE_OR_VIEW_NOT_FOUND]"))
@@ -517,7 +519,7 @@ class PythonPipelineSuite
         |@dp.materialized_view
         |def mv_from_read_table_df():
         |  return read_table_df
-        |""".stripMargin).resolve().validate()
+        
|""".stripMargin).resolve(sessionCaseSensitive).validate(sessionCaseSensitive)
 
     assert(
       graph.resolvedFlows.map(_.identifier).toSet == Set(
@@ -541,7 +543,7 @@ class PythonPipelineSuite
         |def mv_from_read_table_df():
         |  return read_table_df
         |
-        |""".stripMargin).resolve().validate()
+        
|""".stripMargin).resolve(sessionCaseSensitive).validate(sessionCaseSensitive)
     assert(
       graph.resolvedFlows.map(_.identifier).toSet == Set(
         graphIdentifier("mv_from_read_table_df"),
@@ -608,7 +610,7 @@ class PythonPipelineSuite
          |@dp.table(name = "schema_b.st_2")
          |def irrelevant_3():
          |  return spark.readStream.format("rate").load()
-         |""".stripMargin).resolve()
+         |""".stripMargin).resolve(sessionCaseSensitive)
 
     // validate these dataset are properly fully qualified
     assert(
@@ -659,7 +661,7 @@ class PythonPipelineSuite
            |@dp.table(name = "some_catalog.some_schema.st")
            |def irrelevant_2():
            |  return spark.readStream.format("rate").load()
-           |""".stripMargin).resolve()
+           |""".stripMargin).resolve(sessionCaseSensitive)
     }
     assert(graphTry.isSuccess)
     assert(
@@ -689,7 +691,7 @@ class PythonPipelineSuite
          |@dp.temporary_view(name= "view_3")
          |def irrelevant_2():
          |  return spark.read.table("view_1")
-         |""".stripMargin).resolve()
+         |""".stripMargin).resolve(sessionCaseSensitive)
     // views are temporary views, so they're not fully qualified.
     assert(
       Set("view_1", "view_2", "view_3").subsetOf(
@@ -725,7 +727,7 @@ class PythonPipelineSuite
            |@dp.append_flow(target = "default.a")
            |def supplement():
            |  return spark.readStream.format("rate").load()
-           |""".stripMargin).resolve().validate()
+           
|""".stripMargin).resolve(sessionCaseSensitive).validate(sessionCaseSensitive)
 
     assert(graph.tables.map(_.identifier) == Seq(graphIdentifier("a")))
     assert(
@@ -894,8 +896,8 @@ class PythonPipelineSuite
         |def table_with_string_schema():
         |    return spark.range(5).withColumn("name", lit("test"))
         |""".stripMargin)
-      .resolve()
-      .validate()
+      .resolve(sessionCaseSensitive)
+      .validate(sessionCaseSensitive)
 
     assert(graph.flows.size == 1)
     assert(graph.tables.size == 1)
@@ -917,8 +919,8 @@ class PythonPipelineSuite
         |def table_with_struct_schema():
         |    return spark.range(5).withColumn("name", lit("test"))
         |""".stripMargin)
-      .resolve()
-      .validate()
+      .resolve(sessionCaseSensitive)
+      .validate(sessionCaseSensitive)
 
     assert(graph.flows.size == 1)
     assert(graph.tables.size == 1)
@@ -936,9 +938,9 @@ class PythonPipelineSuite
         |def table_with_wrong_schema():
         |    return spark.range(5).withColumn("wrong_column", lit("test"))
         |""".stripMargin)
-      .resolve()
+      .resolve(sessionCaseSensitive)
 
-    val ex = intercept[AnalysisException] { graph.validate() }
+    val ex = intercept[AnalysisException] { 
graph.validate(sessionCaseSensitive) }
     assert(ex.getMessage.contains("has a user-specified schema that is 
incompatible"))
     assert(ex.getMessage.contains("table_with_wrong_schema"))
   }
@@ -955,9 +957,9 @@ class PythonPipelineSuite
         |def table_with_wrong_struct_schema():
         |    return spark.range(5).withColumn("different_column", lit("test"))
         |""".stripMargin)
-      .resolve()
+      .resolve(sessionCaseSensitive)
 
-    val ex = intercept[AnalysisException] { graph.validate() }
+    val ex = intercept[AnalysisException] { 
graph.validate(sessionCaseSensitive) }
     assert(ex.getMessage.contains("has a user-specified schema that is 
incompatible"))
     assert(ex.getMessage.contains("table_with_wrong_struct_schema"))
   }
@@ -1205,7 +1207,7 @@ class PythonPipelineSuite
         |    keys = ["value"],
         |    sequence_by = "timestamp",
         |)
-        |""".stripMargin).resolve()
+        |""".stripMargin).resolve(sessionCaseSensitive)
 
     val resolvedFlow = graph.resolvedFlow(graphIdentifier("target"))
     assert(resolvedFlow.inputs == Set(graphIdentifier("src")))
@@ -1234,7 +1236,8 @@ class PythonPipelineSuite
         |""".stripMargin,
       defaultCatalog = Some("my_catalog"),
       defaultDatabase = Some("my_db"),
-      setupSql = Some("CREATE NAMESPACE IF NOT EXISTS 
my_catalog.my_db")).resolve()
+      setupSql = Some("CREATE NAMESPACE IF NOT EXISTS my_catalog.my_db"))
+      .resolve(sessionCaseSensitive)
 
     val resolvedFlow =
       graph.resolvedFlow(TableIdentifier("target", Some("my_db"), 
Some("my_catalog")))
@@ -1257,7 +1260,7 @@ class PythonPipelineSuite
         |    keys = ["value"],
         |    sequence_by = "timestamp",
         |)
-        |""".stripMargin).resolve()
+        |""".stripMargin).resolve(sessionCaseSensitive)
 
     val targetIdent = TableIdentifier("target", Some("some_schema"), 
Some("some_catalog"))
     val srcIdent = TableIdentifier("src", Some("some_schema"), 
Some("some_catalog"))
@@ -1329,7 +1332,7 @@ class PythonPipelineSuite
         |    apply_as_deletes = "value % 2 = 0",
         |    column_list = ["value", "timestamp"],
         |)
-        |""".stripMargin).resolve().validate()
+        
|""".stripMargin).resolve(sessionCaseSensitive).validate(sessionCaseSensitive)
 
     val resolvedFlow = graph.resolvedFlow(graphIdentifier("target"))
     assert(resolvedFlow.isInstanceOf[AutoCdcMergeFlow])
diff --git 
a/sql/connect/server/src/test/scala/org/apache/spark/sql/connect/pipelines/SparkDeclarativePipelinesServerSuite.scala
 
b/sql/connect/server/src/test/scala/org/apache/spark/sql/connect/pipelines/SparkDeclarativePipelinesServerSuite.scala
index 7ff8504a3e6a..ec06d15ad8db 100644
--- 
a/sql/connect/server/src/test/scala/org/apache/spark/sql/connect/pipelines/SparkDeclarativePipelinesServerSuite.scala
+++ 
b/sql/connect/server/src/test/scala/org/apache/spark/sql/connect/pipelines/SparkDeclarativePipelinesServerSuite.scala
@@ -30,6 +30,9 @@ import org.apache.spark.sql.connect.service.{SessionKey, 
SparkConnectService}
 class SparkDeclarativePipelinesServerSuite
     extends SparkDeclarativePipelinesServerTest
     with Logging {
+
+  private def sessionCaseSensitive: Boolean = 
spark.sessionState.conf.caseSensitiveAnalysis
+
   test("CreateDataflowGraph request creates a new graph") {
     withRawBlockingStub { implicit stub =>
       assert(Option(createDataflowGraph(stub)).isDefined)
@@ -265,7 +268,7 @@ class SparkDeclarativePipelinesServerSuite
       val definition =
         
getDefaultSessionHolder.dataflowGraphRegistry.getDataflowGraphOrThrow(graphId)
 
-      val graph = definition.toDataflowGraph.resolve()
+      val graph = definition.toDataflowGraph.resolve(sessionCaseSensitive)
 
       assert(graph.flows.size == 3)
       assert(graph.tables.size == 2)
@@ -312,7 +315,7 @@ class SparkDeclarativePipelinesServerSuite
 
       registerPipelineOutputs(pipeline)
       val graph = definition.toDataflowGraph
-        .resolve()
+        .resolve(sessionCaseSensitive)
 
       assert(graph.flows.size == 3)
       assert(graph.tables.size == 2)
diff --git 
a/sql/pipelines/src/main/scala/org/apache/spark/sql/pipelines/autocdc/ChangeArgs.scala
 
b/sql/pipelines/src/main/scala/org/apache/spark/sql/pipelines/autocdc/ChangeArgs.scala
index a8c3485da31c..fe28decab357 100644
--- 
a/sql/pipelines/src/main/scala/org/apache/spark/sql/pipelines/autocdc/ChangeArgs.scala
+++ 
b/sql/pipelines/src/main/scala/org/apache/spark/sql/pipelines/autocdc/ChangeArgs.scala
@@ -74,9 +74,8 @@ object ColumnSelection {
    * @param columnSelection The user-provided selection. `None` is a no-op and 
returns `schema`
    *                        unchanged.
    * @param resolver        Determines whether two column names are considered 
equal. Callers
-   *                        should pass the session resolver, e.g.
-   *                        `session.sessionState.conf.resolver`, so column 
matching stays
-   *                        consistent with `spark.sql.caseSensitive`.
+   *                        should pass the resolver for the effective 
`spark.sql.caseSensitive`
+   *                        of the operation being validated.
    */
   def applyToSchema(
       schemaName: String,
diff --git 
a/sql/pipelines/src/main/scala/org/apache/spark/sql/pipelines/graph/AutoCdcAuxiliaryTable.scala
 
b/sql/pipelines/src/main/scala/org/apache/spark/sql/pipelines/graph/AutoCdcAuxiliaryTable.scala
index 9251501887a3..da5b7eb8ad12 100644
--- 
a/sql/pipelines/src/main/scala/org/apache/spark/sql/pipelines/graph/AutoCdcAuxiliaryTable.scala
+++ 
b/sql/pipelines/src/main/scala/org/apache/spark/sql/pipelines/graph/AutoCdcAuxiliaryTable.scala
@@ -143,7 +143,7 @@ object AutoCdcAuxiliaryTable {
   ): AuxiliaryTableSpec = {
     val scd1AuxiliaryTableIdentifier = identifier(targetTable.identifier)
 
-    val resolver = inputAutoCdcFlow.df.sparkSession.sessionState.conf.resolver
+    val resolver = inputAutoCdcFlow.effectiveResolver
     val autoCdcKeyColumnNames = inputAutoCdcFlow.changeArgs.keys.map(_.name)
 
     // The auxiliary table should derive its schema from the exact same 
key/CDC metadata column
@@ -217,7 +217,7 @@ object AutoCdcAuxiliaryTable {
   ): AuxiliaryTableSpec = {
     val scd2AuxiliaryTableIdentifier = identifier(targetTable.identifier)
 
-    val resolver = inputAutoCdcFlow.df.sparkSession.sessionState.conf.resolver
+    val resolver = inputAutoCdcFlow.effectiveResolver
     val autoCdcKeyColumnNames = inputAutoCdcFlow.changeArgs.keys.map(_.name)
 
     // Resolve the key fields from the (evolved) target schema, exactly as 
SCD1 does, so the
@@ -283,7 +283,7 @@ object AutoCdcAuxiliaryTable {
    *
    * @param targetTableSchema the AutoCDC target's evolved schema to resolve 
against
    * @param fieldName the column name to resolve
-   * @param resolver the session resolver used for case-sensitivity-aware 
field lookups
+   * @param resolver the effective resolver used for case-sensitivity-aware 
field lookups
    * @param targetTableIdentifier the AutoCDC target's identifier, named in 
the error message
    * @param autoCdcFlowIdentifier the AutoCDC flow writing to the target, 
named in the error message
    * @return the matching field
@@ -415,7 +415,7 @@ object AutoCdcAuxiliaryTable {
    * @param expectedScdType the SCD type of the incoming AutoCDC flow, which 
determines which inner
    *                        `_cdc_metadata` field carries the recorded 
sequencing type.
    * @param expectedSequencingType the resolved sequencing type of the 
incoming AutoCDC flow.
-   * @param resolver the session resolver, used to match the reserved column 
and inner field names
+   * @param resolver the effective resolver, used to match the reserved column 
and inner field names
    *                 the same case-aware way as every other schema lookup in 
this file.
    */
   private[graph] def validateNoTargetSequencingTypeDrift(
diff --git 
a/sql/pipelines/src/main/scala/org/apache/spark/sql/pipelines/graph/CoreDataflowNodeProcessor.scala
 
b/sql/pipelines/src/main/scala/org/apache/spark/sql/pipelines/graph/CoreDataflowNodeProcessor.scala
index 66f2995ee02d..affa4725a2bf 100644
--- 
a/sql/pipelines/src/main/scala/org/apache/spark/sql/pipelines/graph/CoreDataflowNodeProcessor.scala
+++ 
b/sql/pipelines/src/main/scala/org/apache/spark/sql/pipelines/graph/CoreDataflowNodeProcessor.scala
@@ -32,9 +32,9 @@ import 
org.apache.spark.sql.pipelines.graph.DataflowGraphTransformer.{
  * Processor that is responsible for analyzing each flow and sort the nodes in
  * topological order
  */
-class CoreDataflowNodeProcessor(rawGraph: DataflowGraph) {
+class CoreDataflowNodeProcessor(rawGraph: DataflowGraph, sessionCaseSensitive: 
Boolean) {
 
-  private val flowResolver = new FlowResolver(rawGraph)
+  private val flowResolver = new FlowResolver(rawGraph, sessionCaseSensitive)
 
   // Map of input identifier to resolved [[Input]].
   private val resolvedInputs = new ConcurrentHashMap[TableIdentifier, Input]()
@@ -86,7 +86,8 @@ class CoreDataflowNodeProcessor(rawGraph: DataflowGraph) {
           identifier = table.identifier,
           specifiedSchema = table.specifiedSchema,
           incomingFlowIdentifiers = flowsToTable.map(_.identifier).toSet,
-          availableFlows = resolvedFlowsToTable
+          availableFlows = resolvedFlowsToTable,
+          sessionCaseSensitive = sessionCaseSensitive
         )
         resolvedInputs.put(table.identifier, virtualTableInput)
         Seq(table)
@@ -110,7 +111,7 @@ class CoreDataflowNodeProcessor(rawGraph: DataflowGraph) {
   }
 }
 
-private class FlowResolver(rawGraph: DataflowGraph) {
+private class FlowResolver(rawGraph: DataflowGraph, sessionCaseSensitive: 
Boolean) {
 
   /** Helper used to track which confs were set by which flows. */
   private case class FlowConf(key: String, value: String, flowIdentifier: 
TableIdentifier)
@@ -203,7 +204,7 @@ private class FlowResolver(rawGraph: DataflowGraph) {
       flow: UnresolvedFlow,
       funcResult: FlowFunctionResult): ResolvedFlow = {
     flow match {
-      case acf: AutoCdcFlow => new AutoCdcMergeFlow(acf, funcResult)
+      case acf: AutoCdcFlow => new AutoCdcMergeFlow(acf, funcResult, 
sessionCaseSensitive)
       case utf: UntypedFlow => transformUntypedFlowToResolvedFlow(utf, 
funcResult)
     }
   }
diff --git 
a/sql/pipelines/src/main/scala/org/apache/spark/sql/pipelines/graph/DataflowGraph.scala
 
b/sql/pipelines/src/main/scala/org/apache/spark/sql/pipelines/graph/DataflowGraph.scala
index c5210976d3f9..cd1dbf63ccdf 100644
--- 
a/sql/pipelines/src/main/scala/org/apache/spark/sql/pipelines/graph/DataflowGraph.scala
+++ 
b/sql/pipelines/src/main/scala/org/apache/spark/sql/pipelines/graph/DataflowGraph.scala
@@ -22,7 +22,7 @@ import scala.util.Try
 import org.apache.spark.sql.AnalysisException
 import org.apache.spark.sql.catalyst.TableIdentifier
 import org.apache.spark.sql.pipelines.graph.DataflowGraph.mapUnique
-import org.apache.spark.sql.pipelines.util.SchemaMergingUtils
+import org.apache.spark.sql.pipelines.util.SchemaInferenceUtils
 import org.apache.spark.sql.types.StructType
 
 /**
@@ -146,7 +146,9 @@ case class DataflowGraph(
    *                upstream flows
    * @return The reanalyzed flow
    */
-  protected[graph] def reanalyzeFlow(srcFlow: Flow): ResolvedFlow = {
+  protected[graph] def reanalyzeFlow(
+      srcFlow: Flow,
+      sessionCaseSensitive: Boolean): ResolvedFlow = {
     val upstreamDatasetIdentifiers = dfsInternal(
       flowNodes(srcFlow.identifier).output,
       downstream = false,
@@ -164,35 +166,46 @@ case class DataflowGraph(
       tables = table.get(srcFlow.destinationIdentifier).toSeq,
       sinks = sink.get(srcFlow.destinationIdentifier).toSeq
     )
-    subgraph.resolve().resolvedFlow(srcFlow.identifier)
+    subgraph.resolve(sessionCaseSensitive).resolvedFlow(srcFlow.identifier)
   }
 
   /**
    * A map of the inferred schema of each table, computed by merging the 
analyzed schemas
    * of all flows writing to that table.
+   *
+   * The merge honors the effective `spark.sql.caseSensitive` of the flows 
writing to each table:
+   * under case-insensitive analysis two flows emitting column names that 
differ only in case
+   * contribute a single column rather than both, which would otherwise 
produce a target schema the
+   * engine's own resolver cannot disambiguate. Which spelling survives such a 
fold is fixed by
+   * [[SchemaInferenceUtils.inferSchemaFromFlows]], which merges in sorted 
flow identifier order.
    */
-  lazy val inferredSchema: Map[TableIdentifier, StructType] = {
-    flowsTo.view.mapValues { flows =>
-      flows
-        .map { flow =>
-          resolvedFlow(flow.identifier).schema
-        }
-        .reduce(SchemaMergingUtils.mergeSchemas)
-    }.toMap
+  def inferSchemas(sessionCaseSensitive: Boolean): Map[TableIdentifier, 
StructType] = {
+    flowsTo.map { case (destinationIdentifier, flows) =>
+      val resolvedFlows = flows.map { flow =>
+        resolvedFlow(flow.identifier)
+      }
+      destinationIdentifier -> SchemaInferenceUtils.inferSchemaFromFlows(
+        tableIdentifier = destinationIdentifier,
+        flows = resolvedFlows,
+        userSpecifiedSchema = None,
+        sessionCaseSensitive = sessionCaseSensitive)
+    }
   }
 
   /**
    * The internal auxiliary tables owned by each destination [[Table]], 
derived from the resolved
-   * flows writing to it and that destination's [[inferredSchema]]. Keyed by 
the destination's
-   * identifier; only destinations that actually require auxiliary tables 
appear. Today only
-   * AutoCDC flow destination tables have an auxiliary table, and exactly one.
+   * flows writing to it and the destination schemas inferred from them. Keyed 
by the destination's
+   * identifier; only destinations that actually require auxiliary tables 
appear. Today only AutoCDC
+   * flow destination tables have an auxiliary table, and exactly one.
    *
    * Auxiliary tables are deliberately NOT part of the logical graph (they are 
never resolved,
    * connected, or exposed as [[Input]]s); this is purely a derived view used 
during dataset
    * materialization to create/evolve them alongside their owning table. The 
derivation is pure and
    * performs no catalog access.
    */
-  lazy val auxiliaryTableSpecs: Map[TableIdentifier, AuxiliaryTableSpec] = {
+  def auxiliaryTableSpecs(
+      inferredSchemas: Map[TableIdentifier, StructType]
+  ): Map[TableIdentifier, AuxiliaryTableSpec] = {
     resolvedFlowsTo.flatMap { case (destinationTableIdentifier, 
flowsToDestinationTable) =>
       table.get(destinationTableIdentifier).flatMap { destinationTable =>
         flowsToDestinationTable
@@ -203,7 +216,7 @@ case class DataflowGraph(
           .map { autoCdcFlow =>
             val spec = AutoCdcAuxiliaryTable.buildAuxiliaryTableSpecFor(
               targetTable = destinationTable,
-              targetTableSchema = inferredSchema(destinationTableIdentifier),
+              targetTableSchema = inferredSchemas(destinationTableIdentifier),
               inputAutoCdcFlow = autoCdcFlow
             )
             destinationTableIdentifier -> spec
@@ -213,22 +226,22 @@ case class DataflowGraph(
   }
 
   /** Ensure that the [[DataflowGraph]] is valid and throws errors if not. */
-  def validate(): DataflowGraph = {
-    validationFailure.toOption match {
+  def validate(sessionCaseSensitive: Boolean): DataflowGraph = {
+    validationFailure(sessionCaseSensitive).toOption match {
       case Some(exception) => throw exception
       case None => this
     }
   }
 
   /**
-   * Validate the current [[DataflowGraph]] and cache the validation failure.
+   * Validate the current [[DataflowGraph]] and return the validation failure, 
if one exists.
    *
    * To add more validations, add them in a helper function that throws an 
exception if the
    * validation fails, and invoke the helper function here.
    */
-  private lazy val validationFailure: Try[Throwable] = Try {
+  private def validationFailure(sessionCaseSensitive: Boolean): Try[Throwable] 
= Try {
     validateSuccessfulFlowAnalysis()
-    validateUserSpecifiedSchemas()
+    validateUserSpecifiedSchemas(sessionCaseSensitive)
     // Connecting the graph sorts it topologically
     validateGraphIsTopologicallySorted()
     validateMultiQueryTables()
@@ -236,7 +249,6 @@ case class DataflowGraph(
     validateEveryDatasetHasFlow()
     validateTablesAreResettable()
     validateFlowStreamingness()
-    inferredSchema
   }.failed
 
   /**
@@ -259,10 +271,12 @@ case class DataflowGraph(
   def resolved: Boolean =
     flows.forall(f => resolvedFlow.contains(f.identifier))
 
-  def resolve(): DataflowGraph =
+  def resolve(sessionCaseSensitive: Boolean): DataflowGraph =
     DataflowGraphTransformer.withDataflowGraphTransformer(this) { transformer 
=>
       val coreDataflowNodeProcessor =
-        new CoreDataflowNodeProcessor(rawGraph = this)
+        new CoreDataflowNodeProcessor(
+          rawGraph = this,
+          sessionCaseSensitive = sessionCaseSensitive)
       transformer
         .transformDownNodes(coreDataflowNodeProcessor.processNode)
         .getDataflowGraph
diff --git 
a/sql/pipelines/src/main/scala/org/apache/spark/sql/pipelines/graph/DatasetManager.scala
 
b/sql/pipelines/src/main/scala/org/apache/spark/sql/pipelines/graph/DatasetManager.scala
index cf5f811d3461..61d188f302e1 100644
--- 
a/sql/pipelines/src/main/scala/org/apache/spark/sql/pipelines/graph/DatasetManager.scala
+++ 
b/sql/pipelines/src/main/scala/org/apache/spark/sql/pipelines/graph/DatasetManager.scala
@@ -24,7 +24,7 @@ import org.apache.spark.SparkException
 import org.apache.spark.internal.{Logging, LogKeys}
 import org.apache.spark.sql.AnalysisException
 import org.apache.spark.sql.catalyst.TableIdentifier
-import org.apache.spark.sql.catalyst.analysis.{NoSuchTableException, 
PersistedView}
+import org.apache.spark.sql.catalyst.analysis.{NoSuchTableException, 
PersistedView, Resolver}
 import org.apache.spark.sql.classic.SparkSession
 import org.apache.spark.sql.connector.catalog.{
   CatalogV2Util,
@@ -39,9 +39,12 @@ import 
org.apache.spark.sql.connector.catalog.CatalogV2Util.v2ColumnsToStructTyp
 import org.apache.spark.sql.connector.expressions.{ClusterByTransform, 
Expressions, Transform}
 import org.apache.spark.sql.execution.command.CreateViewCommand
 import org.apache.spark.sql.pipelines.graph.QueryOrigin.ExceptionHelpers
-import org.apache.spark.sql.pipelines.util.PipelinesCatalogUtils
+import org.apache.spark.sql.pipelines.util.{
+  PipelinesCatalogUtils,
+  SchemaInferenceUtils,
+  SchemaMergingUtils
+}
 import org.apache.spark.sql.pipelines.util.SchemaInferenceUtils.diffSchemas
-import org.apache.spark.sql.pipelines.util.SchemaMergingUtils
 import org.apache.spark.sql.types.StructType
 
 /**
@@ -97,6 +100,9 @@ object DatasetManager extends Logging {
     val tablesToMaterialize = {
       tablesToMatz(resolvedDataflowGraph).map(t => t.table.identifier -> 
t).toMap
     }
+    val sessionCaseSensitive = 
context.spark.sessionState.conf.caseSensitiveAnalysis
+    val inferredSchemas = 
resolvedDataflowGraph.inferSchemas(sessionCaseSensitive)
+    val auxiliaryTableSpecs = 
resolvedDataflowGraph.auxiliaryTableSpecs(inferredSchemas)
 
     // materialized [[DataflowGraph]] where each table has been materialized 
and each table
     // has metadata (e.g., normalized table storage path) populated
@@ -112,8 +118,7 @@ object DatasetManager extends Logging {
                 // materializeAuxiliaryTable (which uses it to decide 
evolve-vs-create). Nothing
                 // between them mutates the auxiliary table, so a single load 
is safe and avoids a
                 // redundant catalog round trip.
-                val auxiliaryTableSpecOpt =
-                  
resolvedDataflowGraph.auxiliaryTableSpecs.get(table.identifier)
+                val auxiliaryTableSpecOpt = 
auxiliaryTableSpecs.get(table.identifier)
                 val existingAuxiliaryTable = auxiliaryTableSpecOpt.flatMap { 
spec =>
                   val (auxCatalog, auxId) =
                     PipelinesCatalogUtils.resolveTableCatalog(context.spark, 
spec.identifier)
@@ -122,6 +127,7 @@ object DatasetManager extends Logging {
                 val (tableWithMaterializationMetadata, catalogTableEntity) = 
materializeTable(
                   resolvedDataflowGraph = resolvedDataflowGraph,
                   table = table,
+                  inferredSchemas = inferredSchemas,
                   isFullRefresh = isFullRefresh,
                   auxiliaryTableSpecOpt = auxiliaryTableSpecOpt,
                   existingAuxiliaryTable = existingAuxiliaryTable,
@@ -149,6 +155,10 @@ object DatasetManager extends Logging {
                       auxiliaryTableSpec = auxiliaryTableSpec,
                       isFullRefresh = isFullRefresh,
                       existingAuxiliaryTable = existingAuxiliaryTable,
+                      // The auxiliary schema is derived from its target's, so 
it evolves under the
+                      // target's effective case sensitivity.
+                      caseSensitive = effectiveCaseSensitivityFor(
+                        resolvedDataflowGraph, table.identifier, context),
                       context = context
                     )
                 }
@@ -282,8 +292,9 @@ object DatasetManager extends Logging {
   /**
    * Materializes a table in the catalog. This method will create or update 
the table in the
    * catalog based on the given table and context.
-   * @param resolvedDataflowGraph The resolved [[DataflowGraph]] used to infer 
the table schema.
+   * @param resolvedDataflowGraph The resolved [[DataflowGraph]] used for 
table metadata.
    * @param table The table to be materialized.
+   * @param inferredSchemas The schemas inferred from the resolved graph, 
keyed by table.
    * @param isFullRefresh Whether this table should be full refreshed or not.
    * @param auxiliaryTableSpecOpt The spec for the auxiliary table (if this 
table has one)
    * @param existingAuxiliaryTable The already-loaded auxiliary table for this 
target (if it has one
@@ -296,6 +307,7 @@ object DatasetManager extends Logging {
   private def materializeTable(
       resolvedDataflowGraph: DataflowGraph,
       table: Table,
+      inferredSchemas: Map[TableIdentifier, StructType],
       isFullRefresh: Boolean,
       auxiliaryTableSpecOpt: Option[AuxiliaryTableSpec],
       existingAuxiliaryTable: Option[V2Table],
@@ -306,7 +318,7 @@ object DatasetManager extends Logging {
       PipelinesCatalogUtils.resolveTableCatalog(context.spark, 
table.identifier)
 
     val outputSchema = table.specifiedSchema.getOrElse(
-      resolvedDataflowGraph.inferredSchema(table.identifier).asNullable
+      inferredSchemas(table.identifier).asNullable
     )
     val mergedProperties = resolveTableProperties(table, identifier)
     val partitioning = 
table.partitionCols.toSeq.flatten.map(Expressions.identity)
@@ -354,6 +366,9 @@ object DatasetManager extends Logging {
     val autoCdcAuxTableSpecOpt = auxiliaryTableSpecOpt.collect {
         case autoCdcSpec: AutoCdcAuxiliaryTableSpec => autoCdcSpec
       }
+    val effectiveCaseSensitive = effectiveCaseSensitivityFor(
+      resolvedDataflowGraph, table.identifier, context)
+    val effectiveResolver = 
SchemaInferenceUtils.resolverFor(effectiveCaseSensitive)
 
     // For an incrementally-updated AutoCDC target, validate that the AutoCDC 
configuration recorded
     // on the auxiliary table has not drifted, BEFORE anything is created or 
evolved this run. These
@@ -365,7 +380,7 @@ object DatasetManager extends Logging {
     // Running here turns that into one clear drift error (remedy: full 
refresh).
     if (isTableIncrementallyUpdated) {
       autoCdcAuxTableSpecOpt.foreach {
-        validateNoAutoCdcAuxConfigDrift(_, existingAuxiliaryTable, context)
+        validateNoAutoCdcAuxConfigDrift(_, existingAuxiliaryTable, 
effectiveResolver)
       }
     }
 
@@ -387,7 +402,7 @@ object DatasetManager extends Logging {
               targetTableIdentifier = autoCdcSpec.targetTableIdentifier,
               expectedScdType = autoCdcSpec.expectedScdType,
               expectedSequencingType = autoCdcSpec.expectedSequencingType,
-              resolver = context.spark.sessionState.conf.resolver
+              resolver = effectiveResolver
             )
           }
         }
@@ -397,7 +412,8 @@ object DatasetManager extends Logging {
           existingTable = existingTable,
           desiredSchema = outputSchema,
           properties = mergedProperties,
-          mergeWithExistingSchema = isTableIncrementallyUpdated
+          mergeWithExistingSchema = isTableIncrementallyUpdated,
+          caseSensitive = effectiveCaseSensitive
         )
       case None =>
         createTable(
@@ -455,12 +471,15 @@ object DatasetManager extends Logging {
    * @param existingAuxiliaryTable the already-loaded auxiliary table (if it 
exists), loaded once by
    *                               the caller and shared with the config-drift 
validation in
    *                               [[materializeTable]] rather than re-loaded 
here.
+   * @param caseSensitive the effective case sensitivity of the flows writing 
to the auxiliary
+   *                      table's TARGET, whose schema the auxiliary schema is 
derived from.
    * @param context the context for the pipeline update.
    */
   private def materializeAuxiliaryTable(
       auxiliaryTableSpec: AuxiliaryTableSpec,
       isFullRefresh: Boolean,
       existingAuxiliaryTable: Option[V2Table],
+      caseSensitive: Boolean,
       context: PipelineUpdateContext): Unit = {
     // Get the DSv2 catalog handler and identifier for the aux table.
     val (catalog, auxiliaryTableIdentifier) =
@@ -511,7 +530,8 @@ object DatasetManager extends Logging {
             existingTable = existingAuxTable,
             desiredSchema = auxiliaryTableSpec.schema,
             properties = auxiliaryTableSpec.properties,
-            mergeWithExistingSchema = true
+            mergeWithExistingSchema = true,
+            caseSensitive = caseSensitive
           )
         case None =>
           createTable(
@@ -541,13 +561,12 @@ object DatasetManager extends Logging {
    * @param existingAuxiliaryTableOpt the already-loaded auxiliary table (if 
it exists), shared with
    *                                  the caller and 
[[materializeAuxiliaryTable]] to avoid a
    *                                  redundant load.
-   * @param context the context for the pipeline update.
+   * @param resolver the effective resolver of the flows writing to the 
AutoCDC target.
    */
   private def validateNoAutoCdcAuxConfigDrift(
       autoCdcSpec: AutoCdcAuxiliaryTableSpec,
       existingAuxiliaryTableOpt: Option[V2Table],
-      context: PipelineUpdateContext): Unit = {
-    val resolver = context.spark.sessionState.conf.resolver
+      resolver: Resolver): Unit = {
     existingAuxiliaryTableOpt.foreach { existingAuxiliaryTable =>
       AutoCdcAuxiliaryTable.validateNoKeyColumnDrift(
         existingAuxiliaryTable = existingAuxiliaryTable,
@@ -569,6 +588,23 @@ object DatasetManager extends Logging {
     }
   }
 
+  /**
+   * The effective `spark.sql.caseSensitive` for schema evolution of 
`tableIdentifier`, read from
+   * the flows writing to it rather than from the session, so evolution stays 
consistent with the
+   * flows whose schemas it is evolving (a pipeline-level `SET` never reaches 
the session). Fails if
+   * those flows disagree; see 
[[SchemaInferenceUtils.effectiveCaseSensitivity]].
+   */
+  private def effectiveCaseSensitivityFor(
+      resolvedDataflowGraph: DataflowGraph,
+      tableIdentifier: TableIdentifier,
+      context: PipelineUpdateContext): Boolean = {
+    SchemaInferenceUtils.effectiveCaseSensitivity(
+      tableIdentifier = tableIdentifier,
+      flows = resolvedDataflowGraph.flowsTo.getOrElse(tableIdentifier, 
Seq.empty),
+      sessionCaseSensitive = 
context.spark.sessionState.conf.caseSensitiveAnalysis
+    )
+  }
+
   /**
    * Loads the table at `identifier` from `catalog`, or `None` if it does not 
exist. A single
    * `loadTable` guarded by a `NoSuchTableException` catch, rather than a 
`tableExists` +
@@ -626,6 +662,15 @@ object DatasetManager extends Logging {
    * @param mergeWithExistingSchema whether the effective schema is the merge 
of the existing and
    *                                desired schemas (additive evolution) 
rather than the desired
    *                                schema as-is.
+   * @param caseSensitive           whether the additive schema merge treats 
field names differing
+   *                                only in case as distinct columns. Callers 
should pass the
+   *                                effective `spark.sql.caseSensitive` used 
to resolve the schema
+   *                                being evolved. When `false`, an incoming 
column differing from
+   *                                an existing one only in case is folded 
onto it rather than
+   *                                added as a duplicate. Only affects the 
merge (i.e.
+   *                                `mergeWithExistingSchema = true`); the 
subsequent diff always
+   *                                keys columns on their exact names, so a 
case-only rename on a
+   *                                non-merging path stays an explicit 
drop-then-add.
    */
   private def evolveTable(
       catalog: TableCatalog,
@@ -633,13 +678,18 @@ object DatasetManager extends Logging {
       existingTable: V2Table,
       desiredSchema: StructType,
       properties: Map[String, String],
-      mergeWithExistingSchema: Boolean): Unit = {
+      mergeWithExistingSchema: Boolean,
+      caseSensitive: Boolean): Unit = {
     val currentSchema = v2ColumnsToStructType(existingTable.columns())
     val targetSchema = if (mergeWithExistingSchema) {
-      SchemaMergingUtils.mergeSchemas(currentSchema, desiredSchema)
+      SchemaMergingUtils.mergeSchemas(currentSchema, desiredSchema, 
caseSensitive)
     } else {
       desiredSchema
     }
+    // `diffSchemas` keys column identity on exact field names. On the 
incremental path the merge
+    // above has already folded a case-only-differing incoming field onto the 
persisted one. On the
+    // non-merging paths (materialized views, full refresh), `targetSchema` is 
the declared schema
+    // as-is, where exact-name matching keeps a case-only rename visible as a 
schema change.
     val columnChanges = diffSchemas(currentSchema, targetSchema)
 
     val existingProperties = existingTable.properties()
diff --git 
a/sql/pipelines/src/main/scala/org/apache/spark/sql/pipelines/graph/Flow.scala 
b/sql/pipelines/src/main/scala/org/apache/spark/sql/pipelines/graph/Flow.scala
index 856fd6a3923c..a8be0a4c2ec3 100644
--- 
a/sql/pipelines/src/main/scala/org/apache/spark/sql/pipelines/graph/Flow.scala
+++ 
b/sql/pipelines/src/main/scala/org/apache/spark/sql/pipelines/graph/Flow.scala
@@ -23,6 +23,7 @@ import org.apache.spark.SparkException
 import org.apache.spark.internal.Logging
 import org.apache.spark.sql.{functions => F, AnalysisException, Column}
 import org.apache.spark.sql.catalyst.{AliasIdentifier, TableIdentifier}
+import org.apache.spark.sql.catalyst.analysis.Resolver
 import org.apache.spark.sql.classic.DataFrame
 import org.apache.spark.sql.pipelines.autocdc.{
   AutoCdcReservedNames,
@@ -33,6 +34,7 @@ import org.apache.spark.sql.pipelines.autocdc.{
   Scd2BatchProcessor,
   ScdType
 }
+import org.apache.spark.sql.pipelines.util.SchemaInferenceUtils
 import org.apache.spark.sql.types.{DataType, StructField, StructType}
 
 /**
@@ -250,8 +252,15 @@ class AppendOnceFlow(
  */
 class AutoCdcMergeFlow(
     val flow: AutoCdcFlow,
-    val funcResult: FlowFunctionResult
+    val funcResult: FlowFunctionResult,
+    sessionCaseSensitive: Boolean
 ) extends ResolvedFlow {
+  private[graph] val effectiveResolver: Resolver = 
SchemaInferenceUtils.resolverFor(
+    SchemaInferenceUtils.effectiveCaseSensitivity(
+      tableIdentifier = destinationIdentifier,
+      flows = Seq(this),
+      sessionCaseSensitive = sessionCaseSensitive))
+
   requireReservedPrefixAbsentInSourceColumns()
   requireReservedFrameworkColumnsAbsentInSourceColumns()
 
@@ -263,7 +272,7 @@ class AutoCdcMergeFlow(
       schemaName = "changeDataFeed",
       schema = df.schema,
       columnSelection = changeArgs.columnSelection,
-      resolver = spark.sessionState.conf.resolver
+      resolver = effectiveResolver
     )
     // AutoCDC flows require all key columns to be present in the 
user-selected source schema,
     // so that they survive into the target table where SCD reconciliation 
needs them.
@@ -303,7 +312,7 @@ class AutoCdcMergeFlow(
         Some(Scd2BatchProcessor.computeTrackedHistoryColumns(
           schema = userSelectedSchema,
           changeArgs = changeArgs,
-          resolver = spark.sessionState.conf.resolver))
+          resolver = effectiveResolver))
       case ScdType.Type1 => None
     }
 
@@ -405,7 +414,7 @@ class AutoCdcMergeFlow(
    * names that use the reserved Spark AutoCDC prefix.
    */
   private def requireReservedPrefixAbsentInSourceColumns(): Unit = {
-    val resolver = spark.sessionState.conf.resolver
+    val resolver = effectiveResolver
     val reservedPrefix = AutoCdcReservedNames.prefix
 
     def nameContainsReservedPrefix(name: String): Boolean = {
@@ -437,7 +446,7 @@ class AutoCdcMergeFlow(
    * during preprocessing. No-op for SCD1, which has no such columns.
    */
   private def requireReservedFrameworkColumnsAbsentInSourceColumns(): Unit = {
-    val resolver = spark.sessionState.conf.resolver
+    val resolver = effectiveResolver
     val reservedPrefix = AutoCdcReservedNames.prefix
 
     // Only the non-prefixed reserved names need checking here; prefixed ones 
are already rejected
@@ -469,7 +478,7 @@ class AutoCdcMergeFlow(
    * Validate all keys specified in changeArgs are actually present in the 
user-selected schema.
    */
   private def requireKeysPresentInSelectedSchema(selectedSchema: StructType): 
Unit = {
-    val resolver = spark.sessionState.conf.resolver
+    val resolver = effectiveResolver
 
     changeArgs.keys
       .find(key => !selectedSchema.fieldNames.exists(name => resolver(name, 
key.name)))
diff --git 
a/sql/pipelines/src/main/scala/org/apache/spark/sql/pipelines/graph/FlowExecution.scala
 
b/sql/pipelines/src/main/scala/org/apache/spark/sql/pipelines/graph/FlowExecution.scala
index c36d824bdf4a..c665c344c53b 100644
--- 
a/sql/pipelines/src/main/scala/org/apache/spark/sql/pipelines/graph/FlowExecution.scala
+++ 
b/sql/pipelines/src/main/scala/org/apache/spark/sql/pipelines/graph/FlowExecution.scala
@@ -105,6 +105,10 @@ trait FlowExecution {
   /** Context about this pipeline update. */
   def updateContext: PipelineUpdateContext
 
+  /** The session's `spark.sql.caseSensitive` fallback for resolving this 
flow. */
+  protected def sessionCaseSensitive: Boolean =
+    spark.sessionState.conf.caseSensitiveAnalysis
+
   /** The thread execution context for the current `FlowExecution`. */
   implicit val executionContext: ExecutionContext = {
     ExecutionContext.fromExecutor(FlowExecution.threadPool)
@@ -232,7 +236,7 @@ class StreamingTableWrite(
   override def getOrigin: QueryOrigin = flow.origin
 
   def startStream(): StreamingQuery = {
-    val data = graph.reanalyzeFlow(flow).df
+    val data = graph.reanalyzeFlow(flow, sessionCaseSensitive).df
     val dataStreamWriter = data
       .writeStream
       .queryName(displayName)
@@ -260,7 +264,7 @@ class BatchTableWrite(
   def executeInternal(): Future[Unit] = {
     SparkSessionUtils.withSqlConf(spark, sqlConf.toList: _*) {
       updateContext.flowProgressEventLogger.recordRunning(flow = flow)
-      val data = graph.reanalyzeFlow(flow).df
+      val data = graph.reanalyzeFlow(flow, sessionCaseSensitive).df
       Future {
         val dataFrameWriter = data.write
         destination.format.foreach(dataFrameWriter.format)
@@ -298,7 +302,7 @@ class SinkWrite(
   override def getOrigin: QueryOrigin = flow.origin
 
   def startStream(): StreamingQuery = {
-    val data = graph.reanalyzeFlow(flow).df
+    val data = graph.reanalyzeFlow(flow, sessionCaseSensitive).df
     data.writeStream
       .queryName(displayName)
       .option("checkpointLocation", checkpointPath)
@@ -328,7 +332,7 @@ class Scd1MergeStreamingWrite(
   override def getOrigin: QueryOrigin = flow.origin
 
   override def startStream(): StreamingQuery = {
-    val sourceChangeDataFeed = graph.reanalyzeFlow(flow).df
+    val sourceChangeDataFeed = graph.reanalyzeFlow(flow, 
sessionCaseSensitive).df
 
     // The auxiliary table is created and evolved during dataset 
materialization (see
     // [[DatasetManager]]), so it already exists by the time this flow 
executes; resolve its
@@ -373,7 +377,7 @@ class Scd2MergeStreamingWrite(
   override def getOrigin: QueryOrigin = flow.origin
 
   override def startStream(): StreamingQuery = {
-    val sourceChangeDataFeed = graph.reanalyzeFlow(flow).df
+    val sourceChangeDataFeed = graph.reanalyzeFlow(flow, 
sessionCaseSensitive).df
 
     // The auxiliary table is created and evolved during dataset 
materialization (see
     // [[DatasetManager]]), so it already exists by the time this flow 
executes; resolve its
diff --git 
a/sql/pipelines/src/main/scala/org/apache/spark/sql/pipelines/graph/GraphErrors.scala
 
b/sql/pipelines/src/main/scala/org/apache/spark/sql/pipelines/graph/GraphErrors.scala
index c835665a0f38..7bc26e6288c1 100644
--- 
a/sql/pipelines/src/main/scala/org/apache/spark/sql/pipelines/graph/GraphErrors.scala
+++ 
b/sql/pipelines/src/main/scala/org/apache/spark/sql/pipelines/graph/GraphErrors.scala
@@ -109,4 +109,33 @@ object GraphErrors {
       cause = Option(cause.orNull)
     )
   }
+
+  /**
+   * Throws if the flows writing to one table disagree on a configuration 
whose value determines
+   * how the table's schema is derived, so that the resulting schema would 
otherwise depend on the
+   * order the flows happen to be evaluated in.
+   *
+   * @param tableIdentifier the destination table the conflicting flows write 
to
+   * @param configKey the configuration the flows disagree on
+   * @param valuesByFlow the distinct values, each with the flows that 
declared it
+   */
+  def conflictingFlowConfigurationError(
+      tableIdentifier: TableIdentifier,
+      configKey: String,
+      valuesByFlow: Map[String, Seq[TableIdentifier]]): AnalysisException = {
+    val rendered = valuesByFlow.toSeq
+      .sortBy(_._1)
+      .map { case (value, flows) =>
+        s"$value (${flows.map(_.unquotedString).sorted.mkString(", ")})"
+      }
+      .mkString("; ")
+    new AnalysisException(
+      errorClass = "CONFLICTING_PIPELINE_FLOW_CASE_SENSITIVITY",
+      messageParameters = Map(
+        "tableName" -> tableIdentifier.unquotedString,
+        "configKey" -> configKey,
+        "flowConfigurations" -> rendered
+      )
+    )
+  }
 }
diff --git 
a/sql/pipelines/src/main/scala/org/apache/spark/sql/pipelines/graph/GraphValidations.scala
 
b/sql/pipelines/src/main/scala/org/apache/spark/sql/pipelines/graph/GraphValidations.scala
index ebe90b677aa2..5b4eaba4e709 100644
--- 
a/sql/pipelines/src/main/scala/org/apache/spark/sql/pipelines/graph/GraphValidations.scala
+++ 
b/sql/pipelines/src/main/scala/org/apache/spark/sql/pipelines/graph/GraphValidations.scala
@@ -252,7 +252,7 @@ trait GraphValidations extends Logging {
       }
   }
 
-  protected def validateUserSpecifiedSchemas(): Unit = {
+  protected def validateUserSpecifiedSchemas(sessionCaseSensitive: Boolean): 
Unit = {
     // Look up tables by their destination identifier, not by the flow's own 
identifier. The two
     // coincide only for an implicit/default flow (whose identifier equals its 
destination
     // table's); for a named flow (e.g. `CREATE FLOW <name> AS AUTO CDC INTO 
<target>`) they
@@ -262,8 +262,10 @@ trait GraphValidations extends Logging {
       // schema of all incoming flows. This must be equivalent to the declared 
schema.
       val inferredSchema = SchemaInferenceUtils
         .inferSchemaFromFlows(
+          tableIdentifier = t.identifier,
           flowsTo(t.identifier).map(f => resolvedFlow(f.identifier)),
-          userSpecifiedSchema = t.specifiedSchema
+          userSpecifiedSchema = t.specifiedSchema,
+          sessionCaseSensitive = sessionCaseSensitive
         )
 
       t.specifiedSchema.foreach { ss =>
diff --git 
a/sql/pipelines/src/main/scala/org/apache/spark/sql/pipelines/graph/PipelineExecution.scala
 
b/sql/pipelines/src/main/scala/org/apache/spark/sql/pipelines/graph/PipelineExecution.scala
index d35d701d44e5..724ccc56e35c 100644
--- 
a/sql/pipelines/src/main/scala/org/apache/spark/sql/pipelines/graph/PipelineExecution.scala
+++ 
b/sql/pipelines/src/main/scala/org/apache/spark/sql/pipelines/graph/PipelineExecution.scala
@@ -110,7 +110,8 @@ class PipelineExecution(context: PipelineUpdateContext) {
 
   private def resolveGraph(): DataflowGraph = {
     try {
-      context.unresolvedGraph.resolve().validate()
+      val sessionCaseSensitive = 
context.spark.sessionState.conf.caseSensitiveAnalysis
+      
context.unresolvedGraph.resolve(sessionCaseSensitive).validate(sessionCaseSensitive)
     } catch {
       case e: UnresolvedPipelineException =>
         handleInvalidPipeline(e)
diff --git 
a/sql/pipelines/src/main/scala/org/apache/spark/sql/pipelines/graph/elements.scala
 
b/sql/pipelines/src/main/scala/org/apache/spark/sql/pipelines/graph/elements.scala
index 885755fd78ec..baef25176e9a 100644
--- 
a/sql/pipelines/src/main/scala/org/apache/spark/sql/pipelines/graph/elements.scala
+++ 
b/sql/pipelines/src/main/scala/org/apache/spark/sql/pipelines/graph/elements.scala
@@ -160,12 +160,14 @@ case class Table(
  * @param identifier  The identifier of the parent table.
  * @param specifiedSchema The user-specified schema for the parent table.
  * @param incomingFlowIdentifiers The identifiers of all flows that write to 
the parent table.
+ * @param sessionCaseSensitive The session's `spark.sql.caseSensitive` 
fallback.
  * @param availableFlows  All resolved flows that write to the parent table.
  */
 case class VirtualTableInput(
     identifier: TableIdentifier,
     specifiedSchema: Option[StructType],
     incomingFlowIdentifiers: Set[TableIdentifier],
+    sessionCaseSensitive: Boolean,
     availableFlows: Seq[ResolvedFlow] = Nil
 ) extends TableElement with Input
     with Logging {
@@ -186,7 +188,11 @@ case class VirtualTableInput(
       // Otherwise infer the schema from a combination of the incoming flows 
and the
       // user-specified schema, if provided.
       case _ =>
-        SchemaInferenceUtils.inferSchemaFromFlows(availableFlows, 
specifiedSchema)
+        SchemaInferenceUtils.inferSchemaFromFlows(
+          tableIdentifier = identifier,
+          flows = availableFlows,
+          userSpecifiedSchema = specifiedSchema,
+          sessionCaseSensitive = sessionCaseSensitive)
     }
 
     // Produce either a streaming or batch dataframe, depending on whether 
this is a virtual
diff --git 
a/sql/pipelines/src/main/scala/org/apache/spark/sql/pipelines/util/SchemaInferenceUtils.scala
 
b/sql/pipelines/src/main/scala/org/apache/spark/sql/pipelines/util/SchemaInferenceUtils.scala
index 4777772342d7..10b3202a61af 100644
--- 
a/sql/pipelines/src/main/scala/org/apache/spark/sql/pipelines/util/SchemaInferenceUtils.scala
+++ 
b/sql/pipelines/src/main/scala/org/apache/spark/sql/pipelines/util/SchemaInferenceUtils.scala
@@ -20,14 +20,89 @@ package org.apache.spark.sql.pipelines.util
 import scala.util.control.NonFatal
 
 import org.apache.spark.sql.catalyst.TableIdentifier
+import org.apache.spark.sql.catalyst.analysis.{
+  caseInsensitiveResolution,
+  caseSensitiveResolution,
+  Resolver
+}
 import org.apache.spark.sql.connector.catalog.TableChange
+import org.apache.spark.sql.internal.SQLConf
 import org.apache.spark.sql.pipelines.common.DatasetType
-import org.apache.spark.sql.pipelines.graph.{GraphElementTypeUtils, 
GraphErrors, ResolvedFlow}
+import org.apache.spark.sql.pipelines.graph.{
+  Flow,
+  GraphElementTypeUtils,
+  GraphErrors,
+  ResolvedFlow
+}
 import org.apache.spark.sql.types.{StructField, StructType}
 
 
 object SchemaInferenceUtils {
 
+  def resolverFor(caseSensitive: Boolean): Resolver = {
+    if (caseSensitive) {
+      caseSensitiveResolution
+    } else {
+      caseInsensitiveResolution
+    }
+  }
+
+  /**
+   * The effective `spark.sql.caseSensitive` for schema derivation on 
`tableIdentifier`, taken from
+   * the flows writing to it rather than from the session.
+   *
+   * A pipeline can set `spark.sql.caseSensitive` for itself, and a `SET` in 
pipeline source does
+   * not touch the session: 
[[org.apache.spark.sql.pipelines.graph.GraphRegistrationContext]] folds
+   * it into each flow's `sqlConf`, and it is applied when the flow is 
analyzed and executed. Schema
+   * derivation therefore has to read it from the same place, or evolution can 
disagree with the
+   * flows whose schemas it is deriving from -- e.g. folding an incoming 
`Value` onto a persisted
+   * `value` while the flow, resolving case-sensitively, expects `Value` to be 
its own column.
+   *
+   * All flows writing to a table must agree: the value decides whether names 
differing only in case
+   * identify the same column, so a disagreement would make the resulting 
schema depend on the order
+   * the flows are evaluated in. Throws
+   * 
[[org.apache.spark.sql.pipelines.graph.GraphErrors.conflictingFlowConfigurationError]]
 if they
+   * disagree. Flows that do not set it at all inherit the session's value.
+   */
+  def effectiveCaseSensitivity(
+      tableIdentifier: TableIdentifier,
+      flows: Seq[Flow],
+      sessionCaseSensitive: Boolean): Boolean = {
+    val declaredByFlow = flows.flatMap { flow =>
+      flow.sqlConf.get(SQLConf.CASE_SENSITIVE.key).map(value => value -> 
flow.identifier)
+    }
+    if (declaredByFlow.isEmpty) {
+      return sessionCaseSensitive
+    }
+
+    // Compare the parsed booleans, so that e.g. "TRUE" and "true" are not 
reported as a conflict,
+    // but report the values as written to keep the error recognizable to the 
user.
+    val byParsedValue = declaredByFlow.groupBy { case (value, _) => 
value.trim.toBoolean }
+    // A flow that leaves the conf unset inherits the session value, which 
conflicts just as much as
+    // an explicitly opposite value.
+    val flowsWithoutDeclaration = flows.filterNot { flow =>
+      flow.sqlConf.contains(SQLConf.CASE_SENSITIVE.key)
+    }
+    val effectiveValues = byParsedValue.keySet ++
+      Option.when(flowsWithoutDeclaration.nonEmpty)(sessionCaseSensitive)
+    if (effectiveValues.sizeIs > 1) {
+      val valuesByFlow = declaredByFlow
+        .groupBy { case (value, _) => value }
+        .map { case (value, entries) => value -> entries.map { case (_, id) => 
id } } ++
+        Option
+          .when(flowsWithoutDeclaration.nonEmpty)(
+            s"$sessionCaseSensitive (session default)" -> 
flowsWithoutDeclaration.map(_.identifier)
+          )
+          .toMap
+      throw GraphErrors.conflictingFlowConfigurationError(
+        tableIdentifier = tableIdentifier,
+        configKey = SQLConf.CASE_SENSITIVE.key,
+        valuesByFlow = valuesByFlow
+      )
+    }
+    effectiveValues.head
+  }
+
   /**
    * Given a set of flows that write to the same destination and possibly a 
user-specified schema,
    * we infer the schema of the destination dataset. The logic is as follows:
@@ -39,42 +114,68 @@ object SchemaInferenceUtils {
    *    The user-specified schema will take precedence over the inferred 
schema.
    * Returns an error if encountered during schema inference or merging the 
inferred schema with
    * the user-specified one.
+   *
+   * All merges honor the effective `spark.sql.caseSensitive` of the flows 
writing to
+   * `tableIdentifier`, falling back to `sessionCaseSensitive` for flows that 
do not set it. Under
+   * case-insensitive analysis, flows emitting column names that differ only 
in case contribute a
+   * single column, and a declared column matches a flow column differing only 
in case -- consistent
+   * with how the rest of the engine resolves those names.
+   *
+   * When flows differ only in column casing, the surviving spelling is the 
one from the flow with
+   * the lowest identifier: `flows` is merged in sorted identifier order, not 
in the order given.
+   * Sorting here rather than at the call sites keeps every caller agreeing on 
the result, since the
+   * schemas they derive are compared against each other -- the graph's 
inferred schema materializes
+   * the table, while 
[[org.apache.spark.sql.pipelines.graph.VirtualTableInput]] produces the schema
+   * downstream flows resolve against, and `diffSchemas` keys column identity 
on the exact name. Two
+   * callers ordering the same flows differently would spell one column two 
ways, leaving a
+   * downstream view disagreeing with its source and turning the next refresh 
into a drop-then-add.
    */
   def inferSchemaFromFlows(
+      tableIdentifier: TableIdentifier,
       flows: Seq[ResolvedFlow],
-      userSpecifiedSchema: Option[StructType]): StructType = {
+      userSpecifiedSchema: Option[StructType],
+      sessionCaseSensitive: Boolean): StructType = {
     if (flows.isEmpty) {
       return userSpecifiedSchema.getOrElse(new StructType())
     }
 
     require(
-      flows.forall(_.destinationIdentifier == 
flows.head.destinationIdentifier),
+      flows.forall(_.destinationIdentifier == tableIdentifier),
       "Expected all flows to have the same destination"
     )
 
-    val inferredSchema = flows.map(_.schema).fold(new StructType()) { 
(schemaSoFar, schema) =>
-      try {
-        SchemaMergingUtils.mergeSchemas(schemaSoFar, schema)
-      } catch {
-        case NonFatal(e) =>
-          throw GraphErrors.unableToInferSchemaError(
-            flows.head.destinationIdentifier,
-            schemaSoFar,
-            schema,
-            cause = Option(e)
-          )
+    val caseSensitive = effectiveCaseSensitivity(
+      tableIdentifier = tableIdentifier,
+      flows = flows,
+      sessionCaseSensitive = sessionCaseSensitive
+    )
+
+    val inferredSchema = flows
+      .sortBy(_.identifier.unquotedString)
+      .map(_.schema)
+      .fold(new StructType()) { (schemaSoFar, schema) =>
+        try {
+          SchemaMergingUtils.mergeSchemas(schemaSoFar, schema, caseSensitive)
+        } catch {
+          case NonFatal(e) =>
+            throw GraphErrors.unableToInferSchemaError(
+              tableIdentifier,
+              schemaSoFar,
+              schema,
+              cause = Option(e)
+            )
+        }
       }
-    }
 
-    val identifier = flows.head.destinationIdentifier
     val datasetType = 
GraphElementTypeUtils.getDatasetTypeForMaterializedViewOrStreamingTable(flows)
     // We merge the inferred schema with the user-specified schema to pick up 
any schema metadata
     // that is provided by the user, e.g., comments or column masks.
     mergeInferredAndUserSchemasIfNeeded(
-      identifier,
+      tableIdentifier,
       datasetType,
       inferredSchema,
-      userSpecifiedSchema
+      userSpecifiedSchema,
+      caseSensitive
     )
   }
 
@@ -82,12 +183,13 @@ object SchemaInferenceUtils {
       tableIdentifier: TableIdentifier,
       datasetType: DatasetType,
       inferredSchema: StructType,
-      userSpecifiedSchema: Option[StructType]): StructType = {
+      userSpecifiedSchema: Option[StructType],
+      caseSensitive: Boolean): StructType = {
     userSpecifiedSchema match {
       case Some(userSpecifiedSchema) =>
         try {
           // Merge the inferred schema with the user-provided schema hint
-          SchemaMergingUtils.mergeSchemas(userSpecifiedSchema, inferredSchema)
+          SchemaMergingUtils.mergeSchemas(userSpecifiedSchema, inferredSchema, 
caseSensitive)
         } catch {
           case NonFatal(e) =>
             throw GraphErrors.incompatibleUserSpecifiedAndInferredSchemasError(
@@ -110,6 +212,15 @@ object SchemaInferenceUtils {
    * 1. New columns that need to be added
    * 2. Existing columns that need type updates
    *
+   * Column identity is keyed on the exact field name, not on a 
case-normalized one. On the
+   * incremental streaming-table path, `targetSchema` is the merge of the 
current and desired
+   * schemas, and [[SchemaMergingUtils.mergeSchemas]] has already folded an 
incoming
+   * case-only-differing field onto the persisted one. On the non-merging 
paths (materialized views
+   * and any full refresh), `targetSchema` is the run's declared schema as-is, 
so exact-name keying
+   * keeps a case-only rename visible as an explicit drop-then-add.
+   * Exact keying also avoids silently collapsing two genuinely distinct 
declared columns that
+   * differ only in case (`value` and `Value`) into an arbitrary one of the 
two.
+   *
    * @param currentSchema The current schema of the table
    * @param targetSchema The target schema that we want the table to have
    * @return A sequence of TableChange objects representing the necessary 
changes
diff --git 
a/sql/pipelines/src/main/scala/org/apache/spark/sql/pipelines/util/SchemaMergingUtils.scala
 
b/sql/pipelines/src/main/scala/org/apache/spark/sql/pipelines/util/SchemaMergingUtils.scala
index d15e7ac6425c..aeab5e623431 100644
--- 
a/sql/pipelines/src/main/scala/org/apache/spark/sql/pipelines/util/SchemaMergingUtils.scala
+++ 
b/sql/pipelines/src/main/scala/org/apache/spark/sql/pipelines/util/SchemaMergingUtils.scala
@@ -20,7 +20,26 @@ package org.apache.spark.sql.pipelines.util
 import org.apache.spark.sql.types.StructType
 
 object SchemaMergingUtils {
-  def mergeSchemas(tableSchema: StructType, dataSchema: StructType): 
StructType = {
-    StructType.merge(tableSchema, dataSchema).asInstanceOf[StructType]
+
+  /**
+   * Additively merges `dataSchema` into `tableSchema`, returning a schema 
that is the union of the
+   * two (recursing into nested structs/arrays). On a field present in both, 
`tableSchema`'s name
+   * and position win; `dataSchema` only contributes fields absent from 
`tableSchema`.
+   *
+   * @param caseSensitive whether two field names that differ only in case are 
considered distinct.
+   *                      When `false` (mirroring a case-insensitive session), 
`dataSchema`'s field
+   *                      is folded onto the matching `tableSchema` field 
rather than added as a
+   *                      separate, case-differing column. Deliberately has no 
default: every caller
+   *                      merges schemas that some pipeline will later resolve 
names against, so the
+   *                      choice belongs to the caller and should be visible 
at the call site rather
+   *                      than silently inherited. Callers should pass the 
effective
+   *                      `spark.sql.caseSensitive` of the flows involved (see
+   *                      [[SchemaInferenceUtils.effectiveCaseSensitivity]]).
+   */
+  def mergeSchemas(
+      tableSchema: StructType,
+      dataSchema: StructType,
+      caseSensitive: Boolean): StructType = {
+    StructType.merge(tableSchema, dataSchema, 
caseSensitive).asInstanceOf[StructType]
   }
 }
diff --git 
a/sql/pipelines/src/test/scala/org/apache/spark/sql/pipelines/autocdc/AutoCdcFlowSuite.scala
 
b/sql/pipelines/src/test/scala/org/apache/spark/sql/pipelines/autocdc/AutoCdcFlowSuite.scala
index a3d26e6f47e5..4596e31d778a 100644
--- 
a/sql/pipelines/src/test/scala/org/apache/spark/sql/pipelines/autocdc/AutoCdcFlowSuite.scala
+++ 
b/sql/pipelines/src/test/scala/org/apache/spark/sql/pipelines/autocdc/AutoCdcFlowSuite.scala
@@ -176,7 +176,10 @@ class AutoCdcFlowSuite extends QueryTest with 
SharedSparkSession {
         trackHistorySelection = trackHistorySelection
       )
     )
-    new AutoCdcMergeFlow(flow, successfulFuncResult(sourceDf))
+    new AutoCdcMergeFlow(
+      flow,
+      successfulFuncResult(sourceDf),
+      spark.sessionState.conf.caseSensitiveAnalysis)
   }
 
   /** A stable 3-column source streaming dataframe used across most schema 
tests. */
diff --git 
a/sql/pipelines/src/test/scala/org/apache/spark/sql/pipelines/graph/AutoCdcConfigDriftSuite.scala
 
b/sql/pipelines/src/test/scala/org/apache/spark/sql/pipelines/graph/AutoCdcConfigDriftSuite.scala
index dda1e5a7455a..c26a5248488b 100644
--- 
a/sql/pipelines/src/test/scala/org/apache/spark/sql/pipelines/graph/AutoCdcConfigDriftSuite.scala
+++ 
b/sql/pipelines/src/test/scala/org/apache/spark/sql/pipelines/graph/AutoCdcConfigDriftSuite.scala
@@ -18,6 +18,7 @@
 package org.apache.spark.sql.pipelines.graph
 
 import org.apache.spark.sql.execution.streaming.runtime.MemoryStream
+import org.apache.spark.sql.internal.SQLConf
 import org.apache.spark.sql.pipelines.autocdc.{ColumnSelection, ScdType, 
UnqualifiedColumnName}
 import org.apache.spark.sql.pipelines.utils.{ExecutionTest, 
TestGraphRegistrationContext}
 import org.apache.spark.sql.test.SharedSparkSession
@@ -53,6 +54,28 @@ class AutoCdcConfigDriftSuite
   // Sequencing type drift
   // 
===========================================================================================
 
+  test("AutoCDC source validation uses pipeline case sensitivity, not session 
default") {
+    withSQLConf(SQLConf.CASE_SENSITIVE.key -> "false") {
+      val stream = MemoryStream[(Int, Long, Long)]
+      stream.addData((1, 1L, 1L))
+
+      val ctx = new TestGraphRegistrationContext(
+        spark,
+        Map(SQLConf.CASE_SENSITIVE.key -> "true")) {
+        registerTable("target", catalog = Some(catalog), database = 
Some(namespace))
+        registerFlow(autoCdcFlow(
+          name = "flow",
+          target = "target",
+          query = dfFlowFunc(stream.toDF().toDF("id", "version", 
"__start_at")),
+          keys = Seq("id"),
+          sequencing = $"version",
+          scdType = ScdType.Type2))
+      }
+
+      ctx.resolveToDataflowGraph()
+    }
+  }
+
   test("an SCD1 flow whose sequencing type differs from the recorded type 
triggers " +
     "SEQUENCING_TYPE_DRIFT") {
     spark.sql(
diff --git 
a/sql/pipelines/src/test/scala/org/apache/spark/sql/pipelines/graph/AutoCdcScd1AuxiliaryTableSpecSuite.scala
 
b/sql/pipelines/src/test/scala/org/apache/spark/sql/pipelines/graph/AutoCdcScd1AuxiliaryTableSpecSuite.scala
index 226480ea919a..6de8cf52f7b4 100644
--- 
a/sql/pipelines/src/test/scala/org/apache/spark/sql/pipelines/graph/AutoCdcScd1AuxiliaryTableSpecSuite.scala
+++ 
b/sql/pipelines/src/test/scala/org/apache/spark/sql/pipelines/graph/AutoCdcScd1AuxiliaryTableSpecSuite.scala
@@ -75,7 +75,9 @@ class AutoCdcScd1AuxiliaryTableSpecSuite extends PipelineTest 
with SharedSparkSe
         deleteCondition = None,
         storedAsScdType = ScdType.Type1)))
     val graph = ctx.resolveToDataflowGraph()
-    
graph.auxiliaryTableSpecs(targetIdentifier).asInstanceOf[AutoCdcAuxiliaryTableSpec]
+    val inferredSchemas = 
graph.inferSchemas(spark.sessionState.conf.caseSensitiveAnalysis)
+    graph.auxiliaryTableSpecs(inferredSchemas)(targetIdentifier)
+      .asInstanceOf[AutoCdcAuxiliaryTableSpec]
   }
 
   test("SCD1 aux schema is exactly the key columns plus the CDC metadata 
column") {
diff --git 
a/sql/pipelines/src/test/scala/org/apache/spark/sql/pipelines/graph/AutoCdcScd1KeyDriftSuite.scala
 
b/sql/pipelines/src/test/scala/org/apache/spark/sql/pipelines/graph/AutoCdcScd1KeyDriftSuite.scala
index 1d27b7d69dab..e180056761f2 100644
--- 
a/sql/pipelines/src/test/scala/org/apache/spark/sql/pipelines/graph/AutoCdcScd1KeyDriftSuite.scala
+++ 
b/sql/pipelines/src/test/scala/org/apache/spark/sql/pipelines/graph/AutoCdcScd1KeyDriftSuite.scala
@@ -307,6 +307,46 @@ class AutoCdcScd1KeyDriftSuite
     }
   }
 
+  test("AutoCDC key drift validation uses pipeline case sensitivity, not 
session default") {
+    spark.sql(
+      s"CREATE TABLE $catalog.$namespace.target " +
+      s"(id INT NOT NULL, version BIGINT NOT NULL, $scd1MetadataDdl)"
+    )
+
+    val stream1 = MemoryStream[(Int, Long)]
+    stream1.addData((1, 1L))
+    runPipeline(buildPipeline("flow_v1", stream1.toDF().toDF("id", "version"), 
Seq("id")))
+
+    withSQLConf(SQLConf.CASE_SENSITIVE.key -> "false") {
+      val stream2 = MemoryStream[(Int, Long)]
+      stream2.addData((1, 2L))
+      val ctx2 = new TestGraphRegistrationContext(
+        spark,
+        Map(SQLConf.CASE_SENSITIVE.key -> "true")) {
+        registerTable("target", catalog = Some(catalog), database = 
Some(namespace))
+        registerFlow(autoCdcFlow(
+          name = "flow_v2",
+          target = "target",
+          query = dfFlowFunc(stream2.toDF().toDF("Id", "version")),
+          keys = Seq("Id"),
+          sequencing = $"version"))
+      }
+
+      val ex = intercept[RuntimeException] { runPipeline(ctx2) }
+      checkErrorInPipelineFailure(
+        failure = ex,
+        condition = "AUTOCDC_INVALID_STATE.KEY_SCHEMA_DRIFT",
+        sqlState = Some("42000"),
+        parameters = Map(
+          "tableName" ->
+            fullyQualifiedIdentifier("target", Some(catalog), 
Some(namespace)).unquotedString,
+          "expectedKeySchema" -> "Id INT NOT NULL",
+          "recordedKeySchema" -> "id INT NOT NULL"
+        )
+      )
+    }
+  }
+
   test("under the default (case-insensitive) resolver, an AutoCDC flow whose 
key differs only " +
     "in case from the recorded key does NOT trigger drift") {
     // Pairs with the case-sensitive test above: same recorded key, but under 
the default
@@ -315,11 +355,11 @@ class AutoCdcScd1KeyDriftSuite
     // case-sensitive resolver in the validator is caught.
     //
     // Note that only the *key declaration* (`Seq("Id")`) has different casing 
here -- the
-    // source DF column name still matches the target's `id` exactly. 
Differing the source DF
-    // column casing as well would not exercise drift: 
[[SchemaMergingUtils.mergeSchemas]] is
-    // case-sensitive on column names and would add `Id` as a new column to 
the target,
-    // producing AMBIGUOUS_REFERENCE during the streaming write rather than 
letting drift
-    // validation make the call.
+    // source DF column name still matches the target's `id` exactly. This 
keeps the test focused
+    // on the drift validator: whether the source DF column were `id` or `Id`, 
under the default
+    // (case-insensitive) resolver schema evolution folds it onto the existing 
`id` (SPARK-58517),
+    // so the streaming write itself would not fail either way and drift 
validation remains the
+    // sole decision-maker.
     spark.sql(
       s"CREATE TABLE $catalog.$namespace.target " +
       s"(id INT NOT NULL, version BIGINT NOT NULL, $scd1MetadataDdl)"
diff --git 
a/sql/pipelines/src/test/scala/org/apache/spark/sql/pipelines/graph/AutoCdcScd1SchemaEvolutionSuite.scala
 
b/sql/pipelines/src/test/scala/org/apache/spark/sql/pipelines/graph/AutoCdcScd1SchemaEvolutionSuite.scala
index 635e3d93de56..1a626a6ddcdf 100644
--- 
a/sql/pipelines/src/test/scala/org/apache/spark/sql/pipelines/graph/AutoCdcScd1SchemaEvolutionSuite.scala
+++ 
b/sql/pipelines/src/test/scala/org/apache/spark/sql/pipelines/graph/AutoCdcScd1SchemaEvolutionSuite.scala
@@ -34,8 +34,9 @@ import org.apache.spark.sql.test.SharedSparkSession
 /**
  * Tests covering AutoCDC's interaction with non-key schema evolution across 
pipeline runs. The
  * suite documents the supported additive cases (new top-level columns, new 
nested fields in
- * array-of-struct, broadening / narrowing column selection) and the cases 
that fail loudly
- * today (subtractive nested evolution, type-incompatible changes, case-only 
renames).
+ * array-of-struct, broadening / narrowing column selection, and -- under 
case-insensitive
+ * resolution -- a source column differing from an existing one only in case) 
and the cases that
+ * fail loudly today (subtractive nested evolution, type-incompatible changes).
  *
  * These behaviors are largely inherited from the lower layers 
(`SchemaMergingUtils` for
  * schema merge, the v2 writer's column-resolution layer for nested-field 
handling) rather
@@ -585,18 +586,18 @@ class AutoCdcScd1SchemaEvolutionSuite
     )
   }
 
-  test("a source DF column whose name differs from the target only by case 
fails with " +
-    "AMBIGUOUS_REFERENCE under case-insensitive resolution") {
+  test("a source DF column whose name differs from the target only by case is 
folded onto the " +
+    "existing column under case-insensitive resolution") {
     val session = spark
     import session.implicits._
 
-    // `DatasetManager`'s schema-merge compares the existing target schema and 
the flow's
-    // output schema *case-sensitively*: `SchemaMergingUtils.mergeSchemas` 
calls
-    // `StructType.merge` without forwarding the session-level 
case-sensitivity. When the
-    // target has `value` and the source DF emits `Value`, the merged schema 
ends up with
-    // both as separate columns. Reference resolution downstream is 
case-insensitive
-    // (Spark's default), so the MERGE plan trips on the duplicate and reports
-    // AMBIGUOUS_REFERENCE.
+    // Under case-insensitive resolution (Spark's default), a target `value` 
and a source `Value`
+    // are the same column. Schema evolution honors that by threading 
case-sensitivity into
+    // `SchemaMergingUtils.mergeSchemas`: the merge maps `Value` onto the 
existing `value`, so
+    // `diffSchemas` has no case-only difference left to process. No second 
column is added, and
+    // the write succeeds. (Before SPARK-58517 the merge ran case-sensitively 
regardless of the
+    // session and added a duplicate `Value` column, after which the 
case-insensitive MERGE plan
+    // tripped on the ambiguous reference.)
     withSQLConf(SQLConf.CASE_SENSITIVE.key -> "false") {
       spark.sql(
         s"CREATE TABLE $catalog.$namespace.target " +
@@ -605,8 +606,7 @@ class AutoCdcScd1SchemaEvolutionSuite
 
       val stream = MemoryStream[(Int, Long, String)]
       stream.addData((1, 1L, "alice"))
-      // Source DF emits `Value` (capital), differing only in case from the 
target's
-      // `value` column.
+      // Source DF emits `Value` (capital), differing only in case from the 
target's `value` column.
       val df = stream.toDF().toDF("key", "version", "Value")
       val ctx = singleAutoCdcFlowPipeline(
         flowName = "auto_cdc_flow",
@@ -615,24 +615,18 @@ class AutoCdcScd1SchemaEvolutionSuite
         keys = Seq("key"),
         sequencing = functions.col("version"))
 
-      val ex = intercept[RuntimeException] { runPipeline(ctx) }
-      // The exact `name` and `referenceNames` parameters depend on internal 
merge-plan
-      // synthesis; the condition match is the meaningful invariant for this 
test.
-      checkErrorInPipelineFailure(
-        failure = ex,
-        condition = "AMBIGUOUS_REFERENCE",
-        parameters = Map(
-          "name" -> ".*",
-          "referenceNames" -> ".*"
-        ),
-        matchPVals = true,
-        queryContext = Array(
-          ExpectedContext(
-            fragment = s"`$catalog`.`$namespace`.`target`.`Value`",
-            start = 0,
-            stop = 27
-          )
-        )
+      runPipeline(ctx)
+
+      // The target schema is unchanged (still a single `value` column, 
original case), and the
+      // row lands with the emitted value folded into it.
+      assert(
+        spark.table(s"$catalog.$namespace.target").schema.fieldNames.toSeq ===
+          Seq("key", "version", "value", 
AutoCdcReservedNames.cdcMetadataColName),
+        "the target should keep its single `value` column, not gain a `Value` 
column"
+      )
+      checkAnswer(
+        spark.table(s"$catalog.$namespace.target"),
+        Seq(Row(1, 1L, "alice", cdcMeta(None, Some(1L))))
       )
     }
   }
diff --git 
a/sql/pipelines/src/test/scala/org/apache/spark/sql/pipelines/graph/AutoCdcScd2AuxiliaryTableSpecSuite.scala
 
b/sql/pipelines/src/test/scala/org/apache/spark/sql/pipelines/graph/AutoCdcScd2AuxiliaryTableSpecSuite.scala
index a56fed3e5805..d01ec0f54736 100644
--- 
a/sql/pipelines/src/test/scala/org/apache/spark/sql/pipelines/graph/AutoCdcScd2AuxiliaryTableSpecSuite.scala
+++ 
b/sql/pipelines/src/test/scala/org/apache/spark/sql/pipelines/graph/AutoCdcScd2AuxiliaryTableSpecSuite.scala
@@ -76,7 +76,9 @@ class AutoCdcScd2AuxiliaryTableSpecSuite extends PipelineTest 
with SharedSparkSe
         deleteCondition = None,
         storedAsScdType = ScdType.Type2)))
     val graph = ctx.resolveToDataflowGraph()
-    
graph.auxiliaryTableSpecs(targetIdentifier).asInstanceOf[AutoCdcAuxiliaryTableSpec]
+    val inferredSchemas = 
graph.inferSchemas(spark.sessionState.conf.caseSensitiveAnalysis)
+    graph.auxiliaryTableSpecs(inferredSchemas)(targetIdentifier)
+      .asInstanceOf[AutoCdcAuxiliaryTableSpec]
   }
 
   /** The SCD2 target (inferred) schema for the default single-flow graph. */
@@ -97,7 +99,8 @@ class AutoCdcScd2AuxiliaryTableSpecSuite extends PipelineTest 
with SharedSparkSe
         columnSelection = None,
         deleteCondition = None,
         storedAsScdType = ScdType.Type2)))
-    ctx.resolveToDataflowGraph().inferredSchema(targetIdentifier)
+    ctx.resolveToDataflowGraph()
+      
.inferSchemas(spark.sessionState.conf.caseSensitiveAnalysis)(targetIdentifier)
   }
 
   test("SCD2 aux schema is the full target schema plus the deleted-by-batch-id 
marker") {
diff --git 
a/sql/pipelines/src/test/scala/org/apache/spark/sql/pipelines/graph/ConnectInvalidPipelineSuite.scala
 
b/sql/pipelines/src/test/scala/org/apache/spark/sql/pipelines/graph/ConnectInvalidPipelineSuite.scala
index 62680ebd3835..172d8354932b 100644
--- 
a/sql/pipelines/src/test/scala/org/apache/spark/sql/pipelines/graph/ConnectInvalidPipelineSuite.scala
+++ 
b/sql/pipelines/src/test/scala/org/apache/spark/sql/pipelines/graph/ConnectInvalidPipelineSuite.scala
@@ -31,6 +31,9 @@ import org.apache.spark.sql.types.{IntegerType, StructType}
  */
 class ConnectInvalidPipelineSuite extends PipelineTest with SharedSparkSession 
{
 
+  private def validateGraph(graph: DataflowGraph): DataflowGraph =
+    graph.validate(spark.sessionState.conf.caseSensitiveAnalysis)
+
   test("Missing source") {
     class P extends TestGraphRegistrationContext(spark) {
       registerPersistedView("b", query = readFlowFunc("a"))
@@ -39,7 +42,7 @@ class ConnectInvalidPipelineSuite extends PipelineTest with 
SharedSparkSession {
     val dfg = new P().resolveToDataflowGraph()
     assert(!dfg.resolved, "Pipeline should not have resolved properly")
     val ex = intercept[UnresolvedPipelineException] {
-      dfg.validate()
+      validateGraph(dfg)
     }
     assert(ex.getMessage.contains("Failed to resolve flows in the pipeline"))
     assertAnalysisException(
@@ -64,7 +67,7 @@ class ConnectInvalidPipelineSuite extends PipelineTest with 
SharedSparkSession {
     val dfg = new P().resolveToDataflowGraph()
     assert(!dfg.resolved, "Pipeline should not have resolved properly")
     val ex = intercept[UnresolvedPipelineException] {
-      dfg.validate()
+      validateGraph(dfg)
     }
     assert(ex.getMessage.contains("Failed to resolve flows in the pipeline"))
     assert(
@@ -141,7 +144,9 @@ class ConnectInvalidPipelineSuite extends PipelineTest with 
SharedSparkSession {
       registerFlow("a", "a_2", sqlFlowFunc(spark, "SELECT non_existent_col 
FROM RANGE(5)"))
       registerTable("b", query = Option(readFlowFunc("a")))
     }
-    val ex = intercept[UnresolvedPipelineException] { new 
P().resolveToDataflowGraph().validate() }
+    val ex = intercept[UnresolvedPipelineException] {
+      validateGraph(new P().resolveToDataflowGraph())
+    }
     assert(ex.directFailures.keySet == Set(fullyQualifiedIdentifier("a_2")))
     assert(ex.downstreamFailures.keySet == Set(fullyQualifiedIdentifier("b")))
 
@@ -158,7 +163,7 @@ class ConnectInvalidPipelineSuite extends PipelineTest with 
SharedSparkSession {
 
     val dfg = new P().resolveToDataflowGraph()
     val ex = intercept[UnresolvedPipelineException] {
-      dfg.validate()
+      validateGraph(dfg)
     }.directFailures(fullyQualifiedIdentifier("b")).getMessage
     verifyUnresolveColumnError(ex, "x", Seq("z"))
   }
@@ -175,7 +180,7 @@ class ConnectInvalidPipelineSuite extends PipelineTest with 
SharedSparkSession {
 
     val dfg = new P().resolveToDataflowGraph()
     val ex = intercept[UnresolvedPipelineException] {
-      dfg.validate()
+      validateGraph(dfg)
     }
     assert(
       ex.directFailures(fullyQualifiedIdentifier("c"))
@@ -200,7 +205,7 @@ class ConnectInvalidPipelineSuite extends PipelineTest with 
SharedSparkSession {
     val dfg = new P().resolveToDataflowGraph()
     assert(!dfg.resolved)
     val ex = intercept[UnresolvedPipelineException] {
-      dfg.validate()
+      validateGraph(dfg)
     }
     assert(
       ex.directFailures(fullyQualifiedIdentifier("c"))
@@ -217,7 +222,7 @@ class ConnectInvalidPipelineSuite extends PipelineTest with 
SharedSparkSession {
       registerPersistedView("a", query = readFlowFunc("a"))
     }
     val e = intercept[CircularDependencyException] {
-      new P().resolveToDataflowGraph().validate()
+      validateGraph(new P().resolveToDataflowGraph())
     }
     assert(e.upstreamDataset == fullyQualifiedIdentifier("a"))
     assert(e.downstreamTable == fullyQualifiedIdentifier("a"))
@@ -229,7 +234,7 @@ class ConnectInvalidPipelineSuite extends PipelineTest with 
SharedSparkSession {
       registerPersistedView("b", query = readFlowFunc("a"))
     }
     val e = intercept[CircularDependencyException] {
-      new P().resolveToDataflowGraph().validate()
+      validateGraph(new P().resolveToDataflowGraph())
     }
     val cycle = Set(
       fullyQualifiedIdentifier("a"),
@@ -260,7 +265,7 @@ class ConnectInvalidPipelineSuite extends PipelineTest with 
SharedSparkSession {
         fullyQualifiedIdentifier("d")
       )
     val e = intercept[CircularDependencyException] {
-      new P().resolveToDataflowGraph().validate()
+      validateGraph(new P().resolveToDataflowGraph())
     }
     assert(e.upstreamDataset != e.downstreamTable)
     assert(cycle.contains(e.upstreamDataset))
@@ -287,7 +292,7 @@ class ConnectInvalidPipelineSuite extends PipelineTest with 
SharedSparkSession {
         fullyQualifiedIdentifier("d")
       )
     val e = intercept[CircularDependencyException] {
-      new P().resolveToDataflowGraph().validate()
+      validateGraph(new P().resolveToDataflowGraph())
     }
     assert(e.upstreamDataset != e.downstreamTable)
     assert(cycle.contains(e.upstreamDataset))
@@ -313,7 +318,7 @@ class ConnectInvalidPipelineSuite extends PipelineTest with 
SharedSparkSession {
         fullyQualifiedIdentifier("d")
       )
     val e = intercept[CircularDependencyException] {
-      new P().resolveToDataflowGraph().validate()
+      validateGraph(new P().resolveToDataflowGraph())
     }
     assert(e.upstreamDataset != e.downstreamTable)
     assert(cycle.contains(e.upstreamDataset))
@@ -340,7 +345,7 @@ class ConnectInvalidPipelineSuite extends PipelineTest with 
SharedSparkSession {
         fullyQualifiedIdentifier("d")
       )
     val e = intercept[CircularDependencyException] {
-      new P().resolveToDataflowGraph().validate()
+      validateGraph(new P().resolveToDataflowGraph())
     }
     assert(e.upstreamDataset != e.downstreamTable)
     assert(cycle.contains(e.upstreamDataset))
@@ -408,7 +413,9 @@ class ConnectInvalidPipelineSuite extends PipelineTest with 
SharedSparkSession {
       registerPersistedView("a", query = dfFlowFunc(Seq(1).toDF()))
       registerTable("b", query = Option(readStreamFlowFunc("a")))
     }
-    val ex = intercept[UnresolvedPipelineException] { 
p.resolveToDataflowGraph().validate() }
+    val ex = intercept[UnresolvedPipelineException] {
+      validateGraph(p.resolveToDataflowGraph())
+    }
     assert(
       ex.directFailures(fullyQualifiedIdentifier("b"))
         .getMessage
@@ -429,7 +436,7 @@ class ConnectInvalidPipelineSuite extends PipelineTest with 
SharedSparkSession {
       registerPersistedView("a", query = dfFlowFunc(mem.toDF()))
       registerTable("b", query = Option(readFlowFunc("a")))
     }
-    val ex = intercept[UnresolvedPipelineException] { 
p.resolveToDataflowGraph().validate() }
+    val ex = intercept[UnresolvedPipelineException] { 
validateGraph(p.resolveToDataflowGraph()) }
     assert(
       ex.directFailures(fullyQualifiedIdentifier("b"))
         .getMessage
@@ -449,7 +456,7 @@ class ConnectInvalidPipelineSuite extends PipelineTest with 
SharedSparkSession {
     }.resolveToDataflowGraph()
 
     val ex = intercept[AnalysisException] {
-      graph.validate()
+      validateGraph(graph)
     }
 
     checkError(
@@ -471,7 +478,7 @@ class ConnectInvalidPipelineSuite extends PipelineTest with 
SharedSparkSession {
     }.resolveToDataflowGraph()
 
     val ex = intercept[AnalysisException] {
-      graph.validate()
+      validateGraph(graph)
     }
 
     checkError(
@@ -499,7 +506,7 @@ class ConnectInvalidPipelineSuite extends PipelineTest with 
SharedSparkSession {
     }.resolveToDataflowGraph()
 
     val ex = intercept[AnalysisException] {
-      graph.validate()
+      validateGraph(graph)
     }
 
     checkError(
@@ -522,7 +529,7 @@ class ConnectInvalidPipelineSuite extends PipelineTest with 
SharedSparkSession {
         specifiedSchema = Option(new StructType().add("x", IntegerType))
       )
     }.resolveToDataflowGraph()
-    val ex1 = intercept[AnalysisException] { graph1.validate() }
+    val ex1 = intercept[AnalysisException] { validateGraph(graph1) }
     assert(
       ex1.getMessage.contains(
         s"'${fullyQualifiedIdentifier("a").unquotedString}' " +
@@ -535,7 +542,7 @@ class ConnectInvalidPipelineSuite extends PipelineTest with 
SharedSparkSession {
       registerTable("a", specifiedSchema = Option(new StructType().add("x", 
IntegerType)))
       registerFlow("a", "a", query = dfFlowFunc(Seq(true, false).toDF("x")), 
once = true)
     }.resolveToDataflowGraph()
-    val ex2 = intercept[AnalysisException] { graph2.validate() }
+    val ex2 = intercept[AnalysisException] { validateGraph(graph2) }
     assert(
       ex2.getMessage.contains(
         s"'${fullyQualifiedIdentifier("a").unquotedString}' " +
@@ -592,7 +599,7 @@ class ConnectInvalidPipelineSuite extends PipelineTest with 
SharedSparkSession {
     }.resolveToDataflowGraph()
 
     val ex = intercept[AnalysisException] {
-      graph.validate()
+      validateGraph(graph)
     }
 
     checkError(
@@ -642,7 +649,7 @@ class ConnectInvalidPipelineSuite extends PipelineTest with 
SharedSparkSession {
     }.resolveToDataflowGraph()
 
     val ex = intercept[AnalysisException] {
-      graph.validate()
+      validateGraph(graph)
     }
 
     checkError(
@@ -704,7 +711,7 @@ class ConnectInvalidPipelineSuite extends PipelineTest with 
SharedSparkSession {
     }.resolveToDataflowGraph()
 
     val ex = intercept[AnalysisException] {
-      graph.validate()
+      validateGraph(graph)
     }
 
     checkError(
@@ -749,7 +756,7 @@ class ConnectInvalidPipelineSuite extends PipelineTest with 
SharedSparkSession {
     }.resolveToDataflowGraph()
 
     val ex = intercept[AnalysisException] {
-      graph.validate()
+      validateGraph(graph)
     }
 
     checkError(
diff --git 
a/sql/pipelines/src/test/scala/org/apache/spark/sql/pipelines/graph/ConnectValidPipelineSuite.scala
 
b/sql/pipelines/src/test/scala/org/apache/spark/sql/pipelines/graph/ConnectValidPipelineSuite.scala
index ea6d3202ba86..b90ccbbde7ce 100644
--- 
a/sql/pipelines/src/test/scala/org/apache/spark/sql/pipelines/graph/ConnectValidPipelineSuite.scala
+++ 
b/sql/pipelines/src/test/scala/org/apache/spark/sql/pipelines/graph/ConnectValidPipelineSuite.scala
@@ -504,7 +504,7 @@ class ConnectValidPipelineSuite extends PipelineTest with 
SharedSparkSession {
       registerFlow("sink_a", "sink_flow", query = readStreamFlowFunc("a"))
     }
     val g = P.resolveToDataflowGraph()
-    g.validate()
+    g.validate(spark.sessionState.conf.caseSensitiveAnalysis)
     assert(g.resolved)
     assert(g.sink(TableIdentifier("sink_a")).isInstanceOf[Sink])
     val sink = g.sink(TableIdentifier("sink_a"))
@@ -751,7 +751,7 @@ class ConnectValidPipelineSuite extends PipelineTest with 
SharedSparkSession {
       }.resolveToDataflowGraph()
       assert(!unresolved.resolved, "case-sensitive consumer flow should fail 
to resolve")
       val ex = intercept[UnresolvedPipelineException] {
-        unresolved.validate()
+        unresolved.validate(spark.sessionState.conf.caseSensitiveAnalysis)
       }
       assertAnalysisException(
         ex.directFailures(fullyQualifiedIdentifier("consumer")),
diff --git 
a/sql/pipelines/src/test/scala/org/apache/spark/sql/pipelines/graph/MaterializeTablesSuite.scala
 
b/sql/pipelines/src/test/scala/org/apache/spark/sql/pipelines/graph/MaterializeTablesSuite.scala
index 46e2d6d9ae63..a4df9867e2ab 100644
--- 
a/sql/pipelines/src/test/scala/org/apache/spark/sql/pipelines/graph/MaterializeTablesSuite.scala
+++ 
b/sql/pipelines/src/test/scala/org/apache/spark/sql/pipelines/graph/MaterializeTablesSuite.scala
@@ -32,6 +32,7 @@ import org.apache.spark.sql.connector.catalog.{
 }
 import org.apache.spark.sql.connector.expressions.{ClusterByTransform, 
Expressions, FieldReference}
 import org.apache.spark.sql.execution.streaming.runtime.MemoryStream
+import org.apache.spark.sql.internal.SQLConf
 import 
org.apache.spark.sql.pipelines.graph.DatasetManager.TableMaterializationException
 import org.apache.spark.sql.pipelines.utils.{BaseCoreExecutionTest, 
TestGraphRegistrationContext}
 import org.apache.spark.sql.test.SharedSparkSession
@@ -565,7 +566,7 @@ abstract class MaterializeTablesSuite extends 
BaseCoreExecutionTest {
 
     val graph1 = new TestGraphRegistrationContext(spark) {
       registerTable("a", query = 
Option(dfFlowFunc(spark.readStream.format("rate").load())))
-    }.resolveToDataflowGraph().validate()
+    
}.resolveToDataflowGraph().validate(spark.sessionState.conf.caseSensitiveAnalysis)
 
     materializeGraph(graph1, storageRoot = storageRoot)
   }
@@ -638,7 +639,7 @@ abstract class MaterializeTablesSuite extends 
BaseCoreExecutionTest {
         new TestGraphRegistrationContext(spark) {
           registerView("a", query = dfFlowFunc(streamInts.toDF()))
           registerTable("b", query = Option(sqlFlowFunc(spark, "SELECT value 
AS x FROM STREAM a")))
-        }.resolveToDataflowGraph().validate()
+        
}.resolveToDataflowGraph().validate(spark.sessionState.conf.caseSensitiveAnalysis)
 
       val (refreshSelection, fullRefreshSelection) = if (isFullRefresh) {
         (NoTables, AllTables)
@@ -668,7 +669,7 @@ abstract class MaterializeTablesSuite extends 
BaseCoreExecutionTest {
         new TestGraphRegistrationContext(spark) {
           registerView("a", query = dfFlowFunc(streamInts.toDF()))
           registerTable("b", query = Option(sqlFlowFunc(spark, "SELECT value 
AS y FROM STREAM a")))
-        }.resolveToDataflowGraph().validate(),
+        
}.resolveToDataflowGraph().validate(spark.sessionState.conf.caseSensitiveAnalysis),
         contextOpt = updateContextOpt,
         storageRoot = storageRoot
       )
@@ -1214,6 +1215,407 @@ abstract class MaterializeTablesSuite extends 
BaseCoreExecutionTest {
     }
   }
 
+  test("SPARK-58517: re-materializing with a case-only column difference is a 
no-op under " +
+    "case-insensitive resolution") {
+    withRecordingCatalog {
+      withSQLConf(SQLConf.CASE_SENSITIVE.key -> "false") {
+        // Create the table with `value`, then re-materialize with the same 
column cased as `Value`.
+        // Under case-insensitive resolution these are the same column, so 
schema evolution must
+        // fold `Value` onto the existing `value`: no alterTable, and the 
persisted column keeps its
+        // original name/case. Before SPARK-58517 the case-sensitive merge 
instead added a second
+        // `Value` column, corrupting the table.
+        materializeStreamingTable(
+          "t", new StructType().add("id", IntegerType).add("value", 
StringType), Map.empty)
+        assert(recordingCatalog.recordedAlters.isEmpty)
+
+        materializeStreamingTable(
+          "t", new StructType().add("id", IntegerType).add("Value", 
StringType), Map.empty)
+        assert(recordingCatalog.recordedAlters.isEmpty,
+          s"expected no alter, got: ${recordingCatalog.recordedAlters}")
+
+        assert(
+          loadTableFromRecordingCatalog("t").columns() sameElements
+            CatalogV2Util.structTypeToV2Columns(
+              new StructType().add("id", IntegerType).add("value", StringType)
+            ),
+          "the persisted schema should keep the original `value` column, not 
gain a `Value` column"
+        )
+      }
+    }
+  }
+
+  test("SPARK-58517: multi-flow schema inference folds case-only column 
differences under " +
+    "case-insensitive resolution") {
+    withRecordingCatalog {
+      withSQLConf(SQLConf.CASE_SENSITIVE.key -> "false") {
+        // Two append flows write to the same streaming table, one emitting 
`value` and the other
+        // `Value`. The target schema is INFERRED by merging the flows' 
schemas, which happens
+        // before the evolveTable path runs -- so inference must honor 
case-insensitivity too,
+        // otherwise the table is created with both columns and the engine's 
own resolver cannot
+        // disambiguate them.
+        val df1 = spark.createDataFrame(
+          spark.sparkContext.parallelize(Seq(Row(1, "a"))),
+          new StructType().add("id", IntegerType).add("value", StringType))
+        val df2 = spark.createDataFrame(
+          spark.sparkContext.parallelize(Seq(Row(2, "b"))),
+          new StructType().add("id", IntegerType).add("Value", StringType))
+
+        val ctx = new TestGraphRegistrationContext(spark) {
+          registerTable(
+            "t",
+            catalog = Option(recordingCatalogName),
+            database = Option(recordingNamespace))
+          registerFlow(
+            "t", "f1", dfFlowFunc(df1),
+            catalog = Option(recordingCatalogName), database = 
Option(recordingNamespace))
+          registerFlow(
+            "t", "f2", dfFlowFunc(df2),
+            catalog = Option(recordingCatalogName), database = 
Option(recordingNamespace))
+        }
+
+        val graph = ctx.resolveToDataflowGraph()
+        val inferredSchemas = 
graph.inferSchemas(spark.sessionState.conf.caseSensitiveAnalysis)
+        val (targetIdentifier, inferred) = inferredSchemas.head
+        val lowestIdentifierFlowValueField =
+          graph.resolvedFlowsTo(targetIdentifier)
+            .sortBy(_.identifier.unquotedString)
+            .head
+            .schema
+            .fieldNames(1)
+        // The two spellings must fold into a single column, and the lowest 
flow identifier
+        // supplies the surviving spelling.
+        assert(inferred.fieldNames.toSeq === Seq("id", 
lowestIdentifierFlowValueField))
+      }
+    }
+  }
+
+  test("SPARK-58517: a case-only fold picks the same spelling for the 
materialized table and for " +
+    "downstream resolution, in either flow declaration order") {
+    // The table's schema is derived twice from the same flows, by two 
different callers: the graph
+    // materializes the table from `inferSchemas`, while downstream flows 
resolve against the
+    // `VirtualTableInput` schema, whose `availableFlows` is in declaration 
order, not sorted. Both
+    // go through `inferSchemaFromFlows`, which merges in sorted flow 
identifier order, so the
+    // surviving spelling is the same on both paths and does not depend on 
declaration order. Were
+    // the two to disagree, the downstream view would persist a column spelled 
differently from the
+    // source column it selects, and -- because `diffSchemas` keys on exact 
names -- reordering the
+    // flow definitions would turn the next refresh into a drop-then-add of 
that column.
+    withSQLConf(SQLConf.CASE_SENSITIVE.key -> "false") {
+      // `f_eu` sorts before `f_us`, so `value` is the surviving spelling in 
both orders.
+      def graphWithFlowsDeclared(usFirst: Boolean): DataflowGraph = {
+        val usDf = spark.createDataFrame(
+          spark.sparkContext.parallelize(Seq(Row(1, "a"))),
+          new StructType().add("id", IntegerType).add("Value", StringType))
+        val euDf = spark.createDataFrame(
+          spark.sparkContext.parallelize(Seq(Row(2, "b"))),
+          new StructType().add("id", IntegerType).add("value", StringType))
+
+        new TestGraphRegistrationContext(spark) {
+          registerTable("events")
+          val registerUs = () => registerFlow("events", "f_us", 
dfFlowFunc(usDf))
+          val registerEu = () => registerFlow("events", "f_eu", 
dfFlowFunc(euDf))
+          if (usFirst) {
+            registerUs()
+            registerEu()
+          } else {
+            registerEu()
+            registerUs()
+          }
+          // Reads the table, so it resolves against the VirtualTableInput 
schema.
+          registerMaterializedView(
+            "events_summary",
+            query = sqlFlowFunc(spark, "SELECT id, value FROM events"))
+        }.resolveToDataflowGraph()
+      }
+
+      Seq(true, false).foreach { usFirst =>
+        val graph = graphWithFlowsDeclared(usFirst)
+        val sessionCaseSensitive = 
spark.sessionState.conf.caseSensitiveAnalysis
+        val eventsIdentifier = fullyQualifiedIdentifier("events")
+
+        val materializedSchema = 
graph.inferSchemas(sessionCaseSensitive)(eventsIdentifier)
+        assert(
+          materializedSchema.fieldNames.toSeq === Seq("id", "value"),
+          s"materialized schema for usFirst=$usFirst")
+
+        // The downstream view's own schema reflects what it resolved against 
upstream: Spark takes
+        // the resolved attribute's name, so a `Value` upstream would surface 
here as `Value`.
+        val summarySchema = graph
+          
.inferSchemas(sessionCaseSensitive)(fullyQualifiedIdentifier("events_summary"))
+        assert(
+          summarySchema.fieldNames.toSeq === Seq("id", "value"),
+          s"downstream schema for usFirst=$usFirst")
+      }
+    }
+  }
+
+  test("multi-flow schema inference keeps case-only column differences 
distinct under " +
+    "case-sensitive resolution") {
+    withRecordingCatalog {
+      withSQLConf(SQLConf.CASE_SENSITIVE.key -> "true") {
+        // The case-sensitive control: `value` and `Value` are distinct 
columns, so inference
+        // contributes both.
+        val df1 = spark.createDataFrame(
+          spark.sparkContext.parallelize(Seq(Row(1, "a"))),
+          new StructType().add("id", IntegerType).add("value", StringType))
+        val df2 = spark.createDataFrame(
+          spark.sparkContext.parallelize(Seq(Row(2, "b"))),
+          new StructType().add("id", IntegerType).add("Value", StringType))
+
+        val ctx = new TestGraphRegistrationContext(spark) {
+          registerTable(
+            "t",
+            catalog = Option(recordingCatalogName),
+            database = Option(recordingNamespace))
+          registerFlow(
+            "t", "f1", dfFlowFunc(df1),
+            catalog = Option(recordingCatalogName), database = 
Option(recordingNamespace))
+          registerFlow(
+            "t", "f2", dfFlowFunc(df2),
+            catalog = Option(recordingCatalogName), database = 
Option(recordingNamespace))
+        }
+
+        val graph = ctx.resolveToDataflowGraph()
+        val inferred = graph.inferSchemas(
+          spark.sessionState.conf.caseSensitiveAnalysis).values.head
+        // Both spellings survive as distinct columns in sorted flow 
identifier order.
+        assert(inferred.fieldNames.toSeq === Seq("id", "value", "Value"))
+      }
+    }
+  }
+
+  test("SPARK-58517: a materialized view's case-only column rename is applied 
under " +
+    "case-insensitive resolution") {
+    // The non-merging path: for a materialized view `targetSchema` is the 
run's declared schema
+    // as-is (no merge with the persisted schema), so a case-only rename must 
remain visible to
+    // `diffSchemas` as a drop-then-add. Case-insensitive matching here would 
emit no change at all
+    // and freeze the persisted spelling forever -- the table would 
permanently disagree with its
+    // definition, with no error pointing at the discrepancy, and a colleague 
materializing the same
+    // definition against a fresh table would get the declared casing instead.
+    withSQLConf(SQLConf.CASE_SENSITIVE.key -> "false") {
+      materializeGraph(
+        new TestGraphRegistrationContext(spark) {
+          registerView("src", query = dfFlowFunc(Seq((1, 2L)).toDF("id", 
"total")))
+          registerMaterializedView("mv", query = sqlFlowFunc(spark, "SELECT 
id, total FROM src"))
+        }.resolveToDataflowGraph(),
+        storageRoot = storageRoot
+      )
+
+      val catalog = 
spark.sessionState.catalogManager.currentCatalog.asInstanceOf[TableCatalog]
+      val identifier = 
Identifier.of(Array(TestGraphRegistrationContext.DEFAULT_DATABASE), "mv")
+      assert(
+        catalog.loadTable(identifier).columns() sameElements 
CatalogV2Util.structTypeToV2Columns(
+          new StructType().add("id", IntegerType).add("total", LongType))
+      )
+
+      // Re-materialize with the column cased as `Total`. The table must 
follow the definition.
+      materializeGraph(
+        new TestGraphRegistrationContext(spark) {
+          registerView("src", query = dfFlowFunc(Seq((1, 2L)).toDF("id", 
"total")))
+          registerMaterializedView(
+            "mv", query = sqlFlowFunc(spark, "SELECT id, total AS Total FROM 
src"))
+        }.resolveToDataflowGraph(),
+        storageRoot = storageRoot
+      )
+      assert(
+        catalog.loadTable(identifier).columns() sameElements 
CatalogV2Util.structTypeToV2Columns(
+          new StructType().add("id", IntegerType).add("Total", LongType)),
+        "the materialized view should adopt the declared `Total` casing, not 
keep `total`"
+      )
+    }
+  }
+
+  test("SPARK-58517: a full-refreshed streaming table's case-only column 
rename is applied " +
+    "under case-insensitive resolution") {
+    // The streaming-table analog of the materialized-view case above: a full 
refresh also takes
+    // `targetSchema` as the declared schema without merging, so the same 
case-only rename must be
+    // applied rather than silently ignored.
+    withSQLConf(SQLConf.CASE_SENSITIVE.key -> "false") {
+      val graph = materializeGraph(
+        new TestGraphRegistrationContext(spark) {
+          registerView("src", query = dfFlowFunc(Seq((1, 2L)).toDF("id", 
"total")))
+          registerTable("st", query = Option(sqlFlowFunc(spark, "SELECT id, 
total FROM src")))
+        }.resolveToDataflowGraph(),
+        storageRoot = storageRoot
+      )
+
+      val catalog = 
spark.sessionState.catalogManager.currentCatalog.asInstanceOf[TableCatalog]
+      val identifier = 
Identifier.of(Array(TestGraphRegistrationContext.DEFAULT_DATABASE), "st")
+      assert(
+        catalog.loadTable(identifier).columns() sameElements 
CatalogV2Util.structTypeToV2Columns(
+          new StructType().add("id", IntegerType).add("total", LongType))
+      )
+
+      val renamedGraph =
+        new TestGraphRegistrationContext(spark) {
+          registerView("src", query = dfFlowFunc(Seq((1, 2L)).toDF("id", 
"total")))
+          registerTable(
+            "st", query = Option(sqlFlowFunc(spark, "SELECT id, total AS Total 
FROM src")))
+        }.resolveToDataflowGraph()
+
+      materializeGraph(
+        renamedGraph,
+        contextOpt = Option(
+          TestPipelineUpdateContext(
+            spark = spark,
+            unresolvedGraph = graph,
+            refreshTables = NoTables,
+            fullRefreshTables = AllTables,
+            storageRoot = storageRoot
+          )
+        ),
+        storageRoot = storageRoot
+      )
+      assert(
+        catalog.loadTable(identifier).columns() sameElements 
CatalogV2Util.structTypeToV2Columns(
+          new StructType().add("id", IntegerType).add("Total", LongType)),
+        "a full-refreshed streaming table should adopt the declared `Total` 
casing"
+      )
+    }
+  }
+
+  test("re-materializing with a case-only column difference adds a column 
under case-sensitive " +
+    "resolution") {
+    withRecordingCatalog {
+      withSQLConf(SQLConf.CASE_SENSITIVE.key -> "true") {
+        // The case-sensitive counterpart: `value` and `Value` are distinct, 
so `Value` is added.
+        materializeStreamingTable(
+          "t", new StructType().add("id", IntegerType).add("value", 
StringType), Map.empty)
+        assert(recordingCatalog.recordedAlters.isEmpty)
+
+        materializeStreamingTable(
+          "t", new StructType().add("id", IntegerType).add("Value", 
StringType), Map.empty)
+        assert(recordingCatalog.recordedAlters.size == 1)
+        val changes = recordingCatalog.recordedAlters.flatten
+        assert(changes.collect { case ac: TableChange.AddColumn => 
ac.fieldNames()(0) } ==
+          Seq("Value"))
+      }
+    }
+  }
+
+  test("SPARK-58517: schema evolution uses the pipeline's case sensitivity, 
not the session's") {
+    // A pipeline-level `SET spark.sql.caseSensitive` never reaches the 
session, so evolution must
+    // read it from the flows. Here the session default is case-INsensitive 
while the pipeline asks
+    // for case-SENSITIVE, so `Value` must become its own column alongside the 
persisted `value` --
+    // matching how the flow itself resolves the name.
+    withRecordingCatalog {
+      withSQLConf(SQLConf.CASE_SENSITIVE.key -> "false") {
+        materializeGraph(
+          new TestGraphRegistrationContext(
+            spark, Map(SQLConf.CASE_SENSITIVE.key -> "true")) {
+            registerView("src", query = dfFlowFunc(Seq((1, "a")).toDF("id", 
"value")))
+            registerTable(
+              "t",
+              query = Option(sqlFlowFunc(spark, "SELECT id, value FROM src")),
+              catalog = Option(recordingCatalogName),
+              database = Option(recordingNamespace))
+          }.resolveToDataflowGraph(),
+          storageRoot = storageRoot
+        )
+        assert(
+          loadTableFromRecordingCatalog("t").columns() sameElements
+            CatalogV2Util.structTypeToV2Columns(
+              new StructType().add("id", IntegerType).add("value", 
StringType)))
+
+        materializeGraph(
+          new TestGraphRegistrationContext(
+            spark, Map(SQLConf.CASE_SENSITIVE.key -> "true")) {
+            registerView("src", query = dfFlowFunc(Seq((1, "a")).toDF("id", 
"value")))
+            registerTable(
+              "t",
+              query = Option(sqlFlowFunc(spark, "SELECT id, value AS Value 
FROM src")),
+              catalog = Option(recordingCatalogName),
+              database = Option(recordingNamespace))
+          }.resolveToDataflowGraph(),
+          storageRoot = storageRoot
+        )
+        assert(
+          loadTableFromRecordingCatalog("t").columns() sameElements
+            CatalogV2Util.structTypeToV2Columns(
+              new StructType()
+                .add("id", IntegerType)
+                .add("value", StringType)
+                .add("Value", StringType)),
+          "the pipeline asked for case-sensitive resolution, so `Value` must 
be its own column")
+      }
+    }
+  }
+
+  test("SPARK-58517: flows writing to one table that disagree on case 
sensitivity are rejected") {
+    // The effective value decides whether names differing only in case 
identify the same column, so
+    // if the flows disagree the resulting schema would depend on the order 
they are evaluated in.
+    // Fail with a clear error instead of picking one arbitrarily.
+    val ctx = new TestGraphRegistrationContext(spark) {
+      registerView("src", query = dfFlowFunc(Seq((1, "a")).toDF("id", 
"value")))
+      registerTable("t")
+      registerFlow(
+        "t", "f1", sqlFlowFunc(spark, "SELECT id, value FROM src"),
+        sqlConf = Map(SQLConf.CASE_SENSITIVE.key -> "true"))
+      registerFlow(
+        "t", "f2", sqlFlowFunc(spark, "SELECT id, value AS Value FROM src"),
+        sqlConf = Map(SQLConf.CASE_SENSITIVE.key -> "false"))
+    }
+
+    val ex = intercept[AnalysisException] {
+      ctx.resolveToDataflowGraph().inferSchemas(
+        spark.sessionState.conf.caseSensitiveAnalysis)
+    }
+    checkError(
+      exception = ex,
+      condition = "CONFLICTING_PIPELINE_FLOW_CASE_SENSITIVITY",
+      parameters = Map(
+        "tableName" -> "spark_catalog.test_db.t",
+        "configKey" -> SQLConf.CASE_SENSITIVE.key,
+        "flowConfigurations" ->
+          ("false (spark_catalog.test_db.f2); true (spark_catalog.test_db.f1)")
+      )
+    )
+  }
+
+  test("SPARK-58517: a flow inheriting the session value conflicts with one 
that overrides it") {
+    // f2 leaves the conf unset, so it inherits the session's case-INsensitive 
default, which
+    // conflicts with f1's explicit case-sensitive request just as much as an 
opposite explicit
+    // value would.
+    withSQLConf(SQLConf.CASE_SENSITIVE.key -> "false") {
+      val ctx = new TestGraphRegistrationContext(spark) {
+        registerView("src", query = dfFlowFunc(Seq((1, "a")).toDF("id", 
"value")))
+        registerTable("t")
+        registerFlow(
+          "t", "f1", sqlFlowFunc(spark, "SELECT id, value FROM src"),
+          sqlConf = Map(SQLConf.CASE_SENSITIVE.key -> "true"))
+        registerFlow("t", "f2", sqlFlowFunc(spark, "SELECT id, value FROM 
src"))
+      }
+
+      val ex = intercept[AnalysisException] {
+        ctx.resolveToDataflowGraph().inferSchemas(
+          spark.sessionState.conf.caseSensitiveAnalysis)
+      }
+      assert(ex.getCondition === "CONFLICTING_PIPELINE_FLOW_CASE_SENSITIVITY")
+      assert(ex.getMessage.contains("session default"))
+    }
+  }
+
+  test("SPARK-58517: flows that agree on case sensitivity are accepted") {
+    // The negative control: identical explicit values are not a conflict, and 
neither is a value
+    // that merely differs in spelling from the session's ("TRUE" vs "true").
+    withSQLConf(SQLConf.CASE_SENSITIVE.key -> "false") {
+      val ctx = new TestGraphRegistrationContext(spark) {
+        registerView("src", query = dfFlowFunc(Seq((1, "a")).toDF("id", 
"value")))
+        registerTable("t")
+        registerFlow(
+          "t", "f1", sqlFlowFunc(spark, "SELECT id, value FROM src"),
+          sqlConf = Map(SQLConf.CASE_SENSITIVE.key -> "true"))
+        registerFlow(
+          "t", "f2", sqlFlowFunc(spark, "SELECT id, value AS Value FROM src"),
+          sqlConf = Map(SQLConf.CASE_SENSITIVE.key -> "TRUE"))
+      }
+      val inferred = ctx.resolveToDataflowGraph()
+        .inferSchemas(spark.sessionState.conf.caseSensitiveAnalysis)(
+          fullyQualifiedIdentifier("t"))
+      // Case-sensitive, so both spellings survive.
+      assert(inferred.fieldNames.toSeq === Seq("id", "value", "Value"))
+    }
+  }
+
   test("re-materializing with a dropped property neither removes it nor issues 
an alterTable") {
     withRecordingCatalog {
       val schema = new StructType().add("id", IntegerType)
diff --git 
a/sql/pipelines/src/test/scala/org/apache/spark/sql/pipelines/graph/SqlPipelineSuite.scala
 
b/sql/pipelines/src/test/scala/org/apache/spark/sql/pipelines/graph/SqlPipelineSuite.scala
index cec8db6ec528..8ce7971b405f 100644
--- 
a/sql/pipelines/src/test/scala/org/apache/spark/sql/pipelines/graph/SqlPipelineSuite.scala
+++ 
b/sql/pipelines/src/test/scala/org/apache/spark/sql/pipelines/graph/SqlPipelineSuite.scala
@@ -30,6 +30,12 @@ class SqlPipelineSuite extends PipelineTest with 
SharedSparkSession {
   private val externalTable1Ident = fullyQualifiedIdentifier("external_t1")
   private val externalTable2Ident = fullyQualifiedIdentifier("external_t2")
 
+  private def resolveGraph(graph: DataflowGraph): DataflowGraph =
+    graph.resolve(spark.sessionState.conf.caseSensitiveAnalysis)
+
+  private def validateGraph(graph: DataflowGraph): DataflowGraph =
+    graph.validate(spark.sessionState.conf.caseSensitiveAnalysis)
+
   override def beforeEach(): Unit = {
     super.beforeEach()
     // Create mock external tables that tests can reference, ex. to stream 
from.
@@ -53,7 +59,7 @@ class SqlPipelineSuite extends PipelineTest with 
SharedSparkSession {
                    |SELECT * FROM STREAM $externalTable2Ident;
                    |""".stripMargin
     )
-    val resolvedDataflowGraph = unresolvedDataflowGraph.resolve()
+    val resolvedDataflowGraph = resolveGraph(unresolvedDataflowGraph)
 
     assert(resolvedDataflowGraph.flows.size == 4)
     assert(resolvedDataflowGraph.tables.size == 2)
@@ -127,7 +133,7 @@ class SqlPipelineSuite extends PipelineTest with 
SharedSparkSession {
         "CREATE MATERIALIZED VIEW a COMMENT 'this is a comment' AS SELECT * 
FROM range(1, 4)"
     )
 
-    val resolvedDataflowGraph = unresolvedDataflowGraph.resolve()
+    val resolvedDataflowGraph = resolveGraph(unresolvedDataflowGraph)
 
     val flowA =
       resolvedDataflowGraph.resolvedFlows
@@ -144,7 +150,7 @@ class SqlPipelineSuite extends PipelineTest with 
SharedSparkSession {
                   |""".stripMargin
     )
 
-    val resolvedDataflowGraph = unresolvedDataflowGraph.resolve()
+    val resolvedDataflowGraph = resolveGraph(unresolvedDataflowGraph)
 
     assert(
       resolvedDataflowGraph.resolvedFlows
@@ -168,7 +174,7 @@ class SqlPipelineSuite extends PipelineTest with 
SharedSparkSession {
                   |""".stripMargin
     )
 
-    val resolvedDataflowGraph = unresolvedDataflowGraph.resolve()
+    val resolvedDataflowGraph = resolveGraph(unresolvedDataflowGraph)
 
     Seq("a", "b", "c", "d").foreach { datasetName =>
       val backingFlow = resolvedDataflowGraph.resolvedFlows
@@ -258,7 +264,7 @@ class SqlPipelineSuite extends PipelineTest with 
SharedSparkSession {
                   |PARTITIONED BY (id1, id2)
                   |AS SELECT id as id1, id as id2 FROM range(1,2) 
""".stripMargin
     )
-    val resolvedDataflowGraph = unresolvedDataflowGraph.resolve()
+    val resolvedDataflowGraph = resolveGraph(unresolvedDataflowGraph)
 
     assert(
       resolvedDataflowGraph.tables
@@ -363,7 +369,7 @@ class SqlPipelineSuite extends PipelineTest with 
SharedSparkSession {
     val unresolvedDataflowGraph = unresolvedDataflowGraphFromSql(
       sqlText = "CREATE STREAMING TABLE st TBLPROPERTIES ('prop1'='foo', 
'prop2'='bar') AS SELECT 1"
     )
-    val resolvedDataflowGraph = unresolvedDataflowGraph.resolve()
+    val resolvedDataflowGraph = resolveGraph(unresolvedDataflowGraph)
     assert(
       resolvedDataflowGraph.tables
         .find(_.identifier == fullyQualifiedIdentifier("st"))
@@ -387,7 +393,7 @@ class SqlPipelineSuite extends PipelineTest with 
SharedSparkSession {
                   |""".stripMargin
     )
 
-    val resolvedDataflowGraph = unresolvedDataflowGraph.resolve()
+    val resolvedDataflowGraph = resolveGraph(unresolvedDataflowGraph)
 
     assert(
       resolvedDataflowGraph.flows
@@ -518,7 +524,7 @@ class SqlPipelineSuite extends PipelineTest with 
SharedSparkSession {
                      |""".stripMargin
       )
 
-      val resolvedDataflowGraph = unresolvedDataflowGraph.resolve()
+      val resolvedDataflowGraph = resolveGraph(unresolvedDataflowGraph)
 
       assert(
         resolvedDataflowGraph.resolutionFailedFlows
@@ -577,10 +583,10 @@ class SqlPipelineSuite extends PipelineTest with 
SharedSparkSession {
                   |""".stripMargin
     )
 
-    val resolvedDataflowGraph = unresolvedDataflowGraph.resolve()
+    val resolvedDataflowGraph = resolveGraph(unresolvedDataflowGraph)
 
     // Let inferred/declared schema mismatch detection execute
-    resolvedDataflowGraph.validate()
+    validateGraph(resolvedDataflowGraph)
 
     val expectedSchema = new StructType().add(name = "id", dataType = 
LongType, nullable = false)
 
@@ -651,7 +657,7 @@ class SqlPipelineSuite extends PipelineTest with 
SharedSparkSession {
     val unresolvedDataflowGraph = unresolvedDataflowGraphFromSql(
       sqlText = s"CREATE VIEW b COMMENT 'my persisted comment' AS SELECT * 
FROM range(1, 4);"
     )
-    val graph = unresolvedDataflowGraph.resolve().validate()
+    val graph = validateGraph(resolveGraph(unresolvedDataflowGraph))
 
     val view = graph.views.last
 
@@ -897,9 +903,7 @@ class SqlPipelineSuite extends PipelineTest with 
SharedSparkSession {
 
     checkError(
       exception = intercept[AnalysisException] {
-        unresolvedDataflowGraph
-          .resolve()
-          .validate()
+        validateGraph(resolveGraph(unresolvedDataflowGraph))
       },
       condition = "PIPELINE_DATASET_WITHOUT_FLOW",
       sqlState = Option("0A000"),
diff --git 
a/sql/pipelines/src/test/scala/org/apache/spark/sql/pipelines/graph/TriggeredGraphExecutionSuite.scala
 
b/sql/pipelines/src/test/scala/org/apache/spark/sql/pipelines/graph/TriggeredGraphExecutionSuite.scala
index db8c368ca89d..313f86753222 100644
--- 
a/sql/pipelines/src/test/scala/org/apache/spark/sql/pipelines/graph/TriggeredGraphExecutionSuite.scala
+++ 
b/sql/pipelines/src/test/scala/org/apache/spark/sql/pipelines/graph/TriggeredGraphExecutionSuite.scala
@@ -60,7 +60,7 @@ class TriggeredGraphExecutionSuite extends ExecutionTest with 
SharedSparkSession
       registerMaterializedView("b", query = readFlowFunc("a"))
     }
     val unresolvedGraph = pipelineDef.toDataflowGraph
-    val resolvedGraph = unresolvedGraph.resolve()
+    val resolvedGraph = 
unresolvedGraph.resolve(spark.sessionState.conf.caseSensitiveAnalysis)
     assert(resolvedGraph.flows.size == 2)
     assert(unresolvedGraph.flows.size == 2)
     assert(unresolvedGraph.tables.size == 2)
@@ -109,7 +109,7 @@ class TriggeredGraphExecutionSuite extends ExecutionTest 
with SharedSparkSession
     }
 
     val unresolvedGraph = pipelineDef.toDataflowGraph
-    val resolvedGraph = unresolvedGraph.resolve()
+    val resolvedGraph = 
unresolvedGraph.resolve(spark.sessionState.conf.caseSensitiveAnalysis)
     assert(resolvedGraph.flows.size == 4)
     assert(resolvedGraph.tables.size == 3)
     assert(resolvedGraph.views.size == 1)
diff --git 
a/sql/pipelines/src/test/scala/org/apache/spark/sql/pipelines/graph/UserSpecifiedSchemaValidationSuite.scala
 
b/sql/pipelines/src/test/scala/org/apache/spark/sql/pipelines/graph/UserSpecifiedSchemaValidationSuite.scala
index d216539c93bc..fee3493d01b9 100644
--- 
a/sql/pipelines/src/test/scala/org/apache/spark/sql/pipelines/graph/UserSpecifiedSchemaValidationSuite.scala
+++ 
b/sql/pipelines/src/test/scala/org/apache/spark/sql/pipelines/graph/UserSpecifiedSchemaValidationSuite.scala
@@ -20,10 +20,11 @@ package org.apache.spark.sql.pipelines.graph
 import org.apache.spark.sql.AnalysisException
 import org.apache.spark.sql.execution.streaming.runtime.MemoryStream
 import org.apache.spark.sql.functions
+import org.apache.spark.sql.internal.SQLConf
 import org.apache.spark.sql.pipelines.autocdc.{ChangeArgs, ScdType, 
UnqualifiedColumnName}
 import org.apache.spark.sql.pipelines.utils.{PipelineTest, 
TestGraphRegistrationContext}
 import org.apache.spark.sql.test.SharedSparkSession
-import org.apache.spark.sql.types.StructType
+import org.apache.spark.sql.types.{IntegerType, StringType, StructType}
 
 /**
  * Tests for `GraphValidations.validateUserSpecifiedSchemas`, which requires a 
table's
@@ -100,10 +101,14 @@ class UserSpecifiedSchemaValidationSuite extends 
PipelineTest with SharedSparkSe
 
   /** The full inferred AUTO CDC output schema (data columns plus the reserved 
metadata column). */
   private def autoCdcInferredSchema(flowName: String): StructType =
-    autoCdcGraph(flowName, declaredSchema = 
None).inferredSchema(targetIdentifier)
+    autoCdcGraph(flowName, declaredSchema = None)
+      
.inferSchemas(spark.sessionState.conf.caseSensitiveAnalysis)(targetIdentifier)
+
+  private def validateGraph(graph: DataflowGraph): DataflowGraph =
+    graph.validate(spark.sessionState.conf.caseSensitiveAnalysis)
 
   private def assertSchemaIncompatible(graph: DataflowGraph): Unit = {
-    val ex = intercept[AnalysisException](graph.validate())
+    val ex = intercept[AnalysisException](validateGraph(graph))
     assert(ex.getCondition == 
"USER_SPECIFIED_AND_INFERRED_SCHEMA_NOT_COMPATIBLE")
     assert(ex.getMessage.contains(targetIdentifier.unquotedString))
   }
@@ -111,7 +116,7 @@ class UserSpecifiedSchemaValidationSuite extends 
PipelineTest with SharedSparkSe
   // Plain flows: the inferred schema is exactly the source's data columns.
 
   test("compatible user-specified schema is accepted for an implicit plain 
flow") {
-    plainGraph(flowName = "target", declaredSchema = 
Some(dataSchema)).validate()
+    validateGraph(plainGraph(flowName = "target", declaredSchema = 
Some(dataSchema)))
   }
 
   test("incompatible user-specified schema is rejected for an implicit plain 
flow") {
@@ -120,7 +125,7 @@ class UserSpecifiedSchemaValidationSuite extends 
PipelineTest with SharedSparkSe
   }
 
   test("compatible user-specified schema is accepted for a named plain flow") {
-    plainGraph(flowName = "plain_flow", declaredSchema = 
Some(dataSchema)).validate()
+    validateGraph(plainGraph(flowName = "plain_flow", declaredSchema = 
Some(dataSchema)))
   }
 
   test("incompatible user-specified schema is rejected for a named plain 
flow") {
@@ -128,6 +133,53 @@ class UserSpecifiedSchemaValidationSuite extends 
PipelineTest with SharedSparkSe
       dataSchemaMissingColumn)))
   }
 
+  test("user-specified schema validation uses pipeline case sensitivity, not 
session default") {
+    withSQLConf(SQLConf.CASE_SENSITIVE.key -> "false") {
+      val ctx = new TestGraphRegistrationContext(
+        spark,
+        Map(SQLConf.CASE_SENSITIVE.key -> "true")) {
+        val session = spark
+        import session.implicits._
+
+        registerView("src", query = dfFlowFunc(Seq((1, "alice")).toDF("id", 
"value")))
+        registerTable(
+          "target",
+          specifiedSchema = Some(
+            new StructType().add("id", IntegerType).add("value", StringType)))
+        registerFlow(
+          destinationName = "target",
+          name = "case_sensitive_flow",
+          query = sqlFlowFunc(spark, "SELECT id, value AS Value FROM src"))
+      }
+
+      assertSchemaIncompatible(ctx.resolveToDataflowGraph())
+    }
+  }
+
+  test("user-specified schema validation uses case sensitivity inherited from 
upstream view") {
+    withSQLConf(SQLConf.CASE_SENSITIVE.key -> "false") {
+      val ctx = new TestGraphRegistrationContext(spark) {
+        val session = spark
+        import session.implicits._
+
+        registerPersistedView(
+          "src",
+          query = dfFlowFunc(Seq((1, "alice")).toDF("id", "value")),
+          sqlConf = Map(SQLConf.CASE_SENSITIVE.key -> "true"))
+        registerTable(
+          "target",
+          specifiedSchema = Some(
+            new StructType().add("id", IntegerType).add("value", StringType)))
+        registerFlow(
+          destinationName = "target",
+          name = "case_sensitive_flow",
+          query = sqlFlowFunc(spark, "SELECT id, value AS Value FROM src"))
+      }
+
+      assertSchemaIncompatible(ctx.resolveToDataflowGraph())
+    }
+  }
+
   // AUTO CDC flows: the inferred schema appends a reserved metadata column to 
the data columns.
 
   test("data-only user-specified schema is rejected for an implicit AUTO CDC 
flow") {
@@ -144,12 +196,14 @@ class UserSpecifiedSchemaValidationSuite extends 
PipelineTest with SharedSparkSe
     // Schema includes the appended metadata column, matching the inferred 
schema exactly.
     autoCdcGraph(
       flowName = "target",
-      declaredSchema = Some(autoCdcInferredSchema("target"))).validate()
+      declaredSchema = Some(autoCdcInferredSchema("target"))).validate(
+        spark.sessionState.conf.caseSensitiveAnalysis)
   }
 
   test("full user-specified schema is accepted for a named AUTO CDC flow") {
     autoCdcGraph(
       flowName = "auto_cdc_flow",
-      declaredSchema = Some(autoCdcInferredSchema("auto_cdc_flow"))).validate()
+      declaredSchema = Some(autoCdcInferredSchema("auto_cdc_flow"))).validate(
+        spark.sessionState.conf.caseSensitiveAnalysis)
   }
 }
diff --git 
a/sql/pipelines/src/test/scala/org/apache/spark/sql/pipelines/util/SchemaInferenceUtilsSuite.scala
 
b/sql/pipelines/src/test/scala/org/apache/spark/sql/pipelines/util/SchemaInferenceUtilsSuite.scala
index 41d5bbe14a6b..e6aed97f35dd 100644
--- 
a/sql/pipelines/src/test/scala/org/apache/spark/sql/pipelines/util/SchemaInferenceUtilsSuite.scala
+++ 
b/sql/pipelines/src/test/scala/org/apache/spark/sql/pipelines/util/SchemaInferenceUtilsSuite.scala
@@ -17,7 +17,7 @@
 
 package org.apache.spark.sql.pipelines.util
 
-import org.apache.spark.SparkFunSuite
+import org.apache.spark.{SparkException, SparkFunSuite}
 import org.apache.spark.sql.connector.catalog.TableChange
 import org.apache.spark.sql.types._
 
@@ -270,4 +270,132 @@ class SchemaInferenceUtilsSuite extends SparkFunSuite {
     assert(addedColumnNames === Set("full_name", "email"))
     assert(deletedColumnNames === Set("first_name", "last_name"))
   }
+
+  test("determineColumnChanges - a case-only difference is a drop-then-add, 
not a match") {
+    // diffSchemas keys column identity on the EXACT field name, with no case 
normalization. So a
+    // target `Value` against a persisted `value` is a distinct column: 
`value` is dropped and
+    // `Value` added. This is what makes a case-only rename visible on the 
non-merging paths
+    // (materialized views, full refresh), where targetSchema is the declared 
schema as-is.
+    val currentSchema = new StructType().add("id", IntegerType).add("value", 
StringType)
+    val targetSchema = new StructType().add("id", IntegerType).add("Value", 
StringType)
+
+    val changes = SchemaInferenceUtils.diffSchemas(currentSchema, targetSchema)
+
+    val addChanges = changes.collect { case ac: TableChange.AddColumn => 
ac.fieldNames()(0) }
+    val deleteChanges = changes.collect { case dc: TableChange.DeleteColumn => 
dc.fieldNames()(0) }
+    assert(addChanges === Seq("Value"))
+    assert(deleteChanges === Seq("value"))
+  }
+
+  test("determineColumnChanges - two declared columns differing only in case 
are both kept") {
+    // A declared schema carrying both `value` and `Value` reaches diffSchemas 
verbatim (nothing on
+    // the create path rejects duplicate-cased columns). Exact-name keying 
must surface BOTH as
+    // additions; normalizing the lookup key would collapse them and silently 
keep an arbitrary one
+    // (whichever came last), losing a column the user declared.
+    val currentSchema = new StructType().add("id", IntegerType)
+    val targetSchema = new StructType()
+      .add("id", IntegerType)
+      .add("value", StringType)
+      .add("Value", IntegerType)
+
+    val changes = SchemaInferenceUtils.diffSchemas(currentSchema, targetSchema)
+
+    val added = changes.collect { case ac: TableChange.AddColumn =>
+      ac.fieldNames()(0) -> ac.dataType()
+    }.toMap
+    assert(added === Map("value" -> StringType, "Value" -> IntegerType))
+  }
+
+  test("mergeSchemas - a nested field differing only in case folds onto the 
existing field when " +
+    "case-insensitive") {
+    // The nested analog of the top-level case-only fold. `StructType.merge` 
propagates the
+    // case-sensitivity flag into nested struct merges (SPARK-58525), so the 
incoming `s.Value` is
+    // matched to the existing `s.value` and the struct keeps a single field 
with the persisted
+    // (left) spelling -- rather than growing a second, case-differing nested 
field.
+    val currentSchema = new StructType()
+      .add("id", IntegerType)
+      .add("s", new StructType().add("value", StringType))
+    val dataSchema = new StructType()
+      .add("id", IntegerType)
+      .add("s", new StructType().add("Value", StringType))
+
+    val merged =
+      SchemaMergingUtils.mergeSchemas(currentSchema, dataSchema, caseSensitive 
= false)
+    assert(merged === currentSchema)
+
+    // Because the merge is a no-op, evolution derives no table changes at 
all: in particular the
+    // nested struct is NOT rewritten (which would be an UpdateColumnType on 
`s`).
+    assert(
+      SchemaInferenceUtils.diffSchemas(currentSchema, merged).isEmpty)
+  }
+
+  test("mergeSchemas - a nested field differing only in case stays distinct 
when case-sensitive") {
+    // The case-sensitive control: `s.value` and `s.Value` are different 
fields, so the merged
+    // struct carries both.
+    val currentSchema = new StructType().add("s", new 
StructType().add("value", StringType))
+    val dataSchema = new StructType().add("s", new StructType().add("Value", 
StringType))
+
+    val merged = SchemaMergingUtils.mergeSchemas(currentSchema, dataSchema, 
caseSensitive = true)
+    val expectedStruct = new StructType().add("value", 
StringType).add("Value", StringType)
+    assert(merged === new StructType().add("s", expectedStruct))
+
+    // Unlike the case-insensitive test above (where the merge is a no-op and 
no changes are
+    // derived), evolution here must rewrite the top-level `s` column. 
`diffSchemas` compares nested
+    // types wholesale, so the growth of a nested field surfaces as a single 
UpdateColumnType on `s`
+    // carrying the full new struct -- not as an add of `s.Value`.
+    val changes = SchemaInferenceUtils.diffSchemas(currentSchema, merged)
+    assert(changes.length === 1)
+    val typeChange = changes.collect { case tc: TableChange.UpdateColumnType 
=> tc }
+    assert(typeChange.length === 1)
+    assert(typeChange.head.fieldNames() === Array("s"))
+    assert(typeChange.head.newDataType() === expectedStruct)
+  }
+
+  test("mergeSchemas - a nested case-only field whose type also changes fails 
to merge, and " +
+    "diffSchemas reports it as a type change") {
+    // A nested field that differs only in case AND changes type is rejected 
rather than silently
+    // resolved. Note this is a *type* incompatibility, not a case one: 
`StructType.merge` never
+    // widens numeric types, so `int` -> `long` fails identically for a 
same-cased field and at the
+    // top level. The value of pinning it here is that case-insensitive 
matching does not turn an
+    // incompatible type change into a silent merge -- the run still fails 
loudly, and the user's
+    // remedy is a full refresh.
+    val currentSchema = new StructType().add("s", new 
StructType().add("value", IntegerType))
+    val dataSchema = new StructType().add("s", new StructType().add("Value", 
LongType))
+
+    val ex = intercept[SparkException] {
+      SchemaMergingUtils.mergeSchemas(currentSchema, dataSchema, caseSensitive 
= false)
+    }
+    assert(ex.getCondition === "CANNOT_MERGE_INCOMPATIBLE_DATA_TYPE")
+
+    // Same-cased and top-level widening fail the same way, confirming the 
rejection is about the
+    // type change rather than the case difference.
+    intercept[SparkException] {
+      SchemaMergingUtils.mergeSchemas(
+        currentSchema,
+        new StructType().add("s", new StructType().add("value", LongType)),
+        caseSensitive = false)
+    }
+    intercept[SparkException] {
+      SchemaMergingUtils.mergeSchemas(
+        new StructType().add("v", IntegerType),
+        new StructType().add("v", LongType),
+        caseSensitive = false)
+    }
+
+    // Diffing the two schemas directly (rather than diffing against their 
merge, which fails
+    // above) reports a TYPE change on the enclosing `s` column -- not a 
field-name mismatch, i.e.
+    // not an add of `s.Value` plus a delete of `s.value`. `diffSchemas` keys 
column identity only
+    // at the top level and compares nested types wholesale, so the case 
difference inside the
+    // struct never surfaces as an add/delete pair.
+    {
+      val changes = SchemaInferenceUtils.diffSchemas(currentSchema, dataSchema)
+      assert(changes.length === 1, s"changes=$changes")
+      val typeChange = changes.collect { case tc: TableChange.UpdateColumnType 
=> tc }
+      assert(typeChange.length === 1, s"changes=$changes")
+      assert(typeChange.head.fieldNames() === Array("s"))
+      assert(typeChange.head.newDataType() === new StructType().add("Value", 
LongType))
+      assert(!changes.exists(_.isInstanceOf[TableChange.AddColumn]))
+      assert(!changes.exists(_.isInstanceOf[TableChange.DeleteColumn]))
+    }
+  }
 }
diff --git 
a/sql/pipelines/src/test/scala/org/apache/spark/sql/pipelines/utils/TestGraphRegistrationContext.scala
 
b/sql/pipelines/src/test/scala/org/apache/spark/sql/pipelines/utils/TestGraphRegistrationContext.scala
index 068171a46aa1..9d6067cd51cd 100644
--- 
a/sql/pipelines/src/test/scala/org/apache/spark/sql/pipelines/utils/TestGraphRegistrationContext.scala
+++ 
b/sql/pipelines/src/test/scala/org/apache/spark/sql/pipelines/utils/TestGraphRegistrationContext.scala
@@ -305,7 +305,8 @@ class TestGraphRegistrationContext(
       query: FlowFunction,
       once: Boolean = false,
       catalog: Option[String] = None,
-      database: Option[String] = None
+      database: Option[String] = None,
+      sqlConf: Map[String, String] = Map.empty
   ): Unit = {
     val rawFlowIdentifier = GraphIdentifierManager.parseTableIdentifier(name, 
spark)
     val rawDestinationIdentifier =
@@ -345,7 +346,7 @@ class TestGraphRegistrationContext(
           currentCatalog = catalog.orElse(Some(defaultCatalog)),
           currentDatabase = database.orElse(Some(defaultDatabase))
         ),
-        sqlConf = Map.empty,
+        sqlConf = sqlConf,
         once = once,
         origin = QueryOrigin(
           objectName = Option(flowIdentifier.unquotedString),
@@ -407,7 +408,8 @@ class TestGraphRegistrationContext(
    * Generates a dataflow graph from this pipeline definition and resolves it.
    * @return
    */
-  def resolveToDataflowGraph(): DataflowGraph = toDataflowGraph.resolve()
+  def resolveToDataflowGraph(): DataflowGraph =
+    toDataflowGraph.resolve(spark.sessionState.conf.caseSensitiveAnalysis)
 }
 
 object TestGraphRegistrationContext {


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

Reply via email to