Skip to content

fix: epoch-encoding bugs — timestamp equality/IN returns ZERO rows on compressed partitions - #4

Merged
wezell merged 2 commits into
dotcms-0.2.0from
fix/epoch-encoding-compressed-reads
Aug 21, 2026
Merged

fix: epoch-encoding bugs — timestamp equality/IN returns ZERO rows on compressed partitions#4
wezell merged 2 commits into
dotcms-0.2.0from
fix/epoch-encoding-compressed-reads

Conversation

@wezell

@wezell wezell commented Aug 21, 2026

Copy link
Copy Markdown
Member

Confirmed live on postgres-ovh-east (dot_token)

Any equality or IN-list predicate on a timestamp column against a compressed partition silently returns zero rows. Range predicates are correct, so this is invisible unless you look for it.

-- public.pingbacks_p20260810 (549 rows)
select count(*) from public.pingbacks_p20260810;                                    -- 549
select count(*) from ... where created_date =  '2026-08-10 06:18:41.502+00';        --   0  ← wrong
select count(*) from ... where created_date in ('2026-08-10 06:18:41.502+00');      --   0  ← wrong
select count(*) from ... where created_date >= '...' and created_date <= '...';      --   1  ← correct

-- public.telemetry_request_cost_p20260815 (95,159 rows) — same, on both the
-- time column (received_at) and a non-time timestamp column (ts)
eq_timecol_const=0  eq_nontime_const=0  in_nontime_const=0  range=1

Blast radius: 625 compressed partitions across both deltatables (541 pingbacks + 84 telemetry_request_cost).

Root cause

Unix-epoch vs PostgreSQL-epoch (946,684,800 s offset) confusion in src/scan/exec/segments.rs — fixed upstream, never picked onto this branch:

  • b3bcc69ca — the bloom build side (compress.rs) hashes TypedColumn values as Unix-epoch µs, while the probe side hashed the raw PG-epoch datum of the qual constant. Hashes never match → every segment falsely bloom-rejected. Also converts the meta-table min/max at load to match the colstats encoding contract (latent today, bites any new consumer).
  • b57934975 — third epoch bug: the colstats min/max in-list filter identity-encoded PG-epoch datums for timestamp/date IN-lists on non-time columns, comparing against Unix-epoch-µs bounds → falsely pruned every segment. Routes all constant encoding (eq, range, in-list, bloom probe) through encode_datum_to_i64, and skips minmax pruning entirely when an element is unencodable rather than risk a wrong prune.

This PR

Cherry-picks both upstream commits. src/scan/exec/segments.rs applied clean; the only conflict was an append-at-EOF collision in tests/test_compression.py between this branch's cross-schema tests and upstream's new TestEpochEncoding class — resolved by keeping both (no duplicate test names, file parses).

Brings upstream's regression coverage: bloom_probe_encode_matches_build_domain unit test + TestEpochEncoding (equality, IN-list, absent-value, DATE payload equality, MIN/MAX aggregate shapes, boundary ranges). The equality/IN cases fail without the fix.

No version bump0.2.0-dotcms.6 is already on the branch from #3 and is not yet tagged or released, so both fixes ship in one deb build and one image roll instead of two primary restarts.

Post-roll acceptance test

Re-run the exact probes above against the same two partitions; every 0 must become 1. Existing compressed partitions need no recompression — this is a read/probe-path fix only.

Upstream audit — what else was considered and skipped

dotcms-0.2.0 is v0.2.0 + 16 local commits; upstream main is 83 commits ahead. Reviewed all 83:

