Skip to content

Export partition - check name and pos of columns in partition key. - #2134

Open
k-morozov wants to merge 18 commits into
antalya-26.3from
bugfix/antalya/26.3/export-incorect-order
Open

Export partition - check name and pos of columns in partition key.#2134
k-morozov wants to merge 18 commits into
antalya-26.3from
bugfix/antalya/26.3/export-incorect-order

Conversation

@k-morozov

@k-morozov k-morozov commented Jul 30, 2026

Copy link
Copy Markdown

Closes: #2123

Changelog category (leave one):

  • Bug Fix (user-visible misbehavior in an official stable release)

Changelog entry (a user-readable short description of the changes that goes to CHANGELOG.md):

Fixed ALTER TABLE ... EXPORT PART and EXPORT PARTITION schema validation to prevent silently writing values into incorrect destination columns.

The export now verifies, by name and position only, that the top-level column owning each PARTITION BY column or subcolumn occupies the same position in the source and destination schemas. If that column contains a named
Tuple, its element names must also be declared in the same order - recursively through nested tuples and through container types such as Array and Map (both the key and value type of a Map are checked). This protects both by-name subcolumn access (PARTITION BY t.a) and positional expressions (PARTITION BY tupleElement(t, 1), PARTITION BY tupleElement(arr[1], 'a')) from silently changing meaning after the positional cast.

This check only compares names, positions, and Tuple element layout of the owning columns - it does not verify that the corresponding types are compatible (e.g. it does not detect a DateTime/DateTime64 partition key column
using different time zones in the source and destination tables, nor a Tuple element whose name matches but whose type differs). Such type mismatches, like all non-partition-key type mismatches, are still caught separately by
the existing positional cast check (rejected unless export_merge_tree_part_allow_lossy_cast = 1 is set).

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 30, 2026

Copy link
Copy Markdown

Workflow [PR], commit [9286316]

Signed-off-by: Konstantin Morozov <just.morozov.k@gmail.com>
Signed-off-by: Konstantin Morozov <just.morozov.k@gmail.com>
Signed-off-by: Konstantin Morozov <just.morozov.k@gmail.com>
Signed-off-by: Konstantin Morozov <just.morozov.k@gmail.com>
@k-morozov
k-morozov force-pushed the bugfix/antalya/26.3/export-incorect-order branch from 9e63fd6 to 5322e48 Compare July 31, 2026 15:11
Signed-off-by: Konstantin Morozov <just.morozov.k@gmail.com>
Signed-off-by: Konstantin Morozov <just.morozov.k@gmail.com>
@k-morozov k-morozov changed the title [WIP] Export partition - Positional column matching might lead to incorrect partitioning Export partition - Positional column matching might lead to incorrect partitioning Aug 3, 2026
@k-morozov
k-morozov marked this pull request as ready for review August 3, 2026 12:54
@k-morozov

Copy link
Copy Markdown
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: faef463617

ℹ️ 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 on lines +690 to +693
auto partition_key_columns = source_metadata->getColumnsRequiredForPartitionKey();
const std::unordered_set<String> partition_key_column_set(
std::make_move_iterator(partition_key_columns.begin()),
std::make_move_iterator(partition_key_columns.end()));

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Normalize subcolumns before checking partition positions

When PARTITION BY references a subcolumn such as t.a, getColumnsRequiredForPartitionKey returns the subcolumn name, while source_columns contains only the top-level readable column t; consequently, this set never matches and the new positional check is skipped. For example, source columns (t Tuple(a UInt32), u Tuple(a UInt32)) and destination columns (u Tuple(a UInt32), t Tuple(a UInt32)), both partitioned by t.a, have positionally compatible types and pass validation, but export source u into destination t while constructing the partition from source t.a, silently mispartitioning the data. Map required subcolumns back to their top-level storage columns before constructing this set.

Useful? React with 👍 / 👎.

@k-morozov k-morozov Aug 4, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

It's not a problem. Source-id here https://github.com/Altinity/ClickHouse/blob/antalya-26.3/src/Storages/ObjectStorage/DataLakes/Iceberg/Utils.cpp#L985 mapped only for columns (not for subcolumns).

up: not quite.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

If it is not a problem, can you write a test for it?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Done

const auto & source_column = source_columns[i];
const auto & destination_column = destination_columns[i];

if (partition_key_column_set.contains(source_column.name) && source_column.name != destination_column.name)

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 if partitioning is for several columns, and destination has same columns but in different order?
Both have columns 'a' and 'b', in same order, but source with partition by a,b, and destination partition by b,a.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Or when destination has more columns in partition list than source.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

I added the tests for simular cases:

What if partitioning is for several columns, and destination has same columns but in different order?
https://github.com/Altinity/ClickHouse/pull/2134/changes#diff-28d3e30160a3442e2b9ef01e3bf0b10b4c27de6a39efea3175ccec0a3f2d8949R1786

Or when destination has more columns in partition list than source.
https://github.com/Altinity/ClickHouse/pull/2134/changes#diff-28d3e30160a3442e2b9ef01e3bf0b10b4c27de6a39efea3175ccec0a3f2d8949R1799


