apache/iceberg · error · AnalysisException

Recursive cycle in view detected

Error message

Recursive cycle in view detected: %s (cycle: %s)

What it means

Thrown by Iceberg's CheckViews rule when view resolution detects a recursive reference cycle: a view (directly or through other views) references itself, which would cause infinite resolution. The message names the view and the full cycle path.

Solutions

  1. Break the cycle by redefining one view in the chain so it no longer references (directly or transitively) itself
  2. Base the view on the underlying table instead of a view that resolves back to it
  3. Draw the view dependency graph (the cycle is printed in the error) and reorder creation so dependencies form a DAG

Example fix

// before
CREATE OR REPLACE VIEW catalog.db.v AS SELECT * FROM catalog.db.v
// after
CREATE OR REPLACE VIEW catalog.db.v AS SELECT * FROM catalog.db.base_table
Defensive patterns

Strategy: validation

Validate before calling

// maintain a map viewName -> referencedViews and detect cycles before creating
def hasCycle(deps: Map[String, Set[String]], start: String): Boolean = {
  @annotation.tailrec def walk(frontier: Set[String], seen: Set[String]): Boolean =
    if (frontier.contains(start)) true
    else if (frontier.subsetOf(seen) || frontier.isEmpty) false
    else walk(frontier.flatMap(deps.getOrElse(_, Set.empty)) -- seen, seen ++ frontier)
  walk(deps.getOrElse(start, Set.empty), Set.empty)
}

Try / catch

try { spark.sql(viewDdl) } catch { case e: AnalysisException if e.getMessage.contains("Recursive cycle in view") => // redefine one view to break the loop }

Prevention

When it happens

Trigger: CREATE OR REPLACE VIEW v AS SELECT * FROM v (directly or via a chain v -> w -> v) evaluated against an Iceberg catalog; the CheckViews rule walks view children and matches identifier equality against the current cycle path.

Common situations: CREATE OR REPLACE VIEW on an existing view whose definition already references views that now resolve back to it; renaming views so a dependency chain closes into a loop; generating view DDL programmatically with circular dependencies.

Understand the failure class

Background: "This is a bug, please report it": internal invariant violations, unreachable panics, and SNH errors explained — this error's family across 47 libraries.

Related errors


AI-assisted analysis of apache/iceberg@86d9c8fc54 (2026-09-12). Data as JSON: /api/errors/b727faed22bcd13c. Report an issue: GitHub.

Appendix: source

Thrown at spark/v3.5/spark-extensions/src/main/scala/org/apache/spark/sql/catalyst/analysis/CheckViews.scala:124

        plan.children.foreach(child => checkCyclicViewReference(viewIdent, child, cyclePath))
    }

    plan.expressions.flatMap(_.flatMap {
      case e: SubqueryExpression =>
        checkCyclicViewReference(viewIdent, e.plan, cyclePath)
        None
      case _ => None
    })
  }

  private def checkIfRecursiveView(
      viewIdent: Seq[String],
      currentViewIdent: Seq[String],
      cyclePath: Seq[Seq[String]],
      children: Seq[LogicalPlan]): Unit = {
    val newCyclePath = cyclePath :+ currentViewIdent
    if (currentViewIdent == viewIdent) {
      throw new AnalysisException(
        String.format(
          "Recursive cycle in view detected: %s (cycle: %s)",
          viewIdent.asIdentifier,
          newCyclePath.map(p => p.mkString(".")).mkString(" -> ")))
    } else {
      children.foreach { c =>
        checkCyclicViewReference(viewIdent, c, newCyclePath)
      }
    }
  }
}

View on GitHub (pinned to 86d9c8fc54)