From 79adab1be77833f3cb9aa34933abe3b40cc6a540 Mon Sep 17 00:00:00 2001 From: moke-HU <25567926679@qq.com> Date: Thu, 3 Sep 2026 04:36:53 +0800 Subject: [PATCH] [fix](nereids) gate IcebergRowLevelDmlTransform on catalog identity, not capability alone RowLevelDmlRegistry dispatches a row-level DELETE/UPDATE/MERGE to the first registered RowLevelDmlTransform whose handles() claims the table. Today the single entry, IcebergRowLevelDmlTransform, claims any PluginDrivenExternalTable whose connector declares DELETE or MERGE support -- a pure capability probe with no identity check. That is safe only while iceberg is the sole connector declaring those capabilities. As soon as a second connector does, the capability probe alone lets registry ORDER decide which transform claims the table, and the iceberg transform would silently synthesize its own plan shape (a position-delete stream over ICEBERG_ROWID_COL) for a table whose connector expects a different one. The failure is not a clean error: the plan is built, bound and executed against the wrong write protocol. Check the catalog type first, capability second, so a transform only ever claims tables it can actually synthesize for. The test now stubs the catalog type its handles() reads, and asserts that a capable NON-iceberg table is not claimed. --- .../plans/commands/IcebergRowLevelDmlTransform.java | 6 ++++++ .../commands/IcebergRowLevelDmlTransformTest.java | 12 +++++++++++- 2 files changed, 17 insertions(+), 1 deletion(-) 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));