You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@spark.apache.org by we...@apache.org on 2017/03/08 04:25:41 UTC
spark git commit: [SPARK-18389][SQL] Disallow cyclic view reference
Repository: spark
Updated Branches:
refs/heads/master c96d14aba -> b9783a92f
[SPARK-18389][SQL] Disallow cyclic view reference
## What changes were proposed in this pull request?
Disallow cyclic view references, a cyclic view reference may be created by the following queries:
```
CREATE VIEW testView AS SELECT id FROM tbl
CREATE VIEW testView2 AS SELECT id FROM testView
ALTER VIEW testView AS SELECT * FROM testView2
```
In the above example, a reference cycle (testView -> testView2 -> testView) exsits.
We disallow cyclic view references by checking that in ALTER VIEW command, when the `analyzedPlan` contains the same `View` node with the altered view, we should prevent the behavior and throw an AnalysisException.
## How was this patch tested?
Test by `SQLViewSuite.test("correctly handle a cyclic view reference")`.
Author: jiangxingbo <ji...@gmail.com>
Closes #17152 from jiangxb1987/cyclic-view.
Project: http://git-wip-us.apache.org/repos/asf/spark/repo
Commit: http://git-wip-us.apache.org/repos/asf/spark/commit/b9783a92
Tree: http://git-wip-us.apache.org/repos/asf/spark/tree/b9783a92
Diff: http://git-wip-us.apache.org/repos/asf/spark/diff/b9783a92
Branch: refs/heads/master
Commit: b9783a92f7ba0c3b22d7dceae7a3185de17dedcc
Parents: c96d14a
Author: jiangxingbo <ji...@gmail.com>
Authored: Tue Mar 7 20:25:38 2017 -0800
Committer: Wenchen Fan <we...@databricks.com>
Committed: Tue Mar 7 20:25:38 2017 -0800
----------------------------------------------------------------------
.../spark/sql/execution/command/views.scala | 61 +++++++++++++++++++-
.../spark/sql/execution/SQLViewSuite.scala | 35 +++++++++--
2 files changed, 90 insertions(+), 6 deletions(-)
----------------------------------------------------------------------
http://git-wip-us.apache.org/repos/asf/spark/blob/b9783a92/sql/core/src/main/scala/org/apache/spark/sql/execution/command/views.scala
----------------------------------------------------------------------
diff --git a/sql/core/src/main/scala/org/apache/spark/sql/execution/command/views.scala b/sql/core/src/main/scala/org/apache/spark/sql/execution/command/views.scala
index 921c848..00f0aca 100644
--- a/sql/core/src/main/scala/org/apache/spark/sql/execution/command/views.scala
+++ b/sql/core/src/main/scala/org/apache/spark/sql/execution/command/views.scala
@@ -23,9 +23,9 @@ import org.apache.spark.sql.{AnalysisException, Row, SparkSession}
import org.apache.spark.sql.catalyst.TableIdentifier
import org.apache.spark.sql.catalyst.analysis.{UnresolvedFunction, UnresolvedRelation}
import org.apache.spark.sql.catalyst.catalog.{CatalogStorageFormat, CatalogTable, CatalogTableType}
-import org.apache.spark.sql.catalyst.expressions.Alias
+import org.apache.spark.sql.catalyst.expressions.{Alias, SubqueryExpression}
import org.apache.spark.sql.catalyst.plans.QueryPlan
-import org.apache.spark.sql.catalyst.plans.logical.{LogicalPlan, Project}
+import org.apache.spark.sql.catalyst.plans.logical.{LogicalPlan, Project, View}
import org.apache.spark.sql.types.MetadataBuilder
@@ -154,6 +154,10 @@ case class CreateViewCommand(
} else if (tableMetadata.tableType != CatalogTableType.VIEW) {
throw new AnalysisException(s"$name is not a view")
} else if (replace) {
+ // Detect cyclic view reference on CREATE OR REPLACE VIEW.
+ val viewIdent = tableMetadata.identifier
+ checkCyclicViewReference(analyzedPlan, Seq(viewIdent), viewIdent)
+
// Handles `CREATE OR REPLACE VIEW v0 AS SELECT ...`
catalog.alterTable(prepareTable(sparkSession, analyzedPlan))
} else {
@@ -283,6 +287,10 @@ case class AlterViewAsCommand(
throw new AnalysisException(s"${viewMeta.identifier} is not a view.")
}
+ // Detect cyclic view reference on ALTER VIEW.
+ val viewIdent = viewMeta.identifier
+ checkCyclicViewReference(analyzedPlan, Seq(viewIdent), viewIdent)
+
val newProperties = generateViewProperties(viewMeta.properties, session, analyzedPlan)
val updatedViewMeta = viewMeta.copy(
@@ -358,4 +366,53 @@ object ViewHelper {
generateViewDefaultDatabase(viewDefaultDatabase) ++
generateQueryColumnNames(queryOutput)
}
+
+ /**
+ * Recursively search the logical plan to detect cyclic view references, throw an
+ * AnalysisException if cycle detected.
+ *
+ * A cyclic view reference is a cycle of reference dependencies, for example, if the following
+ * statements are executed:
+ * CREATE VIEW testView AS SELECT id FROM tbl
+ * CREATE VIEW testView2 AS SELECT id FROM testView
+ * ALTER VIEW testView AS SELECT * FROM testView2
+ * The view `testView` references `testView2`, and `testView2` also references `testView`,
+ * therefore a reference cycle (testView -> testView2 -> testView) exists.
+ *
+ * @param plan the logical plan we detect cyclic view references from.
+ * @param path the path between the altered view and current node.
+ * @param viewIdent the table identifier of the altered view, we compare two views by the
+ * `desc.identifier`.
+ */
+ def checkCyclicViewReference(
+ plan: LogicalPlan,
+ path: Seq[TableIdentifier],
+ viewIdent: TableIdentifier): Unit = {
+ plan match {
+ case v: View =>
+ val ident = v.desc.identifier
+ val newPath = path :+ ident
+ // If the table identifier equals to the `viewIdent`, current view node is the same with
+ // the altered view. We detect a view reference cycle, should throw an AnalysisException.
+ if (ident == viewIdent) {
+ throw new AnalysisException(s"Recursive view $viewIdent detected " +
+ s"(cycle: ${newPath.mkString(" -> ")})")
+ } else {
+ v.children.foreach { child =>
+ checkCyclicViewReference(child, newPath, viewIdent)
+ }
+ }
+ case _ =>
+ plan.children.foreach(child => checkCyclicViewReference(child, path, viewIdent))
+ }
+
+ // Detect cyclic references from subqueries.
+ plan.expressions.foreach { expr =>
+ expr match {
+ case s: SubqueryExpression =>
+ checkCyclicViewReference(s.plan, path, viewIdent)
+ case _ => // Do nothing.
+ }
+ }
+ }
}
http://git-wip-us.apache.org/repos/asf/spark/blob/b9783a92/sql/core/src/test/scala/org/apache/spark/sql/execution/SQLViewSuite.scala
----------------------------------------------------------------------
diff --git a/sql/core/src/test/scala/org/apache/spark/sql/execution/SQLViewSuite.scala b/sql/core/src/test/scala/org/apache/spark/sql/execution/SQLViewSuite.scala
index 0e5a1dc..2ca2206 100644
--- a/sql/core/src/test/scala/org/apache/spark/sql/execution/SQLViewSuite.scala
+++ b/sql/core/src/test/scala/org/apache/spark/sql/execution/SQLViewSuite.scala
@@ -609,12 +609,39 @@ abstract class SQLViewSuite extends QueryTest with SQLTestUtils {
}
}
- // TODO: Check for cyclic view references on ALTER VIEW.
- ignore("correctly handle a cyclic view reference") {
- withView("view1", "view2") {
+ test("correctly handle a cyclic view reference") {
+ withView("view1", "view2", "view3") {
sql("CREATE VIEW view1 AS SELECT * FROM jt")
sql("CREATE VIEW view2 AS SELECT * FROM view1")
- intercept[AnalysisException](sql("ALTER VIEW view1 AS SELECT * FROM view2"))
+ sql("CREATE VIEW view3 AS SELECT * FROM view2")
+
+ // Detect cyclic view reference on ALTER VIEW.
+ val e1 = intercept[AnalysisException] {
+ sql("ALTER VIEW view1 AS SELECT * FROM view2")
+ }.getMessage
+ assert(e1.contains("Recursive view `default`.`view1` detected (cycle: `default`.`view1` " +
+ "-> `default`.`view2` -> `default`.`view1`)"))
+
+ // Detect the most left cycle when there exists multiple cyclic view references.
+ val e2 = intercept[AnalysisException] {
+ sql("ALTER VIEW view1 AS SELECT * FROM view3 JOIN view2")
+ }.getMessage
+ assert(e2.contains("Recursive view `default`.`view1` detected (cycle: `default`.`view1` " +
+ "-> `default`.`view3` -> `default`.`view2` -> `default`.`view1`)"))
+
+ // Detect cyclic view reference on CREATE OR REPLACE VIEW.
+ val e3 = intercept[AnalysisException] {
+ sql("CREATE OR REPLACE VIEW view1 AS SELECT * FROM view2")
+ }.getMessage
+ assert(e3.contains("Recursive view `default`.`view1` detected (cycle: `default`.`view1` " +
+ "-> `default`.`view2` -> `default`.`view1`)"))
+
+ // Detect cyclic view reference from subqueries.
+ val e4 = intercept[AnalysisException] {
+ sql("ALTER VIEW view1 AS SELECT * FROM jt WHERE EXISTS (SELECT 1 FROM view2)")
+ }.getMessage
+ assert(e4.contains("Recursive view `default`.`view1` detected (cycle: `default`.`view1` " +
+ "-> `default`.`view2` -> `default`.`view1`)"))
}
}
}
---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@spark.apache.org
For additional commands, e-mail: commits-help@spark.apache.org