@@ -25,7 +25,7 @@ import org.apache.spark.sql.catalyst.analysis.{UnresolvedFunction, UnresolvedRel
2525import org .apache .spark .sql .catalyst .catalog .{CatalogStorageFormat , CatalogTable , CatalogTableType }
2626import org .apache .spark .sql .catalyst .expressions .Alias
2727import 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 }
2929import 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
297341object ViewHelper {
0 commit comments