Skip to content

Commit 1bd0260

Browse files
committed
detect cyclic view references.
1 parent e24f21b commit 1bd0260

File tree

2 files changed

+62
-5
lines changed

2 files changed

+62
-5
lines changed

sql/core/src/main/scala/org/apache/spark/sql/execution/command/views.scala

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ import org.apache.spark.sql.catalyst.analysis.{UnresolvedFunction, UnresolvedRel
2525
import org.apache.spark.sql.catalyst.catalog.{CatalogStorageFormat, CatalogTable, CatalogTableType}
2626
import org.apache.spark.sql.catalyst.expressions.Alias
2727
import org.apache.spark.sql.catalyst.plans.QueryPlan
28-
import org.apache.spark.sql.catalyst.plans.logical.{LogicalPlan, Project}
28+
import org.apache.spark.sql.catalyst.plans.logical.{LogicalPlan, Project, View}
2929
import org.apache.spark.sql.types.MetadataBuilder
3030

3131

@@ -283,6 +283,18 @@ case class AlterViewAsCommand(
283283
throw new AnalysisException(s"${viewMeta.identifier} is not a view.")
284284
}
285285

286+
// Detect cyclic view references, a cyclic view reference may be created by the following
287+
// queries:
288+
// CREATE VIEW testView AS SELECT id FROM tbl
289+
// CREATE VIEW testView2 AS SELECT id FROM testView
290+
// ALTER VIEW testView AS SELECT * FROM testView2
291+
// In the above example, a reference cycle (testView -> testView2 -> testView) exsits.
292+
//
293+
// We disallow cyclic view references by checking that in ALTER VIEW command, when the
294+
// `analyzedPlan` contains the same `View` node with the altered view, we should prevent the
295+
// behavior and throw an AnalysisException.
296+
checkCyclicViewReference(analyzedPlan, Seq(viewMeta.identifier), viewMeta.identifier)
297+
286298
val newProperties = generateViewProperties(viewMeta.properties, session, analyzedPlan)
287299

288300
val updatedViewMeta = viewMeta.copy(
@@ -292,6 +304,38 @@ case class AlterViewAsCommand(
292304

293305
session.sessionState.catalog.alterTable(updatedViewMeta)
294306
}
307+
308+
/**
309+
* Recursively search the logical plan to detect cyclic view references, throw an
310+
* AnalysisException if cycle detected.
311+
*
312+
* @param plan the logical plan we detect cyclic view references from.
313+
* @param path the path between the altered view and current node.
314+
* @param viewIdent the table identifier of the altered view, we compare two views by the
315+
* `desc.identifier`.
316+
*/
317+
private def checkCyclicViewReference(
318+
plan: LogicalPlan,
319+
path: Seq[TableIdentifier],
320+
viewIdent: TableIdentifier): Unit = {
321+
plan match {
322+
case v: View =>
323+
val ident = v.desc.identifier
324+
val newPath = path :+ ident
325+
// If the table identifier equals to the `viewIdent`, current view node is the same with
326+
// the altered view. We detect a view reference cycle, should throw an AnalysisException.
327+
if (ident == viewIdent) {
328+
throw new AnalysisException(s"Recursive view $viewIdent detected " +
329+
s"(cycle: ${newPath.mkString(" -> ")})")
330+
} else {
331+
v.children.foreach { child =>
332+
checkCyclicViewReference(child, newPath, viewIdent)
333+
}
334+
}
335+
case _ =>
336+
plan.children.foreach(child => checkCyclicViewReference(child, path, viewIdent))
337+
}
338+
}
295339
}
296340

297341
object ViewHelper {

sql/core/src/test/scala/org/apache/spark/sql/execution/SQLViewSuite.scala

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -609,12 +609,25 @@ abstract class SQLViewSuite extends QueryTest with SQLTestUtils {
609609
}
610610
}
611611

612-
// TODO: Check for cyclic view references on ALTER VIEW.
613-
ignore("correctly handle a cyclic view reference") {
614-
withView("view1", "view2") {
612+
test("correctly handle a cyclic view reference") {
613+
withView("view1", "view2", "view3") {
615614
sql("CREATE VIEW view1 AS SELECT * FROM jt")
616615
sql("CREATE VIEW view2 AS SELECT * FROM view1")
617-
intercept[AnalysisException](sql("ALTER VIEW view1 AS SELECT * FROM view2"))
616+
sql("CREATE VIEW view3 AS SELECT * FROM view2")
617+
618+
// Detect cyclic view reference on ALTER VIEW.
619+
val e1 = intercept[AnalysisException] {
620+
sql("ALTER VIEW view1 AS SELECT * FROM view2")
621+
}.getMessage
622+
assert(e1.contains("Recursive view `default`.`view1` detected (cycle: `default`.`view1` " +
623+
"-> `default`.`view2` -> `default`.`view1`)"))
624+
625+
// Detect the most left cycle when there exists multiple cyclic view references.
626+
val e2 = intercept[AnalysisException] {
627+
sql("ALTER VIEW view1 AS SELECT * FROM view3 JOIN view2")
628+
}.getMessage
629+
assert(e2.contains("Recursive view `default`.`view1` detected (cycle: `default`.`view1` " +
630+
"-> `default`.`view3` -> `default`.`view2` -> `default`.`view1`)"))
618631
}
619632
}
620633
}

0 commit comments

Comments
 (0)