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 88dbe7f67cf [fix](tvf) Preserve duplicate output column positions 
(#67769)
88dbe7f67cf is described below

commit 88dbe7f67cfe51684e34d7e1c19828f116fd3c9d
Author: Gabriel <[email protected]>
AuthorDate: Fri Sep 11 10:35:33 2026 +0800

    [fix](tvf) Preserve duplicate output column positions (#67769)
    
    ### What problem does this PR solve?
    
    Issue Number: DORIS-28201
    
    Problem Summary:
    
    `INSERT INTO` a file TVF used output names as map keys while binding the
    query output. When multiple expressions shared the same alias, the later
    expression replaced the earlier one, so the later value was written more
    than once.
    
    This change keeps file TVF outputs in query position order. It also adds
    an analyzer unit test and a local file TVF regression test for duplicate
    output names.
    
    ### Release note
    
    Fix duplicate output aliases writing the wrong positional values to file
    TVFs.
    
    ### 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. Duplicate aliases now preserve their original positional
    values.
    
    - 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
---
 .../doris/nereids/rules/analysis/BindSink.java     |  9 ++--
 .../rules/analysis/BindTVFTableSinkTest.java       | 58 ++++++++++++++++++++++
 .../tvf/insert/test_insert_into_local_tvf.out      |  2 +
 .../tvf/insert/test_insert_into_local_tvf.groovy   | 36 +++++++++++---
 4 files changed, 93 insertions(+), 12 deletions(-)

diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/analysis/BindSink.java
 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/analysis/BindSink.java
index 4016dc38f6b..9d7d1f3a743 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/analysis/BindSink.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/analysis/BindSink.java
@@ -716,17 +716,16 @@ public class BindSink implements AnalysisRuleFactory {
                             + ", query output: " + child.getOutput().size());
         }
 
-        // Build columnToOutput mapping and reuse getOutputProjectByCoercion 
for type cast,
-        // same as OlapTable INSERT INTO.
-        Map<String, NamedExpression> columnToOutput = Maps.newLinkedHashMap();
+        // TVF schemas mirror query output positions; display names can repeat 
and must not identify values.
+        ImmutableList.Builder<NamedExpression> outputBuilder = 
ImmutableList.builderWithExpectedSize(cols.size());
         for (int i = 0; i < cols.size(); i++) {
             Column col = cols.get(i);
             NamedExpression childExpr = (NamedExpression) 
child.getOutput().get(i);
             Alias output = new Alias(TypeCoercionUtils.castIfNotSameType(
                     childExpr, DataType.fromCatalogType(col.getType())), 
col.getName());
-            columnToOutput.put(col.getName(), output);
+            outputBuilder.add(output);
         }
-        LogicalProject<?> projectWithCast = getOutputProjectByCoercion(cols, 
child, columnToOutput);
+        LogicalProject<?> projectWithCast = new 
LogicalProject<>(outputBuilder.build(), child);
 
         List<NamedExpression> outputExprs = 
projectWithCast.getOutput().stream()
                 .map(NamedExpression.class::cast)
diff --git 
a/fe/fe-core/src/test/java/org/apache/doris/nereids/rules/analysis/BindTVFTableSinkTest.java
 
b/fe/fe-core/src/test/java/org/apache/doris/nereids/rules/analysis/BindTVFTableSinkTest.java
new file mode 100644
index 00000000000..b76ba57ddb5
--- /dev/null
+++ 
b/fe/fe-core/src/test/java/org/apache/doris/nereids/rules/analysis/BindTVFTableSinkTest.java
@@ -0,0 +1,58 @@
+// 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.nereids.rules.analysis;
+
+import org.apache.doris.nereids.parser.NereidsParser;
+import org.apache.doris.nereids.trees.expressions.NamedExpression;
+import org.apache.doris.nereids.trees.plans.Plan;
+import 
org.apache.doris.nereids.trees.plans.commands.insert.InsertIntoTVFCommand;
+import org.apache.doris.nereids.trees.plans.logical.LogicalProject;
+import org.apache.doris.nereids.trees.plans.logical.LogicalTVFTableSink;
+import org.apache.doris.nereids.util.MemoTestUtils;
+import org.apache.doris.nereids.util.PlanChecker;
+import org.apache.doris.qe.ConnectContext;
+
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+import java.util.List;
+
+class BindTVFTableSinkTest {
+
+    @Test
+    void duplicateOutputNamesKeepDistinctPositions() {
+        ConnectContext context = MemoTestUtils.createConnectContext();
+        InsertIntoTVFCommand command = (InsertIntoTVFCommand) new 
NereidsParser().parseSingle(
+                "INSERT INTO local("
+                        + "'file_path'='/tmp/tvf_duplicate_columns_',"
+                        + "'backend_id'='1',"
+                        + "'format'='csv') "
+                        + "SELECT 1 AS x, 2 AS x");
+
+        Plan analyzed = PlanChecker.from(context)
+                .analyze(command.getExplainPlan(context))
+                .getPlan();
+        LogicalTVFTableSink<?> sink = 
Assertions.assertInstanceOf(LogicalTVFTableSink.class, analyzed);
+        LogicalProject<?> sinkProject = 
Assertions.assertInstanceOf(LogicalProject.class, sink.child());
+        List<NamedExpression> projects = sinkProject.getProjects();
+
+        Assertions.assertEquals(2, projects.size());
+        Assertions.assertNotEquals(projects.get(0).getExprId(), 
projects.get(1).getExprId(),
+                "TVF sink output positions must not be collapsed by duplicate 
display names");
+    }
+}
diff --git 
a/regression-test/data/external_table_p0/tvf/insert/test_insert_into_local_tvf.out
 
b/regression-test/data/external_table_p0/tvf/insert/test_insert_into_local_tvf.out
index e05a406c439..73ab04473b7 100644
--- 
a/regression-test/data/external_table_p0/tvf/insert/test_insert_into_local_tvf.out
+++ 
b/regression-test/data/external_table_p0/tvf/insert/test_insert_into_local_tvf.out
@@ -132,3 +132,5 @@ true        3       300     3000    300000  5.5     6.6     
999.99  2024-12-31      2024-12-31T23:59:59     test    data
 1000   hello
 2000   foo
 
+-- !duplicate_output_names --
+1      2
diff --git 
a/regression-test/suites/external_table_p0/tvf/insert/test_insert_into_local_tvf.groovy
 
b/regression-test/suites/external_table_p0/tvf/insert/test_insert_into_local_tvf.groovy
index 5b7cc98a563..3988ca8adbb 100644
--- 
a/regression-test/suites/external_table_p0/tvf/insert/test_insert_into_local_tvf.groovy
+++ 
b/regression-test/suites/external_table_p0/tvf/insert/test_insert_into_local_tvf.groovy
@@ -639,7 +639,29 @@ suite("test_insert_into_local_tvf", "p0,external") {
         ) ORDER BY c1;
     """
 
-    // ============ 23. Error: missing file_path ============
+    // ============ 23. Duplicate output names preserve positional values 
============
+
+    sshExec("root", be_host, "rm -f ${basePath}/duplicate_output_names_*")
+    sshExec("root", be_host, "mkdir -p ${basePath}")
+    sshExec("root", be_host, "chmod 777 ${basePath}")
+
+    sql """
+        INSERT INTO local(
+            "file_path" = "${basePath}/duplicate_output_names_",
+            "backend_id" = "${be_id}",
+            "format" = "csv"
+        ) SELECT 1 AS x, 2 AS x;
+    """
+
+    qt_duplicate_output_names """
+        SELECT * FROM local(
+            "file_path" = "${basePath}/duplicate_output_names_*",
+            "backend_id" = "${be_id}",
+            "format" = "csv"
+        );
+    """
+
+    // ============ 24. Error: missing file_path ============
 
     test {
         sql """
@@ -651,7 +673,7 @@ suite("test_insert_into_local_tvf", "p0,external") {
         exception "file_path"
     }
 
-    // ============ 24. Error: missing format ============
+    // ============ 25. Error: missing format ============
 
     test {
         sql """
@@ -663,7 +685,7 @@ suite("test_insert_into_local_tvf", "p0,external") {
         exception "format"
     }
 
-    // ============ 25. Error: missing backend_id for local ============
+    // ============ 26. Error: missing backend_id for local ============
 
     test {
         sql """
@@ -675,7 +697,7 @@ suite("test_insert_into_local_tvf", "p0,external") {
         exception "backend_id"
     }
 
-    // ============ 26. Error: unsupported TVF name ============
+    // ============ 27. Error: unsupported TVF name ============
 
     test {
         sql """
@@ -687,7 +709,7 @@ suite("test_insert_into_local_tvf", "p0,external") {
         exception "INSERT INTO TVF only supports"
     }
 
-    // ============ 27. Error: unsupported format ============
+    // ============ 28. Error: unsupported format ============
 
     test {
         sql """
@@ -700,7 +722,7 @@ suite("test_insert_into_local_tvf", "p0,external") {
         exception "Unsupported"
     }
 
-    // ============ 28. Error: wildcard in file_path ============
+    // ============ 29. Error: wildcard in file_path ============
 
     test {
         sql """
@@ -713,7 +735,7 @@ suite("test_insert_into_local_tvf", "p0,external") {
         exception "wildcards"
     }
 
-    // ============ 29. Error: delete_existing_files=true on local TVF 
============
+    // ============ 30. Error: delete_existing_files=true on local TVF 
============
 
     test {
         sql """


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

Reply via email to