apache/iceberg · error · IcebergAnalysisException

ALTER TABLE contains multiple distribution clauses

Error message

ALTER TABLE contains multiple distribution clauses

What it means

An ALTER TABLE write-spec statement may contain at most one distribution clause. The AST builder counts WriteDistributionSpecContext children and throws IcebergAnalysisException when more than one is present, since combining e.g. DISTRIBUTED BY HASH and DIRECTLY is ambiguous. The grammar can syntactically accept repeats, so this is a semantic check.

Solutions

  1. Keep only one distribution clause per WRITE spec
  2. Decide on a single strategy: DISTRIBUTED BY HASH(cols), DISTRIBUTED DIRECTLY, etc.
  3. Split into sequential ALTER statements if the change is transitional

Example fix

// before
ALTER TABLE t WRITE DISTRIBUTED BY HASH(id) DISTRIBUTED DIRECTLY;
// after
ALTER TABLE t WRITE DISTRIBUTED BY HASH(id);
Defensive patterns

Strategy: validation

Validate before calling

def countClauses(stmt: String, kw: String): Int = stmt.toUpperCase(Locale.ROOT).split(kw).length - 1
require(countClauses(stmt, "DISTRIBUTED") <= 1, "Only one distribution clause allowed")

Try / catch

try { spark.sql(stmt) } catch { case e: IcebergAnalysisException if e.getMessage.contains("multiple distribution clauses") => log.error("Deduplicate write distribution clauses", e) }

Prevention

When it happens

Trigger: `ALTER TABLE t WRITE DISTRIBUTED BY HASH(id) DISTRIBUTED DIRECTLY` or any statement with two writeDistributionSpec clauses in one writeSpec.

Common situations: Concatenating generated SQL fragments, editing an existing ALTER statement and leaving the old distribution clause in place, or misunderstanding that the later clause overrides the earlier one (it does not — it is rejected).

Understand the failure class

Background: Conflicting config options: "cannot be used together" — configuration validation errors across open-source libraries — this error's family across 162 libraries.

Related errors


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

Appendix: source

Thrown at spark/v4.0/spark-extensions/src/main/scala/org/apache/spark/sql/catalyst/parser/extensions/IcebergSqlExtensionsAstBuilder.scala:257

      None
    } else {
      Some(DistributionMode.RANGE)
    }

    val ordering = if (orderingSpec != null && orderingSpec.order != null) {
      toSeq(orderingSpec.order.fields).map(typedVisit[(Term, SortDirection, NullOrder)])
    } else {
      Seq.empty
    }

    SetWriteDistributionAndOrdering(tableName, distributionMode, ordering)
  }

  private def toDistributionAndOrderingSpec(
      writeSpec: WriteSpecContext): (WriteDistributionSpecContext, WriteOrderingSpecContext) = {

    if (writeSpec.writeDistributionSpec.size > 1) {
      throw new IcebergAnalysisException("ALTER TABLE contains multiple distribution clauses")
    }

    if (writeSpec.writeOrderingSpec.size > 1) {
      throw new IcebergAnalysisException("ALTER TABLE contains multiple ordering clauses")
    }

    val distributionSpec = toBuffer(writeSpec.writeDistributionSpec).headOption.orNull
    val orderingSpec = toBuffer(writeSpec.writeOrderingSpec).headOption.orNull

    (distributionSpec, orderingSpec)
  }

  /**
   * Create an order field.
   */
  override def visitOrderField(ctx: OrderFieldContext): (Term, SortDirection, NullOrder) = {
    val term = Spark3Util.toIcebergTerm(typedVisit[Transform](ctx.transform))
    val direction = Option(ctx.ASC)

View on GitHub (pinned to 86d9c8fc54)