Repository navigation
fix: read Hive's null partition placeholder as NULL - #26310
xanderbailey wants to merge 5 commits into
Conversation
| @@ -63,164 +63,165 @@ SET datafusion.execution.target_partitions = '1'; | |||
|
|
|||
| The following configuration settings are available: | |||
|
|
|||
| | key | default | description | | |||
There was a problem hiding this comment.
Big diff here sorry, but the actual change is very small
|
Thank you for opening this pull request! Reviewer note: cargo-semver-checks reported the current version number is not SemVer-compatible with the changes in this pull request (compared against the base branch). Details |
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #26310 +/- ##
==========================================
+ Coverage 82.80% 82.85% +0.05%
==========================================
Files 1149 1151 +2
Lines 454146 456142 +1996
Branches 454146 456142 +1996
==========================================
+ Hits 376035 377932 +1897
+ Misses 55260 55250 -10
- Partials 22851 22960 +109 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
|
Hmm so if you specify the partition to be non-null now we will force it to be nullable... but the reverse was true before, if a user said it was non null, we would have made it nullable... This might need to be tracked as a follow-up. I'll have a look into this. |
Hive compliant writers put the rows whose partition value is null into a
directory named `__HIVE_DEFAULT_PARTITION__`. `ListingTable` cast that name
straight to the partition column's type, so a table with a null partition
could not be queried at all unless the column happened to be a string:
Cannot cast string '__HIVE_DEFAULT_PARTITION__' to value of Date32 type
Read such values as a typed NULL instead, as Hive, Spark and Impala do, and
declare partition columns nullable so that the NULL can be represented and
`IS NULL` selects the null partition. The placeholder name follows the new
`datafusion.execution.listing_table_hive_default_partition_name` option, so
tables written with a different `hive.exec.default.partition.name` can be
read; the empty name turns the mapping off.
Closes apache#18083
449165a to
4b6bdae
Compare
|
Wonder if we need to be fancier here and follow what https://github.com/apache/hive/blob/f75c18b57ae4218db689d1e76fabb1343fd78973/common/src/java/org/apache/hadoop/hive/common/FileUtils.java#L280-L359 does? |
|
@andygrove I came across apache/datafusion-ballista#2580 which you opened recently and thought this might be of interest to you. Let me know if you think the approach here is reasonable. It was a little more difficult than I thought it might be. |
andygrove
left a comment
There was a problem hiding this comment.
Heads up: this is an AI-assisted review pass. I've checked the points below against the diff, but I may have missed context or misjudged the intent, so please push back on anything that doesn't hold up.
Thanks for tackling this. Reading __HIVE_DEFAULT_PARTITION__ as NULL matches Hive and Spark, and the filter_partitioned_file change to treat a NULL predicate result as "no match" is a good catch.
Main concerns
1. Absent min/max for null partitions poisons merged statistics (datafusion/datasource/src/mod.rs, with_statistics)
A null partition value now gives min_value/max_value of Precision::Absent. Statistics::try_merge combines them with precision_min/precision_max, and Precision::min/max return Absent if either side is Absent. So a table with d=2024-01-01/ and d=__HIVE_DEFAULT_PARTITION__/ loses the min/max for d at the file-group and table level, even though every non-null file has exact bounds. Stats-based min(d)/max(d) and partition-bound pruning then silently degrade once any null partition directory exists. The slt tests wouldn't catch this because they fall back to a scan.
Could merging skip all-null containers when combining min/max? A test that merges stats from one null-partition file and one non-null file would pin this down.
2. MinMaxStatistics::new_from_files errors on a null partition (datafusion/datasource/src/statistics.rs)
with_statistics appends the partition column's stats to column_statistics, so get_min_max takes the first branch and calls .get_value().zip(...).ok_or_else(|| "statistics not found"). With Absent min/max, that now returns an error for any file with a null partition value. For a table with WITH ORDER (d) or a sort on a partition column, split_groups_by_statistics_with_target_partitions would then fail. If I'm reading this right, the failure is only logged at debug level and the split into non-overlapping ordered groups is skipped, so the plan gets extra sorts or loses its output ordering. Should a null partition be handled here as a null min/max (the row converter can sort nulls)?
3. distinct_count = Exact(0) (datafusion/datasource/src/mod.rs)
I'm not sure this is wrong, but I'd like to understand the intent. If distinct_count is meant to count NULL as a group, then Exact(0) under-reports and could make cardinality estimates for GROUP BY d or joins on d too low. If it excludes NULLs, 0 is consistent. Either way, when num_rows is Absent, null_count is Absent but distinct_count is still Exact(0), which claims more than is known. Would Absent be safer there?
4. Default-on behaviour change (datafusion/common/src/config.rs)
Any existing table with a literal col=__HIVE_DEFAULT_PARTITION__ directory, for example data written by something that used it as a real string value, will now read that value as NULL. WHERE col = '__HIVE_DEFAULT_PARTITION__' will then match nothing. Matching Hive/Spark by default is defensible, but it's a silent change of results, so it deserves an explicit call-out in the PR description and upgrade notes. The opt-out is also session-wide rather than per table (for example a ListingOptions field), so a table that mixes both conventions can't be read correctly. Is a session-wide setting enough for the use cases you have in mind?
5. All partition columns become nullable (datafusion/catalog-listing/src/table.rs)
try_new and scan now declare every partition column nullable, even when no null partition exists. That's why insert_to_external.slt changed. The side effects are that nullability-based optimizations (IS NOT NULL simplification, non-null join keys) are lost for every ListingTable. Other paths still build non-nullable partition fields, so a FileScanConfig built directly with a non-nullable partition field and a null partition value would hit Arrow's "declared non-nullable but contains null" error. Is it worth only making the field nullable when needed, or at least documenting this for direct FileScanConfig users?
Minor
display_default(config.rs): it backticks any default containing_or*across all config entries, including extension configs, to fix the rendering of one new default. Would it be narrower to quote just this default in its doc comment? As written it also changes how existing entries render, and|in a default still isn't escaped.
Overall the approach looks reasonable. Items 1 and 2 are the ones I'd want resolved before merge, since they turn into plan and statistics regressions as soon as a null partition exists.
da61d28 to
c6f7cd9
Compare
|
Thanks for the thorough review @andygrove! Going through the main points: 1. Absent min/max wipes out merged stats 2. 3. 4. Default-on behaviour change 5. All partition columns become nullable |
|
Going to write the update guide and then I want to dig into the first point a little more |
0b442ba to
f736cb0
Compare
|
Dug into point 1 a bit more. I have managed to convince myself that this is okay from a correctness point of view as it currently stands, the proper fix is what you suggested: when merging min/max in |
|
Thanks again @andygrove, this should be good for another look now |
Which issue does this PR close?
Rationale for this change
Hive compliant writers put the rows whose partition value is null into a directory named
__HIVE_DEFAULT_PARTITION__(Hive'shive.exec.default.partition.name).ListingTablecasts that directory name straight to the partition column's type, so a table with a null partition cannot be queried at all unless the partition column happens to be a string:For a string partition column the query runs, but the value comes back as the literal
__HIVE_DEFAULT_PARTITION__rather than NULL, andWHERE d IS NULLnever matches it because partition columns are declared non-nullable. Hive-written tables are a common input, and both Spark and Impala read these values as NULL.We are not touching writes in this PR - just reads.
We also haven’t fixed the /date=/a.parquet problem here either.
What changes are included in this PR?
(
helpers::try_into_partitioned_file).ListingTabledeclares partition columns nullable, in both the table schema and the filesource's
TableSchema, so the NULL can be represented andIS NULLselects the null partition.datafusion.execution.listing_table_hive_default_partition_name, defaulting to__HIVE_DEFAULT_PARTITION__, so a table written with a differenthive.exec.default.partition.namecan be read. The empty name turns the mapping off and reads the directory name as the value. The default is also exposed asdatafusion_common::config::HIVE_DEFAULT_PARTITION_NAME.filter_partitioned_fileno longer treats a NULL predicate result as a match. That is reachable now that a partition value can be NULLconfigs.md, defaults containing_or*are shown as code. Markdown would otherwise read__HIVE_DEFAULT_PARTITION__as emphasis and render it as bold text; this also backticks the two existing such defaults,nulls_maxandstart_offset.What is the testing strategy for this PR?
Unit tests and slt tests added.
Are there any user-facing changes?
partition columns are now forced to be nullable
listing_table_hive_default_partition_name added