apache/iceberg · error · IcebergAnalysisException

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

Error message

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

What it means

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 -> ...).

Source

Thrown at spark/v4.1/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 IcebergAnalysisException(
        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)

Solutions

  1. Break the cycle: base the new definition on the underlying table instead of the view itself
  2. Create the new view under a temporary name, then swap/rename after dropping the old one
  3. Inspect the printed cycle path and remove the self-referencing dependency in the view DDL

Example fix

// before
CREATE OR REPLACE VIEW v AS SELECT * FROM v WHERE id > 0
// after
CREATE OR REPLACE VIEW v AS SELECT * FROM t WHERE id > 0
Defensive patterns

Strategy: validation

Validate before calling

// verify the view name doesn't appear among relations referenced by the query
val refs = spark.sql(query).queryExecution.analyzed.collect { case r: org.apache.spark.sql.catalyst.analysis.ResolvedTable => r.identifier.toString }
require(!refs.contains(viewName.toString), s"Cycle: $viewName references itself")

Try / catch

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 */ }

Prevention

When it happens

Trigger: `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.

Common situations: 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.

Understand the failure class

Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.

Related errors


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