apache/iceberg · error

ALTER TABLE contains multiple distribution clauses

Error message

ALTER TABLE contains multiple distribution clauses

What it means

The grammar permits a list of distribution spec tokens, but Iceberg only supports a single distribution clause per ALTER TABLE ... WRITE statement. More than one triggers an AnalysisException so the statement cannot be ambiguously interpreted.

Solutions

  1. Keep exactly one distribution clause — combine intent into it, e.g. ALTER TABLE t WRITE DISTRIBUTED BY PARTITION;
  2. Use HASH(col) as the single clause if keyed distribution is desired
  3. Split into sequential ALTER TABLE statements if transitioning between modes is intended

Example fix

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

Strategy: validation

Validate before calling

val distCount = "DISTRIBUTED\\s+BY|HASH\\(|RANGE\\(|".r.findAllIn(sqlText).size
require(distCount <= 1, "Only one distribution clause allowed")

Try / catch

try { spark.sql(ddl) } catch { case e: AnalysisException if e.getMessage.contains("multiple distribution") => log.error("duplicate distribution clause"); throw e }

Prevention

When it happens

Trigger: Writing 'ALTER TABLE t WRITE DISTRIBUTED BY PARTITION HASH(id)' or otherwise stacking two writeDistributionSpec clauses in one statement.

Common situations: Users combining DISTRIBUTED BY PARTITION with a second HASH(col) clause expecting both to apply; auto-generated SQL concatenating clauses.

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

Appendix: source

Thrown at spark/v3.5/spark-extensions/src/main/scala/org/apache/spark/sql/catalyst/parser/extensions/IcebergSqlExtensionsAstBuilder.scala:270

      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 AnalysisException("ALTER TABLE contains multiple distribution clauses")
    }

    if (writeSpec.writeOrderingSpec.size > 1) {
      throw new AnalysisException("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)