Skip to content

Export partition - allow non matching partition expressions in case we can prove the destination expression does not split the data - #2074

Merged
arthurpassos merged 46 commits into
antalya-26.3from
feature/antalya-26.3/export-partition-monotonic-compat
Aug 25, 2026
Merged

Export partition - allow non matching partition expressions in case we can prove the destination expression does not split the data#2074
arthurpassos merged 46 commits into
antalya-26.3from
feature/antalya-26.3/export-partition-monotonic-compat

Conversation

@arthurpassos

@arthurpassos arthurpassos commented Jul 17, 2026

Copy link
Copy Markdown
Collaborator

Changelog category (leave one):

  • Improvement

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:

  1. Structural match: in case the source and destination are identical, the destination expression is a subset of the source expression or the destination expression can be entirely computed using only constants and the exact values guaranteed (pinned) by the source.
  2. Dynamic proof: the destination expression is monotonic over the source partition min/max range.

Documentation entry for user-facing changes

...

CI/CD Options

Exclude tests:

  • Fast test
  • Integration Tests
  • Stateless tests
  • Stateful tests
  • Performance tests
  • Aarch64 tests
  • All with ASAN
  • All with TSAN
  • All with MSAN
  • All with UBSAN
  • All with Coverage
  • All Regression
  • Disable CI Cache

Regression jobs to run:

  • Fast suites (mostly <1h)
  • Aggregate Functions (2h)
  • Alter (1.5h)
  • Benchmark (30m)
  • ClickHouse Keeper (1h)
  • Iceberg (2h)
  • LDAP (1h)
  • OAuth (5m)
  • Parquet (1.5h)
  • RBAC (1.5h)
  • SSL Server (1h)
  • S3 (2h)
  • S3 Export (2h)
  • Swarms (30m)
  • Tiered Storage (2h)

@github-actions

github-actions Bot commented Jul 17, 2026

Copy link
Copy Markdown

Workflow [PR], commit [9a1cb91]

@arthurpassos

Copy link
Copy Markdown
Collaborator Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread src/Storages/MergeTree/ExportPartitionUtils.cpp Outdated
Comment thread src/Storages/MergeTree/ExportPartitionUtils.cpp Outdated
@ilejn

ilejn commented Jul 21, 2026

Copy link
Copy Markdown
Collaborator

Consider mentioning 'export' in the title and/or description

@arthurpassos arthurpassos changed the title Allow non matching partition expressions iff monotonicity is proved and data is not repartitioned Export partition - allow non matching partition expressions iff monotonicity is proved and data is not repartitioned Jul 21, 2026
@arthurpassos

Copy link
Copy Markdown
Collaborator Author

Consider mentioning 'export' in the title and/or description

Done, I'll soon add the description

@arthurpassos

Copy link
Copy Markdown
Collaborator Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread src/Storages/MergeTree/ExportPartitionUtils.cpp
@Selfeer

Selfeer commented Aug 6, 2026

Copy link
Copy Markdown
Collaborator

@Selfeer I would appreciate if you could "humanize" a bit more those AI reports. For example, the following issue is very hard to understand and doesn't explain what is happening: 1. 🔴 High — Argument order lost, monotonicity proof evaluates the wrong expression.

It says "anchor" pointing to some code location. Then it says the impact. Then the trigger. It is too much to read to understand the real problem. Brain energy required to process this is very high.

Instead, if you could understand the issue yourself first and then give me a few SQL instructions that repro the case and a human explanation of what's going on, that would be 100 times better.

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.

@arthurpassos arthurpassos changed the title Export partition - allow non matching partition expressions iff monotonicity is proved and data is not repartitioned Export partition - allow non matching partition expressions in case we can prove the destination expression does not split the data Aug 20, 2026
@arthurpassos

Copy link
Copy Markdown
Collaborator Author

@k-morozov hi, I have made some refactorings, fixed conflicts and updated the docs. Could you please re-review it?

@arthurpassos

Copy link
Copy Markdown
Collaborator Author

@Selfeer regarding the 5 AI findings you posted

  1. Should be fixed
  2. Seems like AI gibberish. At the very least, not related to this pr
  3. Should be fixed
  4. Outdated, that code path does not exist anymore
  5. Not related to this pr

"To allow its usage, enable the setting `allow_insert_into_iceberg`.");
}

ExportPartitionUtils::verifyExportSchemaCastable(

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Calls ExportPartitionUtils::verifyExportSchemaCastable twice here and below.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, bad rebase

)


