apache/iceberg · error · IcebergAnalysisException

ALTER TABLE contains multiple distribution clauses

Error message

ALTER TABLE contains multiple distribution clauses

What it means

The grammar allows collecting multiple write-distribution specs into writeSpec.writeDistributionSpec, but a WRITE spec may define at most one distribution mode; the AST builder rejects any statement with more than one, since a table cannot simultaneously be written with two distribution strategies.

Solutions

  1. Keep exactly one distribution clause; delete the redundant `DISTRIBUTED BY ...` occurrence.
  2. Decide the desired mode (NONE / HASH / PARTITION) and express it in a single clause.
  3. If both a partition-based and hash-based intent exist, choose HASH with the explicit column list, e.g. `WRITE DISTRIBUTED BY HASH (col)`.

Example fix

-- before
ALTER TABLE t WRITE DISTRIBUTED BY PARTITION DISTRIBUTED BY HASH(id)
-- after
ALTER TABLE t WRITE DISTRIBUTED BY HASH(id)
Defensive patterns

Strategy: validation

Validate before calling

val distCount = "DISTRIBUTED\\s+BY|DISTRIBUTED\\s+BY\\s+PARTITION|PARTITIONED\\s+BY\\s+PARTITION".r
  .findAllMatchIn(writeSpec).size
require(distCount <= 1, "Only one WRITE distribution clause is allowed")

Prevention

When it happens

Trigger: Executing an ALTER TABLE WRITE statement containing two distribution clauses, e.g. `ALTER TABLE t WRITE DISTRIBUTED BY PARTITION DISTRIBUTED BY HASH(col)`, or concatenating generated DDL fragments that each add a distribution clause.

Common situations: Programmatic DDL builders appending a default distribution plus a user-specified one; copy-paste merging of two ALTER statements.

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/a149de033971e40a. Report an issue: GitHub.

Appendix: source

Thrown at spark/v4.2/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)