peter-toth commented on code in PR #59965:
URL: https://github.com/apache/spark/pull/59965#discussion_r4440779653


##########
sql/core/src/test/scala/org/apache/spark/sql/connector/GeneratedColumnWriteSuite.scala:
##########
@@ -1290,4 +1302,497 @@ class GeneratedColumnWriteSuite extends QueryTest with 
DatasourceV2SQLBase {
         "No CheckInvariant should be added when the table lacks the write 
capability")
     }
   }
+
+  test("SPARK-60757: an auto-filled value does not depend on the parser 
settings of the writer") {
+    import testImplicits._
+    val tblName = "my_tab"
+    withTable(s"testcat.$tblName") {
+      sql(s"""CREATE TABLE testcat.$tblName(
+             |  a STRING,
+             |  b STRING GENERATED ALWAYS AS (concat(a, 'x\\\\y'))
+             |) USING foo""".stripMargin)
+      sql(s"INSERT INTO testcat.$tblName(a) VALUES ('default')")
+      withSQLConf(SQLConf.ESCAPED_STRING_LITERALS.key -> "true") {
+        sql(s"INSERT INTO testcat.$tblName(a) VALUES ('insert')")
+        Seq("append").toDF("a").writeTo(s"testcat.$tblName").append()
+      }
+      checkAnswer(
+        spark.table(s"testcat.$tblName"),
+        Seq("default", "insert", "append").map(a => Row(a, a + "x\\y")))
+
+      withSQLConf(SQLConf.ESCAPED_STRING_LITERALS.key -> "true") {
+        sql(s"INSERT OVERWRITE testcat.$tblName(a) VALUES ('overwrite')")
+      }
+      checkAnswer(spark.table(s"testcat.$tblName"), Row("overwrite", 
"overwritex\\y"))
+    }
+  }
+
+  test("SPARK-60757: the validation of a provided value does not depend on the 
parser settings " +
+    "of the writer") {
+    val tblName = "my_tab"
+    withTable(s"testcat.$tblName") {
+      sql(s"""CREATE TABLE testcat.$tblName(
+             |  a STRING,
+             |  b STRING GENERATED ALWAYS AS (concat(a, 'x\\\\y'))
+             |) USING foo""".stripMargin)
+      withSQLConf(SQLConf.ESCAPED_STRING_LITERALS.key -> "true") {
+        sql(s"INSERT INTO testcat.$tblName(a, b) SELECT 'p', concat('p', 'x', 
chr(92), 'y')")
+        assertWriteFails("CHECK_CONSTRAINT_VIOLATION") {
+          sql(s"INSERT INTO testcat.$tblName(a, b) " +
+            "SELECT 'q', concat('q', 'x', chr(92), chr(92), 'y')")
+        }
+      }
+      checkAnswer(spark.table(s"testcat.$tblName"), Row("p", "px\\y"))
+    }
+  }
+
+  test("SPARK-60757: a DELETE that rewrites rows does not depend on the parser 
settings of the " +
+    "writer") {
+    withRowLevelCatalog {
+      val tblName = "my_tab"
+      withTable(s"$rowLevelCat.$tblName") {
+        sql(s"""CREATE TABLE $rowLevelCat.$tblName(
+               |  id INT,
+               |  a STRING,
+               |  b STRING GENERATED ALWAYS AS (concat(a, 'x\\\\y'))
+               |) USING foo""".stripMargin)
+        sql(s"INSERT INTO $rowLevelCat.$tblName(id, a) VALUES (1, 'p'), (2, 
'q')")
+        withSQLConf(SQLConf.ESCAPED_STRING_LITERALS.key -> "true") {
+          sql(s"DELETE FROM $rowLevelCat.$tblName WHERE id <= 1")
+        }
+        checkAnswer(spark.table(s"$rowLevelCat.$tblName"), Row(2, "q", 
"qx\\y"))
+      }
+    }
+  }
+
+  test("SPARK-60757: a later write carries a row that a non-ANSI writer 
auto-filled") {
+    val tblName = "my_tab"
+    withTable(s"testcat.$tblName") {
+      withSQLConf(SQLConf.ANSI_ENABLED.key -> "false") {
+        sql(s"""CREATE TABLE testcat.$tblName(
+               |  a STRING,
+               |  b DOUBLE GENERATED ALWAYS AS (a + 1)
+               |) USING foo""".stripMargin)
+        sql(s"INSERT INTO testcat.$tblName(a) VALUES ('9007199254740993')")
+        sql(s"INSERT INTO testcat.$tblName SELECT * FROM testcat.$tblName")
+      }
+      val row = Row("9007199254740993", 9007199254740994D)
+      checkAnswer(spark.table(s"testcat.$tblName"), Seq(row, row))
+    }
+  }
+
+  test("SPARK-60757: a DELETE does not check the rows that it carries") {
+    withRowLevelCatalog {
+      val tblName = "my_tab"
+      withTable(s"$rowLevelCat.$tblName") {
+        withSQLConf(SQLConf.ANSI_ENABLED.key -> "false") {
+          // Without the capability, the rows go in as a non-ANSI writer 
stored them before.
+          sql(s"""CREATE TABLE $rowLevelCat.$tblName(
+                 |  id INT,
+                 |  a STRING,
+                 |  b DOUBLE GENERATED ALWAYS AS (a + 1)
+                 |) USING foo TBLPROPERTIES ('$noWriteCapProp' = 
'false')""".stripMargin)
+          sql(s"INSERT INTO $rowLevelCat.$tblName VALUES " +
+            "(1, 'abc', NULL), (2, '2', 3D), (3, '3', 4D)")
+          sql(s"ALTER TABLE $rowLevelCat.$tblName SET TBLPROPERTIES 
('$noWriteCapProp' = 'true')")
+          sql(s"DELETE FROM $rowLevelCat.$tblName WHERE id >= 3")
+        }
+        withSQLConf(SQLConf.ANSI_ENABLED.key -> "true") {
+          sql(s"DELETE FROM $rowLevelCat.$tblName WHERE id >= 2")
+        }
+        checkAnswer(spark.table(s"$rowLevelCat.$tblName"), Row(1, "abc", null))
+        assertWriteFails("CAST_INVALID_INPUT") {
+          sql(s"INSERT INTO $rowLevelCat.$tblName SELECT * FROM 
$rowLevelCat.$tblName")
+        }
+      }
+    }
+  }
+
+  test("SPARK-60757: an explicit cast and arithmetic keep failing under a 
non-ANSI writer") {
+    val tblName = "my_tab"
+    withTable(s"testcat.$tblName") {
+      sql(s"""CREATE TABLE testcat.$tblName(
+             |  a STRING,
+             |  b INT GENERATED ALWAYS AS (CAST(a AS INT)),
+             |  c INT,
+             |  d INT GENERATED ALWAYS AS (c + 1)
+             |) USING foo""".stripMargin)
+      withSQLConf(SQLConf.ANSI_ENABLED.key -> "false") {
+        assertWriteFails("CAST_INVALID_INPUT") {
+          sql(s"INSERT INTO testcat.$tblName(a, c) VALUES ('abc', 1)")
+        }
+        assertWriteFails("ARITHMETIC_OVERFLOW") {
+          sql(s"INSERT INTO testcat.$tblName(a, c) VALUES ('1', 
${Int.MaxValue})")
+        }
+        sql(s"INSERT INTO testcat.$tblName(a, c) VALUES ('1', 1)")
+      }
+      checkAnswer(spark.table(s"testcat.$tblName"), Row("1", 1, 1, 2))
+    }
+  }
+
+  test("SPARK-60757: the validation of a provided value keeps failing under a 
non-ANSI writer") {
+    val tblName = "my_tab"
+    withTable(s"testcat.$tblName") {
+      // UPPER has no way back from its V2 form, so the validation reads the 
SQL text.
+      sql(s"""CREATE TABLE testcat.$tblName(
+             |  a STRING,
+             |  b INT GENERATED ALWAYS AS (CAST(upper(a) AS INT))
+             |) USING foo""".stripMargin)
+      withSQLConf(SQLConf.ANSI_ENABLED.key -> "false") {
+        assertWriteFails("CAST_INVALID_INPUT") {
+          sql(s"INSERT INTO testcat.$tblName(a, b) VALUES ('abc', NULL)")
+        }
+      }
+      checkAnswer(spark.table(s"testcat.$tblName"), Nil)
+    }
+  }
+
+  test("SPARK-60757: a reserved keyword of an auto-filled column does not 
follow the settings " +
+    "of the writer") {
+    val tblName = "my_tab"
+    withTable(s"testcat.$tblName") {
+      sql(s"""CREATE TABLE testcat.$tblName(
+             |  `current_date` DATE,
+             |  b DATE GENERATED ALWAYS AS (current_date)
+             |) USING foo""".stripMargin)
+      withSQLConf(
+          SQLConf.ANSI_ENABLED.key -> "true",
+          SQLConf.ENFORCE_RESERVED_KEYWORDS.key -> "true") {
+        sql(s"INSERT INTO testcat.$tblName(`current_date`) VALUES 
(DATE'2024-06-15')")
+      }
+      val date = sqlDate("2024-06-15")
+      checkAnswer(spark.table(s"testcat.$tblName"), Row(date, date))
+    }
+  }
+
+  test("SPARK-60757: an implicit cast of an auto-filled column does not follow 
a non-ANSI writer") {
+    val tblName = "my_tab"
+    withTable(s"testcat.$tblName") {
+      withSQLConf(SQLConf.ANSI_ENABLED.key -> "true") {
+        sql(s"""CREATE TABLE testcat.$tblName(
+               |  a STRING,
+               |  b BIGINT GENERATED ALWAYS AS (a + 1)
+               |) USING foo""".stripMargin)
+      }
+      withSQLConf(SQLConf.ANSI_ENABLED.key -> "false") {
+        assertWriteFails("CAST_INVALID_INPUT") {
+          sql(s"INSERT INTO testcat.$tblName(a) VALUES ('abc')")
+        }
+        // A non-ANSI cast to DOUBLE would lose the last digit.
+        sql(s"INSERT INTO testcat.$tblName(a) VALUES ('9007199254740993')")
+      }
+      checkAnswer(
+        spark.table(s"testcat.$tblName"),
+        Row("9007199254740993", 9007199254740994L))
+    }
+  }
+
+  test("SPARK-60757: a function of an auto-filled column does not follow the 
settings of the " +
+    "writer") {
+    val tblName = "my_tab"
+    withTable(s"testcat.$tblName") {
+      sql(s"""CREATE TABLE testcat.$tblName(
+             |  a INT,
+             |  arr ARRAY<INT>,
+             |  b INT GENERATED ALWAYS AS (abs(a)),
+             |  c INT GENERATED ALWAYS AS (element_at(arr, 2)),
+             |  d INT GENERATED ALWAYS AS (size(arr))
+             |) USING foo""".stripMargin)
+      withSQLConf(SQLConf.ANSI_ENABLED.key -> "false") {
+        assertWriteFails("ARITHMETIC_OVERFLOW") {
+          sql(s"INSERT INTO testcat.$tblName(a, arr) VALUES (${Int.MinValue}, 
array(1, 2))")
+        }
+        assertWriteFails("INVALID_ARRAY_INDEX_IN_ELEMENT_AT") {
+          sql(s"INSERT INTO testcat.$tblName(a, arr) VALUES (1, array(1))")
+        }
+      }
+      withSQLConf(
+          SQLConf.ANSI_ENABLED.key -> "false",
+          SQLConf.LEGACY_SIZE_OF_NULL.key -> "true") {
+        sql(s"INSERT INTO testcat.$tblName(a, arr) VALUES (1, NULL)")
+      }
+      checkAnswer(spark.table(s"testcat.$tblName"), Row(1, null, 1, null, 
null))
+    }
+  }
+
+  test("SPARK-60757: a temporary function does not replace a function of an 
auto-filled column") {
+    val tblName = "my_tab"
+    withTable(s"testcat.$tblName") {
+      sql(s"""CREATE TABLE testcat.$tblName(
+             |  a STRING,
+             |  b STRING GENERATED ALWAYS AS (upper(a))
+             |) USING foo""".stripMargin)
+      spark.udf.register("upper", (s: String) => s + "-temp")
+      try {
+        withSQLConf(SQLConf.SESSION_FUNCTION_RESOLUTION_ORDER.key -> "first") {
+          sql(s"INSERT INTO testcat.$tblName(a) VALUES ('x')")
+        }
+      } finally {
+        sql("DROP TEMPORARY FUNCTION IF EXISTS upper")
+      }
+      checkAnswer(spark.table(s"testcat.$tblName"), Row("x", "X"))
+    }
+  }
+
+  test("SPARK-60757: an IDENTIFIER clause is not a different reading of a 
generation expression") {
+    val tblName = "my_tab"
+    withTable(s"testcat.$tblName") {
+      sql(s"""CREATE TABLE testcat.$tblName(
+             |  a INT,
+             |  b INT GENERATED ALWAYS AS (IDENTIFIER('a') + 1)
+             |) USING foo""".stripMargin)
+      sql(s"INSERT INTO testcat.$tblName(a) VALUES (1)")
+      checkAnswer(spark.table(s"testcat.$tblName"), Row(1, 2))
+    }
+  }
+
+  test("SPARK-60757: CREATE and REPLACE TABLE reject a generation expression 
the session reads " +
+    "differently") {
+    val tblName = "my_tab"
+    for {
+      (confs, genExpr) <- Seq(
+        (Seq(SQLConf.ESCAPED_STRING_LITERALS.key -> "true"), "concat(a, 
'x\\\\y')"),
+        (Seq(SQLConf.ANSI_ENABLED.key -> "true", 
SQLConf.DOUBLE_QUOTED_IDENTIFIERS.key -> "true"),
+          "concat(\"a\", 'x')"))
+      create <- Seq("CREATE TABLE", "CREATE OR REPLACE TABLE")
+    } withSQLConf(confs: _*) {
+      checkError(
+        exception = intercept[AnalysisException] {
+          sql(s"""$create testcat.$tblName(
+                 |  a STRING,
+                 |  b STRING GENERATED ALWAYS AS ($genExpr)
+                 |) USING foo""".stripMargin)
+        },
+        condition = "UNSUPPORTED_EXPRESSION_GENERATED_COLUMN",
+        parameters = Map(
+          "fieldName" -> "b",
+          "expressionStr" -> genExpr,
+          "reason" -> readsDifferentlyReason))
+    }
+  }
+
+  test("SPARK-60757: createTable with a schema rejects a generation expression 
the session " +
+    "reads differently") {
+    val metadata = new MetadataBuilder()
+      .putString(GeneratedColumn.GENERATION_EXPRESSION_METADATA_KEY, 
"concat(a, 'x\\\\y')")
+      .build()
+    val schema = new StructType()
+      .add("a", StringType)
+      .add("b", StringType, nullable = true, metadata)
+    val tblName = "my_tab"
+    withSQLConf(SQLConf.ESCAPED_STRING_LITERALS.key -> "true") {
+      checkError(
+        exception = intercept[AnalysisException] {
+          spark.catalog.createTable(s"testcat.$tblName", "foo", schema, 
Map.empty[String, String])
+        },
+        condition = "UNSUPPORTED_EXPRESSION_GENERATED_COLUMN",
+        parameters = Map(
+          "fieldName" -> "b",
+          "expressionStr" -> "concat(a, 'x\\\\y')",
+          "reason" -> readsDifferentlyReason))
+    }
+  }
+
+  test("SPARK-60757: a stored text that is not a scalar expression fails the 
analysis of the " +
+    "write") {
+    val tblName = "my_tab"
+    withTable(s"testcat.$tblName") {
+      // A catalog can store a text that CREATE TABLE rejects.
+      catalog("testcat").asInstanceOf[InMemoryCatalog].createTable(
+        Identifier.of(Array(), tblName),
+        Array(
+          Column.create("i", IntegerType),
+          Column.create("b", IntegerType, true, null, "sum(i)", null)),
+        Array.empty[Transform],
+        Collections.emptyMap[String, String])
+      assertWriteFails("MISSING_GROUP_BY") {
+        sql(s"INSERT INTO testcat.$tblName(i) VALUES (1)")
+      }
+    }
+  }
+
+  test("SPARK-60757: a parser setting of the session does not reject an 
expression that reads " +
+    "the same") {
+    val tblName = "my_tab"
+    withTable(s"testcat.$tblName") {
+      withSQLConf(SQLConf.ESCAPED_STRING_LITERALS.key -> "true") {
+        sql(s"""CREATE TABLE testcat.$tblName(
+               |  a STRING,
+               |  b STRING GENERATED ALWAYS AS (concat(a, 'xy'))
+               |) USING foo""".stripMargin)
+        sql(s"INSERT INTO testcat.$tblName(a) VALUES ('v')")
+      }
+      checkAnswer(spark.table(s"testcat.$tblName"), Row("v", "vxy"))
+    }
+  }
+
+  test("SPARK-60757: the validation of a provided value reads the text of a 
table with a V2 form") {
+    val tblName = "my_tab"
+    withTable(s"testcat.$tblName") {
+      withSQLConf(SQLConf.ANSI_ENABLED.key -> "false") {
+        // The V2 form is COALESCE(s, '1'), with the implicit cast of a 
non-ANSI session.
+        sql(s"""CREATE TABLE testcat.$tblName(
+               |  s STRING,
+               |  b STRING GENERATED ALWAYS AS (coalesce(s, 1))
+               |) USING foo""".stripMargin)
+        val table = 
catalog("testcat").asTableCatalog.loadTable(Identifier.of(Array(), tblName))
+        val genExpr = table.columns().find(_.name == 
"b").get.columnGenerationExpression()
+        assert(genExpr.getExpression != null)
+        sql(s"INSERT INTO testcat.$tblName VALUES ('007', '7')")
+        sql(s"INSERT INTO testcat.$tblName(s) VALUES ('008')")
+        checkAnswer(spark.table(s"testcat.$tblName"), Seq(Row("007", "7"), 
Row("008", "8")))
+      }
+    }
+  }
+
+  test("SPARK-60757: the validation of a provided value does not substitute a 
variable") {
+    val tblName = "my_tab"
+    val variable = s"$${system:java.version}"
+    withTable(s"testcat.$tblName") {
+      withSQLConf(SQLConf.VARIABLE_SUBSTITUTE_ENABLED.key -> "false") {
+        sql(s"""CREATE TABLE testcat.$tblName(
+               |  a STRING,
+               |  b STRING GENERATED ALWAYS AS (concat(a, '$variable'))
+               |) USING foo""".stripMargin)
+        sql(s"INSERT INTO testcat.$tblName VALUES ('w', 'w$variable')")

Review Comment:
   **Finding 24.** This write runs with `spark.sql.variable.substitute=false`, 
so the test passes on base too. Base only substitutes when the setting is on, 
which is the default. I ran a write under the default setting on base and head, 
with the DDL as here:
   
   ```scala
   Seq(("v", s"v$variable")).toDF("a", 
"b").writeTo(s"testcat.$tblName").append()
   // base: CHECK_CONSTRAINT_VIOLATION
   // head: succeeds
   ```
   
   A re-insert of an auto-filled row fails the same way on base. So the fix is 
real, but no test pins it. A DataFrame write outside the `withSQLConf` block 
would. A SQL `INSERT` would get its own `${...}` substituted. I tried this 
version, and it fails on base and passes on head:
   
   ```scala
     test("SPARK-60757: the validation of a provided value does not substitute 
a variable") {
       import testImplicits._
       val tblName = "my_tab"
       val variable = s"$${system:java.version}"
       withTable(s"testcat.$tblName") {
         withSQLConf(SQLConf.VARIABLE_SUBSTITUTE_ENABLED.key -> "false") {
           sql(s"""CREATE TABLE testcat.$tblName(
                  |  a STRING,
                  |  b STRING GENERATED ALWAYS AS (concat(a, '$variable'))
                  |) USING foo""".stripMargin)
           sql(s"INSERT INTO testcat.$tblName VALUES ('w', 'w$variable')")
         }
         // Substitution is on by default. A DataFrame write keeps the variable 
in its values.
         Seq(("v", s"v$variable")).toDF("a", 
"b").writeTo(s"testcat.$tblName").append()
         checkAnswer(
           spark.table(s"testcat.$tblName"),
           Seq(Row("w", s"w$variable"), Row("v", s"v$variable")))
       }
     }
   ```
   



##########
docs/sql-migration-guide.md:
##########
@@ -70,6 +70,8 @@ license: |
 - Since Spark 4.4, `CREATE TABLE`, `REPLACE TABLE`, and `ALTER TABLE ... ADD 
CONSTRAINT` on a Data Source V2 table fail with 
`USER_DEFINED_FUNCTION_IN_CHECK_CONSTRAINT` when the condition of a CHECK 
constraint calls a user-defined function, including a Data Source V2 function. 
`ALTER TABLE ... ADD CONSTRAINT` keeps failing with `UNSUPPORTED_SQL_UDF_USAGE` 
on a SQL function and with `INVALID_WHERE_CONDITION` on an aggregate function. 
Previously, a write could resolve the function name in its own session, so that 
it enforced another rule or failed (SPARK-60777). To run these statements, 
change the condition to call only built-in functions. Existing constraints that 
call a user-defined function are not rejected.
 - Since Spark 4.4, a write to a Data Source V2 table that parses the SQL text 
of a CHECK constraint binds an unqualified built-in function name to 
`system.builtin`, regardless of the SQL path and 
`spark.sql.functionResolution.sessionOrder` (SPARK-60777). Previously, an 
existing constraint could call another function with the same name.
 - Since Spark 4.4, `CREATE TABLE`, `REPLACE TABLE`, and `ALTER TABLE ... ADD 
CONSTRAINT` on a Data Source V2 table fail with `SUBQUERY_IN_CHECK_CONSTRAINT` 
when the condition of a CHECK constraint contains a subquery. `ALTER TABLE ... 
ADD CONSTRAINT` keeps failing with 
`UNSUPPORTED_SUBQUERY_EXPRESSION_CATEGORY.HIGHER_ORDER_FUNCTION` on a subquery 
in a lambda function. Previously, most of these statements failed with 
`INTERNAL_ERROR`, but a subquery that needed no resolution, such as `id > 
(SELECT 0 AS a)`, was accepted and every write evaluated it again 
(SPARK-60781). Existing constraints that contain a subquery are not rejected.
+- Since Spark 4.4, a write to a Data Source V2 table parses and analyzes the 
stored text of a generation expression with ANSI mode on and the default values 
of the SQL configs that change how a text reads, such as 
`spark.sql.parser.escapedStringLiterals`, regardless of the session of the 
write. Only the following still follow that session: the session time zone, 
case sensitivity, `spark.sql.timestampType`, 
`spark.sql.decimalOperations.allowPrecisionLoss`, the configs that a view also 
takes from the session that reads it, the functions found through `SET PATH`, 
and the configs that an expression reads during evaluation, such as 
`spark.sql.legacy.timeParserPolicy`. A text that does not parse or resolve 
under these settings, such as `flag = 1` on a `BOOLEAN` column, is read under 
that session. Previously, the session of the write decided how the text read, 
so a write could silently store a wrong value (SPARK-60757). As a result, with 
`spark.sql.ansi.enabled=false`, also for a table cre
 ated with that setting, a write can fail with an error such as 
`CAST_INVALID_INPUT` where Spark 4.3 stored `NULL`, and an implicit cast can 
change the stored value. A table created with a non-default value of a config 
such as `spark.sql.parser.escapedStringLiterals` also gets the default reading 
of its text on every write, without an error. A row that Spark 4.3 stored 
differently can fail with such an error or with `CHECK_CONSTRAINT_VIOLATION` 
when a later write provides it again, such as an `INSERT ... SELECT` from the 
same table. A `DELETE` does not check the rows that it carries. To fix such a 
table, create a new table with a text that reads as intended under these 
settings, and copy the other columns into it with `INSERT ... SELECT`.

Review Comment:
   **Finding 25.** A table CHECK constraint is still read under the writer's 
session, which [SPARK-60453](https://issues.apache.org/jira/browse/SPARK-60453) 
tracks. The generated value no longer is. So a write that passes on base now 
fails when a constraint repeats the generation text. On `testcat`, on base and 
head:
   
   ```sql
   CREATE TABLE t (a STRING, b STRING GENERATED ALWAYS AS (concat(a, 'x\\y')));
   ALTER TABLE t ADD CONSTRAINT c CHECK (b = concat(a, 'x\\y'));
   SET spark.sql.parser.escapedStringLiterals=true;
   INSERT INTO t (a) VALUES ('q');
   -- base: stores b = qx\\y and passes
   -- head: computes b = qx\y, and c fails it with CHECK_CONSTRAINT_VIOLATION
   ```
   
   Base's value is the wrong one, so this is SPARK-60453 showing up rather than 
a new defect. But a writer with a non-default parser setting now gets a failure 
where 4.3 succeeded. Until that ticket lands, one sentence in this entry would 
tell them why.
   



##########
sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/util/StoredExpressionText.scala:
##########
@@ -0,0 +1,105 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.spark.sql.catalyst.util
+
+import org.apache.spark.sql.AnalysisException
+import org.apache.spark.sql.catalyst.analysis.{Analyzer, 
ExpressionWithUnresolvedIdentifier}
+import org.apache.spark.sql.catalyst.expressions.{Alias, Expression, ExprId}
+import org.apache.spark.sql.catalyst.parser.{CatalystSqlParser, ParseException}
+import org.apache.spark.sql.catalyst.plans.logical.LogicalPlan
+import org.apache.spark.sql.internal.SQLConf
+
+/**
+ * Reads the SQL text of a generation expression stored with a table, 
independently of the
+ * settings of the session that reads it.
+ */
+object StoredExpressionText {
+
+  /**
+   * The settings a stored text is read under. The session time zone, case 
sensitivity, timestamp
+   * type and decimal precision loss stay with the given session, because the 
settings of the DDL
+   * are not known here and pinning them would silently change existing tables 
of a deployment that
+   * sets them.
+   */
+  def conf(session: SQLConf): SQLConf = {
+    val c = new SQLConf
+    Analyzer.retainResolutionConfigsForAnalysis(newConf = c, existingConf = 
session)
+    c.setConfString(SQLConf.ANSI_ENABLED.key, "true")

Review Comment:
   **Finding 26.** This pins ANSI for the analysis, but the optimizer of the 
write still runs under the writer's session. `ConstantFolding.hasNoSideEffect` 
(`optimizer/expressions.scala:58`) treats a `Cast` as free of side effects 
whenever the session has ANSI off, whatever the cast's own eval mode. So under 
such a session, `ConstantFolding` folds `size(array(CAST(a AS INT)))` to `1` 
and the ANSI cast is gone. On `testcat`, on base and head:
   
   ```sql
   CREATE TABLE t (a STRING, d INT GENERATED ALWAYS AS (size(array(CAST(a AS 
INT)))));
   INSERT INTO t (a) VALUES ('abc');
   -- ANSI-off writer: d = 1
   -- ANSI writer: CAST_INVALID_INPUT
   ```
   
   So this shape still depends on the writer. It predates the PR, since base 
gives the same, and it is not specific to generated columns. I found no JIRA 
ticket for it. [SPARK-60749](https://issues.apache.org/jira/browse/SPARK-60749) 
is the same kind of problem in another optimizer rule. Could you file one? 
Checking the cast's own eval mode there instead of `conf.ansiEnabled` looks 
like the fix. I haven't tried it.
   



##########
sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/analysis/Analyzer.scala:
##########
@@ -4122,11 +4125,43 @@ class Analyzer(
               cleanedRelation
             case other => other
           }
-          v2Write.withNewQuery(projection).withNewTable(cleanedTable)
+          v2Write
+            .withNewQuery(analyzeAutoFilledGeneratedColumns(
+              projection, autoFilledGenCols, 
resolveOutput(CatalystSqlParser.parseExpression)._1))

Review Comment:
   **Finding 27.** Finding 6 suggested reading a column that the fixed settings 
don't resolve entirely under the session. This does that, but it also gives 
such a column the writer's parser settings, not only its ANSI mode. With ANSI 
off for the DDL and the writes, on `testcat`, on base and head:
   
   ```sql
   CREATE TABLE t (d DATE, s STRING,
     b BOOLEAN GENERATED ALWAYS AS (CAST(d AS BOOLEAN) OR s = 'x\\y'));
   INSERT INTO t (d, s) SELECT DATE'2024-01-01', concat('x', chr(92), 'y');  -- 
b = true
   SET spark.sql.parser.escapedStringLiterals=true;
   INSERT INTO t (d, s) SELECT DATE'2024-01-02', concat('x', chr(92), 'y');  -- 
b = NULL
   ```
   
   Only the ANSI part needs the session here. So the fallback can parse under 
the fixed settings with the session's ANSI value, as the conf in 
`readsAsInSession` does. It then needs the session parse only for a text those 
settings can't parse:
   
   ```scala
     def parseWithSessionAnsi(text: String): Expression = {
       val session = SQLConf.get
       val c = conf(session)
       c.setConfString(SQLConf.ANSI_ENABLED.key, session.ansiEnabled.toString)
       try {
         SQLConf.withExistingConf(c) {
           CatalystSqlParser.parseExpression(text)
         }
       } catch {
         case _: ParseException => CatalystSqlParser.parseExpression(text)
       }
     }
   ```
   
   I tried it here and at the fallback at `ResolveTableConstraints.scala:149`. 
Both writes above then store `true`, and `GeneratedColumnWriteSuite` and 
`DataSourceV2DataFrameSuite` still pass.
   



##########
sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/analysis/ResolveTableConstraints.scala:
##########
@@ -119,27 +135,56 @@ class ResolveTableConstraints(val catalogManager: 
CatalogManager) extends Rule[L
       Option(col.columnGenerationExpression())
         .filter(_ => !autoFilledGenCols.exists(n => resolver(n, col.name)))
         .map { genExpr =>
-          val catalystExpr = buildGenerationCatalystExpression(genExpr)
+          val catalystExpr = buildGenerationCatalystExpression(col.name, 
genExpr)
           val colRef = UnresolvedAttribute.quoted(col.name)
           val columnExtractors = Seq(col.name -> colRef)
           val genExprSql = Option(genExpr.getSql()).getOrElse(catalystExpr.sql)
-          CheckInvariant(
-            EqualNullSafe(colRef, catalystExpr),
+          def check(expr: Expression): Expression = CheckInvariant(
+            EqualNullSafe(colRef, expr),
             columnExtractors,
             "Generated Column",
             s"${col.name} <=> $genExprSql")
+          analyzeGeneratedColumnCheck(check(catalystExpr), 
v2Write.query).getOrElse {
+            // The analysis of the write resolves it under the session that 
writes.
+            check(Option(genExpr.getSql).map(CatalystSqlParser.parseExpression)
+              .getOrElse(catalystExpr))
+          }
         }
     }.toSeq
   }
 
   /**
-   * Convert a V2 GenerationExpression to a Catalyst expression.
-   * Try the V2 expression first, fall back to parsing the SQL string.
+   * Analyzes the check of a provided value under the settings an auto-filled 
value is analyzed
+   * under, so that a row Spark auto-filled passes the check when a later 
write carries it.
+   *
+   * @return the analyzed check, or None if these settings cannot resolve it. 
A catalog can also
+   *         store a text that analyzes to another plan.
    */
-  private def buildGenerationCatalystExpression(genExpr: 
GenerationExpression): Expression = {
-    Option(genExpr.getExpression)
-      .flatMap(V2ExpressionUtils.toCatalyst)
-      
.getOrElse(catalogManager.v1SessionCatalog.parser.parseExpression(genExpr.getSql))
+  private def analyzeGeneratedColumnCheck(
+      check: Expression,
+      query: LogicalPlan): Option[Expression] = {
+    StoredExpressionText.analyze(Filter(check, LocalRelation(query.output)), 
analyze) match {
+      case Some(Filter(analyzed, _: LocalRelation)) if analyzed.resolved => 
Some(analyzed)
+      case _ => None
+    }
+  }
+
+  /**
+   * Convert a V2 GenerationExpression to a Catalyst expression. Read the SQL 
string first, as an
+   * auto-filled value does, and fall back to the V2 expression.
+   */
+  private def buildGenerationCatalystExpression(
+      colName: String,
+      genExpr: GenerationExpression): Expression = {
+    Option(genExpr.getSql)
+      .map(StoredExpressionText.parse)
+      
.orElse(Option(genExpr.getExpression).flatMap(V2ExpressionUtils.toCatalyst))
+      .getOrElse(throw new AnalysisException(
+        errorClass = "UNSUPPORTED_EXPRESSION_GENERATED_COLUMN",

Review Comment:
   **Finding 28.** `UNSUPPORTED_EXPRESSION_GENERATED_COLUMN` reads "Cannot 
create generated column <fieldName> with generation expression <expressionStr> 
because <reason>". So this `INSERT` now says that it cannot create column `b`, 
and the new test pins that message. A condition worded for the write, or a 
sub-condition of an existing write error, would fit better than the DDL one.
   



##########
docs/sql-migration-guide.md:
##########
@@ -70,6 +70,8 @@ license: |
 - Since Spark 4.4, `CREATE TABLE`, `REPLACE TABLE`, and `ALTER TABLE ... ADD 
CONSTRAINT` on a Data Source V2 table fail with 
`USER_DEFINED_FUNCTION_IN_CHECK_CONSTRAINT` when the condition of a CHECK 
constraint calls a user-defined function, including a Data Source V2 function. 
`ALTER TABLE ... ADD CONSTRAINT` keeps failing with `UNSUPPORTED_SQL_UDF_USAGE` 
on a SQL function and with `INVALID_WHERE_CONDITION` on an aggregate function. 
Previously, a write could resolve the function name in its own session, so that 
it enforced another rule or failed (SPARK-60777). To run these statements, 
change the condition to call only built-in functions. Existing constraints that 
call a user-defined function are not rejected.
 - Since Spark 4.4, a write to a Data Source V2 table that parses the SQL text 
of a CHECK constraint binds an unqualified built-in function name to 
`system.builtin`, regardless of the SQL path and 
`spark.sql.functionResolution.sessionOrder` (SPARK-60777). Previously, an 
existing constraint could call another function with the same name.
 - Since Spark 4.4, `CREATE TABLE`, `REPLACE TABLE`, and `ALTER TABLE ... ADD 
CONSTRAINT` on a Data Source V2 table fail with `SUBQUERY_IN_CHECK_CONSTRAINT` 
when the condition of a CHECK constraint contains a subquery. `ALTER TABLE ... 
ADD CONSTRAINT` keeps failing with 
`UNSUPPORTED_SUBQUERY_EXPRESSION_CATEGORY.HIGHER_ORDER_FUNCTION` on a subquery 
in a lambda function. Previously, most of these statements failed with 
`INTERNAL_ERROR`, but a subquery that needed no resolution, such as `id > 
(SELECT 0 AS a)`, was accepted and every write evaluated it again 
(SPARK-60781). Existing constraints that contain a subquery are not rejected.
+- Since Spark 4.4, a write to a Data Source V2 table parses and analyzes the 
stored text of a generation expression with ANSI mode on and the default values 
of the SQL configs that change how a text reads, such as 
`spark.sql.parser.escapedStringLiterals`, regardless of the session of the 
write. Only the following still follow that session: the session time zone, 
case sensitivity, `spark.sql.timestampType`, 
`spark.sql.decimalOperations.allowPrecisionLoss`, the configs that a view also 
takes from the session that reads it, the functions found through `SET PATH`, 
and the configs that an expression reads during evaluation, such as 
`spark.sql.legacy.timeParserPolicy`. A text that does not parse or resolve 
under these settings, such as `flag = 1` on a `BOOLEAN` column, is read under 
that session. Previously, the session of the write decided how the text read, 
so a write could silently store a wrong value (SPARK-60757). As a result, with 
`spark.sql.ansi.enabled=false`, also for a table cre
 ated with that setting, a write can fail with an error such as 
`CAST_INVALID_INPUT` where Spark 4.3 stored `NULL`, and an implicit cast can 
change the stored value. A table created with a non-default value of a config 
such as `spark.sql.parser.escapedStringLiterals` also gets the default reading 
of its text on every write, without an error. A row that Spark 4.3 stored 
differently can fail with such an error or with `CHECK_CONSTRAINT_VIOLATION` 
when a later write provides it again, such as an `INSERT ... SELECT` from the 
same table. A `DELETE` does not check the rows that it carries. To fix such a 
table, create a new table with a text that reads as intended under these 
settings, and copy the other columns into it with `INSERT ... SELECT`.

Review Comment:
   **Finding 29.** "A `DELETE` does not check the rows that it carries" holds 
for the generated columns only. A copy-on-write `DELETE` still checks the 
table's CHECK constraints on the rows it carries 
(`ResolveTableConstraints.scala:54`). Only a `WriteDelta` `DELETE` skips both 
(`:47`). "does not check the generated columns of the rows that it carries" 
would be exact.
   



##########
sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/analysis/ResolveTableConstraints.scala:
##########
@@ -123,23 +132,47 @@ class ResolveTableConstraints(val catalogManager: 
CatalogManager) extends Rule[L
           val colRef = UnresolvedAttribute.quoted(col.name)
           val columnExtractors = Seq(col.name -> colRef)
           val genExprSql = Option(genExpr.getSql()).getOrElse(catalystExpr.sql)
-          CheckInvariant(
-            EqualNullSafe(colRef, catalystExpr),
+          def check(expr: Expression): Expression = CheckInvariant(
+            EqualNullSafe(colRef, expr),

Review Comment:
   **Finding 15 (round 4).** This PR already changes the ANSI-off mode. Base 
rejects `'07'` and `' 7'` there, and head accepts them. So keeping the default 
mode as it is doesn't avoid a behavior change. It picks the side where the 
column can hold a value that its own expression doesn't produce. Base's 
acceptance with ANSI on is the same defect. The fix moves both modes to the 
value the auto-fill stores.
   
   If it stays out of this PR anyway, two things would cover it:
   
   - The migration entry needs the sentence that the description already has 
("With `spark.sql.ansi.enabled=false`, the validation of a provided value now 
uses the ANSI implicit casts ..."). An ANSI-off writer now gets provided values 
accepted that used to fail, and the entry doesn't say so.
   - A JIRA ticket for the check comparing at the expression's type, so the 
default-mode fix has a home.
   



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

To unsubscribe, e-mail: [email protected]

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


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

Reply via email to