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]