Upstream Verdict
1897f4c8f jsonb segment restore skiprestore_segment_rows is DML-era code that doesn't exist on 0.2.0; this branch's decompress path already routes jsonb through jsonb_binary_to_text (dotcms.5)
e41983706 sum with NaN literals skip for now — 0 NaN rows live in the float8 columns; take on re-sync
5e800a8a5 xataio#53 RenderGucGuard re-sync item — real data-loss class for fallback-typed columns, but squashed into a large PR and the maintenance worker runs default DateStyle/extra_float_digits
78ca3bf09 xataio#45 per-table error isolation re-sync item — would have contained the dotcms.4 dead-worker incident, but 550 lines into worker.rs that collide with this branch's custom launcher
planner-stats (33 commits), DML-on-compressed, perf/condition-cache/prefetch skip — features not in use here

Strategic note: upstream now carries its own target_database (xataio#38/xataio#39), this fork's non-text-column fix (xataio#51), and the ProcessUtility guard (xataio#25). The fork's unique delta is down to roughly the launcher pattern and version packaging — at the next upstream release, rebasing onto upstream main likely costs less than continuing to cherry-pick onto the 0.2.0 base.

Next steps (not done here)

Merge → tag v0.2.0-dotcms.6 on the merge commit (captures #3's ANALYZE guard and this) → release.yml builds the debs → bump ARG PG_DELTAX_VERSION in ovh-k8s-cluster's postgres-cnpg/Dockerfile (validate check races the release; re-run once assets exist) → digest-pinned roll of postgres-ovh-east.

🤖 Generated with Claude Code

https://claude.ai/code/session_01XGCecJYCz8dMTNPadEbeRf

Two Unix-epoch vs PostgreSQL-epoch (offset 946,684,800 s) confusions in
src/scan/exec/segments.rs:

1. Bloom probe encoding (live bug): the per-segment bloom BUILD side
   (compress.rs) hashes TypedColumn values, which store timestamps/dates
   as Unix-epoch microseconds — but the probe side hashed the raw
   PG-epoch datum of the qual constant. The hashes never matched, every
   segment was falsely bloom-rejected, and `ts = const` / `ts IN (...)`
   on timestamp/date columns returned ZERO rows on compressed
   partitions. New bloom_probe_encode() converts the constant into the
   build-side domain (also normalizing the f32 bit-pattern case the Eq
   path previously special-cased and the InList path missed).

2. Meta-table min/max encoding (latent): load_segments_heap stored the
   time column's meta `_min_`/`_max_` identity-encoded (PG-epoch datum)
   in ColMinMax, while every consumer (decode_encoded_to_pg_i64,
   segment_all_rows_pass, constant_extract_key_for_segment) decodes the
   colstats encoding (Unix-epoch us) — timestamps 30 years early. On
   current main all reachable consumers either pass load_minmax=false
   (correct block below) or get the value overwritten by the colstats
   load, so no wrong results are reachable today — but the contract
   violation bites any new consumer of the map. Convert at load,
   matching the colstats encoding contract.

Regression tests: bloom_probe_encode_matches_build_domain unit test +
TestEpochEncoding integration class (timestamp sort column with
year-2000-adjacent constants: equality, IN-list, absent-value, DATE
payload equality, MIN/MAX aggregate shapes, boundary range predicates).
The equality/IN tests return zero rows without fix #1.

Extracted from the perf/clickhouse-gap-session branch (commit 7ea1b97
and the bloom-probe portion of fcfb1dc).

(cherry picked from commit b3bcc69)
…epoch bug in in-list minmax filter

The colstats min/max in-list filter identity-encoded raw PG-epoch datums
for timestamp/date IN-lists on non-time columns while comparing against
Unix-epoch-microsecond colstats bounds - falsely pruning every segment
(zero rows). Route all constant encoding (eq, range, in-list, bloom
probe) through encode_datum_to_i64, and skip minmax pruning entirely
when an element is unencodable rather than risk a wrong prune.

(cherry picked from commit b579349)
@wezell
wezell merged commit 5a1267a into dotcms-0.2.0 Aug 21, 2026
6 checks passed
@wezell
wezell deleted the fix/epoch-encoding-compressed-reads branch August 21, 2026 17:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants