This is an automated email from the ASF dual-hosted git repository.
Gabriel39 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/doris.git
The following commit(s) were added to refs/heads/master by this push:
new 37632033807 [fix](iceberg) Fix static partition writes for renamed
identity fields (#69188)
37632033807 is described below
commit 37632033807ac6160a0491ba9361fbe49962622e
Author: daidai <[email protected]>
AuthorDate: Tue Oct 20 10:55:11 2026 +0800
[fix](iceberg) Fix static partition writes for renamed identity fields
(#69188)
### What problem does this PR solve?
Issue Number: N/A
Related PR: N/A
Problem Summary:
Doris could not write an Iceberg partition by value when the identity
partition field had been renamed away from its source column (`ALTER
TABLE t ADD PARTITION KEY id AS id_identity`):
- `INSERT OVERWRITE TABLE t PARTITION (id = 2) (value, metric) VALUES
('b', 2)` failed with `Unknown partition column 'id' in table 't'.
Available partition columns: [id_identity]`, although `id` is the column
Spark resolves for a static partition clause and the one `SHOW CREATE
TABLE` renders for this table (`PARTITION BY LIST (\`id\`)`);
- `... PARTITION (id_identity = 2) (value, metric) ...` passed the
validation but then failed with `Column has no default value,
column=id`: `id_identity` is a partition FIELD name, not a table column,
so the static literal could not be materialized into any row column;
- `... PARTITION (id_identity = 2) SELECT id, value, metric ...` (no
column list) was accepted and wrote data files whose `id` column value
disagreed with their own partition value.
The two checks contradicted each other: the validation required a name
that the binder then rejected, so there was no usable spelling.
Fix:
- resolve a `PARTITION (col = val)` name to the identity partition
field's SOURCE column, case-insensitively — the identifier the binder
can materialize
(`PluginDrivenExternalTable#materializeStaticPartitionValues`);
- reject a renamed partition FIELD name with the source column to use
instead, and list the usable source columns for an unknown name;
non-identity transforms keep their existing rejection;
- rewrite the static partition spec keys to partition FIELD names before
the BE sink (`static_partition_values`) and the commit row filter
consume them, so the thrift contract, the overwrite row filter and the
BE writer stay unchanged.
Behavior is aligned with Spark:
- Spark's `Analyzer.partitionColumnNames` takes the identity transform's
field reference, so a static partition column is the SOURCE column
(`PARTITION (id = 2)`), not the renamed field name;
- `Analyzer.addStaticPartitionColumns` materializes that literal into
the source column and rejects the same column in an explicit insert
column list — Doris does the same after this change;
- the resulting overwrite is an `OverwriteFiles.overwriteByRowFilter(id
= 2)` (`Iceberg SparkWrite`), which is exactly what Doris builds once
the spec is field-keyed.
Effect after the fix: `PARTITION (id = 2)` overwrites only that
partition, the new files carry `id = 2` next to partition value `2`, and
Spark reads the same rows; `PARTITION (id_identity = 2)` now reports
`'id_identity' ... is a partition field name, not a table column; write
the identity partition by its source column 'id' instead.` instead of a
contradictory error or a corrupted write. Tables whose partition field
name already equals its source column are unaffected.
### Release note
Fix `INSERT [OVERWRITE] ... PARTITION (col = val)` on Iceberg tables
whose identity partition field was renamed with `ADD PARTITION KEY ...
AS ...`: the static value is now written through the identity source
column (`PARTITION (id = 2)`), matching Spark, instead of reporting
contradictory errors or writing files whose data column disagreed with
the partition value. Non-identity transforms and unpartitioned tables
keep their existing rejection.
### Check List (For Author)
- Test
- [x] Regression test
- [x] Unit Test
- [x] Manual test (add detailed scripts or steps below)
- [ ] No need to test or manual test. Explain why:
- [ ] This is a refactor/code format and no logic has been changed.
- [ ] Previous test can cover this change.
- [ ] No code files have been changed.
- [ ] Other reason
- Manual test detail: full `fe-connector-iceberg` module unit tests (93
test classes) plus checkstyle pass locally with 0 failures on top of
current master; the new regression case (TC39) needs the external docker
Iceberg environment to run, so it is expected to be covered by the
regression pipeline.
- Behavior changed:
- [ ] No.
- [x] Yes.
- Does this need documentation?
- [x] No.
- [ ] Yes.
### Check List (For Reviewer who merge this PR)
- [ ] Confirm the release note
- [ ] Confirm test cases
- [ ] Confirm document
- [ ] Add branch pick label
---
.../iceberg/IcebergConnectorMetadata.java | 101 ++++++++++---
.../connector/iceberg/IcebergPartitionUtils.java | 99 +++++++++++++
.../iceberg/IcebergWritePlanProvider.java | 32 +++-
.../iceberg/IcebergConnectorMetadataTest.java | 162 +++++++++++++++++++--
.../iceberg/IcebergConnectorTransactionTest.java | 35 +++++
.../iceberg/IcebergPartitionUtilsTest.java | 70 +++++++++
.../iceberg/IcebergWritePlanProviderTest.java | 21 +++
.../test_iceberg_static_partition_overwrite.out | 3 +
.../test_iceberg_static_partition_overwrite.groovy | 62 ++++++--
9 files changed, 535 insertions(+), 50 deletions(-)
diff --git
a/fe/fe-connector/fe-connector-iceberg/src/main/java/org/apache/doris/connector/iceberg/IcebergConnectorMetadata.java
b/fe/fe-connector/fe-connector-iceberg/src/main/java/org/apache/doris/connector/iceberg/IcebergConnectorMetadata.java
index 134bf0cd9de..1e3aa164e26 100644
---
a/fe/fe-connector/fe-connector-iceberg/src/main/java/org/apache/doris/connector/iceberg/IcebergConnectorMetadata.java
+++
b/fe/fe-connector/fe-connector-iceberg/src/main/java/org/apache/doris/connector/iceberg/IcebergConnectorMetadata.java
@@ -1939,12 +1939,20 @@ public class IcebergConnectorMetadata implements
ConnectorMetadata {
}
/**
- * Rejects an illegal static-partition column on {@code INSERT [OVERWRITE]
... PARTITION (col=val)}: the
- * column must be an <em>identity</em> partition field of this
(partitioned) table. Mirrors the legacy
- * fe-resident {@code BindSink.validateStaticPartition} (now dead on the
connector-driven path), but the
- * iceberg {@link PartitionSpec} knowledge and the messages live in the
connector. The lookup is keyed by
- * partition <em>field</em> name (e.g. {@code category_bucket} for {@code
bucket(4, category)}), matching
- * legacy — so a source-column name that is not itself an identity field
reads as "Unknown partition column".
+ * Rejects an illegal static-partition column on {@code INSERT [OVERWRITE]
... PARTITION (col=val)}: the name
+ * must be the SOURCE column of an <em>identity</em> partition field of
this (partitioned) table.
+ *
+ * <p>That is the identifier Spark resolves for a static partition clause
+ * ({@code Analyzer.partitionColumnNames} takes the identity transform's
field reference), the one
+ * {@code SHOW CREATE TABLE} renders ({@code PARTITION BY LIST (`id`)}),
and the only one the engine can
+ * materialize: an identity partition value IS the column value, so the
binder fills that column from the
+ * PARTITION literal ({@code
PluginDrivenExternalTable#materializeStaticPartitionValues}).</p>
+ *
+ * <p>Consequently a partition FIELD name that was renamed away from its
source column
+ * ({@code ADD PARTITION KEY id AS id_identity}) is rejected with the
source name to use instead. The field
+ * name is not a table column, so no value could be written for it —
accepting it is exactly the
+ * "PARTITION names a column the binder then cannot find" contradiction
this replaces. Non-identity
+ * transforms stay rejected, and an unknown name lists the source columns
that a static write can use.</p>
*/
@Override
public void validateStaticPartitionColumns(ConnectorSession session,
ConnectorTableHandle handle,
@@ -1953,30 +1961,83 @@ public class IcebergConnectorMetadata implements
ConnectorMetadata {
return;
}
IcebergTableHandle iceHandle = (IcebergTableHandle) handle;
- Table table = loadTable(session, iceHandle);
- PartitionSpec spec = table.spec();
String tableName = iceHandle.getTableName();
+ // Validate against the schema/spec this statement actually writes
with. A branch-targeted write pins the
+ // BRANCH's writer schema before analysis is complete (getWriteColumns
-> IcebergStatementScope.writeSchema)
+ // and binding, planning and the BE sink all consume that pinned copy,
so after a main-only column rename
+ // the current metadata can disagree with the schema the branch
statement binds to.
+ Optional<IcebergWriteSchemaContext> pinned =
IcebergStatementScope.activeWriteSchema(
+ session, iceHandle.getDbName(), tableName);
+ Schema schema;
+ PartitionSpec spec;
+ if (pinned.isPresent()) {
+ schema = pinned.get().getSchema();
+ spec = pinned.get().getPartitionSpec();
+ } else {
+ Table table = loadTable(session, iceHandle);
+ schema = table.schema();
+ spec = table.spec();
+ }
if (!spec.isPartitioned()) {
throw new DorisConnectorException(String.format(
"Table %s is not partitioned, cannot use static partition
syntax", tableName));
}
- Map<String, PartitionField> partitionFieldMap = new HashMap<>();
- for (PartitionField field : spec.fields()) {
- partitionFieldMap.put(field.name(), field);
- }
for (String colName : staticPartitionColumnNames) {
- PartitionField field = partitionFieldMap.get(colName);
- if (field == null) {
+ validateStaticPartitionColumn(schema, spec, colName, tableName);
+ }
+ }
+
+ /** The single-name rule of {@link #validateStaticPartitionColumns}. */
+ private static void validateStaticPartitionColumn(Schema schema,
PartitionSpec spec, String colName,
+ String tableName) {
+ // The identity partition over this top-level column is the target.
Searching by SOURCE first (across all
+ // fields, not the first source match) keeps a spec that carries a
non-identity field on the same column
+ // legal - e.g. bucket(8, id) declared before identity(id) - and never
lets a nested leaf or a case-folded
+ // field alias stand in for the column the binder will materialize.
+ if (IcebergPartitionUtils.identityPartitionBySourceColumn(schema,
spec, colName) != null) {
+ return;
+ }
+ PartitionField bySource =
IcebergPartitionUtils.partitionFieldBySourceColumn(schema, spec, colName);
+ if (bySource != null) {
+ throw nonIdentityStaticPartition(colName, bySource);
+ }
+ PartitionField byName = partitionFieldByName(spec, colName);
+ if (byName != null) {
+ if (byName.transform().isIdentity()) {
+ // Renamed identity field (ADD PARTITION KEY ... AS ...):
point at the source column the engine can
+ // materialize, instead of accepting a name that no row column
carries.
+ String source =
IcebergPartitionUtils.topLevelSourceColumnName(schema, byName.sourceId());
+ if (source == null) {
+ throw new DorisConnectorException(String.format(
+ "Static partition column '%s' in table '%s' cannot
be written by value: its source is"
+ + " a nested field, and the engine can
only materialize the literal into a"
+ + " top-level column.", byName.name(),
tableName));
+ }
throw new DorisConnectorException(String.format(
- "Unknown partition column '%s' in table '%s'.
Available partition columns: %s",
- colName, tableName, partitionFieldMap.keySet()));
+ "Static partition column '%s' in table '%s' is a
partition field name, not a table"
+ + " column; write the identity partition by
its source column '%s' instead.",
+ byName.name(), tableName, source));
}
- if (!field.transform().isIdentity()) {
- throw new DorisConnectorException(String.format(
- "Cannot use static partition syntax for non-identity
partition field '%s'"
- + " (transform: %s).", colName,
field.transform().toString()));
+ throw nonIdentityStaticPartition(colName, byName);
+ }
+ throw new DorisConnectorException(String.format(
+ "Unknown partition column '%s' in table '%s'. Available
partition columns: %s",
+ colName, tableName,
IcebergPartitionUtils.identityPartitionSourceColumns(schema, spec)));
+ }
+
+ private static DorisConnectorException nonIdentityStaticPartition(String
colName, PartitionField field) {
+ return new DorisConnectorException(String.format(
+ "Cannot use static partition syntax for non-identity partition
field '%s'"
+ + " (transform: %s).", colName,
field.transform().toString()));
+ }
+
+ private static PartitionField partitionFieldByName(PartitionSpec spec,
String name) {
+ for (PartitionField field : spec.fields()) {
+ if (field.name().equalsIgnoreCase(name)) {
+ return field;
}
}
+ return null;
}
// ========== Predicate pushdown ==========
diff --git
a/fe/fe-connector/fe-connector-iceberg/src/main/java/org/apache/doris/connector/iceberg/IcebergPartitionUtils.java
b/fe/fe-connector/fe-connector-iceberg/src/main/java/org/apache/doris/connector/iceberg/IcebergPartitionUtils.java
index 24094ea76da..d9035efb111 100644
---
a/fe/fe-connector/fe-connector-iceberg/src/main/java/org/apache/doris/connector/iceberg/IcebergPartitionUtils.java
+++
b/fe/fe-connector/fe-connector-iceberg/src/main/java/org/apache/doris/connector/iceberg/IcebergPartitionUtils.java
@@ -35,6 +35,7 @@ import org.apache.iceberg.PartitionData;
import org.apache.iceberg.PartitionField;
import org.apache.iceberg.PartitionSpec;
import org.apache.iceberg.Partitioning;
+import org.apache.iceberg.Schema;
import org.apache.iceberg.Snapshot;
import org.apache.iceberg.StructLike;
import org.apache.iceberg.Table;
@@ -138,6 +139,104 @@ final class IcebergPartitionUtils {
return new ArrayList<>(partitionColumns);
}
+ /**
+ * Rewrites a static-partition spec ({@code INSERT [OVERWRITE] ...
PARTITION (col = val)}) so every key is the
+ * matching partition FIELD name, the identifier both downstream consumers
address: the BE sink matches
+ * {@code static_partition_values} against {@code PartitionField#name()},
and the commit row filter resolves
+ * the field to its source column.
+ *
+ * <p>The engine's PARTITION clause names the identity partition's SOURCE
column instead — the identifier Spark
+ * resolves for a static partition clause ({@code
Analyzer.partitionColumnNames} takes the identity transform's
+ * field reference) and the one {@code SHOW CREATE TABLE} renders — so the
two spellings must be bridged once,
+ * here, before either consumer sees the spec. A key that matches no field
of the current spec is kept verbatim
+ * and left to the callers' fail-loud guards.</p>
+ */
+ static Map<String, String> toPartitionFieldKeys(Map<String, String>
staticPartitions, Schema schema,
+ PartitionSpec spec) {
+ if (staticPartitions == null || staticPartitions.isEmpty() || spec ==
null) {
+ return staticPartitions;
+ }
+ Map<String, String> canonical = new HashMap<>(staticPartitions.size());
+ for (Map.Entry<String, String> entry : staticPartitions.entrySet()) {
+ canonical.put(toPartitionFieldName(schema, spec, entry.getKey()),
entry.getValue());
+ }
+ return canonical;
+ }
+
+ /**
+ * The partition field a static-partition name addresses: the IDENTITY
field over that top-level source column
+ * first, then a field with that name. The source lookup must win — a
field may carry a name that collides with
+ * another column's name in a different case ({@code identity(other,
"ID")} next to {@code identity(id,
+ * "id_identity")} is legal, because {@code PartitionSpec.Builder} checks
names case-SENSITIVELY), and the
+ * engine's name is the source column it validated.
+ */
+ private static String toPartitionFieldName(Schema schema, PartitionSpec
spec, String name) {
+ PartitionField identity = identityPartitionBySourceColumn(schema,
spec, name);
+ if (identity != null) {
+ return identity.name();
+ }
+ for (PartitionField field : spec.fields()) {
+ if (field.name().equalsIgnoreCase(name)) {
+ return field.name();
+ }
+ }
+ return name;
+ }
+
+ /**
+ * The top-level column a partition field's source resolves to, or {@code
null} when the source is a nested
+ * field. {@link NestedField#name()} is only the LEAF name, so a nested
{@code payload.id} source must not be
+ * mistaken for the top-level column {@code id}: the engine can only
materialize a top-level column.
+ */
+ static String topLevelSourceColumnName(Schema schema, int sourceId) {
+ for (NestedField column : schema.columns()) {
+ if (column.fieldId() == sourceId) {
+ return column.name();
+ }
+ }
+ return null;
+ }
+
+ /** The spec's <em>identity</em> partition over the top-level column
{@code name}, or null. */
+ static PartitionField identityPartitionBySourceColumn(Schema schema,
PartitionSpec spec, String name) {
+ for (PartitionField field : spec.fields()) {
+ if (!field.transform().isIdentity()) {
+ continue;
+ }
+ String source = topLevelSourceColumnName(schema, field.sourceId());
+ if (source != null && source.equalsIgnoreCase(name)) {
+ return field;
+ }
+ }
+ return null;
+ }
+
+ /** Any partition field over the top-level column {@code name}, or null
(used to report a non-identity one). */
+ static PartitionField partitionFieldBySourceColumn(Schema schema,
PartitionSpec spec, String name) {
+ for (PartitionField field : spec.fields()) {
+ String source = topLevelSourceColumnName(schema, field.sourceId());
+ if (source != null && source.equalsIgnoreCase(name)) {
+ return field;
+ }
+ }
+ return null;
+ }
+
+ /** The top-level source columns of the spec's identity partitions, in
field order. */
+ static List<String> identityPartitionSourceColumns(Schema schema,
PartitionSpec spec) {
+ List<String> names = new ArrayList<>();
+ for (PartitionField field : spec.fields()) {
+ if (!field.transform().isIdentity()) {
+ continue;
+ }
+ String source = topLevelSourceColumnName(schema, field.sourceId());
+ if (source != null) {
+ names.add(source);
+ }
+ }
+ return names;
+ }
+
/**
* Whether <b>any</b> partition spec of the table is partitioned. Scan
planning must use this instead of
* the current default spec: after evolving to an unpartitioned spec, data
files written under an older
diff --git
a/fe/fe-connector/fe-connector-iceberg/src/main/java/org/apache/doris/connector/iceberg/IcebergWritePlanProvider.java
b/fe/fe-connector/fe-connector-iceberg/src/main/java/org/apache/doris/connector/iceberg/IcebergWritePlanProvider.java
index 57f2fa1e338..5c8d0dc7ca3 100644
---
a/fe/fe-connector/fe-connector-iceberg/src/main/java/org/apache/doris/connector/iceberg/IcebergWritePlanProvider.java
+++
b/fe/fe-connector/fe-connector-iceberg/src/main/java/org/apache/doris/connector/iceberg/IcebergWritePlanProvider.java
@@ -805,19 +805,35 @@ public class IcebergWritePlanProvider implements
ConnectorWritePlanProvider {
// Branch-targeted INSERT (INSERT INTO tbl@branch): the branch is
threaded from the generic insert
// command context onto the write handle; beginWrite validates it
against the table refs and points
// the commit at the branch. Empty for a default-ref write.
- return new IcebergWriteContext(op, handle.isOverwrite(),
staticPartitionSpec(handle),
+ return new IcebergWriteContext(op, handle.isOverwrite(),
staticPartitionSpec(handle, schemaContext),
handle.getBranchName(), readSnapshotId, readSnapshotResolved,
schemaContext, handle.getBoundWriteMetadataIdentity());
}
- private static Map<String, String>
staticPartitionSpec(ConnectorWriteHandle handle) {
+ /**
+ * The static-partition spec ({@code INSERT [OVERWRITE] ... PARTITION (col
= val)}) in the shape both consumers
+ * need: partition FIELD names, with SQL NULL folded in.
+ *
+ * <p>The engine keys the spec by the identity partition's SOURCE column
(what Spark resolves and what
+ * {@code SHOW CREATE TABLE} renders), while the BE sink matches {@code
static_partition_values} against
+ * {@code PartitionField#name()} and the commit row filter resolves that
field back to its source column. The
+ * rewrite happens here, once, so the wire contract and the commit filter
stay field-keyed — a source-column
+ * key reaching either of them would silently make the write dynamic on BE
and fail the commit validation.</p>
+ */
+ private static Map<String, String>
staticPartitionSpec(ConnectorWriteHandle handle,
+ IcebergWriteSchemaContext schemaContext) {
Map<String, String> spec = handle.getStaticPartitionSpec();
- if (handle.getStaticPartitionNullKeys().isEmpty()) {
- return spec;
+ Map<String, String> normalized = spec;
+ if (!handle.getStaticPartitionNullKeys().isEmpty()) {
+ // The engine carries SQL NULL separately; both commit validation
and BE must see the same null keys.
+ normalized = new HashMap<>(spec);
+ normalized.replaceAll((key, value) ->
+ handle.getStaticPartitionNullKeys().contains(key) ? null :
value);
+ }
+ if (schemaContext != null) {
+ normalized =
IcebergPartitionUtils.toPartitionFieldKeys(normalized,
schemaContext.getSchema(),
+ schemaContext.getPartitionSpec());
}
- // The engine carries SQL NULL separately; both commit validation and
BE must see the same null keys.
- Map<String, String> normalized = new HashMap<>(spec);
- normalized.replaceAll((key, value) ->
handle.getStaticPartitionNullKeys().contains(key) ? null : value);
return normalized;
}
@@ -872,7 +888,7 @@ public class IcebergWritePlanProvider implements
ConnectorWritePlanProvider {
// Overwrite + static partition values (INSERT OVERWRITE ...
PARTITION).
tSink.setOverwrite(handle.isOverwrite());
- Map<String, String> staticPartitionSpec = staticPartitionSpec(handle);
+ Map<String, String> staticPartitionSpec = staticPartitionSpec(handle,
schemaContext);
if (handle.isOverwrite() && staticPartitionSpec != null &&
!staticPartitionSpec.isEmpty()) {
Map<String, String> values = new java.util.HashMap<>();
java.util.Set<String> nullKeys = new java.util.HashSet<>();
diff --git
a/fe/fe-connector/fe-connector-iceberg/src/test/java/org/apache/doris/connector/iceberg/IcebergConnectorMetadataTest.java
b/fe/fe-connector/fe-connector-iceberg/src/test/java/org/apache/doris/connector/iceberg/IcebergConnectorMetadataTest.java
index 6d3efdbfe42..a8975d561ad 100644
---
a/fe/fe-connector/fe-connector-iceberg/src/test/java/org/apache/doris/connector/iceberg/IcebergConnectorMetadataTest.java
+++
b/fe/fe-connector/fe-connector-iceberg/src/test/java/org/apache/doris/connector/iceberg/IcebergConnectorMetadataTest.java
@@ -19,6 +19,7 @@ package org.apache.doris.connector.iceberg;
import org.apache.doris.connector.spi.ConnectorColumn;
import org.apache.doris.connector.spi.ConnectorDatabaseMetadata;
+import org.apache.doris.connector.spi.ConnectorSession;
import org.apache.doris.connector.spi.ConnectorTableSchema;
import org.apache.doris.connector.spi.ConnectorViewDefinition;
import org.apache.doris.connector.spi.DorisConnectorException;
@@ -32,6 +33,7 @@ import org.apache.iceberg.PartitionSpec;
import org.apache.iceberg.RowLevelOperationMode;
import org.apache.iceberg.Schema;
import org.apache.iceberg.SortOrder;
+import org.apache.iceberg.Table;
import org.apache.iceberg.TableProperties;
import org.apache.iceberg.catalog.Namespace;
import org.apache.iceberg.catalog.TableIdentifier;
@@ -315,21 +317,157 @@ public class IcebergConnectorMetadataTest {
@Test
public void validateStaticPartitionColumnsRejectsUnknownColumn() {
- // WHY: a PARTITION(col=..) naming a column that is not a partition
FIELD must fail loud at analysis time
- // (byte-identical to legacy validateStaticPartition), else the
unknown column is silently swallowed by the
- // sink's materialize block and surfaces as an unrelated planning
error ("Cannot find snapshot"). The lookup
- // is keyed by partition FIELD name, so "id" (the bucket's SOURCE
column, not a field) is also "unknown" —
- // mirroring regression test_iceberg_static_partition_overwrite TC29
PARTITION(category) over bucket(category).
- // MUTATION: dropping the containsKey/null check -> unknown column
admitted -> red.
+ // WHY: a PARTITION(col=..) naming nothing the spec can write must
fail loud at analysis time, else the name
+ // is silently swallowed by the sink's materialize block and surfaces
as an unrelated planning error
+ // ("Cannot find snapshot"). The available list is the identity
partitions' SOURCE columns — the names a
+ // static write can actually use — not the raw field-name set.
+ // MUTATION: dropping the last-resort throw -> unknown column admitted
-> red.
Schema schema = idNameSchema();
IcebergConnectorMetadata md = metadataWithSpec(schema,
PartitionSpec.builderFor(schema).identity("name").bucket("id",
8).build());
- for (String bad : new String[] {"invalid_col", "id"}) {
- DorisConnectorException e =
Assertions.assertThrows(DorisConnectorException.class,
- () -> md.validateStaticPartitionColumns(
- null, new IcebergTableHandle("db1", "t1"),
Collections.singletonList(bad)));
- Assertions.assertTrue(e.getMessage().contains("Unknown partition
column"), e.getMessage());
- }
+ DorisConnectorException e =
Assertions.assertThrows(DorisConnectorException.class,
+ () -> md.validateStaticPartitionColumns(
+ null, new IcebergTableHandle("db1", "t1"),
Collections.singletonList("invalid_col")));
+ Assertions.assertTrue(e.getMessage().contains("Unknown partition
column"), e.getMessage());
+ Assertions.assertTrue(e.getMessage().contains("[name]"),
e.getMessage());
+ }
+
+ @Test
+ public void
validateStaticPartitionColumnsRejectsBucketSourceColumnAsNonIdentity() {
+ // WHY: "id" feeds bucket(8, id), so it cannot be written statically —
but it is a real column, and it is the
+ // name a user reads off SHOW CREATE TABLE (`BUCKET(8, `id`)`).
Reporting it as an unknown COLUMN would send
+ // them looking for a spelling mistake; the non-identity reason is the
actionable one (Spark rejects the same
+ // name as "not a partition column"). Upstream #65991 case 5.5 / TC29
PARTITION(category) over bucket(category).
+ // MUTATION: matching only identity fields and skipping this branch ->
wrong message -> red.
+ Schema schema = idNameSchema();
+ IcebergConnectorMetadata md = metadataWithSpec(schema,
+ PartitionSpec.builderFor(schema).identity("name").bucket("id",
8).build());
+ DorisConnectorException e =
Assertions.assertThrows(DorisConnectorException.class,
+ () -> md.validateStaticPartitionColumns(
+ null, new IcebergTableHandle("db1", "t1"),
Collections.singletonList("id")));
+ Assertions.assertTrue(
+ e.getMessage().contains("Cannot use static partition syntax
for non-identity partition field 'id'"),
+ e.getMessage());
+ }
+
+ @Test
+ public void
validateStaticPartitionColumnsUsesTheIdentitySourceColumnNotTheRenamedField() {
+ // WHY: ADD PARTITION KEY id AS id_identity renames the partition
FIELD away from its source column. A static
+ // partition value must then be written through the SOURCE column
(Spark's contract: partitionColumnNames
+ // takes the identity transform's field reference, and the static
value is materialized into that column),
+ // while the field name is not a table column at all — accepting it is
what produced the binder's
+ // "Column has no default value, column=id". MUTATION: keying the
lookup by field name -> 'id' reads as
+ // unknown (red) and 'id_identity' is admitted even though no row
column can carry it (red).
+ Schema schema = idNameSchema();
+ IcebergConnectorMetadata md = metadataWithSpec(schema,
+ PartitionSpec.builderFor(schema).identity("id",
"id_identity").build());
+ Assertions.assertDoesNotThrow(() -> md.validateStaticPartitionColumns(
+ null, new IcebergTableHandle("db1", "t1"),
Collections.singletonList("id")));
+ // Doris folds identifiers case-insensitively, so the same name in any
case must resolve.
+ Assertions.assertDoesNotThrow(() -> md.validateStaticPartitionColumns(
+ null, new IcebergTableHandle("db1", "t1"),
Collections.singletonList("ID")));
+ DorisConnectorException e =
Assertions.assertThrows(DorisConnectorException.class,
+ () -> md.validateStaticPartitionColumns(
+ null, new IcebergTableHandle("db1", "t1"),
Collections.singletonList("id_identity")));
+ Assertions.assertTrue(e.getMessage().contains("is a partition field
name, not a table column"),
+ e.getMessage());
+ Assertions.assertTrue(e.getMessage().contains("source column 'id'"),
e.getMessage());
+ }
+
+ @Test
+ public void
validateStaticPartitionColumnsPrefersTheIdentitySourceOverACaseFoldedAlias() {
+ // PartitionSpec.Builder checks field names case-SENSITIVELY, so a
field may be named "ID" over source
+ // "other" next to identity(id) named "id_identity". The static name
"id" addresses the SOURCE column, so
+ // it must resolve to that identity field and never to the earlier
alias. MUTATION: matching field names
+ // before sources -> "id" resolves to the field over "other" -> red.
+ Schema schema = new Schema(
+ Types.NestedField.required(1, "id", Types.IntegerType.get()),
+ Types.NestedField.required(2, "other",
Types.IntegerType.get()));
+ PartitionSpec spec = PartitionSpec.builderFor(schema)
+ .identity("other", "ID")
+ .identity("id", "id_identity")
+ .build();
+ IcebergConnectorMetadata md = metadataWithSpec(schema, spec);
+
+ Assertions.assertDoesNotThrow(() -> md.validateStaticPartitionColumns(
+ null, new IcebergTableHandle("db1", "t1"),
Collections.singletonList("id")));
+ }
+
+ @Test
+ public void
validateStaticPartitionColumnsFindsIdentityAfterANonIdentityFieldOnTheSameSource()
{
+ // A legal spec may declare bucket(8, id) before identity(id); static
writes address the identity field, so
+ // the identity must be found regardless of field order. MUTATION:
failing on the first source match ->
+ // a writable table is rejected as non-identity -> red.
+ Schema schema = idNameSchema();
+ PartitionSpec spec = PartitionSpec.builderFor(schema).bucket("id",
8).identity("id").build();
+ IcebergConnectorMetadata md = metadataWithSpec(schema, spec);
+
+ Assertions.assertDoesNotThrow(() -> md.validateStaticPartitionColumns(
+ null, new IcebergTableHandle("db1", "t1"),
Collections.singletonList("id")));
+ }
+
+ @Test
+ public void validateStaticPartitionColumnsRejectsNestedPartitionSources() {
+ // NestedField#name() is only the LEAF name: a nested identity source
must never be reachable through the
+ // top-level column sharing that leaf name, because the binder
materializes the literal into top-level
+ // columns only - accepting it would commit a file whose nested
partition value came from another column.
+ // MUTATION: matching the source by leaf name -> "id" is accepted ->
red.
+ Schema schema = new Schema(
+ Types.NestedField.required(1, "id", Types.IntegerType.get()),
+ Types.NestedField.required(2, "payload", Types.StructType.of(
+ Types.NestedField.required(3, "id",
Types.IntegerType.get()))));
+ PartitionSpec spec =
PartitionSpec.builderFor(schema).identity("payload.id", "nested_id").build();
+ IcebergConnectorMetadata md = metadataWithSpec(schema, spec);
+
+ DorisConnectorException leafName =
Assertions.assertThrows(DorisConnectorException.class,
+ () -> md.validateStaticPartitionColumns(
+ null, new IcebergTableHandle("db1", "t1"),
Collections.singletonList("id")));
+ Assertions.assertTrue(leafName.getMessage().contains("Unknown
partition column 'id'"),
+ leafName.getMessage());
+ DorisConnectorException byFieldName =
Assertions.assertThrows(DorisConnectorException.class,
+ () -> md.validateStaticPartitionColumns(
+ null, new IcebergTableHandle("db1", "t1"),
Collections.singletonList("nested_id")));
+ Assertions.assertTrue(byFieldName.getMessage().contains("source is a
nested field"),
+ byFieldName.getMessage());
+ }
+
+ @Test
+ public void
validateStaticPartitionColumnsUsesThePinnedBranchWriterSchema() {
+ // INSERT INTO t@branch(old) pins the BRANCH's writer schema before
analysis completes, and binding,
+ // planning and the BE sink all consume that pinned copy. After a
main-only rename (id -> key) the current
+ // metadata no longer has column "id" while the branch still does, so
validating against the current schema
+ // would reject the branch statement's own partition column and advise
a name that branch does not carry.
+ // MUTATION: validating against table.schema() -> the pinned assertion
turns red (the unpinned one below
+ // pins the divergence that makes the pin observable).
+ Schema branchSchema = idNameSchema();
+ InMemoryCatalog catalog = new InMemoryCatalog();
+ catalog.initialize("pinned", Collections.emptyMap());
+ catalog.createNamespace(Namespace.of("db1"));
+ Table branchTable = catalog.createTable(TableIdentifier.of("db1",
"t1"), branchSchema,
+ PartitionSpec.builderFor(branchSchema).identity("id").build());
+ IcebergWriteSchemaContext pinned = IcebergWriteSchemaContext.create(
+ branchTable, "t1", Optional.empty(), false, false);
+ TestStatementScope scope = new TestStatementScope();
+ ConnectorSession session = new
IcebergStatementScopeTest.ScopeSession(1L, "q1", scope);
+ IcebergStatementScope.writeSchema(session, "db1", "t1",
Optional.empty(), () -> pinned);
+
+ Schema renamedSchema = new Schema(
+ Types.NestedField.required(1, "key", Types.IntegerType.get()),
+ Types.NestedField.optional(2, "name", Types.StringType.get()));
+ RecordingIcebergCatalogOps ops = new RecordingIcebergCatalogOps();
+ ops.table = new FakeIcebergTable("t1", renamedSchema,
+ PartitionSpec.builderFor(renamedSchema).identity("key",
"id").build(),
+ "s3://bucket/db1/t1", Collections.emptyMap());
+ IcebergConnectorMetadata md = new IcebergConnectorMetadata(ops,
+ IcebergCatalogProperties.of(Collections.emptyMap()), new
RecordingConnectorContext());
+
+ Assertions.assertDoesNotThrow(() -> md.validateStaticPartitionColumns(
+ session, new IcebergTableHandle("db1", "t1"),
Collections.singletonList("id")));
+ DorisConnectorException currentOnly =
Assertions.assertThrows(DorisConnectorException.class,
+ () -> md.validateStaticPartitionColumns(
+ null, new IcebergTableHandle("db1", "t1"),
Collections.singletonList("id")));
+ Assertions.assertTrue(currentOnly.getMessage().contains("source column
'key'"),
+ currentOnly.getMessage());
}
@Test
diff --git
a/fe/fe-connector/fe-connector-iceberg/src/test/java/org/apache/doris/connector/iceberg/IcebergConnectorTransactionTest.java
b/fe/fe-connector/fe-connector-iceberg/src/test/java/org/apache/doris/connector/iceberg/IcebergConnectorTransactionTest.java
index 1b6d83d1dd3..6489c1d317e 100644
---
a/fe/fe-connector/fe-connector-iceberg/src/test/java/org/apache/doris/connector/iceberg/IcebergConnectorTransactionTest.java
+++
b/fe/fe-connector/fe-connector-iceberg/src/test/java/org/apache/doris/connector/iceberg/IcebergConnectorTransactionTest.java
@@ -961,6 +961,41 @@ public class IcebergConnectorTransactionTest {
Assertions.assertEquals("1", snap.summary().get("added-data-files"));
}
+ @Test
+ public void
overwriteStaticPartitionFiltersOnTheSourceColumnOfARenamedIdentityField() {
+ // ADD PARTITION KEY id AS id_identity: the write planner hands the
commit the partition FIELD key
+ // ("id_identity"), and the row filter must still be built on the
SOURCE column ("id") — the column the
+ // identity value is derived from, and what Spark's static-partition
overwrite filters on. The retained
+ // partition proves the filter did not widen (a field-name expression
would fail to bind instead).
+ // MUTATION: evaluating the filter on the field name -> iceberg binder
rejects "id_identity" -> red.
+ InMemoryCatalog catalog = freshCatalog();
+ TableIdentifier id = TableIdentifier.of("db1", "t1");
+ Schema schema = new Schema(
+ Types.NestedField.required(1, "id", Types.IntegerType.get()),
+ Types.NestedField.optional(2, "value",
Types.StringType.get()));
+ PartitionSpec spec = PartitionSpec.builderFor(schema).identity("id",
"id_identity").build();
+ Table table = catalog.createTable(id, schema, spec,
props("write.format.default", "parquet"));
+ table.newAppend().appendFile(partitionedDataFile(spec,
+ "s3://b/db1/t1/id_identity=1/seed.parquet", 1L,
"id_identity=1")).commit();
+
+ IcebergConnectorTransaction txn =
txnFor(opsReturning(catalog.loadTable(id)),
+ new RecordingConnectorContext());
+ txn.beginWrite(SESSION, "db1", "t1",
+ overwriteStaticCtx(table,
Collections.singletonMap("id_identity", "2")));
+ txn.addCommitData(commitBytes(dataFileItem(
+ "s3://b/db1/t1/id_identity=2/f1.parquet", 1L, 1024L,
Collections.singletonList("2"))));
+ txn.commit();
+
+ List<DataFile> remaining = currentDataFiles(catalog.loadTable(id));
+ Assertions.assertEquals(2, remaining.size(), remaining.toString());
+ Assertions.assertTrue(remaining.stream().anyMatch(
+ f -> f.path().toString().contains("id_identity=1")),
+ "the untouched partition must survive: " + remaining);
+ Assertions.assertTrue(remaining.stream().anyMatch(
+ f -> f.path().toString().contains("id_identity=2")),
+ "the overwritten partition must be added: " + remaining);
+ }
+
@Test
public void promotedDateStaticOverwriteRejectsUnsafeRetainedBounds() {
InMemoryCatalog catalog = freshCatalog();
diff --git
a/fe/fe-connector/fe-connector-iceberg/src/test/java/org/apache/doris/connector/iceberg/IcebergPartitionUtilsTest.java
b/fe/fe-connector/fe-connector-iceberg/src/test/java/org/apache/doris/connector/iceberg/IcebergPartitionUtilsTest.java
index cb0bd8d2aee..a4cba8bd550 100644
---
a/fe/fe-connector/fe-connector-iceberg/src/test/java/org/apache/doris/connector/iceberg/IcebergPartitionUtilsTest.java
+++
b/fe/fe-connector/fe-connector-iceberg/src/test/java/org/apache/doris/connector/iceberg/IcebergPartitionUtilsTest.java
@@ -291,6 +291,76 @@ public class IcebergPartitionUtilsTest {
Assertions.assertEquals(Arrays.asList("payload", "int_col"), cols);
}
+ // ---- toPartitionFieldKeys ----
+
+ @Test
+ public void staticPartitionKeysResolveTheSourceColumnToThePartitionField()
{
+ // ADD PARTITION KEY id AS id_identity: the engine's PARTITION clause
names the SOURCE column "id", while
+ // the BE sink matches static_partition_values against the partition
FIELD name and the commit row filter
+ // resolves that field to its source column. Both consumers therefore
need the field-keyed spec.
+ // MUTATION: passing the engine's spelling through -> BE sees no
static column (write silently becomes
+ // dynamic) and the commit filter stops matching -> red.
+ Schema schema = new Schema(
+ Types.NestedField.required(1, "id", Types.LongType.get()),
+ Types.NestedField.optional(2, "region",
Types.StringType.get()));
+ PartitionSpec spec = PartitionSpec.builderFor(schema)
+ .identity("id", "id_identity")
+ .identity("region")
+ .build();
+
+ Map<String, String> canonical =
IcebergPartitionUtils.toPartitionFieldKeys(
+ new HashMap<>(Collections.singletonMap("id", "7")), schema,
spec);
+ Assertions.assertEquals(Collections.singletonMap("id_identity", "7"),
canonical);
+
+ // A field name (the un-renamed spelling, and the bucket-style field
name a caller may already hold) is
+ // canonicalized to itself, and the lookup folds case like every Doris
identifier.
+ Map<String, String> mixed = new HashMap<>();
+ mixed.put("REGION", "us");
+ mixed.put("ID_IDENTITY", "8");
+ Map<String, String> expected = new HashMap<>();
+ expected.put("region", "us");
+ expected.put("id_identity", "8");
+ Assertions.assertEquals(expected,
IcebergPartitionUtils.toPartitionFieldKeys(mixed, schema, spec));
+
+ // A name the current spec does not know is kept verbatim: the callers
own the fail-loud guard for it.
+ Map<String, String> unknown =
IcebergPartitionUtils.toPartitionFieldKeys(
+ new HashMap<>(Collections.singletonMap("ghost", "1")), schema,
spec);
+ Assertions.assertEquals(Collections.singletonMap("ghost", "1"),
unknown);
+ }
+
+ @Test
+ public void
staticPartitionKeysPreferTheIdentitySourceOverACaseFoldedFieldAlias() {
+ // A partition field may carry a name that collides with another
column's name in a different case
+ // (`identity(other, "ID")` beside `identity(id, "id_identity")`),
because the spec builder checks names
+ // case-SENSITIVELY. The engine's PARTITION name is the SOURCE column,
so the key must follow the identity
+ // field over that source rather than the earlier alias.
+ // MUTATION: matching field names first -> the key becomes "ID"
(source "other") -> BE stamps and the commit
+ // filters the WRONG partition -> red.
+ Schema schema = new Schema(
+ Types.NestedField.required(1, "id", Types.IntegerType.get()),
+ Types.NestedField.required(2, "other",
Types.IntegerType.get()));
+ PartitionSpec spec = PartitionSpec.builderFor(schema)
+ .identity("other", "ID")
+ .identity("id", "id_identity")
+ .build();
+
+ Assertions.assertEquals(Collections.singletonMap("id_identity", "7"),
+ IcebergPartitionUtils.toPartitionFieldKeys(
+ new HashMap<>(Collections.singletonMap("id", "7")),
schema, spec));
+ }
+
+ @Test
+ public void
staticPartitionKeysFindTheIdentityFieldRegardlessOfFieldOrder() {
+ // bucket(8, id) may be declared before identity(id); the static
target is still the identity field.
+ // MUTATION: taking the first source match -> the bucket field's name
is returned -> red.
+ Schema schema = new Schema(Types.NestedField.required(1, "id",
Types.IntegerType.get()));
+ PartitionSpec spec = PartitionSpec.builderFor(schema).bucket("id",
8).identity("id").build();
+
+ Assertions.assertEquals(Collections.singletonMap("id", "7"),
+ IcebergPartitionUtils.toPartitionFieldKeys(
+ new HashMap<>(Collections.singletonMap("id", "7")),
schema, spec));
+ }
+
// ---- getIdentityPartitionInfoMap ----
@Test
diff --git
a/fe/fe-connector/fe-connector-iceberg/src/test/java/org/apache/doris/connector/iceberg/IcebergWritePlanProviderTest.java
b/fe/fe-connector/fe-connector-iceberg/src/test/java/org/apache/doris/connector/iceberg/IcebergWritePlanProviderTest.java
index 8ab5566dc80..0518bc94e52 100644
---
a/fe/fe-connector/fe-connector-iceberg/src/test/java/org/apache/doris/connector/iceberg/IcebergWritePlanProviderTest.java
+++
b/fe/fe-connector/fe-connector-iceberg/src/test/java/org/apache/doris/connector/iceberg/IcebergWritePlanProviderTest.java
@@ -1071,6 +1071,27 @@ public class IcebergWritePlanProviderTest {
"INSERT OVERWRITE ... PARTITION must pass the static partition
values to BE");
}
+ @Test
+ public void planWriteStaticPartitionKeysTheRenamedPartitionField() {
+ // ADD PARTITION KEY id AS id_identity: the engine keys the spec by
the SOURCE column (the identifier Spark
+ // resolves for a static PARTITION clause, and the one an identity
value can be materialized through), while
+ // the BE writer matches static_partition_values against the partition
FIELD name. Shipping "id" unchanged
+ // would make BE treat the write as dynamic, so the planner must
rewrite the key to the field name.
+ // MUTATION: dropping the rewrite -> {"id"="7"} != {"id_identity"="7"}
-> red.
+ Schema schema = new Schema(
+ Types.NestedField.required(1, "id", Types.IntegerType.get()),
+ Types.NestedField.optional(2, "value",
Types.StringType.get()));
+ Table table = freshCatalog().createTable(TableIdentifier.of("db1",
"t1"), schema,
+ PartitionSpec.builderFor(schema).identity("id",
"id_identity").build(),
+ Collections.singletonMap("write.format.default", "parquet"));
+
+ TIcebergTableSink sink = planSink(table, contextWithStorage(),
+ new WriteHandle(new IcebergTableHandle("db1",
"t1")).overwrite(true)
+ .writeContext(Collections.singletonMap("id", "7")));
+
+ Assertions.assertEquals(Collections.singletonMap("id_identity", "7"),
sink.getStaticPartitionValues());
+ }
+
@Test
public void planWriteStaticPartitionNullMarkerReachesSink() {
Table table = partitionedSortedTable(freshCatalog());
diff --git
a/regression-test/data/external_table_p0/iceberg/write/test_iceberg_static_partition_overwrite.out
b/regression-test/data/external_table_p0/iceberg/write/test_iceberg_static_partition_overwrite.out
index b600e2b3950..ef9e4289eef 100644
---
a/regression-test/data/external_table_p0/iceberg/write/test_iceberg_static_partition_overwrite.out
+++
b/regression-test/data/external_table_p0/iceberg/write/test_iceberg_static_partition_overwrite.out
@@ -215,3 +215,6 @@
11 Frank 1 85.5 false B
4 David 2 90.0 true A
+-- !renamed_static_source_column --
+1 a 1
+2 b2 22
diff --git
a/regression-test/suites/external_table_p0/iceberg/write/test_iceberg_static_partition_overwrite.groovy
b/regression-test/suites/external_table_p0/iceberg/write/test_iceberg_static_partition_overwrite.groovy
index f8636c23433..c251d6d5447 100644
---
a/regression-test/suites/external_table_p0/iceberg/write/test_iceberg_static_partition_overwrite.groovy
+++
b/regression-test/suites/external_table_p0/iceberg/write/test_iceberg_static_partition_overwrite.groovy
@@ -1097,7 +1097,7 @@ suite("test_iceberg_static_partition_overwrite",
"p0,external") {
PARTITION (category='food')
SELECT 10, 'Eve'
"""
- exception "Unknown partition column"
+ exception "Cannot use static partition syntax for non-identity
partition field"
}
// Using correct partition field name should trigger non-identity error
test {
@@ -1130,7 +1130,7 @@ suite("test_iceberg_static_partition_overwrite",
"p0,external") {
PARTITION (description='hello')
SELECT 10, 'Eve'
"""
- exception "Unknown partition column"
+ exception "Cannot use static partition syntax for non-identity
partition field"
}
// Using correct partition field name should trigger non-identity error
test {
@@ -1163,7 +1163,7 @@ suite("test_iceberg_static_partition_overwrite",
"p0,external") {
PARTITION (ts='2025-01-25 10:00:00')
SELECT 10, 'Eve'
"""
- exception "Unknown partition column"
+ exception "Cannot use static partition syntax for non-identity
partition field"
}
// Using correct partition field name should trigger non-identity error
test {
@@ -1196,7 +1196,7 @@ suite("test_iceberg_static_partition_overwrite",
"p0,external") {
PARTITION (event_date='2024-06-15')
SELECT 10, 'Eve'
"""
- exception "Unknown partition column"
+ exception "Cannot use static partition syntax for non-identity
partition field"
}
// Using correct partition field name should trigger non-identity error
test {
@@ -1229,7 +1229,7 @@ suite("test_iceberg_static_partition_overwrite",
"p0,external") {
PARTITION (event_date='2025-01-15')
SELECT 10, 'Eve'
"""
- exception "Unknown partition column"
+ exception "Cannot use static partition syntax for non-identity
partition field"
}
// Using correct partition field name should trigger non-identity error
test {
@@ -1262,7 +1262,7 @@ suite("test_iceberg_static_partition_overwrite",
"p0,external") {
PARTITION (ts='2025-01-25 10:00:00')
SELECT 10, 'Eve'
"""
- exception "Unknown partition column"
+ exception "Cannot use static partition syntax for non-identity
partition field"
}
// Using correct partition field name should trigger non-identity error
test {
@@ -1297,7 +1297,7 @@ suite("test_iceberg_static_partition_overwrite",
"p0,external") {
PARTITION (id=1)
SELECT 'Eve', 'bj'
"""
- exception "Unknown partition column"
+ exception "Cannot use static partition syntax for non-identity
partition field"
}
// Using correct partition field name should trigger non-identity error
test {
@@ -1330,7 +1330,7 @@ suite("test_iceberg_static_partition_overwrite",
"p0,external") {
PARTITION (amount=100)
SELECT 10, 'Eve'
"""
- exception "Unknown partition column"
+ exception "Cannot use static partition syntax for non-identity
partition field"
}
// Using correct partition field name should trigger non-identity error
test {
@@ -1363,7 +1363,7 @@ suite("test_iceberg_static_partition_overwrite",
"p0,external") {
PARTITION (user_id=1001)
SELECT 10, 'Eve'
"""
- exception "Unknown partition column"
+ exception "Cannot use static partition syntax for non-identity
partition field"
}
// Using correct partition field name should trigger non-identity error
test {
@@ -1404,7 +1404,7 @@ suite("test_iceberg_static_partition_overwrite",
"p0,external") {
PARTITION (ts='2025-01-25 10:00:00')
SELECT 10, 'Eve', 'bj'
"""
- exception "Unknown partition column"
+ exception "Cannot use static partition syntax for non-identity
partition field"
}
// Using correct partition field name should trigger non-identity error
test {
@@ -1415,4 +1415,46 @@ suite("test_iceberg_static_partition_overwrite",
"p0,external") {
"""
exception "Cannot use static partition syntax for non-identity
partition field"
}
+
+ //
============================================================================
+ // Test Case 39: An identity partition field renamed away from its source
column
+ //
============================================================================
+
+ // ADD PARTITION KEY id AS id_identity keeps the identity transform on
column id but names the partition
+ // field id_identity. Such a partition is written by its SOURCE column -
the identifier Spark resolves for a
+ // static PARTITION clause and the one SHOW CREATE TABLE renders - while
the field name is not a table column.
+ String tb_renamed = db1 + "_renamed_identity"
+ sql """ DROP TABLE IF EXISTS ${tb_renamed} """
+ sql """
+ CREATE TABLE ${tb_renamed} (
+ id BIGINT,
+ value STRING,
+ metric INT
+ ) ENGINE=iceberg
+ """
+ sql """ ALTER TABLE ${tb_renamed} ADD PARTITION KEY id AS id_identity """
+ sql """ INSERT INTO ${tb_renamed} VALUES (1, 'a', 1), (2, 'b', 2) """
+
+ // Only the id=2 partition is replaced, and the literal is materialized
into the row's id column (an identity
+ // partition value must equal the column value, otherwise the data file
contradicts its own partition).
+ sql """
+ INSERT OVERWRITE TABLE ${tb_renamed}
+ PARTITION (id = 2) (value, metric) VALUES ('b2', 22)
+ """
+ order_qt_renamed_static_source_column """ SELECT id, value, metric FROM
${tb_renamed} ORDER BY id """
+ def renamedRows = sql("SELECT id, value, metric FROM ${tb_renamed} ORDER
BY id")
+ spark_iceberg "REFRESH TABLE demo.${db1}.${tb_renamed}"
+ assertSparkDorisResultEquals(spark_iceberg("""
+ SELECT id, value, metric FROM demo.${db1}.${tb_renamed} ORDER BY id
+ """), renamedRows)
+
+ // The partition FIELD name is not a table column: report it with the
source column to use instead, rather than
+ // the previous "PARTITION names a column the binder then cannot find"
contradiction.
+ test {
+ sql """
+ INSERT OVERWRITE TABLE ${tb_renamed}
+ PARTITION (id_identity = 2) (value, metric) VALUES ('b3', 33)
+ """
+ exception "is a partition field name, not a table column"
+ }
}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]