def test_export_part_unnamed_tuple_partition_key_owner_matching_named_destination_is_allowed(cluster):

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the reason?

/// 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();

@k-morozov k-morozov Aug 21, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, but verifyOutputMapsToSinglePartition is called within a for loop. What is your suggestion?

@k-morozov k-morozov Aug 21, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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);

@k-morozov k-morozov Aug 21, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread src/Storages/MergeTree/ExportPartitionUtils.cpp
k-morozov
k-morozov previously approved these changes Aug 21, 2026
@Selfeer

Selfeer commented Aug 25, 2026

Copy link
Copy Markdown
Collaborator

export partition regression tests are green, from audit review there is one low issue found:

Low: In StorageReplicatedMergeTree::exportPartitionToTable, export_merge_tree_partition_force_export = 1 removes an existing export znode via tryRemoveRecursive before the new plain-destination partition-compatibility validation runs, so an invalid forced request can cancel an in-progress export and then fail without scheduling a replacement.

@arthurpassos

Copy link
Copy Markdown
Collaborator Author

export partition regression tests are green, from audit review there is one low issue found:

Low: In StorageReplicatedMergeTree::exportPartitionToTable, export_merge_tree_partition_force_export = 1 removes an existing export znode via tryRemoveRecursive before the new plain-destination partition-compatibility validation runs, so an invalid forced request can cancel an in-progress export and then fail without scheduling a replacement.

This is not related to this PR specifically, but we should indeed fix it. Should be trivial

@Selfeer

Selfeer commented Aug 25, 2026

Copy link
Copy Markdown
Collaborator

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 change

1.1 s3_export_part — 1 scenario — stale error-message assertion

Failing test: /s3/minio/export tests/export part/error handling/different partition key

Setup: source MergeTree ... PARTITION BY i, destination S3(..., partition_strategy='hive') PARTITION BY p.

What CI observed:

Code: 36. DB::Exception: Cannot export partition: the destination partition expression
uses column 'p', which is not part of the source MergeTree partition key.. (BAD_ARGUMENTS)

with exitcode == 36 for every one of the 15 exported parts.

Why it failed: the scenario asserts the literal old wording.

    with Then("I should see an error related to the different partition key"):
        assert results[0].exitcode == 36, error()
        assert "Tables have different partition key" in results[0].output, error()

Verdict: not a bug — behaviour is correct, the assertion is stale.

The destination partitions by p, which is not in the source partition key at all, so it is rejected
before and after this PR, with the same BAD_ARGUMENTS / code 36, synchronously at schedule time.
Only the message text changed, from the removed AST string comparison to the new per-column message.
Nothing about the product regressed here.

Note — the export_partition twin was already fixed, export_part was missed. The mirror
scenario in s3/tests/export_partition/error_handling.py already accepts either wording:

    with Then("I should see an error related to the different partition key"):
        assert results[0].exitcode == 36, error()
        assert (
            "Tables have different partition key" in results[0].output
            or "not part of the source MergeTree partition key" in results[0].output
        ), error(results[0].output)

That is exactly why s3_export_partition was green in this same run while s3_export_part failed.
The fix is to apply the same both-wordings assertion (or the new wording alone, once the base branch
has the PR) to s3/tests/export_part/error_handling.py:171. The prose requirement in
s3/requirements/export_part.md:299 and its generated export_part.py:435, which both quote
"Tables have different partition key" verbatim, need the same treatment.

1.2 iceberg_2 — 6 scenarios in /iceberg/export partition/no catalog/replicated merge tree/partition compatibility/rejected

All six failed the same way: the test expected exitcode 36 and got exitcode 0 — the export was
accepted. They split into two distinct root causes.

Group A — cases the PR intentionally accepts now (3 scenarios)

The test expectation itself is obsolete; these need to move from "rejected" to "accepted".

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.

@arthurpassos

Copy link
Copy Markdown
Collaborator Author

@Selfeer issue link for the bug you mentioned above #2258

@Selfeer Selfeer added the verified Approved for release label Aug 25, 2026
@arthurpassos
arthurpassos merged commit d5e8e23 into antalya-26.3 Aug 25, 2026
258 of 274 checks passed
@arthurpassos arthurpassos removed the port-antalya PRs to be ported to all new Antalya releases label Aug 27, 2026
@arthurpassos

Copy link
Copy Markdown
Collaborator Author

Dropped the port-antalya label since we have one for antalya-26.6 as well

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants