HyukjinKwon commented on code in PR #59425:
URL: https://github.com/apache/spark/pull/59425#discussion_r4270893370
##########
sql/connect/server/src/test/scala/org/apache/spark/sql/connect/pipelines/SparkDeclarativePipelinesServerSuite.scala:
##########
@@ -1164,4 +1164,127 @@ class SparkDeclarativePipelinesServerSuite
relation.getCommand.getSqlCommand.getInput)
}
}
+
+ // Helper to build a SqlCommand ExecutePlanRequest with
PipelineAnalysisContext.
+ private def buildSqlCommandRequestWithPipelineContext(
+ graphId: String,
+ input: proto.Relation): proto.ExecutePlanRequest = {
+ val context = proto.PipelineAnalysisContext
+ .newBuilder()
+ .setDataflowGraphId(graphId)
+ .build()
+ val userContext = proto.UserContext
+ .newBuilder()
+ .addExtensions(com.google.protobuf.Any.pack(context))
+ .setUserId("test_user")
+ .build()
+ val plan = proto.Plan
+ .newBuilder()
+ .setCommand(
+ proto.Command
+ .newBuilder()
+ .setSqlCommand(proto.SqlCommand.newBuilder().setInput(input).build())
+ .build())
+ .build()
+ proto.ExecutePlanRequest
+ .newBuilder()
+ .setUserContext(userContext)
+ .setPlan(plan)
+ .setSessionId(UUID.randomUUID().toString)
+ .build()
+ }
+
+ // Helper to build a SQL Relation.
+ private def sqlRelation(query: String): proto.Relation =
+ proto.Relation
+ .newBuilder()
+ .setSql(proto.SQL.newBuilder().setQuery(query))
+ .build()
+
+ // Helper to build a WITH_RELATIONS Relation with the given root SQL and
reference SQLs.
+ private def withRelationsRelation(
+ rootSql: String,
+ refs: Seq[(String, String)]): proto.Relation = {
+ val withRelationsBuilder =
proto.WithRelations.newBuilder().setRoot(sqlRelation(rootSql))
+ refs.foreach { case (alias, refSql) =>
+ withRelationsBuilder.addReferences(
+ proto.Relation
+ .newBuilder()
+ .setSubqueryAlias(
+ proto.SubqueryAlias
+ .newBuilder()
+ .setAlias(alias)
+ .setInput(sqlRelation(refSql))
+ .build())
+ .build())
+ }
+
proto.Relation.newBuilder().setWithRelations(withRelationsBuilder.build()).build()
+ }
+
+ test(
+ "SPARK-57189: blockUnsupportedSqlCommand catches Command at root of
WITH_RELATIONS " +
+ "without executing it") {
+ withRawBlockingStub { implicit stub =>
+ val graphId = createDataflowGraph
+ val tableName = "spark_catalog.default.test_with_relations_root_table"
+ try {
+ sql(s"DROP TABLE IF EXISTS $tableName")
+ val request = buildSqlCommandRequestWithPipelineContext(
+ graphId,
+ withRelationsRelation(
+ rootSql = s"CREATE TABLE $tableName (id INT) USING parquet",
+ refs = Seq("ref0" -> "SELECT 1 AS id")))
+ val ex = intercept[RuntimeException] {
+ stub.executePlan(request).next()
+ }
+
assert(ex.getMessage.contains("UNSUPPORTED_PIPELINE_SPARK_SQL_COMMAND"))
+ // Verify the guard caught the Command before any side effect could
materialize.
+ assert(!spark.catalog.tableExists(tableName))
+ } finally {
+ sql(s"DROP TABLE IF EXISTS $tableName")
+ }
+ }
+ }
+
+ test(
+ "SPARK-57189: blockUnsupportedSqlCommand catches Command inside reference
of " +
Review Comment:
**Nit (P3):** None of the new tests reaches the recursive arm for
references. `withRelationsRelation` always builds the reference input with
`sqlRelation(...)`, so this test goes through `case _` and `transformSql`. If
the recursion were replaced with
`PipelinesHandler.blockUnsupportedSqlCommand(transformRelation(ref.getSubqueryAlias.getInput))`,
all three tests would still pass. A reference whose input is itself a valid
SQL-with-references `WITH_RELATIONS` with a `CREATE TABLE` root would then run
inside the guard again.
That shape is reachable from PySpark: a DataFrame returned by
`spark.sql("...", df=...)` carries its `WITH_RELATIONS` relation, and it
becomes a reference input when you pass it to another `spark.sql` call. Could
you add a case where `ref0`'s input is a `WITH_RELATIONS` with root `CREATE
TABLE $tableName ...` and one plain reference, and assert the same error and
`!tableExists`? The helper would need a variant that takes a `proto.Relation`
for the reference input.
**Verification:**
- **Regression:** With the reference recursion replaced by a non-recursive
transformRelation check, the nested CREATE TABLE executes in the guard and the
new test fails on its error or table-existence assertion.
- **Behavior:** With the PR's recursive walker, the new test passes, and the
existing SPARK-57189 tests still pass.
##########
sql/connect/server/src/main/scala/org/apache/spark/sql/connect/planner/SparkConnectPlanner.scala:
##########
@@ -3198,6 +3203,29 @@ class SparkConnectPlanner(
}
}
+ /**
+ * Walks the relation tree and applies
[[PipelinesHandler.blockUnsupportedSqlCommand]] to every
+ * component that would be executed by [[executeSQLWithRefs]] or
[[executeSQL]] at the normal
+ * path. For a valid SQL-with-references [[proto.WithRelations]], the root
SQL is parsed
+ * (without execution) and each reference's input is inspected recursively
so nested
+ * [[proto.WithRelations]] cannot re-trigger the executing transformation
chain. For other
+ * relation types, [[transformRelation]] produces an unresolved plan without
side effects and is
Review Comment:
**Nit (P3):** This says the `case _` branch is side-effect free, but
`transformRelation(rel)` still recurses into child relations. So a valid
SQL-with-references `WithRelations` nested under any other node still gets
executed here. For example, a reference whose input is
`Limit(WithRelations{root: SQL "CREATE TABLE t ...", refs: [...]})` goes
`transformLimit` -> `transformWithRelations` -> `transformSqlWithRefs` ->
`executeSQLWithRefs`. The CREATE TABLE runs inside the guard, the blocklist
only sees the `Limit`, and the normal path runs it again. Dataset-building
wrappers such as `SHOW_STRING` behave the same way.
The guard behaved this way before this PR too, so it is not a regression.
But this sentence, the "nested [[proto.WithRelations]] cannot re-trigger the
executing transformation chain" sentence above it, and the new call-site
comment in `handleSqlCommand` now promise more than the walker delivers. It
also matters for the planned follow-up: checking the whole parsed plan would
not catch this, because the execution happens before there is a plan to inspect.
Could you narrow the wording to what the two arms actually cover (a valid
SQL-with-references `WithRelations` at the top level or as a direct reference
input) and note the wrapped case as a known limitation?
**Verification:**
- **Inspection:** The revised comments make no unconditional
side-effect-free or no-re-trigger claim for the fallback branch, and they
describe coverage consistently with the code's two match arms.
- **Compile:** The connect server module still compiles, and the Scala style
checks accept the edited comments.
--
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]