namespace
{
std::optional<String> getDateTimeTimeZoneName(const DataTypePtr & 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.

I found method getExplicitTimeZoneOfDateTimeArgument like this.
And fragment from method extractTimeZoneNameFromFunctionArguments.
May be possible to reuse something?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Seems they are a slightly defferent. But I could use checkAndGetDataType from them.

Signed-off-by: Konstantin Morozov <just.morozov.k@gmail.com>

@mkmkme mkmkme left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

The code indeed does what is expected from the description.

However I'm a bit hesitant still to just approve it because I'm not sure whether the description matches to what we actually want to do.

Let's say we have source columns (a, b, c, d, e) with partition by (a, b).
As expected, destination having (e, d, c, b, a) or even (b, a, c, d, e) will be rejected.
However, situations like (a, b, e, d, c) will be accepted (assuming matching types). And sometimes, especially considering how easy it can be to make a typo/mistake, it can lead to quite surprising results.

Overall, for me this fix looks good, but I would like to continue the discussion about all possible outcomes tomorrow. If we decide that this solution is fine for now I'll approve it :)

@mkmkme

mkmkme commented Aug 4, 2026

Copy link
Copy Markdown
Collaborator

Here's the output of /audit-report:

High: Tuple subcolumns bypass positional validation
    Impact: Data can be silently written into the wrong nested field and Hive partition.
    Anchor: src/Storages/MergeTree/ExportPartitionUtils.cpp / verifyExportSchemaCastable, lines 675–714
    Trigger: Both tables partition by `t.a`, but destination changes `Tuple(a, b)` to `Tuple(b, a)` with compatible element types.
    Why defect: Required key name `t.a` never matches the top-level iterated name `t`; positional tuple conversion then swaps values while the path retains source `t.a`.
    Fix direction (short): Resolve subcolumns to physical columns and recursively validate relevant tuple member names/order.
    Regression test direction (short): Export the reordered tuple case to Hive and verify synchronous rejection.

High: Wrapped temporal types bypass timezone protection
    Impact: Files can be committed under one incorrect Hive path or Iceberg partition tuple, breaking pruning and metadata correctness.
    Anchor: src/Storages/MergeTree/ExportPartitionUtils.cpp / getDateTimeTimeZoneName and verifyExportSchemaCastable, lines 643–649 and 714–735
    Trigger: `Nullable` or `LowCardinality(DateTime[64])` uses different source/destination timezones with `export_merge_tree_part_allow_lossy_cast = 1`.
    Why defect: Timezone extraction handles only bare temporal types; lossy-cast opt-in then skips the remaining rejection.
    Fix direction (short): Unwrap `Nullable` and `LowCardinality` before timezone comparison.
    Regression test direction (short): Cover wrapped timestamps spanning a destination timezone boundary for direct and replicated exports.

Medium: Timezone-invariant partition expressions are rejected
    Impact: Valid Hive exports fail with `BAD_ARGUMENTS`.
    Anchor: src/Storages/MergeTree/ExportPartitionUtils.cpp / verifyExportSchemaCastable, lines 714–728
    Trigger: Differing column timezones with `PARTITION BY toUnixTimestamp(ts)` or an explicitly normalized timezone expression.
    Why defect: The guard checks required input columns without determining whether the partition result depends on their declared timezone.
    Fix direction (short): Validate computed partition semantics or recognize timezone-invariant expressions.
    Regression test direction (short): Confirm normalized expressions succeed while timezone-sensitive expressions remain protected.

Note the first one. It's actually the very same as was reported by codex and then dismissed. Could you double-check that please whether this is something we can simply dismiss or need to fix?

@mkmkme

mkmkme commented Aug 5, 2026

Copy link
Copy Markdown
Collaborator

As discussed on the call, this change is good to go. Still, @k-morozov could you double-check the audit report on whether its first "High" issue is actually bogus and inapplicable?

@k-morozov k-morozov changed the title Export partition - Positional column matching might lead to incorrect partitioning Export partition - check name and pos of columns in partition key. Aug 5, 2026
i,
destination_column.name);

if (partition_key_column_set.contains(source_column.name))

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This is not very related to this PR, is it? It seems like a fix for #2138. I pushed a similar fix to it in my pr, but a more permissive one since the goal on my pr is to allow the monotonic stuff.

I think it is ok to keep this change here, and then I'll modify it in my pr. Your pr is likely going to be merged first.

}
}

void verifyMergeTreePartitionCompatibility(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I would rename this to something like assertPartitionKeyASTAreEqual

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

done

Signed-off-by: Konstantin Morozov <just.morozov.k@gmail.com>
Signed-off-by: Konstantin Morozov <just.morozov.k@gmail.com>
Signed-off-by: Konstantin Morozov <just.morozov.k@gmail.com>
@k-morozov

Copy link
Copy Markdown
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: 9ce7987f46

ℹ️ 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
@k-morozov
k-morozov marked this pull request as draft August 6, 2026 15:17
Signed-off-by: Konstantin Morozov <just.morozov.k@gmail.com>
Signed-off-by: Konstantin Morozov <just.morozov.k@gmail.com>
Signed-off-by: Konstantin Morozov <just.morozov.k@gmail.com>
@k-morozov
k-morozov marked this pull request as ready for review August 6, 2026 16:39
@Altinity Altinity deleted a comment from chatgpt-codex-connector Bot Aug 6, 2026
@k-morozov

Copy link
Copy Markdown
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: 2d7f3b2e68

ℹ️ 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 on lines +734 to +735
const auto source_time_zone = getDateTimeTimeZoneName(source_resolved->type);
const auto destination_time_zone = getDateTimeTimeZoneName(destination_resolved->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.

P1 Badge Inspect nested DateTime types in partition columns

When the partition key is a composite column, such as t Tuple(ts DateTime('UTC')), and the destination declares the same t column with DateTime('Asia/Tokyo'), these calls pass the whole Tuple type to getDateTimeTimeZoneName, which returns no timezone. canBeSafelyCast also accepts the tuple because DateTime::equals ignores timezone, so the export bypasses the new guard; with the wildcard strategy, WildcardPartitionStrategy computes the path using toString(t), causing the nested timestamp to be rendered in the destination timezone and silently changing the partition identifier. Recursively compare nested DateTime/DateTime64 members of partition-key columns.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

The comment isn't related to the current changes — yes, we have a problem with DateTime (and more), but I'm adding name and position checks for the column in the partition by expression.

@k-morozov
k-morozov marked this pull request as draft August 6, 2026 17:48
Signed-off-by: Konstantin Morozov <just.morozov.k@gmail.com>
Signed-off-by: Konstantin Morozov <just.morozov.k@gmail.com>
Signed-off-by: Konstantin Morozov <just.morozov.k@gmail.com>
@k-morozov

Copy link
Copy Markdown
Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Hooray!

Reviewed commit: bce08c3b15

ℹ️ 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".

@k-morozov
k-morozov marked this pull request as ready for review August 7, 2026 11:08
@k-morozov

Copy link
Copy Markdown
Author

Here's the output of /audit-report:

High: Tuple subcolumns bypass positional validation
    Impact: Data can be silently written into the wrong nested field and Hive partition.
    Anchor: src/Storages/MergeTree/ExportPartitionUtils.cpp / verifyExportSchemaCastable, lines 675–714
    Trigger: Both tables partition by `t.a`, but destination changes `Tuple(a, b)` to `Tuple(b, a)` with compatible element types.
    Why defect: Required key name `t.a` never matches the top-level iterated name `t`; positional tuple conversion then swaps values while the path retains source `t.a`.
    Fix direction (short): Resolve subcolumns to physical columns and recursively validate relevant tuple member names/order.
    Regression test direction (short): Export the reordered tuple case to Hive and verify synchronous rejection.

High: Wrapped temporal types bypass timezone protection
    Impact: Files can be committed under one incorrect Hive path or Iceberg partition tuple, breaking pruning and metadata correctness.
    Anchor: src/Storages/MergeTree/ExportPartitionUtils.cpp / getDateTimeTimeZoneName and verifyExportSchemaCastable, lines 643–649 and 714–735
    Trigger: `Nullable` or `LowCardinality(DateTime[64])` uses different source/destination timezones with `export_merge_tree_part_allow_lossy_cast = 1`.
    Why defect: Timezone extraction handles only bare temporal types; lossy-cast opt-in then skips the remaining rejection.
    Fix direction (short): Unwrap `Nullable` and `LowCardinality` before timezone comparison.
    Regression test direction (short): Cover wrapped timestamps spanning a destination timezone boundary for direct and replicated exports.

Medium: Timezone-invariant partition expressions are rejected
    Impact: Valid Hive exports fail with `BAD_ARGUMENTS`.
    Anchor: src/Storages/MergeTree/ExportPartitionUtils.cpp / verifyExportSchemaCastable, lines 714–728
    Trigger: Differing column timezones with `PARTITION BY toUnixTimestamp(ts)` or an explicitly normalized timezone expression.
    Why defect: The guard checks required input columns without determining whether the partition result depends on their declared timezone.
    Fix direction (short): Validate computed partition semantics or recognize timezone-invariant expressions.
    Regression test direction (short): Confirm normalized expressions succeed while timezone-sensitive expressions remain protected.

Note the first one. It's actually the very same as was reported by codex and then dismissed. Could you double-check that please whether this is something we can simply dismiss or need to fix?

@mkmkme Regarding first comment - in this description we don;t have a problem. It is safely use Tuple(a, b) instead Tuple(b,a). But we have problem if we use tupleElement with position. I added tests. Other points are not related with current PR.

Signed-off-by: Konstantin Morozov <just.morozov.k@gmail.com>
Signed-off-by: Konstantin Morozov <just.morozov.k@gmail.com>
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.

Export partition - Positional column matching might lead to incorrect partitioning

4 participants