diff --git a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/IcebergRowLevelDmlTransform.java b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/IcebergRowLevelDmlTransform.java index 5e9ba81668f136..8b59ce5147ceca 100644 --- a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/IcebergRowLevelDmlTransform.java +++ b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/IcebergRowLevelDmlTransform.java @@ -83,7 +83,13 @@ public class IcebergRowLevelDmlTransform implements RowLevelDmlTransform { @Override public boolean handles(TableIf table) { + // Identity FIRST, capability second: several connectors now declare row-level write + // capabilities, so the capability probe alone would let registry order decide which + // transform claims a table. The synthesized plan shape is connector-specific (iceberg's + // position-delete stream vs paimon's full-row keyed delete), so the claim must be too. return table instanceof PluginDrivenExternalTable + && "iceberg".equalsIgnoreCase( + ((PluginDrivenExternalTable) table).getCatalog().getType()) && pluginConnectorSupportsRowLevelDml((PluginDrivenExternalTable) table); } diff --git a/fe/fe-core/src/test/java/org/apache/doris/nereids/trees/plans/commands/IcebergRowLevelDmlTransformTest.java b/fe/fe-core/src/test/java/org/apache/doris/nereids/trees/plans/commands/IcebergRowLevelDmlTransformTest.java index a079b576708d97..c6505da1b3e7dc 100644 --- a/fe/fe-core/src/test/java/org/apache/doris/nereids/trees/plans/commands/IcebergRowLevelDmlTransformTest.java +++ b/fe/fe-core/src/test/java/org/apache/doris/nereids/trees/plans/commands/IcebergRowLevelDmlTransformTest.java @@ -96,8 +96,14 @@ private Plan filterOver(TableIf table, String columnName) { * {@code getConnector().getWritePlanProvider(handle).supportedOperations()} probe. */ private static PluginDrivenExternalTable pluginTable(boolean supportsDelete, boolean supportsMerge) { + return pluginTable("iceberg", supportsDelete, supportsMerge); + } + + private static PluginDrivenExternalTable pluginTable( + String catalogType, boolean supportsDelete, boolean supportsMerge) { PluginDrivenExternalTable table = Mockito.mock(PluginDrivenExternalTable.class); PluginDrivenExternalCatalog catalog = Mockito.mock(PluginDrivenExternalCatalog.class); + Mockito.when(catalog.getType()).thenReturn(catalogType); Connector connector = Mockito.mock(Connector.class); Set ops = EnumSet.noneOf(WriteOperation.class); if (supportsDelete) { @@ -121,9 +127,13 @@ public void handlesPluginDrivenTableByRowLevelDmlCapability() { Assertions.assertTrue(transform.handles(pluginTable(true, false))); Assertions.assertTrue(transform.handles(pluginTable(false, true))); Assertions.assertTrue(transform.handles(pluginTable(true, true))); - // A plugin connector with neither capability (e.g. jdbc/es/paimon today) must NOT be admitted, + // A plugin connector with neither capability must NOT be admitted, // else its row-level DML would route through the iceberg synthesis path. Assertions.assertFalse(transform.handles(pluginTable(false, false))); + // Identity gate: a NON-iceberg connector that DOES declare row-level capabilities must not be + // claimed either — with several connectors declaring capabilities, registry order must not + // decide whose connector-specific plan shape a table gets. + Assertions.assertFalse(transform.handles(pluginTable("paimon", true, true))); // Non-plugin table types and null are never admitted. Assertions.assertFalse(transform.handles(Mockito.mock(TableIf.class))); Assertions.assertFalse(transform.handles(null));