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 81cb9c85dfc [fix](maxcompute) Avoid merging exchange on dynamic 
partition writes (#69060)
81cb9c85dfc is described below

commit 81cb9c85dfce76c7e8f56fd1141f5be7caa8b4b6
Author: daidai <[email protected]>
AuthorDate: Fri Oct 16 11:41:41 2026 +0800

    [fix](maxcompute) Avoid merging exchange on dynamic partition writes 
(#69060)
    
    ### What problem does this PR solve?
    
    Problem Summary:
    
    MaxCompute dynamic-partition writes need each partition's rows to be
    contiguous in one writer stream, so the connector sink requested a
    partition shuffle plus a local sort. It used `MustLocalSortOrderSpec`
    for that, which the enforcer reads as "sort locally BEFORE distributing"
    and therefore appends a global sort: an extra merging exchange after the
    partition shuffle. Under local-channel backpressure that merging
    receiver can wait forever on a sender blocked by another full receiver
    queue, hanging `INSERT INTO <dynamic-partition table>` until the
    client-side socket times out.
    
    This PR requests a plain `OrderSpec` on top of the hash distribution
    instead, so the plan becomes `distribution -> local sort` (no global
    sort, no merging exchange) while keeping the same writer-local grouping
    contract. The SPI javadoc, the unit tests, and the MaxCompute write
    cases (explain shape: `VSORT` before `VEXCHANGE`, no
    `VMERGING-EXCHANGE`) are updated accordingly.
    
    ### Release note
    
    Fix intermittent hangs when writing dynamic partitions to MaxCompute.
    
    ### Check List (For Author)
    
    - Test
        - [x] Regression test
        - [x] Unit Test
        - [ ] Manual test (add detailed scripts or steps below)
        - [ ] No need to test or manual test. Explain why:
    - [ ] This is a refactor/code format and no logic has been changed.
            - [ ] Previous test can cover this change.
            - [ ] No code files have been changed.
            - [ ] Other reason
    
    - Behavior changed:
        - [ ] No.
        - [x] Yes.
    
    - Does this need documentation?
        - [x] No.
        - [ ] Yes.
    
    ### Check List (For Reviewer who merge this PR)
    
    - [ ] Confirm the release note
    - [ ] Confirm test cases
    - [ ] Confirm document
    - [ ] Add branch pick label
---
 .../spi/write/ConnectorWritePlanProvider.java      | 11 ++-
 .../plans/physical/PhysicalConnectorTableSink.java | 15 +++-
 .../properties/RequestPropertyDeriverTest.java     | 89 ++++++++++++++++++++++
 .../physical/PhysicalConnectorTableSinkTest.java   | 24 ++++--
 .../write/test_mc_write_large_data.groovy          | 25 ++++++
 .../write/test_mc_write_static_partitions.groovy   | 25 ++++--
 6 files changed, 173 insertions(+), 16 deletions(-)

diff --git 
a/fe/fe-connector/fe-connector-spi/src/main/java/org/apache/doris/connector/spi/write/ConnectorWritePlanProvider.java
 
b/fe/fe-connector/fe-connector-spi/src/main/java/org/apache/doris/connector/spi/write/ConnectorWritePlanProvider.java
index 0336a4f0f54..dd2b2eea03c 100644
--- 
a/fe/fe-connector/fe-connector-spi/src/main/java/org/apache/doris/connector/spi/write/ConnectorWritePlanProvider.java
+++ 
b/fe/fe-connector/fe-connector-spi/src/main/java/org/apache/doris/connector/spi/write/ConnectorWritePlanProvider.java
@@ -284,7 +284,9 @@ public interface ConnectorWritePlanProvider {
 
     /**
      * Whether dynamic-partition writes must be hash-distributed by partition 
columns and locally sorted by
-     * them before the sink (e.g. MaxCompute Storage API). Formerly a static 
{@code ConnectorCapability}
+     * them before the sink (e.g. MaxCompute Storage API). The required shape 
is "shuffle by the partition
+     * columns, then sort locally inside each writer", so every partition's 
rows end up contiguous in one
+     * writer stream. Formerly a static {@code ConnectorCapability}
      * switch; now this per-provider method is the single source of truth. A 
connector declaring this must
      * also declare {@link #requiresParallelWrite()} and {@link 
#requiresFullSchemaWriteOrder()}. Default: no.
      */
@@ -299,8 +301,11 @@ public interface ConnectorWritePlanProvider {
      * partition writer as soon as a different partition value appears and 
therefore needs the rows grouped by a
      * local sort) — the hash distribution alone keeps each partition's rows 
on one instance and the output file
      * count at ~one-per-partition, with no sort cost. The engine ({@code 
PhysicalConnectorTableSink}) picks the
-     * matching distribution: {@code requiresPartitionLocalSort} ⇒ hash + 
{@code MustLocalSortOrderSpec};
-     * {@code requiresPartitionHashWrite} ⇒ hash only. A connector sets at 
most one of the two hash arms; a
+     * matching distribution: {@code requiresPartitionLocalSort} ⇒ hash + a 
local sort <em>after</em> the
+     * shuffle, expressed as a plain {@code OrderSpec} (deliberately <b>not</b>
+     * {@code MustLocalSortOrderSpec}, which the enforcer reads as "sort 
before distributing" and therefore
+     * turns into distribution + a merging exchange); {@code 
requiresPartitionHashWrite} ⇒ hash only. A
+     * connector sets at most one of the two hash arms; a
      * connector declaring this should also declare {@link 
#requiresParallelWrite()} +
      * {@link #requiresFullSchemaWriteOrder()}. Default: no.
      */
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/physical/PhysicalConnectorTableSink.java
 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/physical/PhysicalConnectorTableSink.java
index 749c1bf96b3..d7f2543b613 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/physical/PhysicalConnectorTableSink.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/physical/PhysicalConnectorTableSink.java
@@ -28,8 +28,8 @@ import 
org.apache.doris.nereids.properties.DistributionSpecExternalTableSinkHash
 import org.apache.doris.nereids.properties.DistributionSpecHash.ShuffleType;
 import 
org.apache.doris.nereids.properties.DistributionSpecHiveTableSinkHashPartitioned;
 import org.apache.doris.nereids.properties.LogicalProperties;
-import org.apache.doris.nereids.properties.MustLocalSortOrderSpec;
 import org.apache.doris.nereids.properties.OrderKey;
+import org.apache.doris.nereids.properties.OrderSpec;
 import org.apache.doris.nereids.properties.PhysicalProperties;
 import org.apache.doris.nereids.trees.expressions.ExprId;
 import org.apache.doris.nereids.trees.expressions.NamedExpression;
@@ -431,11 +431,22 @@ public class PhysicalConnectorTableSink<CHILD_TYPE 
extends Plan> extends Physica
                     // Local sort by partition columns so rows for the same 
partition are grouped
                     // together before the streaming partition writer 
(MaxCompute Storage API closes a
                     // partition writer once a different partition value 
appears).
+                    //
+                    // A plain OrderSpec is deliberate, NOT 
MustLocalSortOrderSpec: the enforcer
+                    // 
(EnforceMissingPropertiesHelper#enforceSortAndDistribution) reads
+                    // MustLocalSortOrderSpec as "sort locally BEFORE 
distributing" and therefore emits
+                    // localSort -> distribution -> global sort, i.e. it 
appends a merging exchange
+                    // (VMERGING-EXCHANGE) after the partition shuffle. That 
merging receiver can wait
+                    // forever on a sender whose pipeline task is blocked by 
another full local-channel
+                    // receiver queue, hanging the INSERT until the 
client-side socket times out. A plain
+                    // OrderSpec on top of the hash distribution instead 
yields distribution -> local
+                    // sort: every partition's rows land on one instance and 
are contiguous in its
+                    // writer stream, which is exactly the connector's 
contract, with no global sort.
                     List<OrderKey> orderKeys = columnIdx.stream()
                             .map(idx -> new OrderKey(child().getOutput().get(
                                     idx + (hasRowOperationColumn() ? 1 : 0)), 
true, false))
                             .collect(Collectors.toList());
-                    return requiredProperties.withOrderSpec(new 
MustLocalSortOrderSpec(orderKeys));
+                    return requiredProperties.withOrderSpec(new 
OrderSpec(orderKeys));
                 }
                 // Partition columns exist but none in cols == all partitions 
statically specified;
                 // fall through to the parallel/gather branch (no sort/shuffle 
needed).
diff --git 
a/fe/fe-core/src/test/java/org/apache/doris/nereids/properties/RequestPropertyDeriverTest.java
 
b/fe/fe-core/src/test/java/org/apache/doris/nereids/properties/RequestPropertyDeriverTest.java
index 06e02ccfddb..34426c65e82 100644
--- 
a/fe/fe-core/src/test/java/org/apache/doris/nereids/properties/RequestPropertyDeriverTest.java
+++ 
b/fe/fe-core/src/test/java/org/apache/doris/nereids/properties/RequestPropertyDeriverTest.java
@@ -18,8 +18,11 @@
 package org.apache.doris.nereids.properties;
 
 import org.apache.doris.catalog.Column;
+import org.apache.doris.catalog.PrimitiveType;
 import org.apache.doris.datasource.ExternalDatabase;
 import org.apache.doris.datasource.ExternalTable;
+import org.apache.doris.datasource.plugin.ConnectorWritePlanContext;
+import org.apache.doris.datasource.plugin.PluginDrivenExternalTable;
 import org.apache.doris.nereids.hint.DistributeHint;
 import org.apache.doris.nereids.jobs.JobContext;
 import org.apache.doris.nereids.memo.Group;
@@ -46,9 +49,11 @@ import org.apache.doris.nereids.trees.plans.AggPhase;
 import org.apache.doris.nereids.trees.plans.DistributeType;
 import org.apache.doris.nereids.trees.plans.GroupPlan;
 import org.apache.doris.nereids.trees.plans.JoinType;
+import org.apache.doris.nereids.trees.plans.Plan;
 import org.apache.doris.nereids.trees.plans.RelationId;
 import org.apache.doris.nereids.trees.plans.logical.LogicalOneRowRelation;
 import org.apache.doris.nereids.trees.plans.physical.PhysicalAssertNumRows;
+import 
org.apache.doris.nereids.trees.plans.physical.PhysicalConnectorTableSink;
 import 
org.apache.doris.nereids.trees.plans.physical.PhysicalExternalRowLevelMergeSink;
 import org.apache.doris.nereids.trees.plans.physical.PhysicalHashAggregate;
 import org.apache.doris.nereids.trees.plans.physical.PhysicalHashJoin;
@@ -68,6 +73,7 @@ import org.junit.jupiter.api.Test;
 import org.mockito.MockedStatic;
 import org.mockito.Mockito;
 
+import java.util.BitSet;
 import java.util.List;
 import java.util.Optional;
 
@@ -135,6 +141,89 @@ class RequestPropertyDeriverTest {
         return new RequestPropertyDeriver(ctx, 
jobContext).getRequestChildrenPropertyList(groupExpression);
     }
 
+    // The connector sink's required physical properties carry a 
writer-correctness contract: the MaxCompute
+    // Storage API closes a partition writer as soon as a different partition 
value appears, so a
+    // dynamic-partition write must be hash-distributed and locally sorted by 
the partition columns.
+    // enable_strict_consistency_dml is an optional replica-consistency knob 
(and is forced off in cloud
+    // mode), so it may relax only the generic SINK_RANDOM_PARTITIONED 
preference -- never the connector's
+    // own contract. Both polarities are pinned so a future "unify the gates" 
change cannot silently drop
+    // the connector's distribution + local sort.
+    @Test
+    void 
testConnectorTableSinkKeepsWriterContractWhenStrictConsistencyDmlIsOff() {
+        PhysicalConnectorTableSink<Plan> dynamicPartitionSink = 
testConnectorSink(true);
+        PhysicalProperties dynamicPartitionWrite = 
dynamicPartitionSink.getRequirePhysicalProperties();
+        Assertions.assertEquals(
+                ImmutableList.of(ImmutableList.of(dynamicPartitionWrite)),
+                
requestChildrenPropertiesForConnectorSink(dynamicPartitionSink, false),
+                "a dynamic-partition connector write must keep its 
distribution + local sort even with "
+                        + "strict-consistency DML off");
+        Assertions.assertEquals(
+                ImmutableList.of(ImmutableList.of(dynamicPartitionWrite)),
+                
requestChildrenPropertiesForConnectorSink(dynamicPartitionSink, true),
+                "strict-consistency DML on must not change the connector's 
writer contract");
+
+        PhysicalConnectorTableSink<Plan> parallelSink = 
testConnectorSink(false);
+        PhysicalProperties parallelWrite = 
parallelSink.getRequirePhysicalProperties();
+        Assertions.assertEquals(PhysicalProperties.SINK_RANDOM_PARTITIONED, 
parallelWrite,
+                "a non-partitioned parallel write falls back to the generic 
random preference");
+        Assertions.assertEquals(
+                ImmutableList.of(ImmutableList.of(PhysicalProperties.ANY)),
+                requestChildrenPropertiesForConnectorSink(parallelSink, false),
+                "with strict-consistency DML off only the generic random 
parallel-write preference relaxes");
+        Assertions.assertEquals(
+                ImmutableList.of(ImmutableList.of(parallelWrite)),
+                requestChildrenPropertiesForConnectorSink(parallelSink, true),
+                "with strict-consistency DML on the generic random 
parallel-write preference is kept");
+    }
+
+    private List<List<PhysicalProperties>> 
requestChildrenPropertiesForConnectorSink(
+            PhysicalConnectorTableSink<Plan> sink, boolean 
enableStrictConsistencyDml) {
+        ConnectContext ctx = new ConnectContext();
+        SessionVariable sessionVariable = new SessionVariable();
+        sessionVariable.enableStrictConsistencyDml = 
enableStrictConsistencyDml;
+        ctx.setSessionVariable(sessionVariable);
+
+        GroupExpression groupExpression = new GroupExpression(sink);
+        new Group(null, groupExpression, null);
+
+        return new RequestPropertyDeriver(ctx, 
jobContext).getRequestChildrenPropertyList(groupExpression);
+    }
+
+    /**
+     * A MaxCompute-like connector sink: a non-empty partition column list 
makes the write a
+     * dynamic-partition write (partition hash + local sort), an empty one 
keeps the plain parallel write.
+     * Built through the public constructor (a real connector is still mocked 
out) so the resulting plan can
+     * be wrapped in a {@link GroupExpression}.
+     */
+    private static PhysicalConnectorTableSink<Plan> testConnectorSink(boolean 
dynamicPartition) {
+        Column dataColumn = new Column("data", PrimitiveType.INT);
+        Column partColumn = new Column("part", PrimitiveType.INT);
+        List<Column> fullSchema = ImmutableList.of(dataColumn, partColumn);
+        List<Column> partitionColumns = dynamicPartition ? 
ImmutableList.of(partColumn) : ImmutableList.of();
+
+        PluginDrivenExternalTable table = 
Mockito.mock(PluginDrivenExternalTable.class);
+        ConnectorWritePlanContext writePlanContext = 
Mockito.mock(ConnectorWritePlanContext.class);
+        
Mockito.when(writePlanContext.requiresParallelWrite()).thenReturn(true);
+        
Mockito.when(writePlanContext.requiresPartitionLocalSort()).thenReturn(true);
+        
Mockito.when(writePlanContext.requiresPartitionHashWrite()).thenReturn(false);
+        
Mockito.when(writePlanContext.getDistribution()).thenReturn(Optional.empty());
+        
Mockito.when(table.resolveWritePlanContext()).thenReturn(writePlanContext);
+        Mockito.when(table.getPartitionColumns()).thenReturn(partitionColumns);
+        Mockito.when(table.getFullSchema()).thenReturn(fullSchema);
+
+        Plan child = Mockito.mock(Plan.class);
+        Mockito.when(child.getOutput()).thenReturn(ImmutableList.of(
+                new SlotReference("data", IntegerType.INSTANCE),
+                new SlotReference("part", IntegerType.INSTANCE)));
+        Mockito.when(child.getAllChildrenTypes()).thenReturn(new BitSet());
+        Mockito.when(child.depth()).thenReturn(1);
+
+        return new PhysicalConnectorTableSink<>(
+                Mockito.mock(ExternalDatabase.class), table, fullSchema, 
partitionColumns, fullSchema,
+                ImmutableList.of(), Optional.empty(),
+                new LogicalProperties(child::getOutput, () -> 
DataTrait.EMPTY_TRAIT), false, child);
+    }
+
     @Test
     void testNestedLoopJoin() {
         PhysicalNestedLoopJoin<GroupPlan, GroupPlan> join = new 
PhysicalNestedLoopJoin<>(JoinType.CROSS_JOIN,
diff --git 
a/fe/fe-core/src/test/java/org/apache/doris/nereids/trees/plans/physical/PhysicalConnectorTableSinkTest.java
 
b/fe/fe-core/src/test/java/org/apache/doris/nereids/trees/plans/physical/PhysicalConnectorTableSinkTest.java
index 4c151cc8e7f..7cbcb25f3ac 100644
--- 
a/fe/fe-core/src/test/java/org/apache/doris/nereids/trees/plans/physical/PhysicalConnectorTableSinkTest.java
+++ 
b/fe/fe-core/src/test/java/org/apache/doris/nereids/trees/plans/physical/PhysicalConnectorTableSinkTest.java
@@ -33,6 +33,7 @@ import 
org.apache.doris.nereids.properties.DistributionSpecHiveTableSinkHashPart
 import org.apache.doris.nereids.properties.LogicalProperties;
 import org.apache.doris.nereids.properties.MustLocalSortOrderSpec;
 import org.apache.doris.nereids.properties.OrderKey;
+import org.apache.doris.nereids.properties.OrderSpec;
 import org.apache.doris.nereids.properties.PhysicalProperties;
 import org.apache.doris.nereids.trees.expressions.Slot;
 import org.apache.doris.nereids.trees.expressions.SlotReference;
@@ -105,8 +106,14 @@ public class PhysicalConnectorTableSinkTest {
         // The hash key is the child slot at 'part's full-schema position 
(index 1).
         Assertions.assertEquals(ImmutableList.of(partSlot.getExprId()), 
dist.getOutputColExprIds(),
                 "hash key must be the partition-column slot taken at its 
full-schema position");
-        Assertions.assertTrue(props.getOrderSpec() instanceof 
MustLocalSortOrderSpec,
-                "dynamic-partition write must require a mandatory local sort 
to group partition rows");
+        // A plain OrderSpec, NOT MustLocalSortOrderSpec: the enforcer reads 
MustLocalSortOrderSpec as
+        // "sort locally BEFORE distributing", so it would emit localSort -> 
distribution -> global sort,
+        // i.e. a VMERGING-EXCHANGE after the partition shuffle. That merging 
receiver can deadlock
+        // against local-channel backpressure and hang the write until the 
client-side socket times out.
+        Assertions.assertFalse(props.getOrderSpec() instanceof 
MustLocalSortOrderSpec,
+                "dynamic-partition write must not use MustLocalSortOrderSpec 
(it appends a merging exchange)");
+        Assertions.assertInstanceOf(OrderSpec.class, props.getOrderSpec(),
+                "dynamic-partition write must require a local sort to group 
partition rows");
         List<OrderKey> orderKeys = props.getOrderSpec().getOrderKeys();
         Assertions.assertEquals(1, orderKeys.size(), "exactly one partition 
column to sort by");
         Assertions.assertEquals(partSlot, orderKeys.get(0).getExpr(),
@@ -202,8 +209,12 @@ public class PhysicalConnectorTableSinkTest {
         Assertions.assertEquals(ImmutableList.of(dsSlot.getExprId(), 
regionSlot.getExprId()),
                 dist.getOutputColExprIds(),
                 "hash keys must be the partition-column slots at their 
full-schema positions");
-        Assertions.assertTrue(props.getOrderSpec() instanceof 
MustLocalSortOrderSpec,
-                "partial-static write must require a mandatory local sort");
+        // Same rationale as dynamicPartitionWriteRequiresHashAndLocalSort: a 
plain OrderSpec, so the
+        // remaining dynamic partition column is shuffled and then locally 
sorted without a merging exchange.
+        Assertions.assertFalse(props.getOrderSpec() instanceof 
MustLocalSortOrderSpec,
+                "partial-static write must not use MustLocalSortOrderSpec (it 
appends a merging exchange)");
+        Assertions.assertInstanceOf(OrderSpec.class, props.getOrderSpec(),
+                "partial-static write must require a local sort");
         List<OrderKey> orderKeys = props.getOrderSpec().getOrderKeys();
         Assertions.assertEquals(2, orderKeys.size(), "sort by both partition 
columns in full-schema order");
         Assertions.assertEquals(dsSlot, orderKeys.get(0).getExpr());
@@ -394,7 +405,10 @@ public class PhysicalConnectorTableSinkTest {
 
         
Assertions.assertInstanceOf(DistributionSpecExternalTableSinkHashPartitioned.class,
                 properties.getDistributionSpec());
-        Assertions.assertInstanceOf(MustLocalSortOrderSpec.class, 
properties.getOrderSpec());
+        Assertions.assertFalse(properties.getOrderSpec() instanceof 
MustLocalSortOrderSpec,
+                "a connector-provided distribution must not be paired with 
MustLocalSortOrderSpec, which "
+                        + "would make the enforcer append a merging exchange 
after the shuffle");
+        Assertions.assertInstanceOf(OrderSpec.class, 
properties.getOrderSpec());
         Assertions.assertEquals(partSlot, 
properties.getOrderSpec().getOrderKeys().get(0).getExpr());
     }
 
diff --git 
a/regression-test/suites/external_table_p2/maxcompute/write/test_mc_write_large_data.groovy
 
b/regression-test/suites/external_table_p2/maxcompute/write/test_mc_write_large_data.groovy
index 3bd61371461..f952b416b4b 100644
--- 
a/regression-test/suites/external_table_p2/maxcompute/write/test_mc_write_large_data.groovy
+++ 
b/regression-test/suites/external_table_p2/maxcompute/write/test_mc_write_large_data.groovy
@@ -77,6 +77,18 @@ suite("test_mc_write_large_data", "p2,external") {
     retry(10, 1000) { sql """use ${db}""" }
 
     try {
+        // Explain prints a parent before its child, so a VSORT index that 
precedes the VEXCHANGE index
+        // means the exchange feeds the sort: rows are shuffled by the 
partition columns and then sorted
+        // inside each final writer stream. A VMERGING-EXCHANGE would be the 
extra global sort that
+        // MaxCompute dynamic-partition writes must not have -- its merging 
receiver can deadlock against
+        // local-channel backpressure and hang the INSERT until the 
client-side socket times out.
+        def checkPartitionShuffleThenSort = { String explainString ->
+            int sortIndex = explainString.indexOf(":VSORT")
+            int exchangeIndex = explainString.indexOf(":VEXCHANGE")
+            sortIndex >= 0 && exchangeIndex > sortIndex
+                    && !explainString.contains("VMERGING-EXCHANGE")
+        }
+
         // Non-partition table
         String tb1 = "large_no_part_${uuid}"
         sql """DROP TABLE IF EXISTS ${tb1}"""
@@ -104,6 +116,10 @@ suite("test_mc_write_large_data", "p2,external") {
 
         // Step 3: Insert from internal table
         sql """INSERT INTO ${tb1} SELECT * FROM 
internal.${internal_db}.${internal_tb}"""
+        explain {
+            sql("INSERT INTO ${tb2} SELECT * FROM 
internal.${internal_db}.${internal_tb}")
+            check { explainString -> 
checkPartitionShuffleThenSort(explainString) }
+        }
         sql """INSERT INTO ${tb2} SELECT * FROM 
internal.${internal_db}.${internal_tb}"""
 
         // Step 4: Verify results
@@ -126,6 +142,15 @@ suite("test_mc_write_large_data", "p2,external") {
         PARTITION BY (ds, region)()
         """
 
+        explain {
+            sql("""
+                INSERT INTO ${tb3}
+                SELECT id, name, val, ds,
+                    concat('r', cast((id % 3 + 1) AS STRING)) AS region
+                FROM internal.${internal_db}.${internal_tb}
+            """)
+            check { explainString -> 
checkPartitionShuffleThenSort(explainString) }
+        }
         sql """
         INSERT INTO ${tb3}
         SELECT id, name, val, ds,
diff --git 
a/regression-test/suites/external_table_p2/maxcompute/write/test_mc_write_static_partitions.groovy
 
b/regression-test/suites/external_table_p2/maxcompute/write/test_mc_write_static_partitions.groovy
index a5f2e7318cd..2e7277f66e1 100644
--- 
a/regression-test/suites/external_table_p2/maxcompute/write/test_mc_write_static_partitions.groovy
+++ 
b/regression-test/suites/external_table_p2/maxcompute/write/test_mc_write_static_partitions.groovy
@@ -151,6 +151,18 @@ suite("test_mc_write_static_partitions", "p2,external") {
         order_qt_overwrite_p1 """ SELECT * FROM ${tb4} WHERE ds = '20250101' 
"""
         order_qt_overwrite_p2 """ SELECT * FROM ${tb4} WHERE ds = '20250102' 
"""
 
+        // Explain prints a parent before its child, so a VSORT index that 
precedes the VEXCHANGE index
+        // means the exchange feeds the sort: rows are shuffled by the 
partition columns and then sorted
+        // inside each final writer stream. A VMERGING-EXCHANGE would be the 
extra global sort that
+        // MaxCompute dynamic-partition writes must not have -- its merging 
receiver can deadlock against
+        // local-channel backpressure and hang the INSERT until the 
client-side socket times out.
+        def checkPartitionShuffleThenSort = { String explainString ->
+            int sortIndex = explainString.indexOf(":VSORT")
+            int exchangeIndex = explainString.indexOf(":VEXCHANGE")
+            sortIndex >= 0 && exchangeIndex > sortIndex
+                    && !explainString.contains("VMERGING-EXCHANGE")
+        }
+
         // Test 5: Dynamic partition regression (ensure not broken)
         // Dynamic partition: partition column 'ds' is in the data, SORT node 
IS expected
         String tb5 = "dynamic_reg_${uuid}"
@@ -165,15 +177,16 @@ suite("test_mc_write_static_partitions", "p2,external") {
         sql """INSERT INTO ${tb5} VALUES (1, 'a', '20250101'), (2, 'b', 
'20250102')"""
         order_qt_dynamic_regression """ SELECT * FROM ${tb5} """
 
-        // Explain: dynamic partition INSERT should HAVE SORT node (partition 
col in data)
+        // Dynamic partition writes need a partition shuffle followed by a 
local sort, and no merging
+        // exchange (its merging receiver can deadlock against local-channel 
backpressure).
         explain {
             sql("INSERT INTO ${tb5} VALUES (3, 'c', '20250103')")
-            contains "SORT"
+            check { explainString -> 
checkPartitionShuffleThenSort(explainString) }
         }
-        // Explain: dynamic partition INSERT INTO SELECT should HAVE SORT node
+        // Explain: dynamic partition INSERT INTO SELECT must use the same 
non-merging plan shape.
         explain {
             sql("INSERT INTO ${tb5} SELECT * FROM ${tb3_src}")
-            contains "SORT"
+            check { explainString -> 
checkPartitionShuffleThenSort(explainString) }
         }
 
         // Test 6: INSERT OVERWRITE non-partitioned table
@@ -213,10 +226,10 @@ suite("test_mc_write_static_partitions", "p2,external") {
             region STRING
         ) PARTITION BY (ds, region)()
         """
-        // Explain: partial static partition (ds static, region dynamic) 
should HAVE SORT node
+        // A remaining dynamic partition column also requires shuffle followed 
by local sort.
         explain {
             sql("INSERT INTO ${tb7} PARTITION(ds='20250101') VALUES (1, 'v1', 
'bj')")
-            contains "SORT"
+            check { explainString -> 
checkPartitionShuffleThenSort(explainString) }
         }
         // Explain: all static partition should NOT have SORT node
         explain {


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

Reply via email to