Skip to content

Fix iceberg partition pruning by nanoseconds - #2239

Open
ianton-ru wants to merge 9 commits into
antalya-26.6from
bugfix/antalya-26.6/iceberg_partition_pruning_by_nanoseconds
Open

ianton-ru wants to merge 9 commits into
antalya-26.6from
bugfix/antalya-26.6/iceberg_partition_pruning_by_nanoseconds

Conversation

@ianton-ru

@ianton-ru ianton-ru commented Aug 18, 2026 •

Copy link
Copy Markdown

Solved #2240

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

Fix partition and min/max pruning for Iceberg tables by timestamp_ns type, fix writing DateTime(9) as timestamp_ns.

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)

ianton-ru and others added 2 commits August 18, 2026 21:12
Identity partition pruning on `timestamp_ns` over-prunes matching files.
Min/max pruning also returns empty results when Iceberg `timestamp`
(`DateTime64(6)`) lower/upper bounds are stored as nanoseconds, which is
the customer `toDateTime64(..., 6)` case.

Co-authored-by: Cursor <cursoragent@cursor.com>
Identity partition values for `timestamp_ns` arrive as Avro Int64, and some
writers store nanosecond min/max bytes on Iceberg `timestamp` (`DateTime64(6)`).
Both were compared at the wrong scale and dropped matching files. Write
`DateTime64(9)` as `timestamp_ns` so ClickHouse does not recreate that layout.

Co-authored-by: Cursor <cursoragent@cursor.com>
@ianton-ru ianton-ru changed the title Bugfix/antalya 26.6/iceberg partition pruning by nanoseconds Fix iceberg partition pruning by nanoseconds Aug 18, 2026
@github-actions

github-actions Bot commented Aug 18, 2026 •

Copy link
Copy Markdown

Workflow [PR], commit [6f0719e]

ianton-ru and others added 2 commits August 18, 2026 22:37
`1e16` microseconds is ~year 2286, so spec-correct Spark `9999-12-31` and
`DateTime64` max `2299-12-31` bounds were divided by 1000 and over-pruned.
Convert only `|ticks| > 1e18` (true nanosecond band); fail open in between.

Co-authored-by: Cursor <cursoragent@cursor.com>
`timestamp_ns` is v3-only. Mapping `DateTime64(9)` without checking
`iceberg_format_version` wrote invalid v2 metadata (default is 2).
Throw instead of emitting v3 types into v1/v2 tables.

Co-authored-by: Cursor <cursoragent@cursor.com>
@ianton-ru

Copy link
Copy Markdown
Author

@codex review

@ianton-ru
ianton-ru marked this pull request as ready for review August 19, 2026 08:22

@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: 381b43d5e2

