Skip to content

Commit f487af3

Browse files
committed
detect cyclic references for CREATE OR REPLACE VIEW and subqueries.
1 parent 1bd0260 commit f487af3

2 files changed

Lines changed: 69 additions & 44 deletions

File tree

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

Lines changed: 55 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ import org.apache.spark.sql.{AnalysisException, Row, SparkSession}
2323
import org.apache.spark.sql.catalyst.TableIdentifier
2424
import org.apache.spark.sql.catalyst.analysis.{UnresolvedFunction, UnresolvedRelation}
2525
import org.apache.spark.sql.catalyst.catalog.{CatalogStorageFormat, CatalogTable, CatalogTableType}
26-
import org.apache.spark.sql.catalyst.expressions.Alias
26+
import org.apache.spark.sql.catalyst.expressions.{Alias, SubqueryExpression}
2727
import org.apache.spark.sql.catalyst.plans.QueryPlan
2828
import org.apache.spark.sql.catalyst.plans.logical.{LogicalPlan, Project, View}
2929
import org.apache.spark.sql.types.MetadataBuilder
@@ -154,6 +154,10 @@ case class CreateViewCommand(
154154
} else if (tableMetadata.tableType != CatalogTableType.VIEW) {
155155
throw new AnalysisException(s"$name is not a view")
156156
} else if (replace) {
157+
// Detect cyclic view reference on CREATE OR REPLACE VIEW.
158+
val viewIdent = tableMetadata.identifier
159+
checkCyclicViewReference(analyzedPlan, Seq(viewIdent), viewIdent)
160+
157161
// Handles `CREATE OR REPLACE VIEW v0 AS SELECT ...`
158162
catalog.alterTable(prepareTable(sparkSession, analyzedPlan))
159163
} else {
@@ -283,17 +287,9 @@ case class AlterViewAsCommand(
283287
throw new AnalysisException(s"${viewMeta.identifier} is not a view.")
284288
}
285289

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)
290+
// Detect cyclic view reference on ALTER VIEW.
291+
val viewIdent = viewMeta.identifier
292+
checkCyclicViewReference(analyzedPlan, Seq(viewIdent), viewIdent)
297293

298294
val newProperties = generateViewProperties(viewMeta.properties, session, analyzedPlan)
299295

@@ -304,38 +300,6 @@ case class AlterViewAsCommand(
304300

305301
session.sessionState.catalog.alterTable(updatedViewMeta)
306302
}
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-
}
339303
}
340304

341305
object ViewHelper {
@@ -402,4 +366,51 @@ object ViewHelper {
402366
generateViewDefaultDatabase(viewDefaultDatabase) ++
403367
generateQueryColumnNames(queryOutput)
404368
}
369+
370+
/**
371+
* Recursively search the logical plan to detect cyclic view references, throw an
372+
* AnalysisException if cycle detected.
373+
*
374+
* A cyclic view reference is a cycle of reference dependencies, for example, if the following
375+
* statements are executed:
376+
* CREATE VIEW testView AS SELECT id FROM tbl
377+
* CREATE VIEW testView2 AS SELECT id FROM testView
378+
* ALTER VIEW testView AS SELECT * FROM testView2
379+
* The view `testView` references `testView2`, and `testView2` also references `testView`,
380+
* therefore a reference cycle (testView -> testView2 -> testView) exists.
381+
*
382+
* @param plan the logical plan we detect cyclic view references from.
383+
* @param path the path between the altered view and current node.
384+
* @param viewIdent the table identifier of the altered view, we compare two views by the
385+
* `desc.identifier`.
386+
*/
387+
def checkCyclicViewReference(
388+
plan: LogicalPlan,
389+
path: Seq[TableIdentifier],
390+
viewIdent: TableIdentifier): Unit = {
391+
plan match {
392+
case v: View =>
393+
val ident = v.desc.identifier
394+
val newPath = path :+ ident
395+
// If the table identifier equals to the `viewIdent`, current view node is the same with
396+
// the altered view. We detect a view reference cycle, should throw an AnalysisException.
397+
if (ident == viewIdent) {
398+
throw new AnalysisException(s"Recursive view $viewIdent detected " +
399+
s"(cycle: ${newPath.mkString(" -> ")})")
400+
} else {
401+
v.children.foreach { child =>
402+
checkCyclicViewReference(child, newPath, viewIdent)
403+
}
404+
}
405+
case _ =>
406+
plan.children.foreach(child => checkCyclicViewReference(child, path, viewIdent))
407+
}
408+
409+
// Detect cyclic references from subqueries.
410+
plan.expressions.foreach { expr =>
411+
if (expr.isInstanceOf[SubqueryExpression]) {
412+
checkCyclicViewReference(expr.asInstanceOf[SubqueryExpression].plan, path, viewIdent)
413+
}
414+
}
415+
}
405416
}

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

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -628,6 +628,20 @@ abstract class SQLViewSuite extends QueryTest with SQLTestUtils {
628628
}.getMessage
629629
assert(e2.contains("Recursive view `default`.`view1` detected (cycle: `default`.`view1` " +
630630
"-> `default`.`view3` -> `default`.`view2` -> `default`.`view1`)"))
631+
632+
// Detect cyclic view reference on CREATE OR REPLACE VIEW.
633+
val e3 = intercept[AnalysisException] {
634+
sql("CREATE OR REPLACE VIEW view1 AS SELECT * FROM view2")
635+
}.getMessage
636+
assert(e3.contains("Recursive view `default`.`view1` detected (cycle: `default`.`view1` " +
637+
"-> `default`.`view2` -> `default`.`view1`)"))
638+
639+
// Detect cyclic view reference from subqueries.
640+
val e4 = intercept[AnalysisException] {
641+
sql("ALTER VIEW view1 AS SELECT * FROM jt WHERE EXISTS (SELECT 1 FROM view2)")
642+
}.getMessage
643+
assert(e4.contains("Recursive view `default`.`view1` detected (cycle: `default`.`view1` " +
644+
"-> `default`.`view2` -> `default`.`view1`)"))
631645
}
632646
}
633647
}

0 commit comments

Comments
 (0)