Export partition - allow non matching partition expressions in case we can prove the destination expression does not split the data - #2074
Conversation
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 3f3af7c4fc
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
|
Consider mentioning 'export' in the title and/or description |
Done, I'll soon add the description |
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: ca43cf1f6d
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
I agree. I'd much rather do it the way you described: perform my own investigation on the findings and raise issues as needed-and we actually do that. But even in that case, I would still have to post this exact message first. The purpose of the audit review has always been to perform a quick review of the PR without running any tests first, share the findings with the developer, and let you decide whether they are actual issues. If they are, we then raise separate issues afterward. I can update the skills we use for the audit review to make the output easier to read, but overall, the audit review has always been a separate part of the verification process, separate from our actual testing. |
|
@k-morozov hi, I have made some refactorings, fixed conflicts and updated the docs. Could you please re-review it? |
|
@Selfeer regarding the 5 AI findings you posted
|
| "To allow its usage, enable the setting `allow_insert_into_iceberg`."); | ||
| } | ||
|
|
||
| ExportPartitionUtils::verifyExportSchemaCastable( |
There was a problem hiding this comment.
Calls ExportPartitionUtils::verifyExportSchemaCastable twice here and below.
There was a problem hiding this comment.
Thanks, bad rebase
| ) | ||
|
|
||
|
|
||
| def test_export_part_unnamed_tuple_partition_key_owner_matching_named_destination_is_allowed(cluster): |
| /// so the same expression over the two types can produce different partitions. | ||
| bool isSameTypeForPartitioning(const DataTypePtr & lhs, const DataTypePtr & rhs) | ||
| { | ||
| return lhs->getName() == rhs->getName(); |
There was a problem hiding this comment.
What about unnamed Tuple, Array, etc?
Does it related with test test_export_part_unnamed_tuple_partition_key_owner_matching_named_destination_is_allowed that was removed?
up: maybe haveSameTupleElementLayout will be usefull.
There was a problem hiding this comment.
Answering #2074 (comment) here as it relates to this question.
The main reason I removed that test, and yes, it is affected by this name check, is to draw a line of "good enough" and move on. This PR needs to get merged so that we fix the broader and more common use cases, we can worry about named tuples later.
|
|
||
| /// A NULL value forms its own destination partition, so a Nullable column may split the source | ||
| /// partition; min/max cannot rule that out. Require a structural match for such columns. | ||
| if (isNullableOrLowCardinalityNullable(source_type)) |
There was a problem hiding this comment.
Seems that this behauvor should be in documentation (and motivation maybe?) Also I need to find tests for this condition..
|
|
||
| /// Compute the global min/max index of the parts | ||
| IMergeTreeDataPart::MinMaxIndex minmax; | ||
| for (const auto & part : parts) |
There was a problem hiding this comment.
As I can see, we prepare mimax every time even if we have an equal partition key. But we really need it only for verifyOutputMapsToSinglePartition.
There was a problem hiding this comment.
Right, but verifyOutputMapsToSinglePartition is called within a for loop. What is your suggestion?
There was a problem hiding this comment.
For example, create 2 loops:
1 for happy path.
2. for verifyOutputMapsToSinglePartition.
So we could create minmax between 1 and 2 steps.
Another way - to fill minmax within the current loop only once before verifyOutputMapsToSinglePartition.
There was a problem hiding this comment.
Imo, that's somewhat of an unecessary "optimization" that will not give us any speed up with the cost of more code. I vote for keeping it as is unless you strongly object.
| if (parquet_row_group_size_bytes) | ||
| json.set("parquet_row_group_size_bytes", *parquet_row_group_size_bytes); | ||
| if (iceberg_partition_timezone) | ||
| json.set("iceberg_partition_timezone", *iceberg_partition_timezone); |
There was a problem hiding this comment.
We try to set this setting for export. But if we have several columns with different timezones, we would set the value from the setting for all of them, right?
There was a problem hiding this comment.
I think that's how this setting was designed, @ianton-ru can confirm. AFAIK, this setting was created due to a misconception on the Apache Iceberg specs (the spec itself is poorly written) when it was believed we could tweak the timezone of an apache iceberg column. When in reality, according to 3rd party docs, the types are always UTC.
|
export partition regression tests are green, from audit review there is one low issue found: Low: In |
This is not related to this PR specifically, but we should indeed fix it. Should be trivial |
|
And here is a small report on the regression fails, seems safe to merge right now. These are not broken by the PR but will be addressed on the regression suite for the final version. This report is mostly for the purpose of revisiting it in the future if needed. 1. Regression test failures — all expected fallout of the behaviour change1.1
|
| Scenario | Source PARTITION BY |
Destination PARTITION BY |
Why it is accepted |
|---|---|---|---|
rejected: compound field order reversed |
(year, region) |
(region, year) |
Matching is per column; the destination defines tuple order. Both columns structurally match. |
rejected: field-count mismatch (2 vs 1) |
(year, region) |
year |
The destination key is a subset of the source key, so each source partition holds exactly one year. |
rejected: partitioned source, unpartitioned destination |
year |
(none) | An unpartitioned destination is a single partition, so nothing can split. spec_size == 0 returns early. |
Upstream made the same call in its own suite: this PR deletes test_rejected_field_count_mismatch
and test_rejected_compound_order_reversed from
tests/integration/test_storage_iceberg_with_spark/test_export_partition_iceberg.py, and adds
test_export_partition_multicolumn_subset_accepted / test_export_partition_subset_reversed_order_accepted
plus the new stateless 03572_export_part_hive_partition_subset.sh. Our tests should follow, and the
positive cases should also assert the Iceberg manifest partition tuple, not just a zero exit code.
Group B — cases where a single seed row cannot force a split (3 scenarios)
These schema pairs are genuinely unsafe in general, and the gate does reject them when the data
actually spans two destination partitions. But _run_rejected_case inserts one row:
with And("insert a seed row so the partition exists"):
insert_data(table_name=source_table, values=values)With one row, min == max for every partition column, so the dynamic proof evaluates the destination
expression at two identical endpoints, gets one value, and correctly concludes the partition maps to a
single destination partition. The accept is the gate working as designed on trivially single-valued
data — not a product bug.
| Scenario | Source | Destination | Seed value | Why one row is not enough |
|---|---|---|---|---|
rejected: transform vs identity on same column |
toYearNumSinceEpoch(event_date) |
event_date (identity) |
(1, '2020-01-01') |
Needs ≥2 rows with different dates in the same year, e.g. '2020-01-01' and '2020-06-15'. |
rejected: truncate width mismatch ([4] vs [8]) |
icebergTruncate(4, category) |
icebergTruncate(8, category) |
(1, 'clickhouse') |
Needs ≥2 values sharing the first 4 bytes but differing in bytes 5–8, e.g. 'clickhouse' and 'clickmouse'. |
rejected: unsupported MergeTree expression (intDiv) |
intDiv(year, 100) |
year (identity) |
(1, 2020) |
Needs ≥2 rows in the same century bucket, e.g. year = 2020 and year = 2021. |
Upstream reworked its equivalents the same way — test_rejected_truncate_width_mismatch was
"reworked around coarser source (truncate[4]) → finer destination (truncate[8]) with values that
split".
Positive control that the gate is not just rubber-stamping everything: two sibling scenarios in the
same feature still passed with the same single-row setup —
rejected: bucket width mismatch ([8] vs [16]) (icebergBucket is a hash, so the dynamic path always
rejects it; only a structural bucket match is accepted) and
rejected: different partition columns (year vs id) (the destination column is absent from the source
partition key). That is exactly the documented split between structural-only and dynamically-provable
transforms, so the gate is discriminating correctly.
|
Dropped the |
Changelog category (leave one):
Changelog entry (a user-readable short description of the changes that goes to CHANGELOG.md):
Allow export partition through different partition expressions as long as the destination expression does not repartition the data. This is validated at schedule time through two mechanisms:
Documentation entry for user-facing changes
...
CI/CD Options
Exclude tests:
Regression jobs to run: