{"record":{"id":"fe5828fa7c5d4068","repo":"apache/iceberg","slug":"recursive-cycle-in-view-detected-s-cycle-s-fe5828","errorCode":null,"errorMessage":"Recursive cycle in view detected: %s (cycle: %s)","messagePattern":"Recursive cycle in view detected: (.+?) \\(cycle: (.+?)\\)","errorType":"exception","errorClass":"IcebergAnalysisException","httpStatus":null,"severity":"error","filePath":"spark/v4.1/spark-extensions/src/main/scala/org/apache/spark/sql/catalyst/analysis/CheckViews.scala","lineNumber":124,"sourceCode":"        plan.children.foreach(child => checkCyclicViewReference(viewIdent, child, cyclePath))\n    }\n\n    plan.expressions.flatMap(_.flatMap {\n      case e: SubqueryExpression =>\n        checkCyclicViewReference(viewIdent, e.plan, cyclePath)\n        None\n      case _ => None\n    })\n  }\n\n  private def checkIfRecursiveView(\n      viewIdent: Seq[String],\n      currentViewIdent: Seq[String],\n      cyclePath: Seq[Seq[String]],\n      children: Seq[LogicalPlan]): Unit = {\n    val newCyclePath = cyclePath :+ currentViewIdent\n    if (currentViewIdent == viewIdent) {\n      throw new IcebergAnalysisException(\n        String.format(\n          \"Recursive cycle in view detected: %s (cycle: %s)\",\n          viewIdent.asIdentifier,\n          newCyclePath.map(p => p.mkString(\".\")).mkString(\" -> \")))\n    } else {\n      children.foreach { c =>\n        checkCyclicViewReference(viewIdent, c, newCyclePath)\n      }\n    }\n  }\n}\n","sourceCodeStart":106,"sourceCodeEnd":136,"githubUrl":"https://github.com/apache/iceberg/blob/86d9c8fc543e7c56c9f624eb725f76c9baff9570/spark/v4.1/spark-extensions/src/main/scala/org/apache/spark/sql/catalyst/analysis/CheckViews.scala#L106-L136","documentation":"CheckViews performs recursive view-reference detection when creating or altering Iceberg views. If, while walking the query plan, the current view identifier equals an identifier already on the resolution path, a cyclic view definition exists and this IcebergAnalysisException is thrown, showing the view and the full cycle path (view -> view -> ...).","triggerScenarios":"`CREATE OR REPLACE VIEW v AS SELECT * FROM v` or mutual cycles v1 -> v2 -> v1, where the new view definition (directly or transitively) references itself.","commonSituations":"Re-creating an existing view from a query that still reads the old view name; scripted view refresh that regenerates views from queries captured earlier.","solutions":["Break the cycle: base the new definition on the underlying table instead of the view itself","Create the new view under a temporary name, then swap/rename after dropping the old one","Inspect the printed cycle path and remove the self-referencing dependency in the view DDL"],"exampleFix":"// before\nCREATE OR REPLACE VIEW v AS SELECT * FROM v WHERE id > 0\n// after\nCREATE OR REPLACE VIEW v AS SELECT * FROM t WHERE id > 0","handlingStrategy":"validation","validationCode":"// verify the view name doesn't appear among relations referenced by the query\nval refs = spark.sql(query).queryExecution.analyzed.collect { case r: org.apache.spark.sql.catalyst.analysis.ResolvedTable => r.identifier.toString }\nrequire(!refs.contains(viewName.toString), s\"Cycle: $viewName references itself\")","typeGuard":null,"tryCatchPattern":"try { spark.sql(ddl) } catch { case e: IcebergAnalysisException if e.getMessage.startsWith(\"Recursive cycle in view\") => log.error(e.getMessage); /* break the cycle and retry */ }","preventionTips":["Base new view definitions on tables, not on the view being replaced","Read the cycle path in the message to locate the loop","Use temp views during migration, then rename"],"tags":["spark","views","recursion","sql-extensions"],"backgroundTag":"invalid-state-transition","analyzedSha":"86d9c8fc543e7c56c9f624eb725f76c9baff9570","analyzedAt":"2026-09-12T00:46:39.097Z","contentChangedAt":"2026-09-12T00:46:39.097Z","schemaVersion":2},"datasetVersion":"2026-09-14T16:17:12.679Z"}