This is an automated email from the ASF dual-hosted git repository.
morningman 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 5efa83ee269 [fix](load) Load a literal \N path value as NULL (#69064)
5efa83ee269 is described below
commit 5efa83ee269758b6490f65eea3dc892aa5e8790f
Author: Mingyu Chen (Rayner) <[email protected]>
AuthorDate: Thu Oct 15 14:48:49 2026 +0800
[fix](load) Load a literal \N path value as NULL (#69064)
### What problem does this PR solve?
Issue Number: None
Related PR: #66012, #62821
Problem Summary:
**In short.** A broker load that reads a column from the file path
(`COLUMNS FROM PATH AS (p)`) stores a path value of `\N` as the
two-character string `\N` on master. Before #62821, and on branch-4.1
(#66012), the load stores NULL. This PR ports the load-specific parsing
of branch-4.1 #66012 so that loads store NULL again; queries are not
changed.
**Background.** For `COLUMNS FROM PATH AS`, the FE parses every file
path (for example `s3://bucket/t/p=\N/data.csv`) and sends the values to
the BE in the scan range, together with a null flag per value
(`columns_from_path_is_null`). #62821 added the flag and sets it only
for the Hive default partition, `__HIVE_DEFAULT_PARTITION__`. Before
that, the FE wrote a NULL as `\N` and the BE read the values with its
nullable parser, so a literal `\N` directory value became NULL as well.
branch-4.1 got the same flag in #66012, which also added a load-specific
parser that keeps the old meaning of `\N` for loads.
**The problem, and what it cost.**
| Path value | master | before #62821, and branch-4.1 | with this PR |
|---|---|---|---|
| `p=\N`, string column | the string `\N` | NULL | NULL |
| `p=\N`, INT column | NULL from the load's cast; the row is filtered in
strict mode | NULL | NULL |
| `p=__HIVE_DEFAULT_PARTITION__` | NULL | NULL | NULL |
| Queries (Hive catalog, TVF, `INSERT ... SELECT FROM s3()`) | `\N` is
data | `\N` is data | unchanged |
**How this PR fixes it.**
`FilePartitionUtils.parseColumnsFromPathWithNullInfoForLoad` parses the
path as before and then turns a literal `\N` into an empty value with
the null flag set, like branch-4.1's
`BrokerUtil.parseColumnsFromPathWithNullInfoForLoad`. Both
range-building paths of `NereidsFileGroupInfo` (splittable and
unsplittable files) use it, and so does the legacy `FileGroupInfo`,
which master no longer instantiates. Stream load does not send path
columns and is not affected.
**Results.**
`BrokerUtilTest#parseColumnsFromPathForLoadKeepsLegacyNullSemantics`
(from branch-4.1, plus a case with a regular value and the Hive default
partition) checks the parser.
`NereidsFileGroupInfoTest#loadRangesCarryLiteralNullPathValueAsNull`
builds the scan ranges of a load for a splittable and an unsplittable
CSV file under `p=\N` and checks that the range carries an empty value
with the null flag set. Switching either call site, or the parser, back
to the query behavior makes the corresponding test fail.
`BrokerUtilTest`, `NereidsFileGroupInfoTest` and
`NereidsLoadScanProviderTest` pass (12 tests). There is no regression
test: `test_load_data_from_path` reads fixed objects from the shared S3
test bucket, which has no `p=\N` object.
Co-authored-by: Socrates <[email protected]>
---
.../doris/datasource/scan/FileGroupInfo.java | 4 +-
.../doris/datasource/scan/FilePartitionUtils.java | 25 +++++++
.../doris/nereids/load/NereidsFileGroupInfo.java | 4 +-
.../apache/doris/common/util/BrokerUtilTest.java | 15 ++++
.../nereids/load/NereidsFileGroupInfoTest.java | 81 ++++++++++++++++++++++
5 files changed, 125 insertions(+), 4 deletions(-)
diff --git
a/fe/fe-core/src/main/java/org/apache/doris/datasource/scan/FileGroupInfo.java
b/fe/fe-core/src/main/java/org/apache/doris/datasource/scan/FileGroupInfo.java
index b1d70b467fc..1608a896b99 100644
---
a/fe/fe-core/src/main/java/org/apache/doris/datasource/scan/FileGroupInfo.java
+++
b/fe/fe-core/src/main/java/org/apache/doris/datasource/scan/FileGroupInfo.java
@@ -265,7 +265,7 @@ public class FileGroupInfo {
fileStatus.path);
context.params.setCompressType(compressType);
FilePartitionUtils.ParsedColumnsFromPath columnsFromPath =
-
FilePartitionUtils.parseColumnsFromPathWithNullInfo(fileStatus.path,
+
FilePartitionUtils.parseColumnsFromPathWithNullInfoForLoad(fileStatus.path,
context.fileGroup.getColumnNamesFromPath(),
true, false);
List<String> columnsFromPathKeys =
context.fileGroup.getColumnNamesFromPath();
TFileRangeDesc rangeDesc = createFileRangeDesc(0, fileStatus,
fileStatus.size,
@@ -312,7 +312,7 @@ public class FileGroupInfo {
fileStatus.path);
context.params.setCompressType(compressType);
FilePartitionUtils.ParsedColumnsFromPath columnsFromPath =
-
FilePartitionUtils.parseColumnsFromPathWithNullInfo(fileStatus.path,
+
FilePartitionUtils.parseColumnsFromPathWithNullInfoForLoad(fileStatus.path,
context.fileGroup.getColumnNamesFromPath(), true,
false);
List<String> columnsFromPathKeys =
context.fileGroup.getColumnNamesFromPath();
// Assign scan range locations only for broker load.
diff --git
a/fe/fe-core/src/main/java/org/apache/doris/datasource/scan/FilePartitionUtils.java
b/fe/fe-core/src/main/java/org/apache/doris/datasource/scan/FilePartitionUtils.java
index 096eb074f71..0b6e73ec4f6 100644
---
a/fe/fe-core/src/main/java/org/apache/doris/datasource/scan/FilePartitionUtils.java
+++
b/fe/fe-core/src/main/java/org/apache/doris/datasource/scan/FilePartitionUtils.java
@@ -17,6 +17,7 @@
package org.apache.doris.datasource.scan;
+import org.apache.doris.common.FeConstants;
import org.apache.doris.common.UserException;
import org.apache.doris.connector.spi.scan.ConnectorPartitionValues;
@@ -155,6 +156,30 @@ public final class FilePartitionUtils {
return new ParsedColumnsFromPath(Lists.newArrayList(columns),
Lists.newArrayList(columnValueIsNull));
}
+ /**
+ * Parses partition column values from a load source path. Unlike the
query path, a load also
+ * treats a literal {@code \N} directory value as NULL, the semantics
loads had before the
+ * null flag existed.
+ */
+ public static ParsedColumnsFromPath
parseColumnsFromPathWithNullInfoForLoad(
+ String filePath,
+ List<String> columnsFromPath,
+ boolean caseSensitive,
+ boolean isACID)
+ throws UserException {
+ ParsedColumnsFromPath parsed = parseColumnsFromPathWithNullInfo(
+ filePath, columnsFromPath, caseSensitive, isACID);
+ List<String> values = new ArrayList<>(parsed.getValues());
+ List<Boolean> isNull = new ArrayList<>(parsed.getIsNull());
+ for (int i = 0; i < values.size(); i++) {
+ if (FeConstants.null_string.equals(values.get(i))) {
+ values.set(i, "");
+ isNull.set(i, true);
+ }
+ }
+ return new ParsedColumnsFromPath(values, isNull);
+ }
+
public static ParsedColumnsFromPath normalizeColumnsFromPath(List<String>
columnsFromPath) {
if (columnsFromPath == null || columnsFromPath.isEmpty()) {
return new ParsedColumnsFromPath(Collections.emptyList(),
Collections.emptyList());
diff --git
a/fe/fe-core/src/main/java/org/apache/doris/nereids/load/NereidsFileGroupInfo.java
b/fe/fe-core/src/main/java/org/apache/doris/nereids/load/NereidsFileGroupInfo.java
index 2713365cf28..841965f63aa 100644
---
a/fe/fe-core/src/main/java/org/apache/doris/nereids/load/NereidsFileGroupInfo.java
+++
b/fe/fe-core/src/main/java/org/apache/doris/nereids/load/NereidsFileGroupInfo.java
@@ -276,7 +276,7 @@ public class NereidsFileGroupInfo {
fileStatus.path);
context.params.setCompressType(compressType);
FilePartitionUtils.ParsedColumnsFromPath columnsFromPath =
-
FilePartitionUtils.parseColumnsFromPathWithNullInfo(fileStatus.path,
+
FilePartitionUtils.parseColumnsFromPathWithNullInfoForLoad(fileStatus.path,
context.fileGroup.getColumnNamesFromPath(),
true, false);
List<String> columnsFromPathKeys =
context.fileGroup.getColumnNamesFromPath();
TFileRangeDesc rangeDesc = createFileRangeDesc(0, fileStatus,
fileStatus.size,
@@ -329,7 +329,7 @@ public class NereidsFileGroupInfo {
fileStatus.path);
context.params.setCompressType(compressType);
FilePartitionUtils.ParsedColumnsFromPath columnsFromPath =
-
FilePartitionUtils.parseColumnsFromPathWithNullInfo(fileStatus.path,
+
FilePartitionUtils.parseColumnsFromPathWithNullInfoForLoad(fileStatus.path,
context.fileGroup.getColumnNamesFromPath(), true,
false);
List<String> columnsFromPathKeys =
context.fileGroup.getColumnNamesFromPath();
// Assign scan range locations only for broker load.
diff --git
a/fe/fe-core/src/test/java/org/apache/doris/common/util/BrokerUtilTest.java
b/fe/fe-core/src/test/java/org/apache/doris/common/util/BrokerUtilTest.java
index 5e1d286fc84..bbe5e4340ed 100644
--- a/fe/fe-core/src/test/java/org/apache/doris/common/util/BrokerUtilTest.java
+++ b/fe/fe-core/src/test/java/org/apache/doris/common/util/BrokerUtilTest.java
@@ -151,4 +151,19 @@ public class BrokerUtilTest {
Assertions.assertEquals(Collections.singletonList(false),
parsed.getIsNull());
}
+ @Test
+ public void parseColumnsFromPathForLoadKeepsLegacyNullSemantics() throws
Exception {
+ FilePartitionUtils.ParsedColumnsFromPath parsed =
FilePartitionUtils.parseColumnsFromPathWithNullInfoForLoad(
+ "hdfs://host/table/p=\\N/data.orc",
Collections.singletonList("p"), true, false);
+ Assertions.assertEquals(Collections.singletonList(""),
parsed.getValues());
+ Assertions.assertEquals(Collections.singletonList(true),
parsed.getIsNull());
+
+ // Other values, including the Hive default partition, parse as on the
query path.
+ parsed = FilePartitionUtils.parseColumnsFromPathWithNullInfoForLoad(
+ "hdfs://host/table/p=a/q=" +
ConnectorPartitionValues.NULL_PARTITION_NAME + "/data.orc",
+ Lists.newArrayList("p", "q"), true, false);
+ Assertions.assertEquals(Lists.newArrayList("a", ""),
parsed.getValues());
+ Assertions.assertEquals(Lists.newArrayList(false, true),
parsed.getIsNull());
+ }
+
}
diff --git
a/fe/fe-core/src/test/java/org/apache/doris/nereids/load/NereidsFileGroupInfoTest.java
b/fe/fe-core/src/test/java/org/apache/doris/nereids/load/NereidsFileGroupInfoTest.java
new file mode 100644
index 00000000000..f6d19ff9b40
--- /dev/null
+++
b/fe/fe-core/src/test/java/org/apache/doris/nereids/load/NereidsFileGroupInfoTest.java
@@ -0,0 +1,81 @@
+// 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.load;
+
+import org.apache.doris.analysis.BrokerDesc;
+import org.apache.doris.analysis.StorageBackend;
+import org.apache.doris.catalog.OlapTable;
+import org.apache.doris.datasource.property.fileformat.CsvFileFormatProperties;
+import org.apache.doris.datasource.scan.FederationBackendPolicy;
+import org.apache.doris.load.loadv2.LoadTask;
+import org.apache.doris.system.Backend;
+import org.apache.doris.thrift.TBrokerFileStatus;
+import org.apache.doris.thrift.TFileRangeDesc;
+import org.apache.doris.thrift.TFileScanRangeParams;
+import org.apache.doris.thrift.TFileType;
+import org.apache.doris.thrift.TScanRangeLocations;
+
+import com.google.common.collect.Lists;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+import org.mockito.Mockito;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+
+class NereidsFileGroupInfoTest {
+ @Test
+ void loadRangesCarryLiteralNullPathValueAsNull() throws Exception {
+ // Splittable and unsplittable files build their ranges in different
methods.
+ for (boolean splittable : new boolean[] {false, true}) {
+ CsvFileFormatProperties format = new
CsvFileFormatProperties("csv");
+ format.analyzeFileFormatProperties(new HashMap<>(), true);
+ NereidsBrokerFileGroup fileGroup = new NereidsBrokerFileGroup(1L,
false, null,
+ Lists.newArrayList("s3://bucket/table/*/data.csv"), null,
null, Lists.newArrayList("p"),
+ Lists.newArrayList(), null, null, null, null,
LoadTask.MergeType.APPEND, null, -1L,
+ false, false, format);
+ BrokerDesc brokerDesc = Mockito.mock(BrokerDesc.class);
+
Mockito.when(brokerDesc.getFileType()).thenReturn(TFileType.FILE_S3);
+
Mockito.when(brokerDesc.getStorageType()).thenReturn(StorageBackend.StorageType.S3);
+ FederationBackendPolicy backendPolicy =
Mockito.mock(FederationBackendPolicy.class);
+ Mockito.when(backendPolicy.getNextBe()).thenReturn(new Backend(1L,
"127.0.0.1", 9050));
+ Mockito.when(backendPolicy.numBackends()).thenReturn(1);
+ TBrokerFileStatus fileStatus = new
TBrokerFileStatus("s3://bucket/table/p=\\N/data.csv", false, 10,
+ splittable);
+ NereidsFileGroupInfo fileGroupInfo = new NereidsFileGroupInfo(1L,
2L, Mockito.mock(OlapTable.class),
+ brokerDesc, fileGroup, Lists.newArrayList(fileStatus), 1,
false, 1);
+ fileGroupInfo.getFileStatusAndCalcInstance(backendPolicy);
+ NereidsParamCreateContext context = new
NereidsParamCreateContext();
+ context.fileGroup = fileGroup;
+ context.params = new TFileScanRangeParams();
+
+ List<TScanRangeLocations> locations = new ArrayList<>();
+ fileGroupInfo.createScanRangeLocations(context, backendPolicy,
locations);
+
+ Assertions.assertEquals(1, locations.size());
+ List<TFileRangeDesc> ranges =
locations.get(0).getScanRange().getExtScanRange().getFileScanRange()
+ .getRanges();
+ Assertions.assertEquals(1, ranges.size());
+ // A load keeps the meaning a literal \N had before the null flag
existed: SQL NULL.
+ Assertions.assertEquals(Collections.singletonList(""),
ranges.get(0).getColumnsFromPath());
+ Assertions.assertEquals(Collections.singletonList(true),
ranges.get(0).getColumnsFromPathIsNull());
+ }
+ }
+}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]