ℹ️ 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 +75 to +80
if (scale == 6 && magnitudeExceeds(unscaled, microseconds_ambiguous_threshold))
{
if (!magnitudeExceeds(unscaled, nanoseconds_lower_threshold))
return std::nullopt;

unscaled = nanosecondsToMicrosecondsForBound(unscaled, lower_bound);

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 Skip pruning instead of guessing nanosecond bounds

When an Iceberg timestamp manifest contains a magnitude above this threshold, the schema still identifies the value as microseconds; magnitude alone cannot prove that the writer actually used nanoseconds. Dividing the value and then trusting the resulting range allows mayBeTrueInRange to prune a file whose corrupt or differently encoded bounds do not cover its data, producing missing query results. Treat these suspicious bounds as unavailable and skip min/max pruning rather than taking a consequential fallback path.

AGENTS.md reference: AGENTS.md:L153-L153

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.

This is not a P1. The overlapping band already fail-opens; converting |ticks| > 1e18 is the customer ns-stats case, not a guess inside the µs/ns overlap.
Iceberg timestamp is still microseconds in the schema. Magnitude cannot prove nanoseconds, which is why |ticks| in (1e16, 1e18] already returns nullopt. Callers then skip that hyperrectangle (if (!left || !right) continue) and min/max pruning for that column is skipped. That covers spec-correct far-future microseconds: Spark 9999-12-31 (~2.53e17 µs) and ClickHouse DateTime64 max 2299-12-31 (~1.04e16 µs).
Conversion runs only for |ticks| > 1e18. As microseconds that is ~year 33658, which is outside ClickHouse DateTime64(6) (max ~2299, ~1.04e16 µs) and outside normal Iceberg timestamp sentinels. As nanoseconds it is the customer layout (~1.76e18 for 2026) that previously over-pruned every file.
Fail-opening that band as well would keep query results correct (files scanned) but would disable min/max pruning again for those tables (IcebergMinMaxIndexPrunedFiles: 0). That undoes the point of this heuristic.
A corrupt 8-byte value > 1e18 that is not nanoseconds could still be converted into a fake in-range window. That is a residual, not a realistic P1 on this path.

@subkanthi subkanthi mentioned this pull request Sep 16, 2026
8 of 15 tasks
@DimensionWieldr

DimensionWieldr commented Sep 22, 2026 •

Copy link
Copy Markdown
Collaborator

While writing some tests, AI found a potential issue:

Inserting into a format-version 3 table partitioned by DateTime64(9) fails with BAD_ARGUMENTS:

Unsupported type for iceberg DateTime64(9)

getAvroType in src/Storages/ObjectStorage/DataLakes/Iceberg/Utils.cpp still accepts only DateTime64 scale 6 when it encodes a partition value in the manifest. The data-file write path is fine: the same DateTime64(9) columns insert and prune on an unpartitioned v3 table.

This is the insert in test_writes_partition_pruning_datetime64_nanoseconds. Reproduced on 3948dca (clickhouse-common-static_26.6.4.20001.altinityantalya), table PARTITION BY ts with (ts DateTime64(9), value DateTime64(9), id Int32).

These cases on the same build succeed:

  • Reading and pruning timestamp_ns, including identity partitions written outside ClickHouse.
  • Unpartitioned DateTime64(9) writes stored as timestamp_ns, with min/max pruning.
  • DateTime64(9, 'UTC') stored as timestamptz_ns.
  • Create and ALTER of DateTime64(9) rejected on format versions 1 and 2; ALTER ADD Nullable(DateTime64(9)) allowed on version 3.

ianton-ru and others added 2 commits October 1, 2026 15:53
`getAvroType` required `timestamp-micros` after the ClickHouse#109764 backport, so identity-partitioned `timestamp_ns` writes failed.

Co-authored-by: Cursor <cursoragent@cursor.com>
@ianton-ru

Copy link
Copy Markdown
Author

@DimensionWieldr Thanks, must be fixed now

@DimensionWieldr

DimensionWieldr commented Oct 2, 2026 •

Copy link
Copy Markdown
Collaborator

CI looks good from my end.

Regression tests for timestamp nanosecond pruning are passing on both Ice and Glue catalogs: https://github.com/Altinity/clickhouse-regression/blob/main/iceberg/tests/iceberg_engine/timestamp_ns_pruning.py

@DimensionWieldr

Copy link
Copy Markdown
Collaborator

AI Audit Summary

Two follow-ups on timestamp_ns / timestamptz_ns.


DateTime64(9) with a non-UTC zone reloads as UTC. getIcebergType maps any DateTime64(9) with an explicit timezone to timestamptz_ns. IcebergSchemaProcessor::getSimpleType rebuilds that as DateTime64(9, "UTC"). shouldReloadSchemaForConsistency() returns true, and create plus every query rebuild columns from that mapping, so DateTime64(9, 'Europe/Berlin') becomes DateTime64(9, 'UTC') immediately. A string literal is parsed in the column zone, so WHERE ts = '2024-06-01 10:00:00' means 10:00 UTC afterward.

Iceberg does not keep the original zone name. Microsecond timestamptz already uses iceberg_timezone_for_timestamptz (default UTC). timestamptz_ns ignores that setting and hardcodes UTC. The read path should use the same setting.


timestamp_ns Parquet is marked UTC-adjusted. PrepareForWrite sets isAdjustedToUTC true for every DateTime64. Iceberg timestamp_ns requires TIMESTAMP(NANOS, false). An insert of DateTime64(9) with no zone writes timestamp_ns in the table metadata and a UTC-adjusted timestamp in the Parquet footer. DateTime64(9, 'UTC') is fine: that is timestamptz_ns, which is supposed to be adjusted.

ClickHouse reads the integers through the Iceberg schema, so a ClickHouse round-trip still succeeds. A reader that checks the Parquet logical type against the Iceberg type can reject the file. This flag was already forced true for every DateTime64, including ordinary timestamp. This change makes that mismatch apply to timestamp_ns. The fix belongs in the shared Parquet writer.

This branch has not been deployed

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants