@@ -23,7 +23,7 @@ import org.apache.spark.sql.{AnalysisException, Row, SparkSession}
2323import org .apache .spark .sql .catalyst .TableIdentifier
2424import org .apache .spark .sql .catalyst .analysis .{UnresolvedFunction , UnresolvedRelation }
2525import 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 }
2727import org .apache .spark .sql .catalyst .plans .QueryPlan
2828import org .apache .spark .sql .catalyst .plans .logical .{LogicalPlan , Project , View }
2929import 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
341305object 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}
0 commit comments