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 7224a2d382b [test](iceberg) Expand capability, transaction and 
snapshot coverage (#69551)
7224a2d382b is described below

commit 7224a2d382b4276d30ccd32324e0499ae9a23fd7
Author: Gabriel <[email protected]>
AuthorDate: Sun Oct 25 22:34:06 2026 +0800

    [test](iceberg) Expand capability, transaction and snapshot coverage 
(#69551)
    
    ### What problem does this PR solve?
    
    Iceberg has broad regression coverage, but feature names alone miss
    state transitions and weak oracles. For example, expiring fresh
    snapshots with a cutoff in 2020 never exercises ref retention.
    
    Add 45 connector unit tests, two external regression suites, and an
    inventory mapping the published 84 operations plus read, version, model,
    lineage, type, scanner and storage dimensions to existing tests and
    remaining gaps. The tests cover snapshot/schema/ref isolation, atomic
    writes and failure paths, nested type metadata, exact predicate
    semantics, partition serialization, actual ref retention, and file
    positions across deletes and history.
    
    Nine individually disabled reproductions preserve correct assertions for
    five confirmed issue families: DECIMAL-to-DOUBLE pruning, lineage-name
    collisions during a V2-to-V3 upgrade, ignored per-operation isolation
    properties, mixed MERGE affected-row counts, and expiration counters
    built from stale metadata. These were run and observed failing before
    disabling. Production behavior is unchanged.
    
    ### Release note
    
    None
    
    ### Validation
    
    - Full connector reactor: `mvn -f fe/pom.xml -pl :fe-connector-iceberg
    -am test -DfailIfNoTests=false -Dmaven.build.cache.enabled=false` with
    the matching Thrift 0.24.0 compiler: BUILD SUCCESS, including FE
    Checkstyle.
    - Iceberg connector: 1,734 discovered, 1,720 passed, 14 skipped. New
    tests: 45 discovered, 36 passed, 9 intentionally disabled defect
    reproductions.
    - Both Groovy suites compile with Groovy 4.0.19. They use existing
    REST/MinIO/Spark fixtures and dynamic comparisons, with no handwritten
    golden results.
    - SQL suites were not executed: the managed QA/Jenkins endpoints are
    unreachable from the validation host. BE unit tests and the full
    backend/deployment matrix were not run.
    
    ### Check List (For Author)
    
    - Test
    - [x] Regression test (added; compilation checked; SQL execution
    pending)
        - [x] Unit Test
    - Behavior changed:
        - [x] No.
    - Does this need documentation?
        - [x] No. Test coverage inventory is included.
---
 .../iceberg/IcebergLineageDdlCoverageTest.java     | 122 +++++++
 .../iceberg/IcebergLineageDdlKnownBugsTest.java    |  81 +++++
 .../iceberg/IcebergPartitionUtilsCoverageTest.java | 159 ++++++++
 .../IcebergPredicateConverterCoverageTest.java     | 211 +++++++++++
 .../iceberg/IcebergSnapshotSchemaCoverageTest.java | 157 ++++++++
 .../iceberg/IcebergTypeMappingCoverageTest.java    | 113 ++++++
 .../iceberg/IcebergWriteKnownBugsTest.java         | 179 +++++++++
 .../IcebergWriteTransactionCoverageTest.java       | 404 +++++++++++++++++++++
 .../IcebergSnapshotMaintenanceCoverageTest.java    | 193 ++++++++++
 .../iceberg/ICEBERG_CAPABILITY_TEST_COVERAGE.md    | 160 ++++++++
 ...est_iceberg_file_metadata_delete_history.groovy | 110 ++++++
 ...iceberg_ref_retention_expiration_control.groovy |  76 ++++
 12 files changed, 1965 insertions(+)

diff --git 
a/fe/fe-connector/fe-connector-iceberg/src/test/java/org/apache/doris/connector/iceberg/IcebergLineageDdlCoverageTest.java
 
b/fe/fe-connector/fe-connector-iceberg/src/test/java/org/apache/doris/connector/iceberg/IcebergLineageDdlCoverageTest.java
new file mode 100644
index 00000000000..8bf23e7e190
--- /dev/null
+++ 
b/fe/fe-connector/fe-connector-iceberg/src/test/java/org/apache/doris/connector/iceberg/IcebergLineageDdlCoverageTest.java
@@ -0,0 +1,122 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+package org.apache.doris.connector.iceberg;
+
+import 
org.apache.doris.connector.iceberg.IcebergCatalogOps.CatalogBackedIcebergCatalogOps;
+import org.apache.doris.connector.spi.ConnectorColumn;
+import org.apache.doris.connector.spi.ConnectorType;
+import org.apache.doris.connector.spi.DorisConnectorException;
+
+import org.apache.iceberg.HasTableOperations;
+import org.apache.iceberg.PartitionSpec;
+import org.apache.iceberg.Schema;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.catalog.Namespace;
+import org.apache.iceberg.catalog.TableIdentifier;
+import org.apache.iceberg.inmemory.InMemoryCatalog;
+import org.apache.iceberg.types.Types;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+import java.util.Collections;
+
+public class IcebergLineageDdlCoverageTest {
+    private static final TableIdentifier IDENTIFIER = TableIdentifier.of("db", 
"events");
+    private static final IcebergTableHandle HANDLE = new 
IcebergTableHandle("db", "events");
+
+    @Test
+    public void addDoesNotExposeDuplicateRowIdColumns() {
+        assertAddPreservesUniqueMetadataNames("_row_id");
+    }
+
+    @Test
+    public void addDoesNotExposeDuplicateSequenceColumns() {
+        assertAddPreservesUniqueMetadataNames("_last_updated_sequence_number");
+    }
+
+    @Test
+    public void renameDoesNotExposeDuplicateRowIdColumns() {
+        assertRenamePreservesUniqueMetadataNames("_row_id");
+    }
+
+    @Test
+    public void renameDoesNotExposeDuplicateSequenceColumns() {
+        
assertRenamePreservesUniqueMetadataNames("_last_updated_sequence_number");
+    }
+
+    private void assertAddPreservesUniqueMetadataNames(String reservedName) {
+        InMemoryCatalog catalog = catalog();
+        createV3(catalog);
+        IcebergConnectorMetadata metadata = metadata(catalog);
+        String previousMetadata = metadataLocation(catalog);
+        DorisConnectorException failure = 
Assertions.assertThrows(DorisConnectorException.class,
+                () -> metadata.addColumn(null, HANDLE,
+                        new ConnectorColumn(reservedName, 
ConnectorType.of("BIGINT"), "", true, null, false), null));
+        Assertions.assertTrue(failure.getMessage().contains("reserved row 
lineage column"));
+        Assertions.assertEquals(previousMetadata, metadataLocation(catalog));
+        
Assertions.assertNull(catalog.loadTable(IDENTIFIER).schema().findField(reservedName));
+        assertUniqueColumnName(metadata, reservedName);
+    }
+
+    private void assertRenamePreservesUniqueMetadataNames(String reservedName) 
{
+        InMemoryCatalog catalog = catalog();
+        createV3(catalog);
+        IcebergConnectorMetadata metadata = metadata(catalog);
+        String previousMetadata = metadataLocation(catalog);
+        DorisConnectorException failure = 
Assertions.assertThrows(DorisConnectorException.class,
+                () -> metadata.renameColumn(null, HANDLE, "id", reservedName));
+        Assertions.assertTrue(failure.getMessage().contains("reserved row 
lineage column"));
+        Assertions.assertEquals(previousMetadata, metadataLocation(catalog));
+        
Assertions.assertNotNull(catalog.loadTable(IDENTIFIER).schema().findField("id"));
+        
Assertions.assertNull(catalog.loadTable(IDENTIFIER).schema().findField(reservedName));
+        assertUniqueColumnName(metadata, reservedName);
+    }
+
+    private static void assertUniqueColumnName(IcebergConnectorMetadata 
metadata, String reservedName) {
+        // A public schema must not contain both a physical user field and 
hidden metadata under one name.
+        long matchingColumns = metadata.getTableSchema(null, 
HANDLE).getColumns().stream()
+                .filter(column -> 
reservedName.equalsIgnoreCase(column.getName())).count();
+        Assertions.assertEquals(1L, matchingColumns,
+                "The connector exposed duplicate physical and hidden columns 
named " + reservedName);
+    }
+
+    private static String metadataLocation(InMemoryCatalog catalog) {
+        Table table = catalog.loadTable(IDENTIFIER);
+        return ((HasTableOperations) 
table).operations().current().metadataFileLocation();
+    }
+
+    private static InMemoryCatalog catalog() {
+        InMemoryCatalog catalog = new InMemoryCatalog();
+        catalog.initialize("repro", Collections.emptyMap());
+        catalog.createNamespace(Namespace.of("db"));
+        return catalog;
+    }
+
+    private static void createV3(InMemoryCatalog catalog) {
+        catalog.createTable(IDENTIFIER, new Schema(
+                Types.NestedField.optional(1, "id", Types.LongType.get())),
+                PartitionSpec.unpartitioned(), 
Collections.singletonMap("format-version", "3"));
+    }
+
+    private static IcebergConnectorMetadata metadata(InMemoryCatalog catalog) {
+        return new IcebergConnectorMetadata(new 
CatalogBackedIcebergCatalogOps(catalog),
+                IcebergCatalogProperties.of(Collections.singletonMap(
+                        IcebergCatalogProperties.ICEBERG_CATALOG_TYPE, 
IcebergCatalogProperties.TYPE_REST)),
+                new RecordingConnectorContext());
+    }
+}
diff --git 
a/fe/fe-connector/fe-connector-iceberg/src/test/java/org/apache/doris/connector/iceberg/IcebergLineageDdlKnownBugsTest.java
 
b/fe/fe-connector/fe-connector-iceberg/src/test/java/org/apache/doris/connector/iceberg/IcebergLineageDdlKnownBugsTest.java
new file mode 100644
index 00000000000..ebe9d419db6
--- /dev/null
+++ 
b/fe/fe-connector/fe-connector-iceberg/src/test/java/org/apache/doris/connector/iceberg/IcebergLineageDdlKnownBugsTest.java
@@ -0,0 +1,81 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+package org.apache.doris.connector.iceberg;
+
+import 
org.apache.doris.connector.iceberg.IcebergCatalogOps.CatalogBackedIcebergCatalogOps;
+
+import org.apache.iceberg.PartitionSpec;
+import org.apache.iceberg.Schema;
+import org.apache.iceberg.catalog.Namespace;
+import org.apache.iceberg.catalog.TableIdentifier;
+import org.apache.iceberg.inmemory.InMemoryCatalog;
+import org.apache.iceberg.types.Types;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Disabled;
+import org.junit.jupiter.api.Test;
+
+import java.util.Collections;
+
+public class IcebergLineageDdlKnownBugsTest {
+    private static final TableIdentifier IDENTIFIER = TableIdentifier.of("db", 
"events");
+    private static final IcebergTableHandle HANDLE = new 
IcebergTableHandle("db", "events");
+
+    @Test
+    @Disabled("Known issue: a V2-to-V3 upgrade permits a user column to 
collide with row lineage.")
+    public void upgradeDoesNotExposeDuplicateRowIdColumns() {
+        assertUpgradePreservesUniqueMetadataNames("_row_id");
+    }
+
+    @Test
+    @Disabled("Known issue: a V2-to-V3 upgrade permits a user column to 
collide with row lineage.")
+    public void upgradeDoesNotExposeDuplicateSequenceColumns() {
+        
assertUpgradePreservesUniqueMetadataNames("_last_updated_sequence_number");
+    }
+
+    private void assertUpgradePreservesUniqueMetadataNames(String 
reservedName) {
+        InMemoryCatalog catalog = catalog();
+        catalog.createTable(IDENTIFIER, new Schema(
+                Types.NestedField.optional(1, reservedName, 
Types.LongType.get())),
+                PartitionSpec.unpartitioned(), 
Collections.singletonMap("format-version", "2"));
+        IcebergConnectorMetadata metadata = metadata(catalog);
+        metadata.setTableProperties(null, HANDLE, 
Collections.singletonMap("format-version", "3"));
+        assertUniqueColumnName(metadata, reservedName);
+    }
+
+    private static void assertUniqueColumnName(IcebergConnectorMetadata 
metadata, String reservedName) {
+        // A public schema must not contain both a physical user field and 
hidden metadata under one name.
+        long matchingColumns = metadata.getTableSchema(null, 
HANDLE).getColumns().stream()
+                .filter(column -> 
reservedName.equalsIgnoreCase(column.getName())).count();
+        Assertions.assertEquals(1L, matchingColumns,
+                "The connector exposed duplicate physical and hidden columns 
named " + reservedName);
+    }
+
+    private static InMemoryCatalog catalog() {
+        InMemoryCatalog catalog = new InMemoryCatalog();
+        catalog.initialize("repro", Collections.emptyMap());
+        catalog.createNamespace(Namespace.of("db"));
+        return catalog;
+    }
+
+    private static IcebergConnectorMetadata metadata(InMemoryCatalog catalog) {
+        return new IcebergConnectorMetadata(new 
CatalogBackedIcebergCatalogOps(catalog),
+                IcebergCatalogProperties.of(Collections.singletonMap(
+                        IcebergCatalogProperties.ICEBERG_CATALOG_TYPE, 
IcebergCatalogProperties.TYPE_REST)),
+                new RecordingConnectorContext());
+    }
+}
diff --git 
a/fe/fe-connector/fe-connector-iceberg/src/test/java/org/apache/doris/connector/iceberg/IcebergPartitionUtilsCoverageTest.java
 
b/fe/fe-connector/fe-connector-iceberg/src/test/java/org/apache/doris/connector/iceberg/IcebergPartitionUtilsCoverageTest.java
new file mode 100644
index 00000000000..b6a0ca9c729
--- /dev/null
+++ 
b/fe/fe-connector/fe-connector-iceberg/src/test/java/org/apache/doris/connector/iceberg/IcebergPartitionUtilsCoverageTest.java
@@ -0,0 +1,159 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+package org.apache.doris.connector.iceberg;
+
+import org.apache.iceberg.PartitionData;
+import org.apache.iceberg.PartitionSpec;
+import org.apache.iceberg.Schema;
+import org.apache.iceberg.transforms.Transform;
+import org.apache.iceberg.transforms.Transforms;
+import org.apache.iceberg.types.Type;
+import org.apache.iceberg.types.Types;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+import java.math.BigDecimal;
+import java.nio.ByteBuffer;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.ZoneId;
+import java.time.ZoneOffset;
+import java.util.Arrays;
+import java.util.List;
+
+public class IcebergPartitionUtilsCoverageTest {
+    @Test
+    public void compoundPartitionWireFormatPreservesEscapingAndExactValues() {
+        Schema schema = new Schema(
+                Types.NestedField.optional(1, "text", Types.StringType.get()),
+                Types.NestedField.optional(2, "nullable", 
Types.StringType.get()),
+                Types.NestedField.optional(3, "amount", 
Types.DecimalType.of(38, 18)),
+                Types.NestedField.optional(4, "date", Types.DateType.get()),
+                Types.NestedField.optional(5, "timestamp", 
Types.TimestampNanoType.withoutZone()),
+                Types.NestedField.optional(6, "binary", 
Types.BinaryType.get()));
+        PartitionSpec spec = 
PartitionSpec.builderFor(schema).identity("text").identity("nullable")
+                
.identity("amount").identity("date").identity("timestamp").identity("binary").build();
+        PartitionData original = new PartitionData(spec.partitionType());
+        List<Object> values = Arrays.asList("null/\"quoted\"\\line\n\u0000雪", 
null,
+                new BigDecimal("-99999999999999999999.000000000000000001"), -1,
+                -1L, ByteBuffer.wrap(new byte[] {0, (byte) 0xff, (byte) 0x80, 
34, 92}));
+        for (int i = 0; i < values.size(); i++) {
+            original.set(i, values.get(i));
+        }
+
+        for (ZoneId zone : Arrays.asList(ZoneOffset.UTC, 
ZoneId.of("America/New_York"))) {
+            List<String> encoded = 
IcebergPartitionUtils.parsePartitionValuesFromJson(
+                    IcebergPartitionUtils.getPartitionDataJson(original, spec, 
zone));
+            Assertions.assertEquals(values.size(), encoded.size());
+            Assertions.assertEquals(values.get(0), encoded.get(0));
+            Assertions.assertNull(encoded.get(1));
+            PartitionData decoded = new PartitionData(spec.partitionType());
+            for (int i = 0; i < encoded.size(); i++) {
+                decoded.set(i, 
IcebergPartitionUtils.parsePartitionValueFromString(
+                        encoded.get(i), 
spec.partitionType().fields().get(i).type(), zone));
+            }
+            // PartitionData stores binary as byte[], whose container equality 
compares array identities.
+            for (int i = 0; i < original.size(); i++) {
+                Assertions.assertEquals(original.get(i), decoded.get(i), 
"partition field " + i);
+            }
+            Assertions.assertEquals(18, ((BigDecimal) decoded.get(2)).scale());
+        }
+    }
+
+    @Test
+    public void 
calendarRangesAgreeWithSdkTransformsAcrossEpochAndLeapBoundaries() {
+        List<LocalDateTime> timestamps = Arrays.asList(
+                LocalDateTime.of(1600, 2, 29, 23, 59, 59, 999999000),
+                LocalDateTime.of(1900, 3, 1, 0, 0),
+                LocalDateTime.of(1969, 12, 31, 23, 59, 59, 999999000),
+                LocalDateTime.of(1970, 1, 1, 0, 0),
+                LocalDateTime.of(2000, 2, 29, 23, 59, 59, 999999000),
+                LocalDateTime.of(2100, 3, 1, 0, 0));
+        for (Type type : Arrays.asList(Types.DateType.get(),
+                Types.TimestampType.withoutZone(), 
Types.TimestampType.withZone())) {
+            for (String name : Arrays.asList("year", "month", "day", "hour")) {
+                if (type.typeId() == Type.TypeID.DATE && name.equals("hour")) {
+                    continue;
+                }
+                Transform<Object, Integer> transform = calendarTransform(name);
+                for (LocalDateTime timestamp : timestamps) {
+                    int ordinal = 
transform.bind(type).apply(internalValue(type, timestamp));
+                    IcebergPartitionUtils.RangeBuild range = 
IcebergPartitionUtils.buildRange(
+                            name + "=" + ordinal, Integer.toString(ordinal), 
name, type, 100L, 200L);
+                    LocalDateTime lower = 
parseBound(range.getLowerBound().get(0), type);
+                    LocalDateTime upper = 
parseBound(range.getUpperBound().get(0), type);
+                    LocalDateTime row = type.typeId() == Type.TypeID.DATE
+                            ? timestamp.toLocalDate().atStartOfDay() : 
timestamp;
+                    Assertions.assertFalse(row.isBefore(lower), name + " " + 
timestamp);
+                    Assertions.assertTrue(row.isBefore(upper), name + " " + 
timestamp);
+                    // Both ends must describe exactly one SDK transform 
ordinal, including negative ordinals.
+                    Assertions.assertEquals(ordinal, 
transform.bind(type).apply(internalValue(type, lower)));
+                    LocalDateTime last = type.typeId() == Type.TypeID.DATE
+                            ? upper.minusDays(1) : upper.minusNanos(1000);
+                    Assertions.assertEquals(ordinal, 
transform.bind(type).apply(internalValue(type, last)));
+                    Assertions.assertEquals(ordinal + 1, 
transform.bind(type).apply(internalValue(type, upper)));
+                }
+            }
+        }
+    }
+
+    @Test
+    public void binarySerializationDoesNotConsumeDirectOrReadOnlyBuffers() {
+        ByteBuffer direct = ByteBuffer.allocateDirect(6);
+        direct.put(new byte[] {99, 0, (byte) 0xfe, (byte) 0xff, 1, 99});
+        direct.position(1);
+        direct.limit(5);
+        for (ByteBuffer buffer : Arrays.asList(direct, 
direct.asReadOnlyBuffer(), direct.slice())) {
+            int position = buffer.position();
+            int limit = buffer.limit();
+            for (Type type : Arrays.asList(Types.BinaryType.get(), 
Types.FixedType.ofLength(4))) {
+                String encoded = 
IcebergPartitionUtils.serializePartitionValue(type, buffer, ZoneOffset.UTC);
+                Assertions.assertEquals("0x00feff01", encoded);
+                Assertions.assertEquals(buffer, 
IcebergPartitionUtils.parsePartitionValueFromString(
+                        encoded, type, ZoneOffset.UTC));
+                Assertions.assertEquals(position, buffer.position());
+                Assertions.assertEquals(limit, buffer.limit());
+            }
+        }
+    }
+
+    private static Transform<Object, Integer> calendarTransform(String name) {
+        switch (name) {
+            case "year":
+                return Transforms.year();
+            case "month":
+                return Transforms.month();
+            case "day":
+                return Transforms.day();
+            case "hour":
+                return Transforms.hour();
+            default:
+                throw new IllegalArgumentException(name);
+        }
+    }
+
+    private static Object internalValue(Type type, LocalDateTime timestamp) {
+        return type.typeId() == Type.TypeID.DATE ? (Object) (int) 
timestamp.toLocalDate().toEpochDay()
+                : timestamp.toEpochSecond(ZoneOffset.UTC) * 1_000_000L + 
timestamp.getNano() / 1000;
+    }
+
+    private static LocalDateTime parseBound(String bound, Type type) {
+        return type.typeId() == Type.TypeID.DATE ? 
LocalDate.parse(bound).atStartOfDay()
+                : LocalDateTime.parse(bound.replace("+00:00", "").replace(' ', 
'T'));
+    }
+}
diff --git 
a/fe/fe-connector/fe-connector-iceberg/src/test/java/org/apache/doris/connector/iceberg/IcebergPredicateConverterCoverageTest.java
 
b/fe/fe-connector/fe-connector-iceberg/src/test/java/org/apache/doris/connector/iceberg/IcebergPredicateConverterCoverageTest.java
new file mode 100644
index 00000000000..6412a9f2ade
--- /dev/null
+++ 
b/fe/fe-connector/fe-connector-iceberg/src/test/java/org/apache/doris/connector/iceberg/IcebergPredicateConverterCoverageTest.java
@@ -0,0 +1,211 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+package org.apache.doris.connector.iceberg;
+
+import org.apache.doris.connector.spi.ConnectorType;
+import org.apache.doris.connector.spi.pushdown.ConnectorColumnRef;
+import org.apache.doris.connector.spi.pushdown.ConnectorComparison;
+import org.apache.doris.connector.spi.pushdown.ConnectorIn;
+import org.apache.doris.connector.spi.pushdown.ConnectorLiteral;
+
+import org.apache.iceberg.DataFile;
+import org.apache.iceberg.DataFiles;
+import org.apache.iceberg.FileFormat;
+import org.apache.iceberg.Metrics;
+import org.apache.iceberg.PartitionSpec;
+import org.apache.iceberg.Schema;
+import org.apache.iceberg.StructLike;
+import org.apache.iceberg.expressions.Evaluator;
+import org.apache.iceberg.expressions.Expression;
+import org.apache.iceberg.expressions.InclusiveMetricsEvaluator;
+import org.apache.iceberg.types.Conversions;
+import org.apache.iceberg.types.Type;
+import org.apache.iceberg.types.Types;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Disabled;
+import org.junit.jupiter.api.Test;
+
+import java.math.BigDecimal;
+import java.nio.ByteBuffer;
+import java.time.ZoneOffset;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+
+public class IcebergPredicateConverterCoverageTest {
+    private static final List<ConnectorComparison.Operator> OPERATORS = 
Arrays.asList(
+            ConnectorComparison.Operator.EQ, ConnectorComparison.Operator.LT, 
ConnectorComparison.Operator.LE,
+            ConnectorComparison.Operator.GT, ConnectorComparison.Operator.GE);
+
+    @Test
+    public void 
integralPredicatesPreserveOrderingAtNarrowingAndLongBoundaries() {
+        long[] literals = {Long.MIN_VALUE, (long) Integer.MIN_VALUE - 1, 
Integer.MIN_VALUE, -1, 0, 1,
+                Integer.MAX_VALUE, (long) Integer.MAX_VALUE + 1, 
9007199254740993L, Long.MAX_VALUE};
+        for (Type type : Arrays.asList(Types.IntegerType.get(), 
Types.LongType.get())) {
+            List<? extends Number> rows = type.typeId() == Type.TypeID.INTEGER
+                    ? Arrays.asList(Integer.MIN_VALUE, -1, 0, 1, 
Integer.MAX_VALUE)
+                    : Arrays.asList(Long.MIN_VALUE, (long) Integer.MIN_VALUE, 
-1L, 0L, 1L,
+                            (long) Integer.MAX_VALUE, 9007199254740993L, 
Long.MAX_VALUE);
+            Schema schema = schema(type);
+            for (IcebergPredicateConverter.Mode mode : 
IcebergPredicateConverter.Mode.values()) {
+                for (long literal : literals) {
+                    for (ConnectorComparison.Operator operator : OPERATORS) {
+                        Evaluator evaluator = evaluator(schema, mode, operator,
+                                new 
ConnectorLiteral(ConnectorType.of("BIGINT"), literal));
+                        for (Number row : rows) {
+                            Assertions.assertEquals(matches(operator, 
Long.compare(row.longValue(), literal)),
+                                    evaluator.eval(new SingleValueRow(row)),
+                                    mode + " " + type + " " + row + " " + 
operator + " " + literal);
+                        }
+                    }
+                }
+            }
+        }
+    }
+
+    @Test
+    public void decimalPredicatesPreservePrecisionAndEquivalentLiteralScales() 
{
+        Schema schema = schema(Types.DecimalType.of(38, 18));
+        List<BigDecimal> values = Arrays.asList(
+                new BigDecimal("-99999999999999999999.999999999999999999"),
+                new BigDecimal("-0.000000000000000001"),
+                new BigDecimal("0.000000000000000000"),
+                new BigDecimal("0.000000000000000001"),
+                new BigDecimal("99999999999999999999.999999999999999999"));
+        for (IcebergPredicateConverter.Mode mode : 
IcebergPredicateConverter.Mode.values()) {
+            for (BigDecimal value : values) {
+                for (BigDecimal literal : Arrays.asList(value, 
value.setScale(20))) {
+                    for (ConnectorComparison.Operator operator : OPERATORS) {
+                        Evaluator evaluator = evaluator(schema, mode, operator,
+                                new 
ConnectorLiteral(ConnectorType.of("DECIMAL256", 40, literal.scale()), literal));
+                        for (BigDecimal row : values) {
+                            Assertions.assertEquals(matches(operator, 
row.compareTo(literal)),
+                                    evaluator.eval(new SingleValueRow(row)),
+                                    mode + " " + row + " " + operator + " " + 
literal);
+                        }
+                    }
+                }
+            }
+        }
+    }
+
+    @Test
+    @Disabled("Known issue: stripping a DECIMAL-to-DOUBLE cast can prune 
matching rows and files.")
+    public void decimalToDoubleComparisonMustNotPruneMatchingRows() {
+        Types.DecimalType type = Types.DecimalType.of(38, 0);
+        Schema schema = schema(type);
+        BigDecimal row = new BigDecimal("9007199254740993");
+        double literal = 9007199254740992d;
+        Assertions.assertTrue(row.doubleValue() == literal);
+        // The engine removes the column's DOUBLE cast; pruning must preserve 
its many-to-one equality.
+        ConnectorComparison comparison = new 
ConnectorComparison(ConnectorComparison.Operator.EQ,
+                new ConnectorColumnRef("value", ConnectorType.of("DECIMALV3", 
38, 0)),
+                new ConnectorLiteral(ConnectorType.of("DOUBLE"), literal));
+        List<Expression> converted = new IcebergPredicateConverter(schema, 
ZoneOffset.UTC).convert(comparison);
+        Map<Integer, ByteBuffer> bounds = Collections.singletonMap(1, 
Conversions.toByteBuffer(type, row));
+        Metrics metrics = new Metrics(1L, null, Collections.singletonMap(1, 
1L),
+                Collections.singletonMap(1, 0L), null, bounds, bounds);
+        DataFile file = DataFiles.builder(PartitionSpec.unpartitioned())
+                
.withPath("data/decimal-double.parquet").withFormat(FileFormat.PARQUET)
+                .withFileSizeInBytes(1024L).withMetrics(metrics).build();
+        // Dropping the predicate is safe; every predicate that is pushed must 
retain this matching file.
+        for (Expression predicate : converted) {
+            Assertions.assertAll(
+                    () -> Assertions.assertTrue(new 
Evaluator(schema.asStruct(), predicate)
+                            .eval(new SingleValueRow(row)), "a matching row 
must survive pruning"),
+                    () -> Assertions.assertTrue(new 
InclusiveMetricsEvaluator(schema, predicate).eval(file),
+                            "a file containing a matching row must survive 
pruning"));
+        }
+    }
+
+    @Test
+    public void lineageColumnsNeverBecomeFilePruningPredicates() {
+        for (String name : Arrays.asList("_row_id", "_ROW_ID", 
"_last_updated_sequence_number",
+                "_LAST_UPDATED_SEQUENCE_NUMBER")) {
+            Schema schema = new Schema(Types.NestedField.optional(1, name, 
Types.LongType.get()));
+            ConnectorColumnRef column = new ConnectorColumnRef(name, 
ConnectorType.of("BIGINT"));
+            for (IcebergPredicateConverter.Mode mode : 
IcebergPredicateConverter.Mode.values()) {
+                IcebergPredicateConverter converter = new 
IcebergPredicateConverter(schema, ZoneOffset.UTC, mode);
+                for (ConnectorComparison.Operator operator : 
ConnectorComparison.Operator.values()) {
+                    Assertions.assertTrue(converter.convert(new 
ConnectorComparison(
+                            operator, column, 
ConnectorLiteral.ofLong(1L))).isEmpty(), mode + " " + operator);
+                }
+                Assertions.assertTrue(converter.convert(new ConnectorIn(column,
+                        
Collections.singletonList(ConnectorLiteral.ofLong(1L)), false)).isEmpty());
+                Assertions.assertTrue(converter.convert(new 
ConnectorComparison(
+                        ConnectorComparison.Operator.EQ_FOR_NULL, column,
+                        
ConnectorLiteral.ofNull(ConnectorType.of("BIGINT")))).isEmpty());
+            }
+        }
+    }
+
+    private static Schema schema(Type type) {
+        return new Schema(Types.NestedField.optional(1, "value", type));
+    }
+
+    private static Evaluator evaluator(Schema schema, 
IcebergPredicateConverter.Mode mode,
+            ConnectorComparison.Operator operator, ConnectorLiteral literal) {
+        ConnectorComparison comparison = new ConnectorComparison(operator,
+                new ConnectorColumnRef("value", ConnectorType.of("UNKNOWN")), 
literal);
+        List<Expression> converted = new IcebergPredicateConverter(schema, 
ZoneOffset.UTC, mode).convert(comparison);
+        Assertions.assertEquals(1, converted.size(), mode + " " + comparison);
+        return new Evaluator(schema.asStruct(), converted.get(0));
+    }
+
+    private static boolean matches(ConnectorComparison.Operator operator, int 
comparison) {
+        switch (operator) {
+            case EQ:
+                return comparison == 0;
+            case LT:
+                return comparison < 0;
+            case LE:
+                return comparison <= 0;
+            case GT:
+                return comparison > 0;
+            case GE:
+                return comparison >= 0;
+            default:
+                throw new IllegalArgumentException(operator.toString());
+        }
+    }
+
+    private static final class SingleValueRow implements StructLike {
+        private final Object value;
+
+        private SingleValueRow(Object value) {
+            this.value = value;
+        }
+
+        @Override
+        public int size() {
+            return 1;
+        }
+
+        @Override
+        public <T> T get(int position, Class<T> javaClass) {
+            Assertions.assertEquals(0, position);
+            return javaClass.cast(value);
+        }
+
+        @Override
+        public <T> void set(int position, T value) {
+            throw new UnsupportedOperationException();
+        }
+    }
+}
diff --git 
a/fe/fe-connector/fe-connector-iceberg/src/test/java/org/apache/doris/connector/iceberg/IcebergSnapshotSchemaCoverageTest.java
 
b/fe/fe-connector/fe-connector-iceberg/src/test/java/org/apache/doris/connector/iceberg/IcebergSnapshotSchemaCoverageTest.java
new file mode 100644
index 00000000000..bab9b4c20e9
--- /dev/null
+++ 
b/fe/fe-connector/fe-connector-iceberg/src/test/java/org/apache/doris/connector/iceberg/IcebergSnapshotSchemaCoverageTest.java
@@ -0,0 +1,157 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+package org.apache.doris.connector.iceberg;
+
+import org.apache.doris.connector.spi.ConnectorColumn;
+import org.apache.doris.connector.spi.ConnectorTableSchema;
+import org.apache.doris.connector.spi.handle.ConnectorTableHandle;
+import org.apache.doris.connector.spi.mvcc.ConnectorMvccSnapshot;
+import org.apache.doris.connector.spi.mvcc.ConnectorTimeTravelSpec;
+
+import org.apache.iceberg.DataFiles;
+import org.apache.iceberg.FileFormat;
+import org.apache.iceberg.PartitionSpec;
+import org.apache.iceberg.Schema;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.catalog.Namespace;
+import org.apache.iceberg.catalog.TableIdentifier;
+import org.apache.iceberg.inmemory.InMemoryCatalog;
+import org.apache.iceberg.types.Types;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.stream.Collectors;
+
+public class IcebergSnapshotSchemaCoverageTest {
+    private static final IcebergTableHandle HANDLE = new 
IcebergTableHandle("db1", "history");
+
+    private static Table newTable() {
+        InMemoryCatalog catalog = new InMemoryCatalog();
+        catalog.initialize("test", Collections.emptyMap());
+        catalog.createNamespace(Namespace.of("db1"));
+        return catalog.createTable(TableIdentifier.of("db1", "history"), new 
Schema(
+                Types.NestedField.required(1, "id", Types.IntegerType.get()),
+                Types.NestedField.optional(2, "original_name", 
Types.StringType.get())));
+    }
+
+    private static long append(Table table, String name) {
+        
table.newAppend().appendFile(DataFiles.builder(PartitionSpec.unpartitioned())
+                .withPath("s3://bucket/history/" + name + 
".parquet").withFormat(FileFormat.PARQUET)
+                .withFileSizeInBytes(100).withRecordCount(1).build()).commit();
+        return table.currentSnapshot().snapshotId();
+    }
+
+    private static IcebergConnectorMetadata metadata(Table table) {
+        RecordingIcebergCatalogOps ops = new RecordingIcebergCatalogOps();
+        ops.table = table;
+        return new IcebergConnectorMetadata(ops, 
IcebergCatalogProperties.of(Collections.emptyMap()),
+                new RecordingConnectorContext());
+    }
+
+    private static List<String> names(ConnectorTableSchema schema) {
+        return 
schema.getColumns().stream().map(ConnectorColumn::getName).collect(Collectors.toList());
+    }
+
+    @Test
+    public void 
branchUsesLatestSchemaWhileTagAtTheSameSnapshotUsesHistoricalSchema() {
+        Table table = newTable();
+        long snapshotId = append(table, "initial");
+        int originalSchemaId = table.schema().schemaId();
+        table.manageSnapshots().createTag("audit_tag", snapshotId)
+                .createBranch("audit_branch", snapshotId).commit();
+        table.updateSchema().renameColumn("original_name", "renamed").commit();
+        IcebergConnectorMetadata metadata = metadata(table);
+
+        ConnectorMvccSnapshot branch = metadata.resolveTimeTravel(null, HANDLE,
+                ConnectorTimeTravelSpec.branch("audit_branch")).get();
+        ConnectorMvccSnapshot tag = metadata.resolveTimeTravel(null, HANDLE,
+                ConnectorTimeTravelSpec.tag("audit_tag")).get();
+
+        // Iceberg branches follow the table schema, whereas tags retain the 
referenced snapshot's schema.
+        Assertions.assertEquals(snapshotId, branch.getSnapshotId());
+        Assertions.assertEquals(snapshotId, tag.getSnapshotId());
+        Assertions.assertEquals(table.schema().schemaId(), 
branch.getSchemaId());
+        Assertions.assertEquals(originalSchemaId, tag.getSchemaId());
+        Assertions.assertEquals(Arrays.asList("id", "renamed", "_file", 
"_pos"),
+                names(metadata.getTableSchema(null, HANDLE, branch)));
+        Assertions.assertEquals(Arrays.asList("id", "original_name", "_file", 
"_pos"),
+                names(metadata.getTableSchema(null, HANDLE, tag)));
+    }
+
+    @Test
+    public void 
explicitSnapshotRetainsItsSchemaAfterRollbackChangesTheCurrentSnapshot() {
+        Table table = newTable();
+        long initial = append(table, "initial");
+        table.updateSchema().renameColumn("original_name", "renamed").commit();
+        long newer = append(table, "newer");
+        int newerSchema = table.schema().schemaId();
+        table.manageSnapshots().rollbackTo(initial).commit();
+        IcebergConnectorMetadata metadata = metadata(table);
+
+        ConnectorMvccSnapshot historical = metadata.resolveTimeTravel(null, 
HANDLE,
+                
ConnectorTimeTravelSpec.snapshotId(String.valueOf(newer))).get();
+
+        Assertions.assertEquals(initial, table.currentSnapshot().snapshotId());
+        Assertions.assertEquals(newer, historical.getSnapshotId());
+        Assertions.assertEquals(newerSchema, historical.getSchemaId());
+        Assertions.assertEquals(Arrays.asList("id", "renamed", "_file", 
"_pos"),
+                names(metadata.getTableSchema(null, HANDLE, historical)));
+    }
+
+    @Test
+    public void 
historicalSchemaKeepsLineageColumnsAfterFormatUpgradeWithoutAnotherSnapshot() {
+        Table table = newTable();
+        long initial = append(table, "initial");
+        int originalSchema = table.schema().schemaId();
+        table.updateSchema().renameColumn("original_name", "renamed").commit();
+        table.updateProperties().set("format-version", "3").commit();
+        IcebergConnectorMetadata metadata = metadata(table);
+        ConnectorMvccSnapshot historical = metadata.resolveTimeTravel(null, 
HANDLE,
+                
ConnectorTimeTravelSpec.snapshotId(String.valueOf(initial))).get();
+
+        Assertions.assertEquals(originalSchema, historical.getSchemaId());
+        List<ConnectorColumn> columns = metadata.getTableSchema(null, HANDLE, 
historical).getColumns();
+        Assertions.assertEquals(Arrays.asList("id", "original_name", "_file", 
"_pos",
+                "_row_id", "_last_updated_sequence_number"),
+                
columns.stream().map(ConnectorColumn::getName).collect(Collectors.toList()));
+        Assertions.assertFalse(columns.get(4).isVisible());
+        Assertions.assertFalse(columns.get(5).isVisible());
+        Assertions.assertTrue(columns.get(4).isReservedPassthrough());
+        Assertions.assertTrue(columns.get(5).isReservedPassthrough());
+    }
+
+    @Test
+    public void 
metadataTablesNeverAcquireDataFileOrLineageColumnsFromAV3Base() {
+        Table table = newTable();
+        table.updateProperties().set("format-version", "3").commit();
+        append(table, "initial");
+        IcebergConnectorMetadata metadata = metadata(table);
+
+        for (String systemTable : metadata.listSupportedSysTables(null, 
HANDLE)) {
+            ConnectorTableHandle system = metadata.getSysTableHandle(null, 
HANDLE, systemTable).get();
+            List<String> columnNames = names(metadata.getTableSchema(null, 
system));
+            Assertions.assertFalse(columnNames.contains("_file"), systemTable);
+            Assertions.assertFalse(columnNames.contains("_pos"), systemTable);
+            Assertions.assertFalse(columnNames.contains("_row_id"), 
systemTable);
+            
Assertions.assertFalse(columnNames.contains("_last_updated_sequence_number"), 
systemTable);
+        }
+    }
+}
diff --git 
a/fe/fe-connector/fe-connector-iceberg/src/test/java/org/apache/doris/connector/iceberg/IcebergTypeMappingCoverageTest.java
 
b/fe/fe-connector/fe-connector-iceberg/src/test/java/org/apache/doris/connector/iceberg/IcebergTypeMappingCoverageTest.java
new file mode 100644
index 00000000000..6350b02952b
--- /dev/null
+++ 
b/fe/fe-connector/fe-connector-iceberg/src/test/java/org/apache/doris/connector/iceberg/IcebergTypeMappingCoverageTest.java
@@ -0,0 +1,113 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+package org.apache.doris.connector.iceberg;
+
+import org.apache.doris.connector.spi.ConnectorColumn;
+import org.apache.doris.connector.spi.ConnectorType;
+
+import org.apache.iceberg.Schema;
+import org.apache.iceberg.types.Type;
+import org.apache.iceberg.types.TypeUtil;
+import org.apache.iceberg.types.Types;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+import java.util.Collections;
+import java.util.List;
+
+public class IcebergTypeMappingCoverageTest {
+    @Test
+    public void nestedCollectionMetadataSurvivesReadAndCreateRoundTrip() {
+        Types.StructType value = Types.StructType.of(
+                Types.NestedField.required(71, "EventTime", 
Types.TimestampType.withZone(), "event instant"),
+                Types.NestedField.optional(19, "Amounts", 
Types.ListType.ofRequired(113,
+                        Types.DecimalType.of(38, 18)), "exact amounts"),
+                Types.NestedField.required(104, "Labels", 
Types.MapType.ofOptional(92, 27,
+                        Types.StringType.get(), Types.UUIDType.get()), 
"optional labels"));
+        Type nested = Types.ListType.ofRequired(31,
+                Types.MapType.ofRequired(46, 58, Types.StringType.get(), 
value));
+        Schema original = new Schema(Types.NestedField.required(53, "Events", 
nested, "nested events"));
+
+        for (boolean binaryFlag : new boolean[] {false, true}) {
+            for (boolean timestampFlag : new boolean[] {false, true}) {
+                ConnectorType mapped = 
IcebergTypeMapping.fromIcebergType(nested, binaryFlag, timestampFlag);
+                assertNestedMetadata(nested, mapped);
+                ConnectorColumn column = new ConnectorColumn("Events", mapped, 
"nested events", false, null, false);
+                Schema rebuilt = 
IcebergSchemaBuilder.buildSchema(Collections.singletonList(column));
+                // CREATE allocates new IDs; all other metadata must survive 
the recursive read/write conversion.
+                Assertions.assertEquals(original.asStruct(), 
TypeUtil.reassignIds(rebuilt, original).asStruct());
+                assertNestedMetadata(rebuilt.findType("Events"), 
IcebergTypeMapping.fromIcebergType(
+                        rebuilt.findType("Events"), binaryFlag, 
timestampFlag));
+            }
+        }
+    }
+
+    @Test
+    public void 
unsupportedNestedLeavesPreserveSupportedSiblingsAndFieldMetadata() {
+        Types.StructType nested = Types.StructType.of(
+                Types.NestedField.required(7, "Values", 
Types.ListType.ofOptional(8,
+                        Types.StructType.of(
+                                Types.NestedField.optional(9, 
"UnsupportedTime", Types.TimeType.get()),
+                                Types.NestedField.required(10, 
"SupportedCount", Types.LongType.get(), "count")))),
+                Types.NestedField.optional(11, "ZonedNanos", 
Types.TimestampNanoType.withZone()),
+                Types.NestedField.required(12, "Payload", 
Types.VariantType.get()),
+                Types.NestedField.optional(13, "Bytes", 
Types.FixedType.ofLength(16)));
+
+        ConnectorType mapped = IcebergTypeMapping.fromIcebergType(nested, 
false, false);
+        assertNestedMetadata(nested, mapped);
+        ConnectorType element = 
mapped.getChildren().get(0).getChildren().get(0);
+        Assertions.assertEquals("UNSUPPORTED", 
element.getChildren().get(0).getTypeName());
+        Assertions.assertEquals("BIGINT", 
element.getChildren().get(1).getTypeName());
+        Assertions.assertEquals("UNSUPPORTED", 
mapped.getChildren().get(1).getTypeName());
+        Assertions.assertEquals("VARIANT_COMPUTE_V2", 
mapped.getChildren().get(2).getTypeName());
+        Assertions.assertEquals(ConnectorType.of("VARBINARY", 16, 0), 
mapped.getChildren().get(3));
+    }
+
+    @Test
+    public void decimalPrecisionAndScaleRoundTripAtEverySupportedBoundary() {
+        for (int precision : new int[] {1, 9, 18, 28, 38}) {
+            for (int scale : new int[] {0, precision / 2, precision}) {
+                Types.DecimalType source = Types.DecimalType.of(precision, 
scale);
+                ConnectorType mapped = 
IcebergTypeMapping.fromIcebergType(source, false, false);
+                Assertions.assertEquals(ConnectorType.of("DECIMALV3", 
precision, scale), mapped);
+                Assertions.assertEquals(source, 
IcebergTypeMapping.toIcebergPrimitive(mapped));
+                Assertions.assertTrue(IcebergTypeMapping.isReadAs(source, 
mapped));
+                Assertions.assertFalse(IcebergTypeMapping.isReadAs(source,
+                        ConnectorType.of("DECIMALV3", precision + 1, scale)));
+            }
+        }
+    }
+
+    private static void assertNestedMetadata(Type source, ConnectorType 
mapped) {
+        if (!source.isNestedType()) {
+            return;
+        }
+        List<Types.NestedField> fields = source.asNestedType().fields();
+        Assertions.assertEquals(fields.size(), mapped.getChildren().size());
+        for (int i = 0; i < fields.size(); i++) {
+            Types.NestedField field = fields.get(i);
+            Assertions.assertEquals(field.fieldId(), 
mapped.getChildFieldId(i), field.toString());
+            Assertions.assertEquals(field.isOptional(), 
mapped.isChildNullable(i), field.toString());
+            if (source.isStructType()) {
+                Assertions.assertEquals(field.name(), 
mapped.getFieldNames().get(i));
+                Assertions.assertEquals(field.doc(), 
mapped.getChildComment(i));
+            }
+            assertNestedMetadata(field.type(), mapped.getChildren().get(i));
+        }
+    }
+}
diff --git 
a/fe/fe-connector/fe-connector-iceberg/src/test/java/org/apache/doris/connector/iceberg/IcebergWriteKnownBugsTest.java
 
b/fe/fe-connector/fe-connector-iceberg/src/test/java/org/apache/doris/connector/iceberg/IcebergWriteKnownBugsTest.java
new file mode 100644
index 00000000000..f18a79a4e95
--- /dev/null
+++ 
b/fe/fe-connector/fe-connector-iceberg/src/test/java/org/apache/doris/connector/iceberg/IcebergWriteKnownBugsTest.java
@@ -0,0 +1,179 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+package org.apache.doris.connector.iceberg;
+
+import org.apache.doris.connector.spi.ConnectorSession;
+import org.apache.doris.connector.spi.handle.WriteOperation;
+import org.apache.doris.thrift.TFileContent;
+import org.apache.doris.thrift.TIcebergCommitData;
+
+import org.apache.iceberg.DataFiles;
+import org.apache.iceberg.PartitionSpec;
+import org.apache.iceberg.Schema;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.catalog.Namespace;
+import org.apache.iceberg.catalog.TableIdentifier;
+import org.apache.iceberg.inmemory.InMemoryCatalog;
+import org.apache.iceberg.types.Types;
+import org.apache.thrift.TSerializer;
+import org.apache.thrift.protocol.TBinaryProtocol;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Disabled;
+import org.junit.jupiter.api.Test;
+
+import java.util.Collections;
+import java.util.Map;
+import java.util.Optional;
+
+public class IcebergWriteKnownBugsTest {
+    private static final TableIdentifier IDENTIFIER = TableIdentifier.of("db", 
"events");
+    private static final String SEED = "s3://bucket/events/seed.parquet";
+    private static final String ADDED = "s3://bucket/events/added.parquet";
+    private static final String DELETED = "s3://bucket/events/deleted.parquet";
+
+    @Test
+    @Disabled("Known issue: DELETE ignores the standard snapshot isolation 
property.")
+    public void deleteHonorsStandardSnapshotIsolationProperty() throws 
Exception {
+        assertStandardSnapshotIsolation(WriteOperation.DELETE, 
"write.delete.isolation-level");
+    }
+
+    @Test
+    @Disabled("Known issue: UPDATE ignores the standard snapshot isolation 
property.")
+    public void updateHonorsStandardSnapshotIsolationProperty() throws 
Exception {
+        assertStandardSnapshotIsolation(WriteOperation.UPDATE, 
"write.update.isolation-level");
+    }
+
+    @Test
+    @Disabled("Known issue: MERGE ignores the standard snapshot isolation 
property.")
+    public void mergeHonorsStandardSnapshotIsolationProperty() throws 
Exception {
+        assertStandardSnapshotIsolation(WriteOperation.MERGE, 
"write.merge.isolation-level");
+    }
+
+    private void assertStandardSnapshotIsolation(WriteOperation operation, 
String property) throws Exception {
+        InMemoryCatalog catalog = catalog();
+        Table table = create(catalog);
+        table.updateProperties().set(property, "snapshot").commit();
+        
table.newAppend().appendFile(DataFiles.builder(table.spec()).withPath(SEED)
+                
.withFileSizeInBytes(1024).withRecordCount(3).build()).commit();
+        IcebergConnectorTransaction transaction = 
transaction(catalog.loadTable(IDENTIFIER));
+        transaction.beginWrite(session(), "db", "events", new 
IcebergWriteContext(
+                operation, false, Collections.emptyMap(), Optional.empty()));
+        
catalog.loadTable(IDENTIFIER).newAppend().appendFile(DataFiles.builder(table.spec()).withPath(ADDED)
+                
.withFileSizeInBytes(1024).withRecordCount(1).build()).commit();
+        transaction.addCommitData(serialize(fragment(DELETED, 1, 
TFileContent.POSITION_DELETES)));
+        // Snapshot isolation permits a concurrent append while still 
validating referenced data files.
+        Assertions.assertDoesNotThrow(transaction::commit);
+    }
+
+    @Test
+    @Disabled("Known issue: mixed INSERT and DELETE MERGE actions undercount 
affected rows.")
+    public void mergeCountsBothInsertedAndDeletedRows() throws Exception {
+        InMemoryCatalog catalog = catalog();
+        Table table = create(catalog);
+        
table.newAppend().appendFile(DataFiles.builder(table.spec()).withPath(SEED)
+                
.withFileSizeInBytes(1024).withRecordCount(3).build()).commit();
+        IcebergConnectorTransaction transaction = 
transaction(catalog.loadTable(IDENTIFIER));
+        transaction.beginWrite(session(), "db", "events", new 
IcebergWriteContext(
+                WriteOperation.MERGE, false, Collections.emptyMap(), 
Optional.empty()));
+        transaction.addCommitData(serialize(fragment(ADDED, 1, 
TFileContent.DATA)));
+        transaction.addCommitData(serialize(fragment(DELETED, 1, 
TFileContent.POSITION_DELETES)));
+        transaction.commit();
+        // These actions target disjoint rows; counting both does not 
double-count an UPDATE pair.
+        Assertions.assertEquals(2, transaction.getUpdateCnt());
+    }
+
+    private static InMemoryCatalog catalog() {
+        InMemoryCatalog catalog = new InMemoryCatalog();
+        catalog.initialize("repro", Collections.emptyMap());
+        catalog.createNamespace(Namespace.of("db"));
+        return catalog;
+    }
+
+    private static Table create(InMemoryCatalog catalog) {
+        return catalog.createTable(IDENTIFIER, new Schema(
+                Types.NestedField.required(1, "id", Types.IntegerType.get())),
+                PartitionSpec.unpartitioned(), 
Collections.singletonMap("format-version", "2"));
+    }
+
+    private static IcebergConnectorTransaction transaction(Table table) {
+        RecordingIcebergCatalogOps operations = new 
RecordingIcebergCatalogOps();
+        operations.table = table;
+        return new IcebergConnectorTransaction(9, operations, new 
RecordingConnectorContext());
+    }
+
+    private static TIcebergCommitData fragment(String path, long count, 
TFileContent content) {
+        TIcebergCommitData fragment = new TIcebergCommitData();
+        fragment.setFilePath(path);
+        fragment.setFileContent(content);
+        fragment.setFileSize(1024);
+        fragment.setRowCount(count);
+        fragment.setAffectedRows(count);
+        if (content == TFileContent.POSITION_DELETES) {
+            fragment.setReferencedDataFilePath(SEED);
+        }
+        return fragment;
+    }
+
+    private static byte[] serialize(TIcebergCommitData fragment) throws 
Exception {
+        return new TSerializer(new 
TBinaryProtocol.Factory()).serialize(fragment);
+    }
+
+    private static ConnectorSession session() {
+        return new ConnectorSession() {
+            @Override
+            public String getQueryId() {
+                return "repro";
+            }
+
+            @Override
+            public String getUser() {
+                return "test";
+            }
+
+            @Override
+            public String getTimeZone() {
+                return "UTC";
+            }
+
+            @Override
+            public String getLocale() {
+                return "en_US";
+            }
+
+            @Override
+            public long getCatalogId() {
+                return 1;
+            }
+
+            @Override
+            public String getCatalogName() {
+                return "repro";
+            }
+
+            @Override
+            public Map<String, String> getCatalogProperties() {
+                return Collections.emptyMap();
+            }
+
+            @Override
+            public <T> T getProperty(String name, Class<T> type) {
+                return null;
+            }
+        };
+    }
+}
diff --git 
a/fe/fe-connector/fe-connector-iceberg/src/test/java/org/apache/doris/connector/iceberg/IcebergWriteTransactionCoverageTest.java
 
b/fe/fe-connector/fe-connector-iceberg/src/test/java/org/apache/doris/connector/iceberg/IcebergWriteTransactionCoverageTest.java
new file mode 100644
index 00000000000..83412d30c0c
--- /dev/null
+++ 
b/fe/fe-connector/fe-connector-iceberg/src/test/java/org/apache/doris/connector/iceberg/IcebergWriteTransactionCoverageTest.java
@@ -0,0 +1,404 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+package org.apache.doris.connector.iceberg;
+
+import org.apache.doris.connector.spi.ConnectorSession;
+import org.apache.doris.connector.spi.DorisConnectorException;
+import org.apache.doris.connector.spi.handle.WriteOperation;
+import org.apache.doris.thrift.TFileContent;
+import org.apache.doris.thrift.TIcebergCommitData;
+
+import org.apache.iceberg.DataFile;
+import org.apache.iceberg.DataFiles;
+import org.apache.iceberg.FileFormat;
+import org.apache.iceberg.FileScanTask;
+import org.apache.iceberg.PartitionSpec;
+import org.apache.iceberg.Schema;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.catalog.Namespace;
+import org.apache.iceberg.catalog.TableIdentifier;
+import org.apache.iceberg.inmemory.InMemoryCatalog;
+import org.apache.iceberg.io.CloseableIterable;
+import org.apache.iceberg.types.Types;
+import org.apache.thrift.TException;
+import org.apache.thrift.TSerializer;
+import org.apache.thrift.protocol.TBinaryProtocol;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+
+public class IcebergWriteTransactionCoverageTest {
+    private static final Schema SCHEMA = new Schema(
+            Types.NestedField.required(1, "id", Types.IntegerType.get()),
+            Types.NestedField.optional(2, "region", Types.StringType.get()));
+    private static final TableIdentifier IDENTIFIER = TableIdentifier.of("db", 
"events");
+    private static final String SEED = "s3://bucket/events/seed.parquet";
+    private static final String ADDED = "s3://bucket/events/added.parquet";
+    private static final String DELETED = "s3://bucket/events/deleted.parquet";
+
+    @Test
+    public void appendV1PreservesHistoricalSnapshot() throws IOException {
+        assertAppendPreservesHistory(1);
+    }
+
+    @Test
+    public void appendV2PreservesHistoricalSnapshot() throws IOException {
+        assertAppendPreservesHistory(2);
+    }
+
+    @Test
+    public void appendV3PreservesHistoricalSnapshot() throws IOException {
+        assertAppendPreservesHistory(3);
+    }
+
+    private void assertAppendPreservesHistory(int formatVersion) throws 
IOException {
+        InMemoryCatalog catalog = catalog();
+        Table table = create(catalog, formatVersion, false);
+        table.newAppend().appendFile(file(table.spec(), SEED, 2, 
null)).commit();
+        long previousSnapshot = table.currentSnapshot().snapshotId();
+
+        IcebergConnectorTransaction transaction = 
transaction(catalog.loadTable(IDENTIFIER));
+        transaction.beginWrite(session(null), "db", "events", 
context(WriteOperation.INSERT));
+        transaction.addCommitData(serialize(data(ADDED, 3)));
+        transaction.commit();
+
+        Table current = catalog.loadTable(IDENTIFIER);
+        Assertions.assertEquals(paths(SEED, ADDED), paths(current, null, 
null));
+        Assertions.assertEquals(paths(SEED), paths(current, null, 
previousSnapshot));
+        Assertions.assertEquals(3, transaction.getUpdateCnt());
+        Assertions.assertEquals("5", 
current.currentSnapshot().summary().get("total-records"));
+    }
+
+    @Test
+    public void dynamicOverwriteReplacesEveryTouchedPartitionOnly() throws 
IOException {
+        InMemoryCatalog catalog = catalog();
+        Table table = create(catalog, 2, true);
+        String west = "s3://bucket/events/west.parquet";
+        String east = "s3://bucket/events/east.parquet";
+        String north = "s3://bucket/events/north.parquet";
+        table.newAppend().appendFile(file(table.spec(), west, 2, "west"))
+                .appendFile(file(table.spec(), east, 3, "east"))
+                .appendFile(file(table.spec(), north, 4, "north")).commit();
+        long previousSnapshot = table.currentSnapshot().snapshotId();
+
+        IcebergConnectorTransaction transaction = 
transaction(catalog.loadTable(IDENTIFIER));
+        transaction.beginWrite(session("dynamic"), "db", "events", 
context(WriteOperation.OVERWRITE));
+        TIcebergCommitData westReplacement = data(ADDED, 1);
+        westReplacement.setPartitionValues(Collections.singletonList("west"));
+        TIcebergCommitData eastReplacement = data(SEED, 2);
+        eastReplacement.setPartitionValues(Collections.singletonList("east"));
+        transaction.addCommitData(serialize(westReplacement));
+        transaction.addCommitData(serialize(eastReplacement));
+        transaction.commit();
+
+        Table current = catalog.loadTable(IDENTIFIER);
+        Assertions.assertEquals(paths(ADDED, SEED, north), paths(current, 
null, null));
+        Assertions.assertEquals(paths(west, east, north), paths(current, null, 
previousSnapshot));
+    }
+
+    @Test
+    public void 
staticPartitionOverwriteOnBranchPreservesMainAndOtherPartitions() throws 
IOException {
+        InMemoryCatalog catalog = catalog();
+        Table table = create(catalog, 2, true);
+        table.newAppend().appendFile(file(table.spec(), SEED, 2, "west"))
+                .appendFile(file(table.spec(), ADDED, 3, "east")).commit();
+        table.manageSnapshots().createBranch("audit", 
table.currentSnapshot().snapshotId()).commit();
+        IcebergWriteSchemaContext schema = IcebergWriteSchemaContext.create(
+                table, table.name(), Optional.of("audit"), false, false);
+        IcebergWriteContext writeContext = new 
IcebergWriteContext(WriteOperation.OVERWRITE, true,
+                Collections.singletonMap("region", "west"), 
Optional.of("audit"), -1L, schema);
+
+        IcebergConnectorTransaction transaction = 
transaction(catalog.loadTable(IDENTIFIER));
+        transaction.beginWrite(session(null), "db", "events", writeContext);
+        TIcebergCommitData replacement = data(DELETED, 1);
+        replacement.setPartitionValues(Collections.singletonList("west"));
+        transaction.addCommitData(serialize(replacement));
+        transaction.commit();
+
+        Table current = catalog.loadTable(IDENTIFIER);
+        Assertions.assertEquals(paths(SEED, ADDED), paths(current, null, 
null));
+        Assertions.assertEquals(paths(DELETED, ADDED), paths(current, "audit", 
null));
+    }
+
+    @Test
+    public void branchAppendDoesNotPublishConcurrentMainFilesToBranch() throws 
IOException {
+        InMemoryCatalog catalog = catalog();
+        Table table = create(catalog, 2, false);
+        table.newAppend().appendFile(file(table.spec(), SEED, 1, 
null)).commit();
+        table.manageSnapshots().createBranch("audit", 
table.currentSnapshot().snapshotId()).commit();
+        IcebergConnectorTransaction transaction = 
transaction(catalog.loadTable(IDENTIFIER));
+        transaction.beginWrite(session(null), "db", "events", new 
IcebergWriteContext(
+                WriteOperation.INSERT, false, Collections.emptyMap(), 
Optional.of("audit")));
+        
catalog.loadTable(IDENTIFIER).newAppend().appendFile(file(table.spec(), ADDED, 
1, null)).commit();
+        transaction.addCommitData(serialize(data(DELETED, 1)));
+        transaction.commit();
+
+        Table current = catalog.loadTable(IDENTIFIER);
+        Assertions.assertEquals(paths(SEED, ADDED), paths(current, null, 
null));
+        Assertions.assertEquals(paths(SEED, DELETED), paths(current, "audit", 
null));
+    }
+
+    @Test
+    public void updatePublishesDataAndDeletesInOneSnapshot() throws 
IOException {
+        InMemoryCatalog catalog = catalog();
+        Table table = create(catalog, 2, false);
+        table.newAppend().appendFile(file(table.spec(), SEED, 3, 
null)).commit();
+        long previousSnapshot = table.currentSnapshot().snapshotId();
+        IcebergConnectorTransaction transaction = 
transaction(catalog.loadTable(IDENTIFIER));
+        transaction.beginWrite(session(null), "db", "events", 
context(WriteOperation.UPDATE));
+        transaction.addCommitData(serialize(data(ADDED, 2)));
+        transaction.addCommitData(serialize(delete(DELETED, 2, SEED)));
+        transaction.commit();
+
+        Table current = catalog.loadTable(IDENTIFIER);
+        Assertions.assertEquals(Long.valueOf(previousSnapshot), 
current.currentSnapshot().parentId());
+        Assertions.assertEquals(paths(SEED, ADDED), paths(current, null, 
null));
+        Assertions.assertEquals(paths(SEED), paths(current, null, 
previousSnapshot));
+        Assertions.assertEquals("1", 
current.currentSnapshot().summary().get("added-data-files"));
+        Assertions.assertEquals("1", 
current.currentSnapshot().summary().get("added-delete-files"));
+        Assertions.assertEquals(2, transaction.getUpdateCnt());
+    }
+
+    @Test
+    public void mergeWithOnlyInsertActionsCommitsWithoutDeleteFiles() throws 
IOException {
+        InMemoryCatalog catalog = catalog();
+        create(catalog, 2, false);
+        IcebergConnectorTransaction transaction = 
transaction(catalog.loadTable(IDENTIFIER));
+        transaction.beginWrite(session(null), "db", "events", 
context(WriteOperation.MERGE));
+        transaction.addCommitData(serialize(data(ADDED, 3)));
+        transaction.commit();
+
+        Table current = catalog.loadTable(IDENTIFIER);
+        Assertions.assertEquals(paths(ADDED), paths(current, null, null));
+        Assertions.assertEquals("0", 
current.currentSnapshot().summary().get("total-delete-files"));
+        Assertions.assertEquals(3, transaction.getUpdateCnt());
+    }
+
+    @Test
+    public void mergeWithOnlyDeleteActionsPreservesOriginalDataFile() throws 
IOException {
+        InMemoryCatalog catalog = catalog();
+        Table table = create(catalog, 2, false);
+        table.newAppend().appendFile(file(table.spec(), SEED, 3, 
null)).commit();
+        IcebergConnectorTransaction transaction = 
transaction(catalog.loadTable(IDENTIFIER));
+        transaction.beginWrite(session(null), "db", "events", 
context(WriteOperation.MERGE));
+        transaction.addCommitData(serialize(delete(DELETED, 2, SEED)));
+        transaction.commit();
+
+        Table current = catalog.loadTable(IDENTIFIER);
+        Assertions.assertEquals(paths(SEED), paths(current, null, null));
+        Assertions.assertEquals("1", 
current.currentSnapshot().summary().get("total-delete-files"));
+        Assertions.assertEquals("1", 
current.currentSnapshot().summary().get("total-data-files"));
+        Assertions.assertEquals(2, transaction.getUpdateCnt());
+    }
+
+    @Test
+    public void updateV1IsRejectedBeforeAnySnapshotIsPublished() {
+        InMemoryCatalog catalog = catalog();
+        Table table = create(catalog, 1, false);
+        IcebergConnectorTransaction transaction = transaction(table);
+        DorisConnectorException failure = 
Assertions.assertThrows(DorisConnectorException.class,
+                () -> transaction.beginWrite(session(null), "db", "events", 
context(WriteOperation.UPDATE)));
+        Assertions.assertTrue(failure.getMessage().contains("format version 2 
or higher"));
+        Assertions.assertNull(catalog.loadTable(IDENTIFIER).currentSnapshot());
+    }
+
+    @Test
+    public void malformedDeleteSpecLeavesPublishedSnapshotUntouched() {
+        InMemoryCatalog catalog = catalog();
+        Table table = create(catalog, 2, true);
+        table.newAppend().appendFile(file(table.spec(), SEED, 3, 
"west")).commit();
+        long previousSnapshot = table.currentSnapshot().snapshotId();
+        IcebergConnectorTransaction transaction = 
transaction(catalog.loadTable(IDENTIFIER));
+        transaction.beginWrite(session(null), "db", "events", 
context(WriteOperation.DELETE));
+        TIcebergCommitData fragment = delete(DELETED, 1, SEED);
+        fragment.setPartitionSpecId(99);
+        fragment.setPartitionValues(Collections.singletonList("west"));
+        transaction.addCommitData(serialize(fragment));
+
+        DorisConnectorException failure = 
Assertions.assertThrows(DorisConnectorException.class, transaction::commit);
+        Assertions.assertTrue(failure.getMessage().contains("Unknown partition 
spec id 99"));
+        Assertions.assertEquals(previousSnapshot, 
catalog.loadTable(IDENTIFIER).currentSnapshot().snapshotId());
+    }
+
+    @Test
+    public void beginCanRetryAfterCatalogFailureWithoutKeepingPartialState() 
throws IOException {
+        InMemoryCatalog catalog = catalog();
+        Table table = create(catalog, 2, false);
+        RecordingIcebergCatalogOps operations = new 
RecordingIcebergCatalogOps();
+        operations.table = table;
+        operations.throwOnLoadTable = true;
+        IcebergConnectorTransaction transaction = new 
IcebergConnectorTransaction(
+                7, operations, new RecordingConnectorContext());
+        Assertions.assertThrows(DorisConnectorException.class,
+                () -> transaction.beginWrite(session(null), "db", "events", 
context(WriteOperation.INSERT)));
+        Assertions.assertNull(transaction.getTransaction());
+        Assertions.assertNull(transaction.getTable());
+        operations.throwOnLoadTable = false;
+        transaction.beginWrite(session(null), "db", "events", 
context(WriteOperation.INSERT));
+        transaction.addCommitData(serialize(data(ADDED, 1)));
+        transaction.commit();
+        Assertions.assertEquals(paths(ADDED), 
paths(catalog.loadTable(IDENTIFIER), null, null));
+    }
+
+    @Test
+    public void closeBeforeBeginPreventsCatalogAccess() {
+        RecordingIcebergCatalogOps operations = new 
RecordingIcebergCatalogOps();
+        IcebergConnectorTransaction transaction = new 
IcebergConnectorTransaction(
+                7, operations, new RecordingConnectorContext());
+        transaction.close();
+        Assertions.assertThrows(DorisConnectorException.class,
+                () -> transaction.beginWrite(session(null), "db", "events", 
context(WriteOperation.INSERT)));
+        Assertions.assertTrue(operations.log.isEmpty());
+    }
+
+    private static InMemoryCatalog catalog() {
+        InMemoryCatalog catalog = new InMemoryCatalog();
+        catalog.initialize("coverage", Collections.emptyMap());
+        catalog.createNamespace(Namespace.of("db"));
+        return catalog;
+    }
+
+    private static Table create(InMemoryCatalog catalog, int version, boolean 
partitioned) {
+        PartitionSpec spec = partitioned
+                ? PartitionSpec.builderFor(SCHEMA).identity("region").build() 
: PartitionSpec.unpartitioned();
+        return catalog.createTable(IDENTIFIER, SCHEMA, spec,
+                Collections.singletonMap("format-version", 
Integer.toString(version)));
+    }
+
+    private static DataFile file(PartitionSpec spec, String path, long rows, 
String region) {
+        DataFiles.Builder builder = 
DataFiles.builder(spec).withPath(path).withFormat(FileFormat.PARQUET)
+                .withFileSizeInBytes(1024).withRecordCount(rows);
+        if (region != null) {
+            builder.withPartitionPath("region=" + region);
+        }
+        return builder.build();
+    }
+
+    private static Set<String> paths(String... paths) {
+        return new HashSet<>(Arrays.asList(paths));
+    }
+
+    private static Set<String> paths(Table table, String branch, Long 
snapshot) throws IOException {
+        org.apache.iceberg.TableScan scan = table.newScan();
+        if (branch != null) {
+            scan = scan.useRef(branch);
+        }
+        if (snapshot != null) {
+            scan = scan.useSnapshot(snapshot);
+        }
+        Set<String> paths = new HashSet<>();
+        try (CloseableIterable<FileScanTask> tasks = scan.planFiles()) {
+            for (FileScanTask task : tasks) {
+                paths.add(task.file().path().toString());
+            }
+        }
+        return paths;
+    }
+
+    private static IcebergConnectorTransaction transaction(Table table) {
+        RecordingIcebergCatalogOps operations = new 
RecordingIcebergCatalogOps();
+        operations.table = table;
+        return new IcebergConnectorTransaction(7, operations, new 
RecordingConnectorContext());
+    }
+
+    private static IcebergWriteContext context(WriteOperation operation) {
+        return new IcebergWriteContext(operation, operation == 
WriteOperation.OVERWRITE,
+                Collections.emptyMap(), Optional.empty());
+    }
+
+    private static TIcebergCommitData data(String path, long rows) {
+        TIcebergCommitData fragment = new TIcebergCommitData();
+        fragment.setFilePath(path);
+        fragment.setFileSize(1024);
+        fragment.setRowCount(rows);
+        fragment.setAffectedRows(rows);
+        fragment.setFileContent(TFileContent.DATA);
+        return fragment;
+    }
+
+    private static TIcebergCommitData delete(String path, long rows, String 
referencedDataFile) {
+        TIcebergCommitData fragment = data(path, rows);
+        fragment.setFileContent(TFileContent.POSITION_DELETES);
+        fragment.setReferencedDataFilePath(referencedDataFile);
+        return fragment;
+    }
+
+    private static byte[] serialize(TIcebergCommitData fragment) {
+        try {
+            return new TSerializer(new 
TBinaryProtocol.Factory()).serialize(fragment);
+        } catch (TException exception) {
+            throw new AssertionError(exception);
+        }
+    }
+
+    private static ConnectorSession session(String overwriteMode) {
+        Map<String, String> properties = new HashMap<>();
+        if (overwriteMode != null) {
+            properties.put("iceberg_partition_overwrite_mode", overwriteMode);
+        }
+        return new ConnectorSession() {
+            @Override
+            public String getQueryId() {
+                return "coverage";
+            }
+
+            @Override
+            public String getUser() {
+                return "test";
+            }
+
+            @Override
+            public String getTimeZone() {
+                return "UTC";
+            }
+
+            @Override
+            public String getLocale() {
+                return "en_US";
+            }
+
+            @Override
+            public long getCatalogId() {
+                return 1;
+            }
+
+            @Override
+            public String getCatalogName() {
+                return "coverage";
+            }
+
+            @Override
+            public Map<String, String> getCatalogProperties() {
+                return Collections.emptyMap();
+            }
+
+            @Override
+            public <T> T getProperty(String name, Class<T> type) {
+                return type.cast(properties.get(name));
+            }
+        };
+    }
+}
diff --git 
a/fe/fe-connector/fe-connector-iceberg/src/test/java/org/apache/doris/connector/iceberg/action/IcebergSnapshotMaintenanceCoverageTest.java
 
b/fe/fe-connector/fe-connector-iceberg/src/test/java/org/apache/doris/connector/iceberg/action/IcebergSnapshotMaintenanceCoverageTest.java
new file mode 100644
index 00000000000..0c3323bd4d0
--- /dev/null
+++ 
b/fe/fe-connector/fe-connector-iceberg/src/test/java/org/apache/doris/connector/iceberg/action/IcebergSnapshotMaintenanceCoverageTest.java
@@ -0,0 +1,193 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+package org.apache.doris.connector.iceberg.action;
+
+import org.apache.doris.connector.spi.DorisConnectorException;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Iterables;
+import org.apache.iceberg.DeleteFile;
+import org.apache.iceberg.FileFormat;
+import org.apache.iceberg.FileMetadata;
+import org.apache.iceberg.PartitionSpec;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.inmemory.InMemoryCatalog;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Disabled;
+import org.junit.jupiter.api.Test;
+
+import java.util.Collections;
+import java.util.List;
+
+public class IcebergSnapshotMaintenanceCoverageTest {
+
+    private static IcebergExpireSnapshotsAction expiration(String key, String 
value) {
+        IcebergExpireSnapshotsAction action = new IcebergExpireSnapshotsAction(
+                ImmutableMap.of("older_than", "4102444800000", key, value), 
Collections.emptyList(), null);
+        action.validate();
+        return action;
+    }
+
+    @Test
+    public void retainLastPreservesSnapshotsReferencedByTagsAndBranches() {
+        InMemoryCatalog catalog = ActionTestTables.freshCatalog();
+        Table table = ActionTestTables.createTable(catalog, "retained_refs");
+        long tagged = ActionTestTables.appendSnapshot(catalog, 
"retained_refs", "tagged.parquet", 1);
+        table.refresh();
+        table.manageSnapshots().createTag("audit_tag", tagged).commit();
+        long branched = ActionTestTables.appendSnapshot(catalog, 
"retained_refs", "branched.parquet", 1);
+        table.refresh();
+        table.manageSnapshots().createBranch("audit_branch", 
branched).commit();
+        long unreferenced = ActionTestTables.appendSnapshot(catalog, 
"retained_refs", "expired.parquet", 1);
+        long current = ActionTestTables.appendSnapshot(catalog, 
"retained_refs", "current.parquet", 1);
+        table.refresh();
+
+        expiration("retain_last", "1").execute(table, 
ActionTestTables.session("UTC"));
+        table.refresh();
+
+        // Retention is evaluated for each live ref; a main-branch cutoff must 
not erase pinned history.
+        Assertions.assertNotNull(table.snapshot(tagged));
+        Assertions.assertNotNull(table.snapshot(branched));
+        Assertions.assertNull(table.snapshot(unreferenced));
+        Assertions.assertEquals(current, table.currentSnapshot().snapshotId());
+        Assertions.assertEquals(tagged, 
table.refs().get("audit_tag").snapshotId());
+        Assertions.assertEquals(branched, 
table.refs().get("audit_branch").snapshotId());
+    }
+
+    @Test
+    public void 
explicitExpirationOfTaggedSnapshotFailsWithoutChangingHistory() {
+        InMemoryCatalog catalog = ActionTestTables.freshCatalog();
+        Table table = ActionTestTables.createTable(catalog, "protected_tag");
+        long tagged = ActionTestTables.appendSnapshot(catalog, 
"protected_tag", "tagged.parquet", 1);
+        table.refresh();
+        table.manageSnapshots().createTag("audit_tag", tagged).commit();
+        long current = ActionTestTables.appendSnapshot(catalog, 
"protected_tag", "current.parquet", 1);
+        table.refresh();
+
+        Assertions.assertThrows(DorisConnectorException.class,
+                () -> expiration("snapshot_ids", String.valueOf(tagged))
+                        .execute(table, ActionTestTables.session("UTC")));
+        table.refresh();
+        Assertions.assertNotNull(table.snapshot(tagged));
+        Assertions.assertEquals(current, table.currentSnapshot().snapshotId());
+        Assertions.assertEquals(tagged, 
table.refs().get("audit_tag").snapshotId());
+    }
+
+    @Test
+    public void fastForwardRejectsDivergedBranchesWithoutMovingEitherRef() {
+        InMemoryCatalog catalog = ActionTestTables.freshCatalog();
+        Table table = ActionTestTables.createTable(catalog, "diverged_refs");
+        long base = ActionTestTables.appendSnapshot(catalog, "diverged_refs", 
"base.parquet", 1);
+        table.refresh();
+        table.manageSnapshots().createBranch("audit_branch", base).commit();
+        long main = ActionTestTables.appendSnapshot(catalog, "diverged_refs", 
"main.parquet", 1);
+        table.refresh();
+        
table.newAppend().appendFile(ActionTestTables.dataFile("branch.parquet", 1))
+                .toBranch("audit_branch").commit();
+        long branch = table.snapshot("audit_branch").snapshotId();
+        IcebergFastForwardAction action = new IcebergFastForwardAction(
+                ImmutableMap.of("branch", "main", "to", "audit_branch"), 
Collections.emptyList(), null);
+        action.validate();
+
+        Assertions.assertThrows(DorisConnectorException.class,
+                () -> action.execute(table, ActionTestTables.session("UTC")));
+        table.refresh();
+        Assertions.assertEquals(main, table.currentSnapshot().snapshotId());
+        Assertions.assertEquals(branch, 
table.snapshot("audit_branch").snapshotId());
+    }
+
+    @Test
+    public void rollbackRejectsSnapshotOutsideTheCurrentAncestry() {
+        InMemoryCatalog catalog = ActionTestTables.freshCatalog();
+        Table table = ActionTestTables.createTable(catalog, 
"rollback_ancestry");
+        long base = ActionTestTables.appendSnapshot(catalog, 
"rollback_ancestry", "base.parquet", 1);
+        table.refresh();
+        table.manageSnapshots().createBranch("audit_branch", base).commit();
+        
table.newAppend().appendFile(ActionTestTables.dataFile("branch.parquet", 1))
+                .toBranch("audit_branch").commit();
+        long branch = table.snapshot("audit_branch").snapshotId();
+        long current = ActionTestTables.appendSnapshot(catalog, 
"rollback_ancestry", "main.parquet", 1);
+        table.refresh();
+        IcebergRollbackToSnapshotAction action = new 
IcebergRollbackToSnapshotAction(
+                ImmutableMap.of("snapshot_id", String.valueOf(branch)), 
Collections.emptyList(), null);
+        action.validate();
+
+        Assertions.assertThrows(DorisConnectorException.class,
+                () -> action.execute(table, ActionTestTables.session("UTC")));
+        table.refresh();
+        Assertions.assertEquals(current, table.currentSnapshot().snapshotId());
+        Assertions.assertEquals(branch, 
table.snapshot("audit_branch").snapshotId());
+    }
+
+    @Test
+    @Disabled("Known issue: stale expiration metadata misclassifies newly 
committed position delete files.")
+    public void 
expirationCountsPositionDeletesCommittedAfterTheTableWasLoaded() {
+        assertLateDeleteClassification(true, true);
+    }
+
+    @Test
+    @Disabled("Known issue: stale expiration metadata misclassifies newly 
committed equality delete files.")
+    public void 
expirationCountsEqualityDeletesCommittedAfterTheTableWasLoaded() {
+        assertLateDeleteClassification(false, true);
+    }
+
+    @Test
+    public void expirationCountsPositionDeletesFromAFreshTable() {
+        assertLateDeleteClassification(true, false);
+    }
+
+    @Test
+    public void expirationCountsEqualityDeletesFromAFreshTable() {
+        assertLateDeleteClassification(false, false);
+    }
+
+    private static void assertLateDeleteClassification(boolean positionDelete, 
boolean staleTable) {
+        InMemoryCatalog catalog = ActionTestTables.freshCatalog();
+        ActionTestTables.createTable(catalog, "late_delete");
+        ActionTestTables.appendSnapshot(catalog, "late_delete", 
"base.parquet", 2);
+        Table loadedBeforeDelete = 
catalog.loadTable(ActionTestTables.id("late_delete"));
+        FileMetadata.Builder builder = 
FileMetadata.deleteFileBuilder(PartitionSpec.unpartitioned());
+        if (positionDelete) {
+            
builder.ofPositionDeletes().withReferencedDataFile("s3://b/db1/base.parquet");
+        } else {
+            builder.ofEqualityDeletes(1);
+        }
+        DeleteFile delete = builder.withPath("s3://b/db1/late-delete.parquet")
+                
.withFormat(FileFormat.PARQUET).withFileSizeInBytes(64).withRecordCount(1).build();
+        Table writer = catalog.loadTable(ActionTestTables.id("late_delete"));
+        writer.newRowDelta().addDeletes(delete).commit();
+        writer.newRowDelta().removeDeletes(delete).commit();
+        ActionTestTables.appendSnapshot(catalog, "late_delete", 
"final.parquet", 1);
+        if (!staleTable) {
+            loadedBeforeDelete.refresh();
+        }
+
+        // SDK expiration refreshes metadata. Its counters must classify 
delete files from that same generation.
+        List<String> row = expiration("retain_last", "1")
+                .execute(loadedBeforeDelete, 
ActionTestTables.session("UTC")).getRows().get(0);
+
+        Assertions.assertEquals(1, 
Iterables.size(catalog.loadTable(ActionTestTables.id("late_delete")).snapshots()),
+                "the append and delete-removal history must actually expire");
+        Assertions.assertEquals("0", row.get(0),
+                "data files remain reachable; stale=" + staleTable + ", 
counters=" + row);
+        Assertions.assertEquals(positionDelete ? "1" : "0", row.get(1),
+                "position delete classification; stale=" + staleTable + ", 
counters=" + row);
+        Assertions.assertEquals(positionDelete ? "0" : "1", row.get(2),
+                "equality delete classification; stale=" + staleTable + ", 
counters=" + row);
+    }
+}
diff --git 
a/regression-test/suites/external_table_p0/iceberg/ICEBERG_CAPABILITY_TEST_COVERAGE.md
 
b/regression-test/suites/external_table_p0/iceberg/ICEBERG_CAPABILITY_TEST_COVERAGE.md
new file mode 100644
index 00000000000..15c952f8a8d
--- /dev/null
+++ 
b/regression-test/suites/external_table_p0/iceberg/ICEBERG_CAPABILITY_TEST_COVERAGE.md
@@ -0,0 +1,160 @@
+# Iceberg capability and test coverage inventory
+
+This inventory maps the [published capability 
matrix](https://doris.apache.org/docs/dev/lakehouse/best-practices/doris-iceberg-capability-matrix/)
+and [catalog 
guide](https://doris.apache.org/docs/dev/lakehouse/catalogs/iceberg-catalog/)
+to tests in this repository. Test presence does not imply that every backend, 
format, version,
+scanner, and deployment combination has been executed.
+
+## Scope
+
+- Iceberg format V1/V2/V3, the bundled Iceberg SDK version, and Doris release 
versions are separate dimensions.
+- Iceberg has no Doris Duplicate/Unique/Aggregate target model. Those are 
INSERT SELECT source
+  models; see `write/test_iceberg_write_source_models.groovy`. Iceberg target 
dimensions include
+  format version, partition/sort specs, delete mode, data format, and branch.
+- A recording fake checks routing and errors; it does not validate a live 
catalog or filesystem.
+- `Y`/`N` below transcribe the published matrix in REST/HMS/Hadoop/Glue/DLF/S3 
Tables/JDBC order.
+  They are documentation input, not new executable promises. Current 
implementation takes precedence.
+- The published matrix lags code for table-property mutation and 
`remove_orphan_files`, which
+  have implementation and tests despite all-N rows. The catalog guide's 
backend summary differs
+  from the detailed matrix too. Documentation reconciliation remains necessary.
+
+## Published operations
+
+`Partial` means representative tests exist, with unvalidated combinations 
remaining.
+`Negative gap` means the published operation is unsupported and exhaustive 
rejection coverage
+is absent. Evidence on those rows is related routing/contract coverage, not 
successful execution.
+
+| Category | Operation | Published backend support | Assessment | Unit 
evidence | SQL evidence |
+| --- | --- | --- | --- | --- | --- |
+| Namespace / Database | `LIST_NAMESPACES` | Y/Y/Y/Y/Y/Y/Y | Partial | 
[CatalogBackedIcebergCatalogOpsDdlTest.java](../../../../fe/fe-connector/fe-connector-iceberg/src/test/java/org/apache/doris/connector/iceberg/CatalogBackedIcebergCatalogOpsDdlTest.java)
 | [test_iceberg_drop_db.groovy](test_iceberg_drop_db.groovy) |
+| Namespace / Database | `LOAD_NAMESPACE` | Y/Y/Y/Y/Y/Y/Y | Partial | 
[CatalogBackedIcebergCatalogOpsDdlTest.java](../../../../fe/fe-connector/fe-connector-iceberg/src/test/java/org/apache/doris/connector/iceberg/CatalogBackedIcebergCatalogOpsDdlTest.java)
 | [test_iceberg_drop_db.groovy](test_iceberg_drop_db.groovy) |
+| Namespace / Database | `NAMESPACE_EXISTS` | Y/Y/Y/Y/Y/Y/Y | Partial | 
[CatalogBackedIcebergCatalogOpsDdlTest.java](../../../../fe/fe-connector/fe-connector-iceberg/src/test/java/org/apache/doris/connector/iceberg/CatalogBackedIcebergCatalogOpsDdlTest.java)
 | [test_iceberg_drop_db.groovy](test_iceberg_drop_db.groovy) |
+| Namespace / Database | `CREATE_NAMESPACE` | Y/Y/Y/Y/N/Y/Y | Partial | 
[CatalogBackedIcebergCatalogOpsDdlTest.java](../../../../fe/fe-connector/fe-connector-iceberg/src/test/java/org/apache/doris/connector/iceberg/CatalogBackedIcebergCatalogOpsDdlTest.java)
 | [test_iceberg_drop_db.groovy](test_iceberg_drop_db.groovy) |
+| Namespace / Database | `ALTER_NAMESPACE_PROPERTIES` | N/N/N/N/N/N/N | 
Negative gap | 
[CatalogBackedIcebergCatalogOpsDdlTest.java](../../../../fe/fe-connector/fe-connector-iceberg/src/test/java/org/apache/doris/connector/iceberg/CatalogBackedIcebergCatalogOpsDdlTest.java)
 | [test_iceberg_drop_db.groovy](test_iceberg_drop_db.groovy) |
+| Namespace / Database | `DROP_NAMESPACE` | Y/Y/Y/Y/N/Y/Y | Partial | 
[CatalogBackedIcebergCatalogOpsDdlTest.java](../../../../fe/fe-connector/fe-connector-iceberg/src/test/java/org/apache/doris/connector/iceberg/CatalogBackedIcebergCatalogOpsDdlTest.java)
 | [test_iceberg_drop_db.groovy](test_iceberg_drop_db.groovy) |
+| Namespace / Database | `DROP_NAMESPACE_CASCADE` | Y/Y/Y/Y/N/Y/Y | Partial | 
[CatalogBackedIcebergCatalogOpsDdlTest.java](../../../../fe/fe-connector/fe-connector-iceberg/src/test/java/org/apache/doris/connector/iceberg/CatalogBackedIcebergCatalogOpsDdlTest.java)
 | [test_iceberg_drop_db.groovy](test_iceberg_drop_db.groovy) |
+| Namespace / Database | `MULTILEVEL_NAMESPACE` | Y/N/N/N/N/N/N | Partial | 
[CatalogBackedIcebergCatalogOpsDdlTest.java](../../../../fe/fe-connector/fe-connector-iceberg/src/test/java/org/apache/doris/connector/iceberg/CatalogBackedIcebergCatalogOpsDdlTest.java)
 | [test_iceberg_drop_db.groovy](test_iceberg_drop_db.groovy) |
+| Table lifecycle | `LIST_TABLES` | Y/Y/Y/Y/Y/Y/Y | Partial | 
[CatalogBackedIcebergCatalogOpsTest.java](../../../../fe/fe-connector/fe-connector-iceberg/src/test/java/org/apache/doris/connector/iceberg/CatalogBackedIcebergCatalogOpsTest.java)
 | 
[write/test_iceberg_create_table.groovy](write/test_iceberg_create_table.groovy)
 |
+| Table lifecycle | `LOAD_TABLE` | Y/Y/Y/Y/Y/Y/Y | Partial | 
[CatalogBackedIcebergCatalogOpsTest.java](../../../../fe/fe-connector/fe-connector-iceberg/src/test/java/org/apache/doris/connector/iceberg/CatalogBackedIcebergCatalogOpsTest.java)
 | 
[write/test_iceberg_create_table.groovy](write/test_iceberg_create_table.groovy)
 |
+| Table lifecycle | `TABLE_EXISTS` | Y/Y/Y/Y/Y/Y/Y | Partial | 
[CatalogBackedIcebergCatalogOpsTest.java](../../../../fe/fe-connector/fe-connector-iceberg/src/test/java/org/apache/doris/connector/iceberg/CatalogBackedIcebergCatalogOpsTest.java)
 | 
[write/test_iceberg_create_table.groovy](write/test_iceberg_create_table.groovy)
 |
+| Table lifecycle | `CREATE_TABLE` | Y/Y/Y/Y/N/Y/Y | Partial | 
[CatalogBackedIcebergCatalogOpsDdlTest.java](../../../../fe/fe-connector/fe-connector-iceberg/src/test/java/org/apache/doris/connector/iceberg/CatalogBackedIcebergCatalogOpsDdlTest.java)
 | 
[write/test_iceberg_create_table.groovy](write/test_iceberg_create_table.groovy)
 |
+| Table lifecycle | `CREATE_TABLE_WITH_LOCATION` | N/N/N/N/N/N/N | Negative 
gap | 
[CatalogBackedIcebergCatalogOpsDdlTest.java](../../../../fe/fe-connector/fe-connector-iceberg/src/test/java/org/apache/doris/connector/iceberg/CatalogBackedIcebergCatalogOpsDdlTest.java)
 | 
[write/test_iceberg_create_table.groovy](write/test_iceberg_create_table.groovy)
 |
+| Table lifecycle | `CTAS` | Y/Y/Y/Y/N/Y/Y | Partial | 
[IcebergCreateTableValidationTest.java](../../../../fe/fe-connector/fe-connector-iceberg/src/test/java/org/apache/doris/connector/iceberg/IcebergCreateTableValidationTest.java)
 | 
[write/test_iceberg_write_ctas_format_boundary.groovy](write/test_iceberg_write_ctas_format_boundary.groovy)
 |
+| Table lifecycle | `ATOMIC_CTAS` | N/N/N/N/N/N/N | Negative gap | 
[CatalogBackedIcebergCatalogOpsDdlTest.java](../../../../fe/fe-connector/fe-connector-iceberg/src/test/java/org/apache/doris/connector/iceberg/CatalogBackedIcebergCatalogOpsDdlTest.java)
 | 
[write/test_iceberg_create_table.groovy](write/test_iceberg_create_table.groovy)
 |
+| Table lifecycle | `REGISTER_TABLE` | N/N/N/N/N/N/N | Negative gap | 
[CatalogBackedIcebergCatalogOpsDdlTest.java](../../../../fe/fe-connector/fe-connector-iceberg/src/test/java/org/apache/doris/connector/iceberg/CatalogBackedIcebergCatalogOpsDdlTest.java)
 | 
[write/test_iceberg_create_table.groovy](write/test_iceberg_create_table.groovy)
 |
+| Table lifecycle | `REPLACE_TABLE` | N/N/N/N/N/N/N | Negative gap | 
[CatalogBackedIcebergCatalogOpsDdlTest.java](../../../../fe/fe-connector/fe-connector-iceberg/src/test/java/org/apache/doris/connector/iceberg/CatalogBackedIcebergCatalogOpsDdlTest.java)
 | 
[write/test_iceberg_create_table.groovy](write/test_iceberg_create_table.groovy)
 |
+| Table lifecycle | `DROP_TABLE` | Y/Y/Y/Y/N/Y/Y | Partial | 
[CatalogBackedIcebergCatalogOpsDdlTest.java](../../../../fe/fe-connector/fe-connector-iceberg/src/test/java/org/apache/doris/connector/iceberg/CatalogBackedIcebergCatalogOpsDdlTest.java)
 | 
[write/test_iceberg_create_table.groovy](write/test_iceberg_create_table.groovy)
 |
+| Table lifecycle | `PURGE_TABLE` | Y/Y/Y/Y/N/Y/Y | Partial | 
[CatalogBackedIcebergCatalogOpsDdlTest.java](../../../../fe/fe-connector/fe-connector-iceberg/src/test/java/org/apache/doris/connector/iceberg/CatalogBackedIcebergCatalogOpsDdlTest.java)
 | 
[write/test_iceberg_create_table.groovy](write/test_iceberg_create_table.groovy)
 |
+| Table lifecycle | `RENAME_TABLE` | Y/Y/N/Y/N/Y/Y | Partial | 
[CatalogBackedIcebergCatalogOpsDdlTest.java](../../../../fe/fe-connector/fe-connector-iceberg/src/test/java/org/apache/doris/connector/iceberg/CatalogBackedIcebergCatalogOpsDdlTest.java)
 | 
[write/test_iceberg_create_table.groovy](write/test_iceberg_create_table.groovy)
 |
+| Table lifecycle | `SET_TABLE_LOCATION` | N/N/N/N/N/N/N | Negative gap | 
[CatalogBackedIcebergCatalogOpsDdlTest.java](../../../../fe/fe-connector/fe-connector-iceberg/src/test/java/org/apache/doris/connector/iceberg/CatalogBackedIcebergCatalogOpsDdlTest.java)
 | 
[write/test_iceberg_create_table.groovy](write/test_iceberg_create_table.groovy)
 |
+| Schema and layout evolution | `ADD_COLUMN` | Y/Y/Y/Y/Y/Y/Y | Partial | 
[CatalogBackedIcebergCatalogOpsColumnEvolutionTest.java](../../../../fe/fe-connector/fe-connector-iceberg/src/test/java/org/apache/doris/connector/iceberg/CatalogBackedIcebergCatalogOpsColumnEvolutionTest.java)
 | 
[test_iceberg_schema_ref_actions_matrix.groovy](test_iceberg_schema_ref_actions_matrix.groovy)
 |
+| Schema and layout evolution | `DROP_COLUMN` | Y/Y/Y/Y/Y/Y/Y | Partial | 
[CatalogBackedIcebergCatalogOpsColumnEvolutionTest.java](../../../../fe/fe-connector/fe-connector-iceberg/src/test/java/org/apache/doris/connector/iceberg/CatalogBackedIcebergCatalogOpsColumnEvolutionTest.java)
 | 
[test_iceberg_schema_ref_actions_matrix.groovy](test_iceberg_schema_ref_actions_matrix.groovy)
 |
+| Schema and layout evolution | `RENAME_COLUMN` | Y/Y/Y/Y/Y/Y/Y | Partial | 
[CatalogBackedIcebergCatalogOpsColumnEvolutionTest.java](../../../../fe/fe-connector/fe-connector-iceberg/src/test/java/org/apache/doris/connector/iceberg/CatalogBackedIcebergCatalogOpsColumnEvolutionTest.java)
 | 
[test_iceberg_schema_ref_actions_matrix.groovy](test_iceberg_schema_ref_actions_matrix.groovy)
 |
+| Schema and layout evolution | `ALTER_COLUMN_TYPE` | Y/Y/Y/Y/Y/Y/Y | Partial 
| 
[CatalogBackedIcebergCatalogOpsColumnEvolutionTest.java](../../../../fe/fe-connector/fe-connector-iceberg/src/test/java/org/apache/doris/connector/iceberg/CatalogBackedIcebergCatalogOpsColumnEvolutionTest.java)
 | 
[test_iceberg_schema_ref_actions_matrix.groovy](test_iceberg_schema_ref_actions_matrix.groovy)
 |
+| Schema and layout evolution | `ALTER_COLUMN_NULLABILITY` | Y/Y/Y/Y/Y/Y/Y | 
Partial | 
[CatalogBackedIcebergCatalogOpsColumnEvolutionTest.java](../../../../fe/fe-connector/fe-connector-iceberg/src/test/java/org/apache/doris/connector/iceberg/CatalogBackedIcebergCatalogOpsColumnEvolutionTest.java)
 | 
[test_iceberg_schema_ref_actions_matrix.groovy](test_iceberg_schema_ref_actions_matrix.groovy)
 |
+| Schema and layout evolution | `SET_TABLE_PROPERTIES` | N/N/N/N/N/N/N | 
Documentation differs | 
[CatalogBackedIcebergCatalogOpsDdlTest.java](../../../../fe/fe-connector/fe-connector-iceberg/src/test/java/org/apache/doris/connector/iceberg/CatalogBackedIcebergCatalogOpsDdlTest.java)
 | [test_iceberg_alter_properties.groovy](test_iceberg_alter_properties.groovy) 
|
+| Schema and layout evolution | `UNSET_TABLE_PROPERTIES` | N/N/N/N/N/N/N | 
Negative gap | 
[CatalogBackedIcebergCatalogOpsColumnEvolutionTest.java](../../../../fe/fe-connector/fe-connector-iceberg/src/test/java/org/apache/doris/connector/iceberg/CatalogBackedIcebergCatalogOpsColumnEvolutionTest.java)
 | 
[test_iceberg_schema_ref_actions_matrix.groovy](test_iceberg_schema_ref_actions_matrix.groovy)
 |
+| Schema and layout evolution | `EVOLVE_PARTITION_SPEC` | Y/Y/Y/Y/Y/Y/Y | 
Partial | 
[CatalogBackedIcebergCatalogOpsColumnEvolutionTest.java](../../../../fe/fe-connector/fe-connector-iceberg/src/test/java/org/apache/doris/connector/iceberg/CatalogBackedIcebergCatalogOpsColumnEvolutionTest.java)
 | 
[test_iceberg_schema_ref_actions_matrix.groovy](test_iceberg_schema_ref_actions_matrix.groovy)
 |
+| Schema and layout evolution | `EVOLVE_SORT_ORDER` | N/N/N/N/N/N/N | Negative 
gap | 
[CatalogBackedIcebergCatalogOpsColumnEvolutionTest.java](../../../../fe/fe-connector/fe-connector-iceberg/src/test/java/org/apache/doris/connector/iceberg/CatalogBackedIcebergCatalogOpsColumnEvolutionTest.java)
 | 
[test_iceberg_schema_ref_actions_matrix.groovy](test_iceberg_schema_ref_actions_matrix.groovy)
 |
+| View | `LIST_VIEWS` | Y/Y/N/N/N/N/Y | Partial | 
[IcebergConnectorMetadataTest.java](../../../../fe/fe-connector/fe-connector-iceberg/src/test/java/org/apache/doris/connector/iceberg/IcebergConnectorMetadataTest.java)
 | [test_iceberg_view_query_p0.groovy](test_iceberg_view_query_p0.groovy) |
+| View | `LOAD_VIEW` | Y/Y/N/N/N/N/Y | Partial | 
[IcebergConnectorMetadataTest.java](../../../../fe/fe-connector/fe-connector-iceberg/src/test/java/org/apache/doris/connector/iceberg/IcebergConnectorMetadataTest.java)
 | [test_iceberg_view_query_p0.groovy](test_iceberg_view_query_p0.groovy) |
+| View | `READ_VIEW` | Y/Y/N/N/N/N/Y | Partial | 
[IcebergConnectorMetadataTest.java](../../../../fe/fe-connector/fe-connector-iceberg/src/test/java/org/apache/doris/connector/iceberg/IcebergConnectorMetadataTest.java)
 | [test_iceberg_view_query_p0.groovy](test_iceberg_view_query_p0.groovy) |
+| View | `CREATE_VIEW` | N/N/N/N/N/N/N | Negative gap | 
[IcebergJdbcViewProbeTest.java](../../../../fe/fe-connector/fe-connector-iceberg/src/test/java/org/apache/doris/connector/iceberg/IcebergJdbcViewProbeTest.java)
 | [test_iceberg_view_query_p0.groovy](test_iceberg_view_query_p0.groovy) |
+| View | `REPLACE_VIEW` | N/N/N/N/N/N/N | Negative gap | 
[IcebergJdbcViewProbeTest.java](../../../../fe/fe-connector/fe-connector-iceberg/src/test/java/org/apache/doris/connector/iceberg/IcebergJdbcViewProbeTest.java)
 | [test_iceberg_view_query_p0.groovy](test_iceberg_view_query_p0.groovy) |
+| View | `ALTER_VIEW` | N/N/N/N/N/N/N | Negative gap | 
[IcebergJdbcViewProbeTest.java](../../../../fe/fe-connector/fe-connector-iceberg/src/test/java/org/apache/doris/connector/iceberg/IcebergJdbcViewProbeTest.java)
 | [test_iceberg_view_query_p0.groovy](test_iceberg_view_query_p0.groovy) |
+| View | `RENAME_VIEW` | N/N/N/N/N/N/N | Negative gap | 
[IcebergJdbcViewProbeTest.java](../../../../fe/fe-connector/fe-connector-iceberg/src/test/java/org/apache/doris/connector/iceberg/IcebergJdbcViewProbeTest.java)
 | [test_iceberg_view_query_p0.groovy](test_iceberg_view_query_p0.groovy) |
+| View | `DROP_VIEW` | Y/Y/N/N/N/N/Y | Partial | 
[IcebergJdbcViewProbeTest.java](../../../../fe/fe-connector/fe-connector-iceberg/src/test/java/org/apache/doris/connector/iceberg/IcebergJdbcViewProbeTest.java)
 | [test_iceberg_view_query_p0.groovy](test_iceberg_view_query_p0.groovy) |
+| Data write | `APPEND` | Y/Y/Y/Y/Y/Y/Y | Partial | 
[IcebergWriteTransactionCoverageTest.java](../../../../fe/fe-connector/fe-connector-iceberg/src/test/java/org/apache/doris/connector/iceberg/IcebergWriteTransactionCoverageTest.java)
 | 
[write/test_iceberg_write_insert.groovy](write/test_iceberg_write_insert.groovy)
 |
+| Data write | `OVERWRITE_TABLE` | Y/Y/Y/Y/Y/Y/Y | Partial | 
[IcebergWriteTransactionCoverageTest.java](../../../../fe/fe-connector/fe-connector-iceberg/src/test/java/org/apache/doris/connector/iceberg/IcebergWriteTransactionCoverageTest.java)
 | 
[write/test_iceberg_partition_overwrite_mode.groovy](write/test_iceberg_partition_overwrite_mode.groovy)
 |
+| Data write | `OVERWRITE_PARTITION` | Y/Y/Y/Y/Y/Y/Y | Partial | 
[IcebergWriteTransactionCoverageTest.java](../../../../fe/fe-connector/fe-connector-iceberg/src/test/java/org/apache/doris/connector/iceberg/IcebergWriteTransactionCoverageTest.java)
 | 
[write/test_iceberg_static_partition_overwrite.groovy](write/test_iceberg_static_partition_overwrite.groovy)
 |
+| Data write | `DYNAMIC_PARTITION_OVERWRITE` | Y/Y/Y/Y/Y/Y/Y | Partial | 
[IcebergWriteTransactionCoverageTest.java](../../../../fe/fe-connector/fe-connector-iceberg/src/test/java/org/apache/doris/connector/iceberg/IcebergWriteTransactionCoverageTest.java)
 | 
[write/test_iceberg_write_overwrite_evolution.groovy](write/test_iceberg_write_overwrite_evolution.groovy)
 |
+| Data write | `UPDATE` | Y/Y/Y/Y/Y/Y/Y | Partial | 
[IcebergWriteTransactionCoverageTest.java](../../../../fe/fe-connector/fe-connector-iceberg/src/test/java/org/apache/doris/connector/iceberg/IcebergWriteTransactionCoverageTest.java)
 | 
[dml/test_iceberg_update_delete_advanced.groovy](dml/test_iceberg_update_delete_advanced.groovy)
 |
+| Data write | `DELETE` | Y/Y/Y/Y/Y/Y/Y | Partial | 
[IcebergMetadataDeleteTest.java](../../../../fe/fe-connector/fe-connector-iceberg/src/test/java/org/apache/doris/connector/iceberg/IcebergMetadataDeleteTest.java)
 | 
[dml/test_iceberg_update_delete_basic.groovy](dml/test_iceberg_update_delete_basic.groovy)
 |
+| Data write | `MERGE_INTO` | Y/Y/Y/Y/Y/Y/Y | Partial | 
[IcebergWriteTransactionCoverageTest.java](../../../../fe/fe-connector/fe-connector-iceberg/src/test/java/org/apache/doris/connector/iceberg/IcebergWriteTransactionCoverageTest.java)
 | 
[write/test_iceberg_write_merge_semantics.groovy](write/test_iceberg_write_merge_semantics.groovy)
 |
+| Data write | `UPSERT` | N/N/N/N/N/N/N | Negative gap | 
[IcebergWriteTransactionCoverageTest.java](../../../../fe/fe-connector/fe-connector-iceberg/src/test/java/org/apache/doris/connector/iceberg/IcebergWriteTransactionCoverageTest.java)
 | 
[write/test_iceberg_write_dml_modes_evolution.groovy](write/test_iceberg_write_dml_modes_evolution.groovy)
 |
+| Data write | `PARTIAL_UPDATE` | N/N/N/N/N/N/N | Negative gap | 
[IcebergWriteTransactionCoverageTest.java](../../../../fe/fe-connector/fe-connector-iceberg/src/test/java/org/apache/doris/connector/iceberg/IcebergWriteTransactionCoverageTest.java)
 | 
[write/test_iceberg_write_dml_modes_evolution.groovy](write/test_iceberg_write_dml_modes_evolution.groovy)
 |
+| Data write | `WRITE_TO_BRANCH` | Y/Y/Y/Y/Y/Y/Y | Partial | 
[IcebergWriteTransactionCoverageTest.java](../../../../fe/fe-connector/fe-connector-iceberg/src/test/java/org/apache/doris/connector/iceberg/IcebergWriteTransactionCoverageTest.java)
 | 
[write/test_iceberg_write_evolution_refs.groovy](write/test_iceberg_write_evolution_refs.groovy)
 |
+| Data write | `SCHEMA_EVOLUTION_ON_WRITE` | N/N/N/N/N/N/N | Negative gap | 
[IcebergWriteTransactionCoverageTest.java](../../../../fe/fe-connector/fe-connector-iceberg/src/test/java/org/apache/doris/connector/iceberg/IcebergWriteTransactionCoverageTest.java)
 | 
[write/test_iceberg_write_dml_modes_evolution.groovy](write/test_iceberg_write_dml_modes_evolution.groovy)
 |
+| Transactions and concurrency | `ATOMIC_SINGLE_TABLE_COMMIT` | Y/Y/Y/Y/Y/Y/Y 
| Partial | 
[IcebergConnectorTransactionTest.java](../../../../fe/fe-connector/fe-connector-iceberg/src/test/java/org/apache/doris/connector/iceberg/IcebergConnectorTransactionTest.java)
 | 
[write/test_iceberg_write_concurrent_merge_invariants.groovy](write/test_iceberg_write_concurrent_merge_invariants.groovy)
 |
+| Transactions and concurrency | `MULTI_TABLE_COMMIT` | N/N/N/N/N/N/N | 
Negative gap | 
[IcebergConnectorTransactionTest.java](../../../../fe/fe-connector/fe-connector-iceberg/src/test/java/org/apache/doris/connector/iceberg/IcebergConnectorTransactionTest.java)
 | 
[write/test_iceberg_write_concurrent_merge_invariants.groovy](write/test_iceberg_write_concurrent_merge_invariants.groovy)
 |
+| Transactions and concurrency | `STAGED_CREATE` | N/N/N/N/N/N/N | Negative 
gap | 
[IcebergConnectorTransactionTest.java](../../../../fe/fe-connector/fe-connector-iceberg/src/test/java/org/apache/doris/connector/iceberg/IcebergConnectorTransactionTest.java)
 | 
[write/test_iceberg_write_concurrent_merge_invariants.groovy](write/test_iceberg_write_concurrent_merge_invariants.groovy)
 |
+| Transactions and concurrency | `OPTIMISTIC_CONCURRENCY` | Y/Y/Y/Y/Y/Y/Y | 
Partial | 
[IcebergConnectorTransactionTest.java](../../../../fe/fe-connector/fe-connector-iceberg/src/test/java/org/apache/doris/connector/iceberg/IcebergConnectorTransactionTest.java)
 | 
[write/test_iceberg_write_concurrent_merge_invariants.groovy](write/test_iceberg_write_concurrent_merge_invariants.groovy)
 |
+| Transactions and concurrency | `COMMIT_CONFLICT_DETECTION` | Y/Y/Y/Y/Y/Y/Y | 
Partial | 
[IcebergConnectorTransactionTest.java](../../../../fe/fe-connector/fe-connector-iceberg/src/test/java/org/apache/doris/connector/iceberg/IcebergConnectorTransactionTest.java)
 | 
[write/test_iceberg_write_concurrent_merge_invariants.groovy](write/test_iceberg_write_concurrent_merge_invariants.groovy)
 |
+| Transactions and concurrency | `COMMIT_RETRY` | N/N/N/N/N/N/N | Negative gap 
| 
[IcebergConnectorTransactionTest.java](../../../../fe/fe-connector/fe-connector-iceberg/src/test/java/org/apache/doris/connector/iceberg/IcebergConnectorTransactionTest.java)
 | 
[write/test_iceberg_write_concurrent_merge_invariants.groovy](write/test_iceberg_write_concurrent_merge_invariants.groovy)
 |
+| Transactions and concurrency | `COMMIT_IDEMPOTENCY` | N/N/N/N/N/N/N | 
Negative gap | 
[IcebergConnectorTransactionTest.java](../../../../fe/fe-connector/fe-connector-iceberg/src/test/java/org/apache/doris/connector/iceberg/IcebergConnectorTransactionTest.java)
 | 
[write/test_iceberg_write_concurrent_merge_invariants.groovy](write/test_iceberg_write_concurrent_merge_invariants.groovy)
 |
+| Transactions and concurrency | `COMMIT_STATUS_RECOVERY` | N/N/N/N/N/N/N | 
Negative gap | 
[IcebergConnectorTransactionTest.java](../../../../fe/fe-connector/fe-connector-iceberg/src/test/java/org/apache/doris/connector/iceberg/IcebergConnectorTransactionTest.java)
 | 
[write/test_iceberg_write_concurrent_merge_invariants.groovy](write/test_iceberg_write_concurrent_merge_invariants.groovy)
 |
+| Transactions and concurrency | `LOCK_REQUIRED` | N/N/N/N/N/N/N | Negative 
gap | 
[IcebergConnectorTransactionTest.java](../../../../fe/fe-connector/fe-connector-iceberg/src/test/java/org/apache/doris/connector/iceberg/IcebergConnectorTransactionTest.java)
 | 
[write/test_iceberg_write_concurrent_merge_invariants.groovy](write/test_iceberg_write_concurrent_merge_invariants.groovy)
 |
+| Transactions and concurrency | `LOCK_CONFIGURED` | N/N/N/N/N/N/N | Negative 
gap | 
[IcebergConnectorTransactionTest.java](../../../../fe/fe-connector/fe-connector-iceberg/src/test/java/org/apache/doris/connector/iceberg/IcebergConnectorTransactionTest.java)
 | 
[write/test_iceberg_write_concurrent_merge_invariants.groovy](write/test_iceberg_write_concurrent_merge_invariants.groovy)
 |
+| Transactions and concurrency | `SERIALIZABLE_ISOLATION` | Y/Y/Y/Y/Y/Y/Y | 
Partial | 
[IcebergConnectorTransactionTest.java](../../../../fe/fe-connector/fe-connector-iceberg/src/test/java/org/apache/doris/connector/iceberg/IcebergConnectorTransactionTest.java)
 | 
[write/test_iceberg_write_concurrent_merge_invariants.groovy](write/test_iceberg_write_concurrent_merge_invariants.groovy)
 |
+| Transactions and concurrency | `FAILED_WRITE_CLEANUP` | N/N/N/N/N/N/N | 
Negative gap | 
[IcebergConnectorTransactionTest.java](../../../../fe/fe-connector/fe-connector-iceberg/src/test/java/org/apache/doris/connector/iceberg/IcebergConnectorTransactionTest.java)
 | 
[write/test_iceberg_write_concurrent_merge_invariants.groovy](write/test_iceberg_write_concurrent_merge_invariants.groovy)
 |
+| Branch / Tag | `BRANCH_CREATE` | Y/Y/Y/Y/Y/Y/Y | Partial | 
[CatalogBackedIcebergCatalogOpsDdlTest.java](../../../../fe/fe-connector/fe-connector-iceberg/src/test/java/org/apache/doris/connector/iceberg/CatalogBackedIcebergCatalogOpsDdlTest.java)
 | 
[test_iceberg_schema_ref_actions_matrix.groovy](test_iceberg_schema_ref_actions_matrix.groovy)
 |
+| Branch / Tag | `BRANCH_DROP` | Y/Y/Y/Y/Y/Y/Y | Partial | 
[CatalogBackedIcebergCatalogOpsDdlTest.java](../../../../fe/fe-connector/fe-connector-iceberg/src/test/java/org/apache/doris/connector/iceberg/CatalogBackedIcebergCatalogOpsDdlTest.java)
 | 
[test_iceberg_schema_ref_actions_matrix.groovy](test_iceberg_schema_ref_actions_matrix.groovy)
 |
+| Branch / Tag | `READ_BY_BRANCH` | Y/Y/Y/Y/Y/Y/Y | Partial | 
[IcebergSnapshotSchemaCoverageTest.java](../../../../fe/fe-connector/fe-connector-iceberg/src/test/java/org/apache/doris/connector/iceberg/IcebergSnapshotSchemaCoverageTest.java)
 | [iceberg_query_tag_branch.groovy](iceberg_query_tag_branch.groovy) |
+| Branch / Tag | `BRANCH_WRITE` | Y/Y/Y/Y/Y/Y/Y | Partial | 
[IcebergWriteTransactionCoverageTest.java](../../../../fe/fe-connector/fe-connector-iceberg/src/test/java/org/apache/doris/connector/iceberg/IcebergWriteTransactionCoverageTest.java)
 | 
[write/test_iceberg_write_branch_dml_boundary.groovy](write/test_iceberg_write_branch_dml_boundary.groovy)
 |
+| Branch / Tag | `BRANCH_FAST_FORWARD` | Y/Y/Y/Y/Y/Y/Y | Partial | 
[IcebergSnapshotMaintenanceCoverageTest.java](../../../../fe/fe-connector/fe-connector-iceberg/src/test/java/org/apache/doris/connector/iceberg/action/IcebergSnapshotMaintenanceCoverageTest.java)
 | 
[branch_tag/iceberg_branch_cross_operations.groovy](branch_tag/iceberg_branch_cross_operations.groovy)
 |
+| Branch / Tag | `TAG_CREATE` | Y/Y/Y/Y/Y/Y/Y | Partial | 
[CatalogBackedIcebergCatalogOpsDdlTest.java](../../../../fe/fe-connector/fe-connector-iceberg/src/test/java/org/apache/doris/connector/iceberg/CatalogBackedIcebergCatalogOpsDdlTest.java)
 | 
[test_iceberg_schema_ref_actions_matrix.groovy](test_iceberg_schema_ref_actions_matrix.groovy)
 |
+| Branch / Tag | `TAG_DROP` | Y/Y/Y/Y/Y/Y/Y | Partial | 
[CatalogBackedIcebergCatalogOpsDdlTest.java](../../../../fe/fe-connector/fe-connector-iceberg/src/test/java/org/apache/doris/connector/iceberg/CatalogBackedIcebergCatalogOpsDdlTest.java)
 | 
[test_iceberg_schema_ref_actions_matrix.groovy](test_iceberg_schema_ref_actions_matrix.groovy)
 |
+| Branch / Tag | `READ_BY_TAG` | Y/Y/Y/Y/Y/Y/Y | Partial | 
[IcebergSnapshotSchemaCoverageTest.java](../../../../fe/fe-connector/fe-connector-iceberg/src/test/java/org/apache/doris/connector/iceberg/IcebergSnapshotSchemaCoverageTest.java)
 | [iceberg_query_tag_branch.groovy](iceberg_query_tag_branch.groovy) |
+| Branch / Tag | `TAG_WRITE` | N/N/N/N/N/N/N | Negative gap | 
[CatalogBackedIcebergCatalogOpsDdlTest.java](../../../../fe/fe-connector/fe-connector-iceberg/src/test/java/org/apache/doris/connector/iceberg/CatalogBackedIcebergCatalogOpsDdlTest.java)
 | 
[test_iceberg_schema_ref_actions_matrix.groovy](test_iceberg_schema_ref_actions_matrix.groovy)
 |
+| Branch / Tag | `TAG_FAST_FORWARD` | N/N/N/N/N/N/N | Negative gap | 
[CatalogBackedIcebergCatalogOpsDdlTest.java](../../../../fe/fe-connector/fe-connector-iceberg/src/test/java/org/apache/doris/connector/iceberg/CatalogBackedIcebergCatalogOpsDdlTest.java)
 | 
[test_iceberg_schema_ref_actions_matrix.groovy](test_iceberg_schema_ref_actions_matrix.groovy)
 |
+| Snapshot | `READ_BY_VERSION` | Y/Y/Y/Y/Y/Y/Y | Partial | 
[IcebergSnapshotSchemaCoverageTest.java](../../../../fe/fe-connector/fe-connector-iceberg/src/test/java/org/apache/doris/connector/iceberg/IcebergSnapshotSchemaCoverageTest.java)
 | 
[test_iceberg_schema_time_travel_matrix.groovy](test_iceberg_schema_time_travel_matrix.groovy)
 |
+| Snapshot | `READ_BY_TIME` | Y/Y/Y/Y/Y/Y/Y | Partial | 
[IcebergTimeUtilsTest.java](../../../../fe/fe-connector/fe-connector-iceberg/src/test/java/org/apache/doris/connector/iceberg/IcebergTimeUtilsTest.java)
 | [test_iceberg_time_travel.groovy](test_iceberg_time_travel.groovy) |
+| Snapshot | `ROLLBACK` | Y/Y/Y/Y/Y/Y/Y | Partial | 
[IcebergSnapshotMaintenanceCoverageTest.java](../../../../fe/fe-connector/fe-connector-iceberg/src/test/java/org/apache/doris/connector/iceberg/action/IcebergSnapshotMaintenanceCoverageTest.java)
 | 
[action/test_iceberg_execute_actions.groovy](action/test_iceberg_execute_actions.groovy)
 |
+| Snapshot | `EXPIRE` | Y/Y/Y/Y/Y/Y/Y | Partial | 
[IcebergSnapshotMaintenanceCoverageTest.java](../../../../fe/fe-connector/fe-connector-iceberg/src/test/java/org/apache/doris/connector/iceberg/action/IcebergSnapshotMaintenanceCoverageTest.java)
 | 
[test_iceberg_ref_retention_expiration_control.groovy](test_iceberg_ref_retention_expiration_control.groovy)
 |
+| Metadata Table | `READ_METADATA_TABLE` | Y/Y/Y/Y/Y/Y/Y | Partial | 
[IcebergConnectorMetadataSysTableTest.java](../../../../fe/fe-connector/fe-connector-iceberg/src/test/java/org/apache/doris/connector/iceberg/IcebergConnectorMetadataSysTableTest.java)
 | 
[test_iceberg_system_table_projection.groovy](test_iceberg_system_table_projection.groovy)
 |
+| Maintenance | `REMOVE_ORPHAN_FILES` | N/N/N/N/N/N/N | Documentation differs 
| 
[IcebergRemoveOrphanFilesActionTest.java](../../../../fe/fe-connector/fe-connector-iceberg/src/test/java/org/apache/doris/connector/iceberg/action/IcebergRemoveOrphanFilesActionTest.java)
 | 
[action/test_iceberg_execute_actions.groovy](action/test_iceberg_execute_actions.groovy)
 |
+| Maintenance | `REWRITE_DATA_FILES / COMPACT` | Y/Y/Y/Y/Y/Y/Y | Partial | 
[RewriteDataFilePlannerTest.java](../../../../fe/fe-connector/fe-connector-iceberg/src/test/java/org/apache/doris/connector/iceberg/rewrite/RewriteDataFilePlannerTest.java)
 | 
[action/test_iceberg_v3_row_lineage_rewrite_data_files.groovy](action/test_iceberg_v3_row_lineage_rewrite_data_files.groovy)
 |
+| Maintenance | `REWRITE_MANIFESTS` | Y/Y/Y/Y/Y/Y/Y | Partial | 
[IcebergRewriteManifestsActionTest.java](../../../../fe/fe-connector/fe-connector-iceberg/src/test/java/org/apache/doris/connector/iceberg/action/IcebergRewriteManifestsActionTest.java)
 | 
[action/test_iceberg_rewrite_manifests.groovy](action/test_iceberg_rewrite_manifests.groovy)
 |
+| Maintenance | `REWRITE_DELETE_FILES` | N/N/N/N/N/N/N | Negative gap | 
[IcebergExpireSnapshotsActionTest.java](../../../../fe/fe-connector/fe-connector-iceberg/src/test/java/org/apache/doris/connector/iceberg/action/IcebergExpireSnapshotsActionTest.java)
 | 
[action/test_iceberg_execute_actions.groovy](action/test_iceberg_execute_actions.groovy)
 |
+| Maintenance | `EXPIRE_CHANGELOGS` | N/N/N/N/N/N/N | Negative gap | 
[IcebergExpireSnapshotsActionTest.java](../../../../fe/fe-connector/fe-connector-iceberg/src/test/java/org/apache/doris/connector/iceberg/action/IcebergExpireSnapshotsActionTest.java)
 | 
[action/test_iceberg_execute_actions.groovy](action/test_iceberg_execute_actions.groovy)
 |
+| Data ingestion | `ADD_FILES` | N/N/N/N/N/N/N | Negative gap | 
[IcebergConnectorTest.java](../../../../fe/fe-connector/fe-connector-iceberg/src/test/java/org/apache/doris/connector/iceberg/IcebergConnectorTest.java)
 | 
[test_external_catalog_iceberg_common.groovy](test_external_catalog_iceberg_common.groovy)
 |
+| Data ingestion | `MIGRATE_TABLE` | N/N/N/N/N/N/N | Negative gap | 
[IcebergConnectorTest.java](../../../../fe/fe-connector/fe-connector-iceberg/src/test/java/org/apache/doris/connector/iceberg/IcebergConnectorTest.java)
 | 
[test_external_catalog_iceberg_common.groovy](test_external_catalog_iceberg_common.groovy)
 |
+| Data ingestion | `COMPUTE_TABLE_STATS` | N/N/N/N/N/N/N | Negative gap | 
[IcebergConnectorTest.java](../../../../fe/fe-connector/fe-connector-iceberg/src/test/java/org/apache/doris/connector/iceberg/IcebergConnectorTest.java)
 | 
[test_external_catalog_iceberg_common.groovy](test_external_catalog_iceberg_common.groovy)
 |
+
+## Read, model, version, lineage, and data-type dimensions
+
+| Dimension | Existing evidence | Added coverage / remaining limit |
+| --- | --- | --- |
+| V1/V2/V3 and delete representations | 
`write/test_iceberg_write_v1_void_partition.groovy`, 
`test_iceberg_equality_delete.groovy`, `test_iceberg_deletion_vector.groovy` | 
New transaction append preserves historical files in all three versions. 
Position/Equality/DV ordering and concurrent rewrite/failure combinations 
remain partial. |
+| Source models and COW/MOR | `write/test_iceberg_write_source_models.groovy`, 
`write/test_iceberg_write_dml_modes_evolution.groovy` | Final rows alone cannot 
validate affected-row protocol or per-operation isolation properties. |
+| Parquet, ORC, mixed files | `test_iceberg_mixed_file_formats.groovy`, 
`test_iceberg_partition_evolution_format_scanner.groovy` | New 
metadata/delete/history suite crosses V2/V3 with Parquet/ORC; unsupported AVRO 
native data reads are not a positive-test promise. |
+| Branch versus tag/snapshot schema | 
`test_iceberg_schema_dual_relation_matrix.groovy`, 
`test_iceberg_schema_ref_actions_matrix.groovy` | New unit puts branch and tag 
at the same old snapshot after schema-only evolution; tests explicit snapshot 
after rollback. |
+| V3 row lineage | `test_iceberg_v3_row_lineage_uniqueness_stability.groovy`, 
`test_iceberg_v3_row_lineage_schema_evolution.groovy`, 
`action/test_iceberg_v3_row_lineage_rewrite_data_files.groovy` | New V2-to-V3 
schema-only transition and ADD/RENAME reserved-name atomicity checks. Overflow 
and mixed inherited/physical lineage remain partial. |
+| File metadata and TopN | `test_iceberg_file_metadata_columns.groovy` | New 
`test_iceberg_file_metadata_delete_history.groovy` checks physical position 
holes after deletes, partition predicates, historical refs and 
eager/TopN-setting parity. |
+| Boolean/integer/long/float/double | `IcebergTypeMappingReadTest`, 
`test_iceberg_float_predicate_pushdown.groovy` | New evaluator oracles test 
integer narrowing and long limits in SCAN/CONFLICT/REWRITE modes. A separate 
disabled reproduction preserves the DECIMAL-to-DOUBLE cast pruning invariant. |
+| Decimal precision/scale | `write/test_iceberg_write_decimal_bounds.groovy`, 
`IcebergPredicateConverterTest` | New precision 1/9/18/28/38 roundtrips and 
exact 38-digit comparison oracle. Manifest bound encoding needs separate 
reader/writer tests. |
+| Dates, micro/nanoseconds, timestamp zones | 
`test_iceberg_timestamp_tz.groovy`, `test_iceberg_timestamp_ns.groovy`, 
`write/test_iceberg_write_timestamp_partitions.groovy` | New 
negative-epoch/leap-century partition range oracle uses SDK transforms; DST 
overlap/gap filtering still needs managed execution. |
+| Binary/fixed/UUID | `test_iceberg_uuid_binary_compatibility.groovy`, 
`test_iceberg_varbinary.groovy`, 
`write/test_iceberg_write_binary_partitions.groovy` | New compound partition 
JSON covers binary values; fixed/binary serialization preserves 
direct/read-only buffer positions. |
+| List/map/struct and nested evolution | 
`test_iceberg_nested_schema_evolution_spark_doris_interop.groovy`, 
`write/test_iceberg_write_complex_evolution.groovy` | New deep roundtrip checks 
field IDs, requiredness, comments and supported siblings beside unsupported 
leaves. |
+| Initial/write defaults and Variant | `test_iceberg_initial_defaults.groovy`, 
`test_iceberg_variant_defaults.groovy`, `test_iceberg_variant_read.groovy` | 
Existing detailed reader tests do not establish all catalog, encryption and 
scanner combinations. |
+| Identity/bucket/truncate/calendar/void partitions | 
`test_iceberg_transform_partitions.groovy`, 
`write/test_iceberg_write_v1_void_partition.groovy`, 
`test_iceberg_partition_evolution_query_write.groovy` | New compound wire and 
SDK calendar oracles; transform/type/scanner cross-product remains partial. |
+| Metadata tables/auth/projections | 
`test_iceberg_system_table_projection.groovy`, 
`test_iceberg_sys_table_auth.groovy` | New unit enumerates every exposed 
metadata table on a real V3 base and checks absence of data-only hidden 
columns. |
+| Cache/REFRESH/statistics/count pushdown | 
`test_iceberg_table_meta_cache.groovy`, `test_iceberg_statistics.groovy`, 
`test_iceberg_optimize_count.groovy` | Concurrent metadata generation, TTL 
changes and multi-FE failover require stress execution. |
+| Native/JNI/streaming/Arrow/REST planning | 
`test_iceberg_rest_server_planning_guard.groovy`, 
`test_iceberg_arrow_flight_split_source.groovy`, scanner unit tests | REST 
server scan planning is explicitly rejected. Default settings do not prove 
alternate scanner/planning paths ran. |
+| Catalogs/filesystems/credentials | `IcebergAuthenticatedFileIOTest`, 
`IcebergProcedureOpsKerberosIoTest`, external P2 catalog/storage suites | 
Recording fakes do not validate live HMS/Glue/DLF/S3 Tables/JDBC/Hadoop, 
credential vending, HDFS or all object stores. |
+| Cleanup/retry/conflicts/resources | 
`write/test_iceberg_failed_write_atomicity_retry.groovy`, 
`IcebergCatalogResourceTrackerTest`, `IcebergConnectorWorkerPoolPinTest` | New 
begin-failure retry and close-before-begin tests. Unknown commit status, FE 
restart/replay, cancellation, spill/OOM and outages remain partial. |
+
+## Oracle gaps addressed
+
+Existing branch/tag retention suites expire newly created snapshots with a 
cutoff in 2020.
+Those snapshots are not expiration candidates, so preserved refs alone cannot 
prove protection.
+`test_iceberg_ref_retention_expiration_control.groovy` adds a future cutoff, 
`retain_last=1`,
+and an unreferenced snapshot that must disappear while protected 
main/branch/tag snapshots stay readable.
+The equivalent unit uses real in-memory Iceberg metadata.
+
+New transaction tests verify published file sets and historical snapshot 
contents, rather than
+only API calls. They cover multi-partition dynamic overwrite, static overwrite 
on a branch,
+concurrent main/branch append, and atomic publication of update data plus 
delete files.
+
+Known-bug reproductions retain correct assertions and are individually 
disabled with a reason.
+A skipped test is not passing coverage. Enable them when validating a fix by 
adding
+`-Djunit.jupiter.conditions.deactivate=org.junit.*DisabledCondition` to the 
Maven test command.
+
+## Execution boundaries
+
+Added FE tests use actual Iceberg metadata operations and recording connector 
contexts.
+The Groovy suites use existing REST/MinIO/Spark fixtures with metamorphic and 
cross-engine
+comparisons, so they do not require handwritten golden results. SQL product 
execution needs
+the managed test environment. Offline unit success and Groovy compilation do 
not imply SQL
+execution, BE unit success, a release compatibility matrix, or full backend 
coverage.
+Actual commands and results belong in the PR validation section.
diff --git 
a/regression-test/suites/external_table_p0/iceberg/test_iceberg_file_metadata_delete_history.groovy
 
b/regression-test/suites/external_table_p0/iceberg/test_iceberg_file_metadata_delete_history.groovy
new file mode 100644
index 00000000000..4380c05ff83
--- /dev/null
+++ 
b/regression-test/suites/external_table_p0/iceberg/test_iceberg_file_metadata_delete_history.groovy
@@ -0,0 +1,110 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+suite("test_iceberg_file_metadata_delete_history",
+        "p0,external,iceberg,external_docker,external_docker_iceberg") {
+    if 
(!"true".equalsIgnoreCase(context.config.otherConfigs.get("enableIcebergTest")))
 {
+        logger.info("Iceberg test is disabled")
+        return
+    }
+
+    String host = context.config.otherConfigs.get("externalEnvIp")
+    String restPort = context.config.otherConfigs.get("iceberg_rest_uri_port")
+    String minioPort = context.config.otherConfigs.get("iceberg_minio_port")
+    String catalog = "iceberg_file_metadata_delete_history"
+    String database = "iceberg_file_metadata_delete_history_db"
+    def lazyThreshold = sql("SELECT 
@@topn_lazy_materialization_threshold")[0][0]
+
+    sql "DROP CATALOG IF EXISTS ${catalog}"
+    sql """CREATE CATALOG ${catalog} PROPERTIES (
+        'type' = 'iceberg', 'iceberg.catalog.type' = 'rest',
+        'uri' = 'http://${host}:${restPort}',
+        's3.endpoint' = 'http://${host}:${minioPort}',
+        's3.access_key' = 'admin', 's3.secret_key' = 'password',
+        's3.region' = 'us-east-1')"""
+    sql "SWITCH ${catalog}"
+    sql "CREATE DATABASE IF NOT EXISTS ${database}"
+    sql "USE ${database}"
+
+    try {
+        for (int version : [2, 3]) {
+            for (String format : ["parquet", "orc"]) {
+                String table = "metadata_v${version}_${format}"
+                spark_iceberg "DROP TABLE IF EXISTS demo.${database}.${table}"
+                spark_iceberg """CREATE TABLE demo.${database}.${table} (
+                    id INT, batch INT, payload STRING) USING iceberg 
PARTITIONED BY (batch)
+                    TBLPROPERTIES ('format-version' = '${version}',
+                        'write.format.default' = '${format}',
+                        'write.delete.mode' = 'merge-on-read',
+                        'write.distribution-mode' = 'none')"""
+                // A single writer keeps multiple rows in each file, so 
deleting two rows really
+                // exercises holes in physical positions rather than removing 
one-row files.
+                spark_iceberg """INSERT INTO demo.${database}.${table}
+                    SELECT /*+ COALESCE(1) */ id, batch, payload FROM VALUES
+                    (1, 1, 'a'), (2, 1, 'b'), (3, 1, 'c'), (4, 1, 'd') AS 
v(id, batch, payload)"""
+                spark_iceberg """INSERT INTO demo.${database}.${table}
+                    SELECT /*+ COALESCE(1) */ id, batch, payload FROM VALUES
+                    (5, 2, 'e'), (6, 2, 'f'), (7, 2, 'g'), (8, 2, 'h') AS 
v(id, batch, payload)"""
+                sql "REFRESH DATABASE ${database}"
+                sql "SET topn_lazy_materialization_threshold = -1"
+                def before = sql("""SELECT id, batch, payload, `_file`, `_pos`
+                    FROM ${table} ORDER BY id""")
+                def snapshot = sql("""SELECT snapshot_id FROM ${table}\$refs 
WHERE name = 'main'""")[0][0]
+                sql "ALTER TABLE ${table} CREATE TAG before_delete AS OF 
VERSION ${snapshot}"
+
+                // Deletes leave holes in physical positions. A filtered-row 
ordinal would silently
+                // change the identity of a surviving row, even when its 
visible values look correct.
+                sql "DELETE FROM ${table} WHERE id IN (2, 4)"
+                def survivors = before.findAll { 
!(it[0].toString().toInteger() in [2, 4]) }
+                assertEquals(survivors, sql("""SELECT id, batch, payload, 
`_file`, `_pos`
+                    FROM ${table} ORDER BY id"""))
+                spark_iceberg "REFRESH TABLE demo.${database}.${table}"
+                assertSparkDorisResultEquals(
+                        spark_iceberg("SELECT id, batch, payload FROM 
demo.${database}.${table} ORDER BY id"),
+                        sql("SELECT id, batch, payload FROM ${table} ORDER BY 
id"))
+
+                // Reusing the current scan's delete state for a historical 
relation loses rows.
+                for (String relation : ["${table} FOR VERSION AS OF 
${snapshot}",
+                        "${table}@tag(before_delete)"]) {
+                    assertEquals(before, sql("""SELECT id, batch, payload, 
`_file`, `_pos`
+                        FROM ${relation} ORDER BY id"""))
+                }
+
+                // Phase two must reconstruct metadata from physical file 
coordinates after deletes,
+                // including when the predicate column is absent from the 
final projection.
+                for (String projection : ["`_file`", "`_pos`", "`_file`, 
`_pos`",
+                        "payload, `_file`, `_pos`"]) {
+                    String query = """SELECT ${projection} FROM ${table}
+                        WHERE id > 1 ORDER BY id LIMIT 3"""
+                    sql "SET topn_lazy_materialization_threshold = -1"
+                    def eager = sql(query)
+                    sql "SET topn_lazy_materialization_threshold = 10"
+                    assertEquals(eager, sql(query))
+                }
+
+                // An identity-partition predicate must preserve both file 
coordinates and the
+                // delete mask; it must not synthesize positions relative to 
the selected partition.
+                sql "SET topn_lazy_materialization_threshold = -1"
+                assertEquals(survivors.findAll { it[1].toString() == "1" },
+                        sql("""SELECT id, batch, payload, `_file`, `_pos`
+                            FROM ${table} WHERE batch = 1 ORDER BY id"""))
+            }
+        }
+    } finally {
+        sql "SET topn_lazy_materialization_threshold = ${lazyThreshold}"
+    }
+}
diff --git 
a/regression-test/suites/external_table_p0/iceberg/test_iceberg_ref_retention_expiration_control.groovy
 
b/regression-test/suites/external_table_p0/iceberg/test_iceberg_ref_retention_expiration_control.groovy
new file mode 100644
index 00000000000..16fe286142b
--- /dev/null
+++ 
b/regression-test/suites/external_table_p0/iceberg/test_iceberg_ref_retention_expiration_control.groovy
@@ -0,0 +1,76 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+suite("test_iceberg_ref_retention_expiration_control",
+        "p0,external,iceberg,external_docker,external_docker_iceberg") {
+    if 
(!"true".equalsIgnoreCase(context.config.otherConfigs.get("enableIcebergTest")))
 {
+        logger.info("Iceberg test is disabled")
+        return
+    }
+    String host = context.config.otherConfigs.get("externalEnvIp")
+    String restPort = context.config.otherConfigs.get("iceberg_rest_uri_port")
+    String minioPort = context.config.otherConfigs.get("iceberg_minio_port")
+    sql "DROP CATALOG IF EXISTS iceberg_ref_retention_control"
+    sql """CREATE CATALOG iceberg_ref_retention_control PROPERTIES (
+        'type' = 'iceberg', 'iceberg.catalog.type' = 'rest',
+        'uri' = 'http://${host}:${restPort}',
+        's3.endpoint' = 'http://${host}:${minioPort}',
+        's3.access_key' = 'admin', 's3.secret_key' = 'password',
+        's3.region' = 'us-east-1')"""
+    sql "SWITCH iceberg_ref_retention_control"
+    sql "CREATE DATABASE IF NOT EXISTS iceberg_ref_retention_control_db"
+    sql "USE iceberg_ref_retention_control_db"
+    sql "DROP TABLE IF EXISTS retention_control"
+    sql "CREATE TABLE retention_control (id INT, payload STRING) PROPERTIES 
('format-version' = '2')"
+
+    def currentSnapshot = {
+        sql("SELECT snapshot_id FROM retention_control\$refs WHERE name = 
'main'")[0][0].toString()
+    }
+    sql "INSERT INTO retention_control VALUES (1, 'tagged')"
+    String tagSnapshot = currentSnapshot()
+    sql "ALTER TABLE retention_control CREATE TAG protected_tag AS OF VERSION 
${tagSnapshot}"
+    def tagRows = sql("SELECT * FROM retention_control@tag(protected_tag) 
ORDER BY id")
+    sql "INSERT INTO retention_control VALUES (2, 'branched')"
+    String branchSnapshot = currentSnapshot()
+    sql """ALTER TABLE retention_control CREATE BRANCH protected_branch
+        AS OF VERSION ${branchSnapshot} WITH SNAPSHOT RETENTION 1 SNAPSHOTS"""
+    def branchRows = sql("SELECT * FROM 
retention_control@branch(protected_branch) ORDER BY id")
+    sql "INSERT INTO retention_control VALUES (3, 'unreferenced')"
+    String unreferencedSnapshot = currentSnapshot()
+    sql "INSERT INTO retention_control VALUES (4, 'current')"
+    String mainSnapshot = currentSnapshot()
+    def mainRows = sql("SELECT * FROM retention_control ORDER BY id")
+    def refsBefore = sql("SELECT name, type, snapshot_id FROM 
retention_control\$refs ORDER BY name")
+
+    // A cutoff older than the freshly written snapshots cannot test retention 
protection. The
+    // unreferenced snapshot is a positive control proving that this call 
actually expires history.
+    sql """ALTER TABLE retention_control EXECUTE expire_snapshots(
+        'older_than' = '2100-01-01T00:00:00', 'retain_last' = '1')"""
+    def retained = sql("SELECT snapshot_id FROM retention_control\$snapshots 
ORDER BY snapshot_id")
+            .collect { it[0].toString() }
+    assertEquals([tagSnapshot, branchSnapshot, mainSnapshot].sort(), 
retained.sort())
+    assertFalse(retained.contains(unreferencedSnapshot))
+    assertEquals(refsBefore,
+            sql("SELECT name, type, snapshot_id FROM retention_control\$refs 
ORDER BY name"))
+    assertEquals(tagRows, sql("SELECT * FROM 
retention_control@tag(protected_tag) ORDER BY id"))
+    assertEquals(branchRows, sql("SELECT * FROM 
retention_control@branch(protected_branch) ORDER BY id"))
+    assertEquals(mainRows, sql("SELECT * FROM retention_control ORDER BY id"))
+    spark_iceberg "REFRESH TABLE 
demo.iceberg_ref_retention_control_db.retention_control"
+    assertSparkDorisResultEquals(
+            spark_iceberg("SELECT * FROM 
demo.iceberg_ref_retention_control_db.retention_control ORDER BY id"),
+            mainRows)
+}


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

Reply via email to