github-actions[bot] commented on code in PR #66627:
URL: https://github.com/apache/doris/pull/66627#discussion_r3752071581


##########
fe/fe-connector/fe-connector-iceberg/src/main/java/org/apache/doris/connector/iceberg/IcebergNestedColumnEvolution.java:
##########
@@ -87,10 +96,75 @@ public static void addColumn(Table table, 
ConnectorColumnPath path, IcebergColum
 
     /** Drops the nested field at {@code path}; its parent must resolve to a 
struct that contains the leaf. */
     public static void dropColumn(Table table, ConnectorColumnPath path) {
-        ResolvedColumnPath resolvedPath = 
validateNestedStructFieldPath(table.schema(), path, "drop");
-        UpdateSchema updateSchema = table.updateSchema();
-        updateSchema.deleteColumn(resolvedPath.getFullPath());
-        updateSchema.commit();
+        dropColumnWithPartitionSpecFence(table, path, true);
+    }
+
+    static void dropTopLevelColumn(Table table, String columnName) {
+        dropColumnWithPartitionSpecFence(table, 
ConnectorColumnPath.of(columnName), false);
+    }
+
+    private static void dropColumnWithPartitionSpecFence(
+            Table table, ConnectorColumnPath path, boolean nested) {
+        TableOperations operations = ((HasTableOperations) table).operations();
+        TableMetadata initial = operations.refresh();

Review Comment:
   [P1] Pin the loaded table identity across refreshes
   
   This refresh discards the identity of the `Table` returned by `loadTable`. 
If a REST table is dropped and recreated under the same identifier before this 
refresh (or between retry attempts), `RESTTableOperations` adopts the 
replacement metadata and this code builds `AssertTableUUID` from the 
replacement UUID, so the stale DROP can remove a column from the new table. 
Capture the originally loaded identity before refreshing and reject any attempt 
whose refreshed table differs; cover both the initial-refresh and retry 
replacement cases.



##########
fe/fe-connector/fe-connector-iceberg/src/main/java/org/apache/doris/connector/iceberg/IcebergNestedColumnEvolution.java:
##########
@@ -87,10 +96,75 @@ public static void addColumn(Table table, 
ConnectorColumnPath path, IcebergColum
 
     /** Drops the nested field at {@code path}; its parent must resolve to a 
struct that contains the leaf. */
     public static void dropColumn(Table table, ConnectorColumnPath path) {
-        ResolvedColumnPath resolvedPath = 
validateNestedStructFieldPath(table.schema(), path, "drop");
-        UpdateSchema updateSchema = table.updateSchema();
-        updateSchema.deleteColumn(resolvedPath.getFullPath());
-        updateSchema.commit();
+        dropColumnWithPartitionSpecFence(table, path, true);
+    }
+
+    static void dropTopLevelColumn(Table table, String columnName) {
+        dropColumnWithPartitionSpecFence(table, 
ConnectorColumnPath.of(columnName), false);
+    }
+
+    private static void dropColumnWithPartitionSpecFence(
+            Table table, ConnectorColumnPath path, boolean nested) {
+        TableOperations operations = ((HasTableOperations) table).operations();
+        TableMetadata initial = operations.refresh();
+        // A commit conflict must rerun resolution and retained-spec 
validation against refreshed metadata.
+        Tasks.foreach(operations)
+                .retry(initial.propertyAsInt(
+                        TableProperties.COMMIT_NUM_RETRIES, 
TableProperties.COMMIT_NUM_RETRIES_DEFAULT))
+                .exponentialBackoff(
+                        
initial.propertyAsInt(TableProperties.COMMIT_MIN_RETRY_WAIT_MS,
+                                
TableProperties.COMMIT_MIN_RETRY_WAIT_MS_DEFAULT),
+                        
initial.propertyAsInt(TableProperties.COMMIT_MAX_RETRY_WAIT_MS,
+                                
TableProperties.COMMIT_MAX_RETRY_WAIT_MS_DEFAULT),
+                        
initial.propertyAsInt(TableProperties.COMMIT_TOTAL_RETRY_TIME_MS,
+                                
TableProperties.COMMIT_TOTAL_RETRY_TIME_MS_DEFAULT),
+                        2.0)
+                .onlyRetryOn(CommitFailedException.class)
+                .run(ops -> commitDropAttempt(ops, table.name(), path, 
nested));
+    }
+
+    private static void commitDropAttempt(
+            TableOperations operations, String tableName, ConnectorColumnPath 
path, boolean nested) {
+        TableMetadata base = operations.refresh();
+        ResolvedColumnPath resolvedPath = nested
+                ? validateNestedStructFieldPath(base.schema(), path, "drop")
+                : resolveColumnPath(base.schema(), path, "drop");
+        validateNotUsedByRetainedPartitionSpec(base, resolvedPath);
+
+        Table attemptTable = new BaseTable(operations.temp(base), tableName);
+        Schema updatedSchema = attemptTable.updateSchema()
+                .deleteColumn(resolvedPath.getFullPath()).apply();
+        int fenceSpecId = 
base.specs().stream().mapToInt(PartitionSpec::specId).max().orElse(-1) + 1;
+        String fenceFieldName = "_doris_schema_drop_fence_" + 
(base.lastAssignedPartitionId() + 1);
+        PartitionSpec fenceSpec = PartitionSpec.builderFor(base.schema())
+                .withSpecId(fenceSpecId)
+                .alwaysNull(resolvedPath.getFullPath(), fenceFieldName)

Review Comment:
   [P2] Make the synthetic partition name collision-free
   
   For an unpartitioned table this always chooses 
`_doris_schema_drop_fence_1000`. That is also a legal user column name; if an 
unrelated column already has it, `PartitionSpec.Builder.alwaysNull` rejects the 
target name because it belongs to a different source field, so dropping any 
other column fails before commit. Choose a fence name after checking the 
complete schema (or use a construction that guarantees no unrelated-field 
collision), and add a regression for this valid schema.



##########
fe/fe-connector/fe-connector-iceberg/src/main/java/org/apache/doris/connector/iceberg/IcebergNestedColumnEvolution.java:
##########
@@ -87,10 +96,75 @@ public static void addColumn(Table table, 
ConnectorColumnPath path, IcebergColum
 
     /** Drops the nested field at {@code path}; its parent must resolve to a 
struct that contains the leaf. */
     public static void dropColumn(Table table, ConnectorColumnPath path) {
-        ResolvedColumnPath resolvedPath = 
validateNestedStructFieldPath(table.schema(), path, "drop");
-        UpdateSchema updateSchema = table.updateSchema();
-        updateSchema.deleteColumn(resolvedPath.getFullPath());
-        updateSchema.commit();
+        dropColumnWithPartitionSpecFence(table, path, true);
+    }
+
+    static void dropTopLevelColumn(Table table, String columnName) {
+        dropColumnWithPartitionSpecFence(table, 
ConnectorColumnPath.of(columnName), false);
+    }
+
+    private static void dropColumnWithPartitionSpecFence(
+            Table table, ConnectorColumnPath path, boolean nested) {
+        TableOperations operations = ((HasTableOperations) table).operations();
+        TableMetadata initial = operations.refresh();
+        // A commit conflict must rerun resolution and retained-spec 
validation against refreshed metadata.
+        Tasks.foreach(operations)
+                .retry(initial.propertyAsInt(
+                        TableProperties.COMMIT_NUM_RETRIES, 
TableProperties.COMMIT_NUM_RETRIES_DEFAULT))
+                .exponentialBackoff(
+                        
initial.propertyAsInt(TableProperties.COMMIT_MIN_RETRY_WAIT_MS,
+                                
TableProperties.COMMIT_MIN_RETRY_WAIT_MS_DEFAULT),
+                        
initial.propertyAsInt(TableProperties.COMMIT_MAX_RETRY_WAIT_MS,
+                                
TableProperties.COMMIT_MAX_RETRY_WAIT_MS_DEFAULT),
+                        
initial.propertyAsInt(TableProperties.COMMIT_TOTAL_RETRY_TIME_MS,
+                                
TableProperties.COMMIT_TOTAL_RETRY_TIME_MS_DEFAULT),
+                        2.0)
+                .onlyRetryOn(CommitFailedException.class)
+                .run(ops -> commitDropAttempt(ops, table.name(), path, 
nested));
+    }
+
+    private static void commitDropAttempt(
+            TableOperations operations, String tableName, ConnectorColumnPath 
path, boolean nested) {
+        TableMetadata base = operations.refresh();
+        ResolvedColumnPath resolvedPath = nested
+                ? validateNestedStructFieldPath(base.schema(), path, "drop")
+                : resolveColumnPath(base.schema(), path, "drop");
+        validateNotUsedByRetainedPartitionSpec(base, resolvedPath);
+
+        Table attemptTable = new BaseTable(operations.temp(base), tableName);
+        Schema updatedSchema = attemptTable.updateSchema()
+                .deleteColumn(resolvedPath.getFullPath()).apply();
+        int fenceSpecId = 
base.specs().stream().mapToInt(PartitionSpec::specId).max().orElse(-1) + 1;
+        String fenceFieldName = "_doris_schema_drop_fence_" + 
(base.lastAssignedPartitionId() + 1);
+        PartitionSpec fenceSpec = PartitionSpec.builderFor(base.schema())
+                .withSpecId(fenceSpecId)
+                .alwaysNull(resolvedPath.getFullPath(), fenceFieldName)
+                .build();
+        TableMetadata.Builder builder = 
TableMetadata.buildFrom(base).addPartitionSpec(fenceSpec);
+        new 
MetadataUpdate.RemovePartitionSpecs(Set.of(fenceSpecId)).applyTo(builder);
+        builder.setCurrentSchema(updatedSchema, base.lastColumnId());

Review Comment:
   [P2] Preserve schema-drop property cleanup
   
   This combined commit bypasses `SchemaUpdate.commit()`'s 
`applyChangesToMetadata` step. A normal Iceberg drop removes the deleted path's 
metrics, Parquet bloom-filter, and Parquet stats overrides, but 
`buildFrom(base).setCurrentSchema(...)` retains them. The drop therefore leaves 
stale writer settings that can break a later metadata update or silently apply 
if the same name is added again. Apply the same property transformation in this 
metadata transaction and cover it with a configured-column drop test.



##########
fe/fe-connector/fe-connector-iceberg/src/main/java/org/apache/doris/connector/iceberg/IcebergNestedColumnEvolution.java:
##########
@@ -87,10 +96,75 @@ public static void addColumn(Table table, 
ConnectorColumnPath path, IcebergColum
 
     /** Drops the nested field at {@code path}; its parent must resolve to a 
struct that contains the leaf. */
     public static void dropColumn(Table table, ConnectorColumnPath path) {
-        ResolvedColumnPath resolvedPath = 
validateNestedStructFieldPath(table.schema(), path, "drop");
-        UpdateSchema updateSchema = table.updateSchema();
-        updateSchema.deleteColumn(resolvedPath.getFullPath());
-        updateSchema.commit();
+        dropColumnWithPartitionSpecFence(table, path, true);
+    }
+
+    static void dropTopLevelColumn(Table table, String columnName) {
+        dropColumnWithPartitionSpecFence(table, 
ConnectorColumnPath.of(columnName), false);
+    }
+
+    private static void dropColumnWithPartitionSpecFence(
+            Table table, ConnectorColumnPath path, boolean nested) {
+        TableOperations operations = ((HasTableOperations) table).operations();
+        TableMetadata initial = operations.refresh();
+        // A commit conflict must rerun resolution and retained-spec 
validation against refreshed metadata.
+        Tasks.foreach(operations)
+                .retry(initial.propertyAsInt(
+                        TableProperties.COMMIT_NUM_RETRIES, 
TableProperties.COMMIT_NUM_RETRIES_DEFAULT))
+                .exponentialBackoff(
+                        
initial.propertyAsInt(TableProperties.COMMIT_MIN_RETRY_WAIT_MS,
+                                
TableProperties.COMMIT_MIN_RETRY_WAIT_MS_DEFAULT),
+                        
initial.propertyAsInt(TableProperties.COMMIT_MAX_RETRY_WAIT_MS,
+                                
TableProperties.COMMIT_MAX_RETRY_WAIT_MS_DEFAULT),
+                        
initial.propertyAsInt(TableProperties.COMMIT_TOTAL_RETRY_TIME_MS,
+                                
TableProperties.COMMIT_TOTAL_RETRY_TIME_MS_DEFAULT),
+                        2.0)
+                .onlyRetryOn(CommitFailedException.class)
+                .run(ops -> commitDropAttempt(ops, table.name(), path, 
nested));
+    }
+
+    private static void commitDropAttempt(
+            TableOperations operations, String tableName, ConnectorColumnPath 
path, boolean nested) {
+        TableMetadata base = operations.refresh();
+        ResolvedColumnPath resolvedPath = nested
+                ? validateNestedStructFieldPath(base.schema(), path, "drop")
+                : resolveColumnPath(base.schema(), path, "drop");
+        validateNotUsedByRetainedPartitionSpec(base, resolvedPath);
+
+        Table attemptTable = new BaseTable(operations.temp(base), tableName);
+        Schema updatedSchema = attemptTable.updateSchema()
+                .deleteColumn(resolvedPath.getFullPath()).apply();
+        int fenceSpecId = 
base.specs().stream().mapToInt(PartitionSpec::specId).max().orElse(-1) + 1;
+        String fenceFieldName = "_doris_schema_drop_fence_" + 
(base.lastAssignedPartitionId() + 1);
+        PartitionSpec fenceSpec = PartitionSpec.builderFor(base.schema())
+                .withSpecId(fenceSpecId)
+                .alwaysNull(resolvedPath.getFullPath(), fenceFieldName)
+                .build();
+        TableMetadata.Builder builder = 
TableMetadata.buildFrom(base).addPartitionSpec(fenceSpec);
+        new 
MetadataUpdate.RemovePartitionSpecs(Set.of(fenceSpecId)).applyTo(builder);

Review Comment:
   [P1] Do not remove a client-predicted spec ID after REST rebasing
   
   `AssertLastAssignedPartitionId` does not fence the spec-ID namespace. A 
concurrent non-default spec can reuse existing partition-field IDs while 
consuming `fenceSpecId`, so every generated requirement still passes. When the 
REST server applies this request, `AddPartitionSpec` renumbers the fence to the 
next free spec ID, but this update still removes the original numeric ID: the 
concurrent spec is deleted and the fence remains after its source column is 
dropped. Tie removal to the actually added spec or reject this spec-ID rebase, 
and add a regression where the concurrent spec does not advance 
`lastAssignedPartitionId`.



##########
fe/fe-connector/fe-connector-iceberg/src/main/java/org/apache/doris/connector/iceberg/IcebergNestedColumnEvolution.java:
##########
@@ -87,10 +96,75 @@ public static void addColumn(Table table, 
ConnectorColumnPath path, IcebergColum
 
     /** Drops the nested field at {@code path}; its parent must resolve to a 
struct that contains the leaf. */
     public static void dropColumn(Table table, ConnectorColumnPath path) {
-        ResolvedColumnPath resolvedPath = 
validateNestedStructFieldPath(table.schema(), path, "drop");
-        UpdateSchema updateSchema = table.updateSchema();
-        updateSchema.deleteColumn(resolvedPath.getFullPath());
-        updateSchema.commit();
+        dropColumnWithPartitionSpecFence(table, path, true);
+    }
+
+    static void dropTopLevelColumn(Table table, String columnName) {
+        dropColumnWithPartitionSpecFence(table, 
ConnectorColumnPath.of(columnName), false);
+    }
+
+    private static void dropColumnWithPartitionSpecFence(
+            Table table, ConnectorColumnPath path, boolean nested) {
+        TableOperations operations = ((HasTableOperations) table).operations();
+        TableMetadata initial = operations.refresh();
+        // A commit conflict must rerun resolution and retained-spec 
validation against refreshed metadata.
+        Tasks.foreach(operations)
+                .retry(initial.propertyAsInt(
+                        TableProperties.COMMIT_NUM_RETRIES, 
TableProperties.COMMIT_NUM_RETRIES_DEFAULT))
+                .exponentialBackoff(
+                        
initial.propertyAsInt(TableProperties.COMMIT_MIN_RETRY_WAIT_MS,
+                                
TableProperties.COMMIT_MIN_RETRY_WAIT_MS_DEFAULT),
+                        
initial.propertyAsInt(TableProperties.COMMIT_MAX_RETRY_WAIT_MS,
+                                
TableProperties.COMMIT_MAX_RETRY_WAIT_MS_DEFAULT),
+                        
initial.propertyAsInt(TableProperties.COMMIT_TOTAL_RETRY_TIME_MS,
+                                
TableProperties.COMMIT_TOTAL_RETRY_TIME_MS_DEFAULT),
+                        2.0)
+                .onlyRetryOn(CommitFailedException.class)
+                .run(ops -> commitDropAttempt(ops, table.name(), path, 
nested));
+    }
+
+    private static void commitDropAttempt(
+            TableOperations operations, String tableName, ConnectorColumnPath 
path, boolean nested) {
+        TableMetadata base = operations.refresh();

Review Comment:
   [P1] Preserve the original field identity across retries
   
   Each retry resolves `path` against the latest schema. If another writer 
drops the original field and adds or renames a different field to the same 
path, the first attempt conflicts, but the retry resolves the replacement field 
ID and successfully deletes it. The table UUID is unchanged, so pinning only 
table identity does not prevent this ABA. Capture the originally targeted field 
ID (and relevant subtree identity for parent drops), require every attempt to 
resolve that same object, and add a same-path replacement regression.



##########
fe/fe-connector/fe-connector-iceberg/src/main/java/org/apache/doris/connector/iceberg/IcebergNestedColumnEvolution.java:
##########
@@ -87,10 +96,75 @@ public static void addColumn(Table table, 
ConnectorColumnPath path, IcebergColum
 
     /** Drops the nested field at {@code path}; its parent must resolve to a 
struct that contains the leaf. */
     public static void dropColumn(Table table, ConnectorColumnPath path) {
-        ResolvedColumnPath resolvedPath = 
validateNestedStructFieldPath(table.schema(), path, "drop");
-        UpdateSchema updateSchema = table.updateSchema();
-        updateSchema.deleteColumn(resolvedPath.getFullPath());
-        updateSchema.commit();
+        dropColumnWithPartitionSpecFence(table, path, true);
+    }
+
+    static void dropTopLevelColumn(Table table, String columnName) {
+        dropColumnWithPartitionSpecFence(table, 
ConnectorColumnPath.of(columnName), false);
+    }
+
+    private static void dropColumnWithPartitionSpecFence(
+            Table table, ConnectorColumnPath path, boolean nested) {
+        TableOperations operations = ((HasTableOperations) table).operations();
+        TableMetadata initial = operations.refresh();
+        // A commit conflict must rerun resolution and retained-spec 
validation against refreshed metadata.
+        Tasks.foreach(operations)
+                .retry(initial.propertyAsInt(
+                        TableProperties.COMMIT_NUM_RETRIES, 
TableProperties.COMMIT_NUM_RETRIES_DEFAULT))
+                .exponentialBackoff(
+                        
initial.propertyAsInt(TableProperties.COMMIT_MIN_RETRY_WAIT_MS,
+                                
TableProperties.COMMIT_MIN_RETRY_WAIT_MS_DEFAULT),
+                        
initial.propertyAsInt(TableProperties.COMMIT_MAX_RETRY_WAIT_MS,
+                                
TableProperties.COMMIT_MAX_RETRY_WAIT_MS_DEFAULT),
+                        
initial.propertyAsInt(TableProperties.COMMIT_TOTAL_RETRY_TIME_MS,
+                                
TableProperties.COMMIT_TOTAL_RETRY_TIME_MS_DEFAULT),
+                        2.0)
+                .onlyRetryOn(CommitFailedException.class)
+                .run(ops -> commitDropAttempt(ops, table.name(), path, 
nested));
+    }
+
+    private static void commitDropAttempt(
+            TableOperations operations, String tableName, ConnectorColumnPath 
path, boolean nested) {
+        TableMetadata base = operations.refresh();
+        ResolvedColumnPath resolvedPath = nested
+                ? validateNestedStructFieldPath(base.schema(), path, "drop")
+                : resolveColumnPath(base.schema(), path, "drop");
+        validateNotUsedByRetainedPartitionSpec(base, resolvedPath);
+
+        Table attemptTable = new BaseTable(operations.temp(base), tableName);
+        Schema updatedSchema = attemptTable.updateSchema()
+                .deleteColumn(resolvedPath.getFullPath()).apply();
+        int fenceSpecId = 
base.specs().stream().mapToInt(PartitionSpec::specId).max().orElse(-1) + 1;
+        String fenceFieldName = "_doris_schema_drop_fence_" + 
(base.lastAssignedPartitionId() + 1);
+        PartitionSpec fenceSpec = PartitionSpec.builderFor(base.schema())
+                .withSpecId(fenceSpecId)
+                .alwaysNull(resolvedPath.getFullPath(), fenceFieldName)
+                .build();
+        TableMetadata.Builder builder = 
TableMetadata.buildFrom(base).addPartitionSpec(fenceSpec);

Review Comment:
   [P1] Preserve v1 partition IDs after the fence
   
   On a never-partitioned format-v1 table this transient field is assigned ID 
1000. `addPartitionSpec` advances the final `lastAssignedPartitionId` to 1000, 
while `RemovePartitionSpecs` removes only the spec. After this drop, the first 
real `updateSpec().addField(...)` therefore gets 1001; v1's sequential-ID check 
requires the first field to be 1000, so that commit fails. Keep the fence from 
leaving v1's counter ahead of its current sequential layout and add a 
regression that drops an ordinary column, then adds the table's first partition 
field.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to