You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@kyuubi.apache.org by ch...@apache.org on 2026/09/20 16:02:06 UTC

(kyuubi) branch master updated: [KYUUBI #7746][AUTHZ] Report nondeterministic permanent views as nondeterministic to InlineCTE

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

pan3793 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/kyuubi.git


The following commit(s) were added to refs/heads/master by this push:
     new df5aa6c8b9 [KYUUBI #7746][AUTHZ] Report nondeterministic permanent views as nondeterministic to InlineCTE
df5aa6c8b9 is described below

commit df5aa6c8b966d868f5ad2a0d4f5d148ebddc63ba
Author: Jiwon Park <ji...@apache.org>
AuthorDate: Mon Sep 21 00:01:52 2026 +0800

    [KYUUBI #7746][AUTHZ] Report nondeterministic permanent views as nondeterministic to InlineCTE
    
    ### Why are the changes needed?
    
    Fixes #7746.
    
    `PermanentViewMarker` is a `LeafNode`, so `QueryPlan.deterministic` reports `true` for any
    permanent view, even one over `rand()`, `uuid()` or `reflect()`. `InlineCTE` reads that flag and
    inlines a nondeterministic view CTE that is referenced more than once, so each reference
    recomputes the view and, for expressions that draw a fresh value per evaluation, the references
    disagree. Without authz Spark keeps the CTE out of line and shares one `ReusedExchange`.
    
    This overrides `deterministic` to delegate to the wrapped plan. The same flag feeds
    `SubqueryExpression.deterministic`, so subqueries over such a view are classified the same way
    as without authz.
    
    `override lazy val` compiles against both the `lazy val` in Spark 3.5 and the `def` in 4.0+.
    
    ### How was this patch tested?
    
    Two tests in `HiveCatalogRangerSparkExtensionSuite`:
    
    - a `uuid()` view referenced twice through a CTE keeps a `RepartitionByExpression` in the
      optimized plan and a `ReusedExchange` in the executed plan;
    - a `reflect('java.util.UUID', 'randomUUID')` view referenced twice through a CTE yields the same
      value for both references.
    
    Both fail with `PermanentViewMarker` reverted to master and pass with the change, on Spark 3.5.8,
    4.0.3, 4.1.3 and 4.2.0.
    
    ### Was this patch assisted by generative AI tooling?
    
    Assisted-by: Claude Fable 5.1
    
    Closes #7747 from j1wonpark/KYUUBI-7746.
    
    Closes #7746
    
    3052a8633 [Jiwon Park] [KYUUBI #7746][AUTHZ] Report nondeterministic permanent views as nondeterministic to InlineCTE
    
    Authored-by: Jiwon Park <ji...@apache.org>
    Signed-off-by: Cheng Pan <ch...@apache.org>
---
 .../rule/permanentview/PermanentViewMarker.scala   |  5 +++
 .../authz/ranger/RangerSparkExtensionSuite.scala   | 43 +++++++++++++++++++++-
 2 files changed, 47 insertions(+), 1 deletion(-)

diff --git a/extensions/spark/kyuubi-spark-authz/src/main/scala/org/apache/kyuubi/plugin/spark/authz/rule/permanentview/PermanentViewMarker.scala b/extensions/spark/kyuubi-spark-authz/src/main/scala/org/apache/kyuubi/plugin/spark/authz/rule/permanentview/PermanentViewMarker.scala
index cb233b4651..d3688bf469 100644
--- a/extensions/spark/kyuubi-spark-authz/src/main/scala/org/apache/kyuubi/plugin/spark/authz/rule/permanentview/PermanentViewMarker.scala
+++ b/extensions/spark/kyuubi-spark-authz/src/main/scala/org/apache/kyuubi/plugin/spark/authz/rule/permanentview/PermanentViewMarker.scala
@@ -37,6 +37,11 @@ case class PermanentViewMarker(child: LogicalPlan, catalogTable: CatalogTable)
 
   override def computeStats(): Statistics = child.stats
 
+  // As a LeafNode the marker has no expressions or children, so QueryPlan.deterministic would
+  // report true even for a view over rand() or uuid(). InlineCTE relies on this flag to keep a
+  // nondeterministic CTE out of line when it is referenced more than once.
+  override lazy val deterministic: Boolean = child.deterministic
+
   override def newInstance(): LogicalPlan = {
     val projectList = child.output.map { case attr =>
       Alias(Cast(attr, attr.dataType), attr.name)(explicitMetadata = Some(attr.metadata))
diff --git a/extensions/spark/kyuubi-spark-authz/src/test/scala/org/apache/kyuubi/plugin/spark/authz/ranger/RangerSparkExtensionSuite.scala b/extensions/spark/kyuubi-spark-authz/src/test/scala/org/apache/kyuubi/plugin/spark/authz/ranger/RangerSparkExtensionSuite.scala
index 8805f0cd52..4c04cd6843 100644
--- a/extensions/spark/kyuubi-spark-authz/src/test/scala/org/apache/kyuubi/plugin/spark/authz/ranger/RangerSparkExtensionSuite.scala
+++ b/extensions/spark/kyuubi-spark-authz/src/test/scala/org/apache/kyuubi/plugin/spark/authz/ranger/RangerSparkExtensionSuite.scala
@@ -28,7 +28,7 @@ import org.apache.spark.SparkConf
 import org.apache.spark.sql.{Row, SparkSessionExtensions}
 import org.apache.spark.sql.catalyst.analysis.NoSuchTableException
 import org.apache.spark.sql.catalyst.catalog.HiveTableRelation
-import org.apache.spark.sql.catalyst.plans.logical.Statistics
+import org.apache.spark.sql.catalyst.plans.logical.{RepartitionByExpression, Statistics}
 import org.apache.spark.sql.execution.columnar.InMemoryRelation
 import org.apache.spark.sql.execution.datasources.LogicalRelation
 
@@ -741,6 +741,47 @@ class HiveCatalogRangerSparkExtensionSuite extends RangerSparkExtensionSuite {
     }
   }
 
+  test("a nondeterministic view referenced twice through a CTE is computed once") {
+    val view = s"$defaultDb.pvm_nondeterministic_view"
+    withCleanTmpResources(Seq((view, "view"))) {
+      doAs(admin) {
+        sql(s"CREATE VIEW $view AS SELECT id, uuid() AS r FROM range(20)")
+        val query = sql(s"WITH c AS (SELECT * FROM $view) " +
+          "SELECT a.id, a.r AS l, b.r AS r FROM c a JOIN c b ON a.id = b.id")
+        // Spark keeps a nondeterministic CTE out of line when it is referenced more than once:
+        // each reference is planned under a rebalance shuffle and the physical plan reuses one
+        // Exchange. Inlining it would evaluate uuid() separately for each reference.
+        assert(
+          query.queryExecution.optimizedPlan.exists(_.isInstanceOf[RepartitionByExpression]),
+          "Expected the nondeterministic CTE to stay out of line")
+        val rows = query.collect()
+        assert(rows.length == 20)
+        assert(rows.forall(row => row.getString(1) == row.getString(2)))
+        assert(
+          query.queryExecution.executedPlan.toString.contains("ReusedExchange"),
+          "Expected both CTE references to share one Exchange")
+      }
+    }
+  }
+
+  test("a nondeterministic view referenced twice through a CTE yields the same values") {
+    val view = s"$defaultDb.pvm_reflect_nondeterministic_view"
+    withCleanTmpResources(Seq((view, "view"))) {
+      doAs(admin) {
+        // uuid() and rand() are seeded at analysis time, so inlined copies of the view happen to
+        // agree. reflect() draws a fresh value on every evaluation, so they do not.
+        sql(s"CREATE VIEW $view AS " +
+          "SELECT id, reflect('java.util.UUID', 'randomUUID') AS r FROM range(20)")
+        val rows = sql(s"WITH c AS (SELECT * FROM $view) " +
+          "SELECT a.id, a.r AS l, b.r AS r FROM c a JOIN c b ON a.id = b.id").collect()
+        assert(rows.length == 20)
+        assert(
+          rows.forall(row => row.getString(1) == row.getString(2)),
+          "Expected both CTE references to see the same evaluation of the view")
+      }
+    }
+  }
+
   test("[KYUUBI #3608] Support {OWNER} variable for queries") {
     val db = defaultDb
     val table = "owner_variable"