spark-test commented on code in PR #59681:
URL: https://github.com/apache/spark/pull/59681#discussion_r4348346563


##########
sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/analysis/Analyzer.scala:
##########
@@ -2796,6 +2796,10 @@ class Analyzer(
         resolveSubQueries(j, j)
       case tvf: UnresolvedTableValuedFunction =>
         resolveSubQueries(tvf, tvf)
+      // An inline table has no output, so its subqueries cannot reference its 
columns.
+      case t: UnresolvedInlineTable

Review Comment:
   **Non-blocking (P2):** This case makes scalar subqueries in VALUES lists 
resolvable when the conf is on, but `CheckAnalysis` still has the unconditional 
`case u: UnresolvedInlineTable if 
unresolvedInlineTableContainsScalarSubquery(u)` (CheckAnalysis.scala:495). It 
matches any unresolved inline table containing a `ScalarSubquery`, resolved or 
not, and runs inside `foreachUp` before the generic `case operator` branch that 
reports `UNRESOLVED_COLUMN`. It also runs before the subquery plan is checked, 
which is where a missing table would be reported.
   
   So with the default conf, `SELECT * FROM VALUES ((SELECT 1), unknown_col)` 
and `SELECT * FROM VALUES ((SELECT x FROM missing_table))` (and the same in 
`INSERT ... VALUES`) fail with `SCALAR_SUBQUERY_IN_VALUES`, i.e. "scalar 
subqueries in VALUES are unsupported". That contradicts the new docs and points 
users away from the actual typo. Applying that check only when 
`INLINE_TABLE_NON_FOLDABLE_EXPRESSIONS_ENABLED` is false would let the generic 
errors surface and keep the conf-off behavior unchanged.
   
   **Verification:**
   - **Regression:** With the feature conf on, a VALUES list that has a 
resolved scalar subquery plus an unknown column, and a VALUES list whose scalar 
subquery reads a missing table, report the unresolved column and the missing 
table respectively instead of the unsupported-scalar-subquery error.
   - **Compatibility:** With the feature conf off, scalar subqueries in VALUES 
lists still fail with the unsupported-scalar-subquery error at the same query 
context as before.



##########
sql/core/src/test/scala/org/apache/spark/sql/InlineTableNonFoldableExpressionsSuite.scala:
##########
@@ -0,0 +1,301 @@
+/*
+ * 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
+
+import java.util.UUID
+
+import org.apache.spark.{SparkRuntimeException, TaskContext}
+import org.apache.spark.sql.IntegratedUDFTestUtils.{registerTestUDF, 
shouldTestPythonUDFs, TestPythonUDF}
+import org.apache.spark.sql.execution.{ExpandExec, LocalTableScanExec}
+import org.apache.spark.sql.execution.adaptive.AdaptiveSparkPlanHelper
+import org.apache.spark.sql.functions.udf
+import org.apache.spark.sql.internal.SQLConf
+import org.apache.spark.sql.test.SharedSparkSession
+import org.apache.spark.sql.types.{DoubleType, LongType, StructField, 
StructType}
+
+/**
+ * End-to-end tests for VALUES lists with expressions that are not foldable, 
which are evaluated
+ * by an `Expand` over a `OneRowRelation` when the query runs.
+ */
+class InlineTableNonFoldableExpressionsSuite
+  extends QueryTest with SharedSparkSession with AdaptiveSparkPlanHelper {
+  import testImplicits._
+
+  private def expandExecs(df: DataFrame): Seq[ExpandExec] =
+    collect(df.queryExecution.executedPlan) { case e: ExpandExec => e }
+
+  private def contextOf(query: String, fragment: String): ExpectedContext = {
+    val start = query.indexOf(fragment)
+    assert(start >= 0, s"'$fragment' is not part of '$query'")
+    ExpectedContext(fragment, start, start + fragment.length - 1)
+  }
+
+  test("non-foldable expressions are evaluated on executors") {
+    withUserDefinedFunction("in_task" -> true) {
+      spark.udf.register("in_task", udf(() => TaskContext.get() != 
null).asNondeterministic())
+      val df = sql("SELECT * FROM VALUES (1, in_task()), (2, in_task()) AS 
t(id, in_task)")
+      checkAnswer(df, Seq(Row(1, true), Row(2, true)))
+      val execs = expandExecs(df)
+      assert(execs.size == 1)
+      // All the rows are evaluated in a single task.
+      assert(execs.head.execute().getNumPartitions == 1)
+    }
+  }
+
+  test("VALUES lists with only foldable expressions still become local 
relations") {
+    val df = sql("SELECT * FROM VALUES (1, 'a'), (2 + 1, upper('b')) AS t(id, 
s)")
+    checkAnswer(df, Seq(Row(1, "a"), Row(3, "B")))
+    assert(expandExecs(df).isEmpty)
+    assert(collect(df.queryExecution.executedPlan) { case l: 
LocalTableScanExec => l }.size == 1)
+  }
+
+  test("VALUES lists that were supported before are evaluated during analysis 
as before") {
+    withUserDefinedFunction("on_driver" -> true) {
+      spark.udf.register("on_driver", udf((s: String) => s != null && 
TaskContext.get() == null))
+      val queries = Seq(
+        "SELECT * FROM VALUES (1, 'a'), (2 + 1, upper('b')) AS t(a, b)",
+        "SELECT * FROM VALUES (current_date(), 1 + 1) AS t(a, b)",
+        // The UDF is not foldable, but it was supported because its input is 
a CURRENT_LIKE
+        // expression. It is evaluated on the driver at the end of the 
analysis phase.
+        "SELECT * FROM VALUES (on_driver(current_user())) AS t(a)")
+      queries.foreach { query =>
+        val df = sql(query)
+        val legacy = withSQLConf(
+            SQLConf.INLINE_TABLE_NON_FOLDABLE_EXPRESSIONS_ENABLED.key -> 
"false") {
+          sql(query)
+        }
+        comparePlans(
+          df.queryExecution.analyzed, legacy.queryExecution.analyzed, 
checkAnalysis = false)
+        checkAnswer(df, legacy)
+        assert(expandExecs(df).isEmpty)
+      }
+      checkAnswer(sql(queries.last), Row(true))
+    }
+
+    // The expressions of inline tables with CURRENT_LIKE expressions are 
evaluated at the end of
+    // the analysis phase, so evaluation errors are reported when the query is 
optimized.
+    withSQLConf(SQLConf.ANSI_ENABLED.key -> "true") {
+      val df = sql("SELECT * FROM VALUES (current_date(), 1 / 0) AS t(a, b)")
+      val e = intercept[AnalysisException](df.collect())
+      assert(e.getCondition == 
"INVALID_INLINE_TABLE.FAILED_SQL_EXPRESSION_EVALUATION")
+    }
+  }
+
+  test("nondeterministic expressions") {
+    // Every expression has its own seed.
+    val uuids = sql("SELECT * FROM VALUES (uuid()), (uuid()), 
(uuid())").collect()
+    assert(uuids.map(_.getString(0)).distinct.length == 3)
+
+    // Each row is evaluated like the only row of a one-row relation.
+    checkAnswer(
+      sql("SELECT * FROM VALUES (rand(5)), (rand(5))"),
+      sql("SELECT rand(5) UNION ALL SELECT rand(5)"))
+
+    // The seeds are chosen during analysis, so executing the plan again 
returns the same values.
+    val df = sql("SELECT * FROM VALUES (rand(), 1), (rand(), 2)")
+    checkAnswer(df, df.collect().toSeq)
+  }
+
+  test("equal nondeterministic expressions are evaluated separately for every 
row") {
+    withUserDefinedFunction("random_uuid" -> true) {
+      spark.udf.register("random_uuid", udf(() => 
UUID.randomUUID().toString).asNondeterministic())
+      Seq(true, false).foreach { codegen =>
+        withSQLConf(SQLConf.WHOLESTAGE_CODEGEN_ENABLED.key -> 
codegen.toString) {
+          val rows = sql("SELECT * FROM VALUES (random_uuid()), 
(random_uuid())").collect()
+          assert(rows.map(_.getString(0)).distinct.length == 2)
+        }
+      }
+    }
+  }
+
+  test("CURRENT_LIKE expressions are evaluated once per query") {
+    val df = sql(
+      """SELECT count(DISTINCT ts), count(DISTINCT r)
+        |FROM VALUES (current_timestamp(), rand()), (current_timestamp(), 
rand()) AS t(ts, r)
+        |""".stripMargin)
+    checkAnswer(df, Row(1, 2))
+  }
+
+  test("type coercion and nullability") {
+    val df = sql("SELECT * FROM VALUES (1, rand()), (2L, NULL) AS t(a, b)")
+    assert(df.schema == StructType(Seq(
+      StructField("a", LongType, nullable = false),
+      StructField("b", DoubleType, nullable = true))))
+    checkAnswer(df.select($"a", $"b".isNull), Seq(Row(1L, false), Row(2L, 
true)))
+  }
+
+  test("uncorrelated scalar subqueries") {
+    checkAnswer(sql("SELECT * FROM VALUES ((SELECT 1) + (SELECT 2), 'a')"), 
Row(3, "a"))
+    withTable("t") {
+      sql("CREATE TABLE t(id INT) USING parquet")
+      sql("INSERT INTO t VALUES (1), (2)")
+      sql("INSERT INTO t VALUES ((SELECT max(id) FROM t) + 1)")
+      checkAnswer(sql("SELECT * FROM t"), Seq(Row(1), Row(2), Row(3)))
+    }
+  }
+
+  test("uncorrelated scalar subqueries with CURRENT_LIKE expressions") {
+    // The CURRENT_LIKE expressions inside and next to the subqueries share 
the value of the rest
+    // of the query.
+    checkAnswer(
+      sql("SELECT d = current_date() FROM VALUES ((SELECT current_date())) AS 
t(d)"),
+      Row(true))
+    checkAnswer(
+      sql(
+        """SELECT d = date_add(current_date(), 1)
+          |FROM VALUES (date_add(current_date(), (SELECT 1))) AS t(d)
+          |""".stripMargin),
+      Row(true))
+    withTable("t") {
+      sql("CREATE TABLE t(id INT, ts TIMESTAMP) USING parquet")
+      sql("INSERT INTO t VALUES (1, TIMESTAMP'2000-01-01 00:00:00')")
+      sql(
+        """INSERT INTO t VALUES
+          |  ((SELECT max(id) FROM t WHERE ts < current_timestamp()) + 1, 
current_timestamp())
+          |""".stripMargin)
+      checkAnswer(sql("SELECT id FROM t"), Seq(Row(1), Row(2)))
+    }
+  }
+
+  test("correlated references and subqueries other than scalar subqueries are 
rejected") {
+    withTempView("t") {
+      sql("CREATE TEMPORARY VIEW t AS SELECT * FROM VALUES (1) AS t(a)")
+      val query = "SELECT * FROM t, LATERAL (VALUES (t.a, rand())) AS v(b, r)"
+      checkError(
+        exception = intercept[AnalysisException](sql(query)),
+        condition = 
"INVALID_INLINE_TABLE.CANNOT_EVALUATE_EXPRESSION_IN_INLINE_TABLE",

Review Comment:
   **Nit (P3):** This test covers `LATERAL (VALUES (t.a, ...))`, `EXISTS` and 
`IN`, but not a correlated scalar subquery. The `case s: ScalarSubquery => 
s.isCorrelated` branch of `isRuntimeEvaluable` is checked only with a 
hand-built `ScalarSubquery(correlatedPlan, outerAttrs = ...)` in 
`ResolveInlineTablesSuite`.
   
   The real analyzer may never produce that shape. The new `ResolveSubquery` 
case calls `resolveSubQueries(t, t)`, and `resolveSubQuery` runs the subquery 
under `AnalysisContext.withOuterPlan(outer)`. That makes the inline table, 
which has no children or output, the only outer plan, and `resolveOuterRef` 
only looks at `outerPlan.resolveChildren`. So in `SELECT * FROM t, LATERAL 
(VALUES ((SELECT t.a), rand()))`, `t.a` likely stays unresolved, `isCorrelated` 
is never reached, and users get a different error, possibly the stale 
`SCALAR_SUBQUERY_IN_VALUES`. I haven't run it to confirm which. Adding this 
query to the test would show the actual error and pin it to a clear one.
   
   **Verification:**
   - **Regression:** A correlated scalar subquery in a VALUES list, analyzed by 
the real analyzer, is rejected with a clear error that names the correlated 
reference. If the outer column is not resolved because the inline table is used 
as the subquery's outer plan, the test makes that visible, and the error is 
fixed or documented accordingly.



##########
sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/util/EvaluateUnresolvedInlineTable.scala:
##########
@@ -85,18 +120,19 @@ object EvaluateUnresolvedInlineTable extends SQLConfHelper
   }
 
   /**
-   * Validates that all inline table data are valid expressions that can be 
evaluated
-   * (in this they must be foldable).
-   * Note that nondeterministic expressions are not supported since they are 
not foldable.
-   * Exception are CURRENT_LIKE expressions, which are replaced by a literal 
in later stages.
+   * Validates that all inline table data are valid expressions that can be 
evaluated: during the
+   * analysis phase (see [[canEvalDuringAnalysis]]), or, if
+   * [[SQLConf.INLINE_TABLE_NON_FOLDABLE_EXPRESSIONS_ENABLED]] is true, when 
the query runs (see
+   * [[isRuntimeEvaluable]]).
    * This is package visible for unit testing.
    */
   def validateInputEvaluable(table: UnresolvedInlineTable): Unit = {
+    val allowRuntimeEvaluation =
+      conf.getConf(SQLConf.INLINE_TABLE_NON_FOLDABLE_EXPRESSIONS_ENABLED)
     table.rows.foreach { row =>
       row.foreach { e =>
-        if (e.containsPattern(CURRENT_LIKE)) {
-          // Do nothing.
-        } else if (!e.resolved || !trimAliases(prepareForEval(e)).foldable) {
+        if (!canEvalDuringAnalysis(e) &&

Review Comment:
   **Nit (P3):** `canEvalDuringAnalysis` is true for any cell that contains a 
CURRENT_LIKE expression and no subquery, so `isRuntimeEvaluable` never runs for 
such cells. When another cell forces the `Expand` path, `toExpand` keeps the 
cell as a runtime projection because it contains CURRENT_LIKE.
   
   For `SELECT * FROM VALUES (max(current_date()), rand())`, the aggregate ends 
up in the `Expand`. CheckAnalysis then fails through 
`PlanHelper.specialExpressionsInUnsupportedOperator` with 
`UNSUPPORTED_EXPR_FOR_OPERATOR` ("The query operator `Expand` contains one or 
more unsupported expressions"), naming an operator the user never wrote. The PR 
description says such cells fail with 
`INVALID_INLINE_TABLE.CANNOT_EVALUATE_EXPRESSION_IN_INLINE_TABLE`, and before 
this PR this query did fail with that error (on `rand()`). Window and generator 
functions over `current_date()` take the same route, and this also makes the 
CURRENT_LIKE-leaf carve-out in `isRuntimeEvaluable` unreachable. When the table 
goes to the `Expand` path, also requiring `isRuntimeEvaluable` for non-foldable 
CURRENT_LIKE cells would fix this without touching lists that are fully 
evaluable during analysis.
   
   **Verification:**
   - **Regression:** An aggregate, a window function and a generator applied to 
a CURRENT_LIKE expression, each in a VALUES list that also contains a 
nondeterministic cell, are rejected with the inline-table cannot-evaluate error 
instead of an operator-level error.
   - **Compatibility:** Lists that were valid before, including a deterministic 
Scala UDF over current_user() and current_date() next to arithmetic, keep 
identical analyzed plans and results with the feature conf on and off, and 
current_timestamp() next to rand() still yields one timestamp per query.



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