github-actions[bot] commented on code in PR #66010:
URL: https://github.com/apache/doris/pull/66010#discussion_r3712631501
##########
fe/fe-core/src/main/java/org/apache/doris/datasource/FileQueryScanNode.java:
##########
@@ -410,11 +409,14 @@ private TScanRangeLocations splitToScanRange(
HiveSplit hiveSplit = (HiveSplit) fileSplit;
isACID = hiveSplit.isACID();
}
- List<String> partitionValuesFromPath = fileSplit.getPartitionValues()
== null
- ? BrokerUtil.parseColumnsFromPath(fileSplit.getPathString(),
pathPartitionKeys,
- false, isACID) : fileSplit.getPartitionValues();
+ FilePartitionUtils.ParsedColumnsFromPath partitionValuesFromPath =
+ fileSplit.getPartitionValues() == null
+ ? FilePartitionUtils.parseColumnsFromPathWithNullInfo(
Review Comment:
[P1] Keep explicit NULLs representable for TVF path columns
This shared path now turns `__HIVE_DEFAULT_PARTITION__` into `""/true` for
static file TVFs too, but `ExternalFileTableValuedFunction.fillColumns()`
declares every `path_partition_keys` column non-nullable. BE's explicit-NULL
branch DCHECKs that the slot is nullable and then inserts defaults; for this
slot that is an assertion failure in debug builds and an empty string rather
than SQL NULL in release builds. Please make TVF path columns nullable (or keep
a clearly defined literal contract for non-nullable TVFs) and add an HDFS/S3
TVF case that selects and filters a sentinel-valued path key.
##########
fe/fe-core/src/main/java/org/apache/doris/datasource/iceberg/IcebergUtils.java:
##########
@@ -638,59 +639,70 @@ public static Type
icebergTypeToDorisType(org.apache.iceberg.types.Type type, bo
}
/**
- * Get partition info map for identity partitions only, considering
partition
- * evolution.
- * For non-identity partitions (e.g., day, bucket, truncate), returns null
to
- * skip
- * dynamic partition pruning.
- *
- * @param partitionData The partition data from the file
- * @param partitionSpec The partition spec corresponding to the file's
specId
- * (required)
- * @param timeZone The time zone for timestamp serialization
- * @return Map of partition field name to partition value string, or null
if
- * there are non-identity partitions
+ * Get identity partition columns that exist in all partition specs.
+ * The file scanner uses partition columns in the first scan range for all
ranges,
+ * so only common identity partition columns can be used for partition
pruning.
*/
- public static Map<String, String> getPartitionInfoMap(PartitionData
partitionData, PartitionSpec partitionSpec,
- String timeZone) {
- Map<String, String> partitionInfoMap = new HashMap<>();
- List<NestedField> fields =
partitionData.getPartitionType().asNestedType().fields();
+ public static List<String> getCommonIdentityPartitionColumns(Table table) {
+ LinkedHashSet<Integer> commonSourceIds = new LinkedHashSet<>();
+ for (PartitionField field : table.spec().fields()) {
+ NestedField sourceField =
table.schema().findField(field.sourceId());
+ if (field.transform().isIdentity() && sourceField != null
+ &&
isSupportedPartitionValueType(sourceField.type().typeId())) {
+ commonSourceIds.add(field.sourceId());
+ }
+ }
+ for (PartitionSpec spec : table.specs().values()) {
+ Set<Integer> specIdentitySourceIds = spec.fields().stream()
+ .filter(field -> field.transform().isIdentity())
+ .map(PartitionField::sourceId)
+ .collect(Collectors.toSet());
+ commonSourceIds.retainAll(specIdentitySourceIds);
Review Comment:
[P1] Keep old manifest values available after partition evolution
Intersecting every historical spec with `table.spec()` drops an identity
source as soon as a later default spec removes or transforms it—even if the
selected snapshot contains only old-spec files. For Hive-migrated files, that
manifest value may be the only copy because the physical file omits the Hive
partition column. Once the key is removed here, `setPartitionValues` emits no
metadata and branch-4.0's missing-column path fills NULL/default instead, so
current or time-travel queries return wrong values. Please separate the
scan-wide pruning key set from spec-specific materialization metadata (and
collect it by each planned file's `specId`), with a migrated-file plus
spec-evolution test.
##########
fe/fe-core/src/main/java/org/apache/doris/datasource/iceberg/source/IcebergScanNode.java:
##########
@@ -299,23 +301,44 @@ private void setIcebergParams(TFileRangeDesc rangeDesc,
IcebergSplit icebergSpli
}
}
tableFormatFileDesc.setIcebergParams(fileDesc);
- Map<String, String> partitionValues =
icebergSplit.getIcebergPartitionValues();
- if (partitionValues != null) {
- List<String> fromPathKeys = new ArrayList<>();
- List<String> fromPathValues = new ArrayList<>();
- List<Boolean> fromPathIsNull = new ArrayList<>();
- for (Map.Entry<String, String> entry : partitionValues.entrySet())
{
- fromPathKeys.add(entry.getKey());
- fromPathValues.add(entry.getValue() != null ? entry.getValue()
: "");
- fromPathIsNull.add(entry.getValue() == null);
- }
- rangeDesc.setColumnsFromPathKeys(fromPathKeys);
- rangeDesc.setColumnsFromPath(fromPathValues);
- rangeDesc.setColumnsFromPathIsNull(fromPathIsNull);
- }
+ setPartitionValues(rangeDesc,
icebergSplit.getIcebergPartitionValues());
rangeDesc.setTableFormatParams(tableFormatFileDesc);
}
+ private List<String> getOrderedPathPartitionKeys() {
+ if (icebergTable == null) {
+ return Collections.emptyList();
+ }
+ return IcebergUtils.getCommonIdentityPartitionColumns(icebergTable);
+ }
+
+ @VisibleForTesting
+ void setPartitionValues(TFileRangeDesc rangeDesc, Map<String, String>
partitionValues) {
+ rangeDesc.unsetColumnsFromPathKeys();
+ rangeDesc.unsetColumnsFromPath();
+ rangeDesc.unsetColumnsFromPathIsNull();
+
+ List<String> orderedPartitionKeys = getOrderedPathPartitionKeys();
+ if (orderedPartitionKeys.isEmpty()) {
+ return;
+ }
+ Preconditions.checkState(partitionValues != null,
+ "Missing partition values for Iceberg identity-partitioned
table");
+
+ List<String> fromPathValues = new
ArrayList<>(orderedPartitionKeys.size());
+ List<Boolean> fromPathIsNull = new
ArrayList<>(orderedPartitionKeys.size());
+ for (String partitionKey : orderedPartitionKeys) {
+ Preconditions.checkState(partitionValues.containsKey(partitionKey),
+ "Missing partition value for Iceberg partition key: %s",
partitionKey);
+ String partitionValue = partitionValues.get(partitionKey);
+ fromPathValues.add(partitionValue == null ? "" : partitionValue);
+ fromPathIsNull.add(partitionValue == null);
+ }
+ rangeDesc.setColumnsFromPathKeys(orderedPartitionKeys);
Review Comment:
[P1] Make Iceberg missing-column fallback per range and per column
Sending these keys on every dual file/partition slot exposes branch-4.0's
scan-wide fallback bug. When any advertised identity column is absent,
`FileScanner` permanently flips `_fill_partition_from_path` to true and passes
*all* partition descriptors to the reader. A later range (or another identity
column in the same range) that physically contains the column is still read by
Parquet/ORC and then has another `rows` manifest values appended, yielding
inconsistent column sizes or wrong materialization. Reset the decision per
reader and pass only the current file's actually missing keys; please cover
partial-missing and missing-then-present Parquet/ORC ranges with pruning both
on and off.
##########
fe/fe-core/src/main/java/org/apache/doris/datasource/hudi/source/HudiScanNode.java:
##########
@@ -324,8 +325,11 @@ private void setHudiParams(TFileRangeDesc rangeDesc,
HudiSplit hudiSplit) {
formPathKeys.add(entry.getKey());
formPathValues.add(entry.getValue());
}
+ FilePartitionUtils.ParsedColumnsFromPath parsedColumnsFromPath =
+
FilePartitionUtils.normalizeColumnsFromPath(formPathValues);
Review Comment:
[P1] Preserve NULL before stringifying Hudi partition keys
This normalizer cannot recover a Hudi NULL on the normal snapshot path.
`TablePartitionValues` first converts the Hive sentinel to a `NullLiteral` with
`isHive=false`; `getPartitionValuesAsStringList()` then turns it into the
ordinary string `"NULL"`. Both the generic split values and this Hudi map
therefore reach `normalizeColumnsFromPath` as `"NULL"/false`, so BE
materializes a literal string (or fails to parse a typed partition) instead of
SQL NULL. The existing Hudi MV regression even documents the resulting lost
null rows. Please carry Java null/an explicit null bit out of `PartitionKey`
before stringification and cover pruning on and off.
##########
fe/fe-core/src/main/java/org/apache/doris/datasource/iceberg/IcebergUtils.java:
##########
@@ -638,59 +639,70 @@ public static Type
icebergTypeToDorisType(org.apache.iceberg.types.Type type, bo
}
/**
- * Get partition info map for identity partitions only, considering
partition
- * evolution.
- * For non-identity partitions (e.g., day, bucket, truncate), returns null
to
- * skip
- * dynamic partition pruning.
- *
- * @param partitionData The partition data from the file
- * @param partitionSpec The partition spec corresponding to the file's
specId
- * (required)
- * @param timeZone The time zone for timestamp serialization
- * @return Map of partition field name to partition value string, or null
if
- * there are non-identity partitions
+ * Get identity partition columns that exist in all partition specs.
+ * The file scanner uses partition columns in the first scan range for all
ranges,
+ * so only common identity partition columns can be used for partition
pruning.
*/
- public static Map<String, String> getPartitionInfoMap(PartitionData
partitionData, PartitionSpec partitionSpec,
- String timeZone) {
- Map<String, String> partitionInfoMap = new HashMap<>();
- List<NestedField> fields =
partitionData.getPartitionType().asNestedType().fields();
+ public static List<String> getCommonIdentityPartitionColumns(Table table) {
+ LinkedHashSet<Integer> commonSourceIds = new LinkedHashSet<>();
+ for (PartitionField field : table.spec().fields()) {
+ NestedField sourceField =
table.schema().findField(field.sourceId());
+ if (field.transform().isIdentity() && sourceField != null
+ &&
isSupportedPartitionValueType(sourceField.type().typeId())) {
+ commonSourceIds.add(field.sourceId());
+ }
+ }
+ for (PartitionSpec spec : table.specs().values()) {
+ Set<Integer> specIdentitySourceIds = spec.fields().stream()
+ .filter(field -> field.transform().isIdentity())
+ .map(PartitionField::sourceId)
+ .collect(Collectors.toSet());
+ commonSourceIds.retainAll(specIdentitySourceIds);
+ }
+ return commonSourceIds.stream()
+ .map(table.schema()::findColumnName)
+ .filter(Objects::nonNull)
+ .collect(Collectors.toList());
+ }
- // Check if all partition fields are identity transform
- // If any field is not identity, return null to skip dynamic partition
pruning
+ public static Map<String, String>
getIdentityPartitionInfoMap(PartitionData partitionData,
+ PartitionSpec partitionSpec, Table table, String timeZone) {
+ Map<String, String> partitionInfoMap = Maps.newLinkedHashMap();
+ List<NestedField> fields =
partitionData.getPartitionType().asNestedType().fields();
List<PartitionField> partitionFields = partitionSpec.fields();
Preconditions.checkArgument(fields.size() == partitionFields.size(),
"PartitionData fields size does not match PartitionSpec fields
size");
for (int i = 0; i < fields.size(); i++) {
NestedField field = fields.get(i);
PartitionField partitionField = partitionFields.get(i);
-
- // Only process identity transform partitions
- // For other transforms (day, bucket, truncate, etc.), skip
dynamic partition
- // pruning
if (!partitionField.transform().isIdentity()) {
- if (LOG.isDebugEnabled()) {
- LOG.debug(
- "Skip dynamic partition pruning for non-identity
partition field: {} with transform: {}",
- field.name(),
partitionField.transform().toString());
- }
- return null;
+ continue;
+ }
+ if (!isSupportedPartitionValueType(field.type().typeId())) {
+ continue;
+ }
+ String columnName =
table.schema().findColumnName(partitionField.sourceId());
Review Comment:
[P1] Resolve partition names from the selected snapshot schema
`useSnapshot`/`useRef` selects historical data, and Doris builds that
query's tuple from the selected snapshot schema, but both new identity helpers
resolve the stable source ID through the latest `table.schema()`. After
`old_name` is renamed to `new_name`, an old-snapshot tuple still has `old_name`
while these range keys use `new_name`; BE matches them by exact name, so a
migrated file that omits the physical column receives NULL/default instead of
its manifest value. Please derive both ordered keys and per-spec values from
the selected schema/tuple mapping, with snapshot/tag/branch coverage across an
identity-source rename.
##########
fe/fe-core/src/main/java/org/apache/doris/datasource/iceberg/source/IcebergScanNode.java:
##########
@@ -771,25 +794,15 @@ private Split createIcebergSplit(FileScanTask
fileScanTask) {
split.setTargetSplitSize(targetSplitSize);
if (isPartitionedTable) {
PartitionData partitionData = (PartitionData)
fileScanTask.file().partition();
- if (sessionVariable.isEnableRuntimeFilterPartitionPrune()) {
- // Get specId and corresponding PartitionSpec to handle
partition evolution
- int specId = fileScanTask.file().specId();
- PartitionSpec partitionSpec = icebergTable.specs().get(specId);
-
- Preconditions.checkNotNull(partitionSpec, "Partition spec with
specId %s not found for table %s",
- specId, icebergTable.name());
- Map<String, String> partitionInfoMap =
partitionMapInfos.computeIfAbsent(
- partitionData, k -> {
- return
IcebergUtils.getPartitionInfoMap(partitionData, partitionSpec,
- sessionVariable.getTimeZone());
- });
- // Only set partition values if all partitions are identity
transform
- // For non-identity partitions, getPartitionInfoMap returns
null to skip dynamic partition pruning
- if (partitionInfoMap != null) {
- split.setIcebergPartitionValues(partitionInfoMap);
- }
- } else {
- partitionMapInfos.put(partitionData, null);
+ int specId = fileScanTask.file().specId();
+ PartitionSpec partitionSpec = icebergTable.specs().get(specId);
+ Preconditions.checkNotNull(partitionSpec, "Partition spec with
specId %s not found for table %s",
+ specId, icebergTable.name());
+ Map<String, String> partitionInfoMap =
partitionMapInfos.computeIfAbsent(
+ Pair.of(specId, partitionData), k ->
IcebergUtils.getIdentityPartitionInfoMap(
Review Comment:
[P2] Normalize negative timestamp microseconds before materializing them
This call is now unconditional, but `serializePartitionValue` still splits
timestamps with `/` and `%`. For a valid pre-epoch value such as
`timestampMicros = -1`, Java produces epoch seconds `0` and nanoseconds
`-1000`, so `LocalDateTime.ofEpochSecond` throws and the split cannot be
planned (the caller catches only `UnsupportedOperationException`). Use
`Math.floorDiv`/`Math.floorMod` and add negative fractional
TIMESTAMP/TIMESTAMPTZ cases, especially now that disabling runtime partition
pruning no longer avoids serialization.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]