Repository navigation
Packed MergeTree format support - #289
fuziontech wants to merge 7 commits into
Conversation
Co-authored-by: Shelley <shelley@exe.dev>
Co-authored-by: Shelley <shelley@exe.dev>
Co-authored-by: Shelley <shelley@exe.dev>
There was a problem hiding this comment.
Checked the V25 migration (format column, FK, triggers that block changes to packed tables during rolling deploys), the commit, upload-claim, alter, truncate and table-creation validation, the compaction/hydrator exclusions, and the pyhoglake.packed adapter: subprocess use, path-escape checks, download size limits, ATTACH flow. The server-side design holds together. The DB triggers fence old replicas, registerInitialFiles sends create-with-files through the same packed validation, and the drop/retire paths stay legal because markDropped sets dropped_snapshot before it ends the columns.
P1
- V25 full-scans
hog_data_fileunder ACCESS EXCLUSIVE (V25__packed_mergetree_format.sql:34).VALIDATE CONSTRAINTruns in the same transaction as the precedingDROP/ADD CONSTRAINT, so the exclusive lock is held for the whole scan of the largest table, blocking all commits and reads. The scan is redundant because the old CHECK only allowed'parquet'. Leave the new CHECKNOT VALID, as the FK already is.
P2
- Packed read order across 10+ parts (
pyhoglake/src/pyhoglake/packed.py:503).ORDER BY _partsorts part names as strings (all_10_10_0<all_2_2_0), so rows don't come back in scan-plan order. Order by the numeric block number instead.
Inline comments
server/src/main/resources/db/migration/V25__packed_mergetree_format.sql:34— P1: ThisVALIDATEruns in the same Flyway transaction as theDROP CONSTRAINT/ADD CONSTRAINTon lines 29-33, and those already hold ACCESS EXCLUSIVE onhog_data_fileuntil commit. So the lower SHARE UPDATE EXCLUSIVE lock thatVALIDATEnormally takes gives nothing here. It does a full scan of the largest table in the schema while blocking every commit, scan plan and maintenance query in every catalog.lock_timeoutonly limits lock acquisition, not the scan. The scan is also unnecessary: V1's inline CHECK only allowed'parquet', so existing rows already satisfy the new constraint, and aNOT VALIDCHECK is still enforced on new writes. That is the same reasoning the file already uses for leaving the FKNOT VALID. Fix: drop this line, or move the validate into a separate migration that runs on its own. Thehog_tablevalidate on line 14 has the same shape but the table is small.pyhoglake/src/pyhoglake/packed.py:503— P2:_partis a string, so with 10+ partsall_10_10_0sorts beforeall_2_2_0. Rows then come back out of scan-plan (append) order. Row counts still match, but callers that expect append order get rows from later parts interleaved. Order by the numeric block instead, e.g.ORDER BY toUInt64(splitByChar('_', _part)[2]), _part_offset, and extend the multi-part live test past 9 parts.
— Robo Bill v2 (opus, high reasoning)
|
@fuziontech is this an experiment or something you're wanting to ship? |
|
Thanks for the thorough write-up; the server-side contract and the adapter read cleanly. One structural request before this goes in, and it's about the enforcement mechanism rather than the feature. Could we rework the trigger-based fencing into service-layer checks? V25 adds four row-level PL/pgSQL triggers (
Suggested shape: keep Two smaller things:
Happy to pair on the migration if useful. |
|
Full review, following up on the trigger comment above. I ran the touched server classes (119 tests incl. V25, MigrationLockWindow, SchemaEquivalence, MapperCoverage), the pyhoglake suite (844 passed, 1 skipped) and the real-ClickHouse test against the pinned 26.9.8.3 image, plus 13 mutations against the branch. Numbers below are measured on PG 18 with the PR schema unless stated. Blocker1. V25 scans the whole manifest under ACCESS EXCLUSIVE. Should fix2. 3. Per-row cost on the hot 4. Trino has no packed refusal (trino repo, not this PR, but it gates the feature). 5. Two sources of truth for the format. Commit path and FK read 6. Maintenance surfaces have no format filter. 7. Packed requires 8. pyhoglake resource defaults ( 9. Test gaps from the mutation runs. Pinned (red): file vs table format, the gate, DV refusal, claim format, truncate, fixed-schema alter, the claim trigger, the hydrator filter, the packed footer. Unpinned (suite stays green): removing the 10. Rollback to a pre-V25 binary with a packed table present: the old planner has no format filter, picks packed files, downloads up to 8 GiB each, fails in the rewriter and is blocked by the end guard on every sweep. Forward rolling-deploy fencing is correct (the FK covers any binary; 11. Part bytes are persisted with no writer version recorded. Whether a part can be ATTACHed depends on the reader's ClickHouse version relative to the writer's; 26.9.8.3 is pinned only in CI. Record the writer/part-format version per file or as a table property. Nits
What is irreversibleOnce one packed row exists, removing the format (FK, triggers, column, or the parquet-only CHECK) means dropping every packed table and waiting for retirement and expiry to purge its rows, and the S3 objects are in a non-interchange, version-coupled format. Where I landThe boundary is defensible: one object per file row keeps the "file" abstraction, and the catalog gives packed tables snapshots, append concurrency control, liveness and retention. But every parquet-specific invariant (row ids, footer stats, field-id binding, split offsets, DVs, compaction's row-id carrier) now needs a format branch in every planner, client and engine, and this PR covers DuckDB, Hedgerow, pyhoglake, compaction and the hydrator while missing Trino, the sampler/metrics surfaces and AGENT.md. Before the gate flips I'd want: (1) the migration fixed, (2) the trigger/FK rework from the comment above, (3) a Trino refusal, (4) counts-only stats for packed, (5) the AGENT.md contract. Happy to go through any of it on a call. |
…dback - Rework DB triggers and manifest FK into Kotlin service-layer checks - Leave hog_data_file format check NOT VALID without ACCESS EXCLUSIVE full scan - Make hog_table.file_format the single source of truth and derive write.format.default - Filter packed tables from MaintenanceSummarySampler to prevent small-file debt leaks - Remove per-group format re-query from CompactionService - Relax packed stats requirement to counts-only (no per-column bounds required) - Fix pyhoglake numeric block ordering for 10+ parts with test coverage - Lower pyhoglake resource defaults and stream Arrow output - Document format invariants and no-rollback constraint in AGENT.md and README.md Co-authored-by: Shelley <shelley@exe.dev>
|
Thanks @jghoman, this is very helpful and concrete architectural feedback. All items have been addressed to your specs in commit 803d035:
(Note: we also updated Trino PR PostHog/trino#107 to address Bill's feedback and fix commit message conventions for Trino CI). |
…w gaps - Merge main (resolves UploadService claim query onto REGISTER_CLAIMS_SQL, now selecting file_format); renumber V25__packed_mergetree_format to V26 since main took V25 for the reindex task. - Packed registrations are always stats_state='provided': column_stats is optional and the hydrator never claims packed files, so 'pending' was permanent and inflated the stats-pending gauge. - TableRepo reads hog_table.file_format directly; drop the per-call information_schema probe and the catch-all in the row mapper. - Packed alter refusal is an allow-list (rename, table comment, properties) so new AlterOps fail closed. - Write responses report write.format.default canonically (absent for parquet, lowercase otherwise), matching the next read. - Tests: sort/partition/rename/drop refusals and format immutability via the API, each compaction exclusion pinned separately, sampler excludes packed debt, migration test asserts the constraint that fired. - Docs: AGENT.md invariant 13 no longer claims the server never opens Parquet; OpenAPI column_stats text matches counts-only behavior. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
pyhoglake packed adapter: - Read each part into its own Arrow file (INTO OUTFILE, sorted by _part_offset within the part) through the bounded _run path. The old streamed read never applied its timeout while reading, could deadlock on an undrained stderr pipe, leaked the child on a mid-stream failure, and sorted the whole snapshot under one memory limit. - Verify attached parts against the scan plan (block order + per-part row counts) instead of assuming ATTACH numbering. - Squash inserts into one block so appends over ~256 MiB stay one part. - Coherent defaults: 4 GiB memory, 1 GiB part, 2 GiB snapshot and result. - Reject column names starting with '_' (they shadow ClickHouse virtual columns and silently reorder reads); the server refuses them for packed tables too. - Drop dead min/max stats code; README states counts-only registration, the idempotency-key contract and the limits. CI: - ci/clickhouse-local.sh is the one digest-pinned ClickHouse wrapper. python-live pulls it, enables HOGLAKE_PACKED_MERGETREE_ENABLED on the live server and runs the server-backed packed test plus both real-ClickHouse tests (now marked integration), where a skip fails the run. This replaces the separate packed-clickhouse job, whose test only ran one of the two. - Dev stack (compose + just server run) opens the packed gate so the duckdb-client live suite runs against a default stack. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The fixture seeds through today's CatalogService at V22, which now reads hog_table.file_format; same treatment V17's test already gives V26. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
@jghoman follow-up after a second pass over your review. Head is 4b04c12, CI green. CI / chain
Fixed in this round
Still open, not addressed here
|
Summary
Adds first-scope native ClickHouse packed MergeTree support to Hoglake:
clickhouse-mergetree-packed(one immutabledata.packedobject perhog_data_file)HOGLAKE_PACKED_MERGETREE_ENABLEDpyhoglake.packed) producing parts viaclickhouse local, uploading through fenced claims, and reading exact snapshot part lists into an isolated local table viaATTACH PART+table_readonlyScope constraints
Append-only, homogeneous, fixed schema. No partition specification, sort order, DVs, explicit row IDs, packed compaction, mixed-format scans, Trino/Iceberg packed reads, or remote-read guarantees.
Testing
clickhouse/clickhouse-server:26.9.8.3(skips fail the job)Rollout
Deploy V25 + new binary fleet-wide with the gate disabled, then enable
HOGLAKE_PACKED_MERGETREE_ENABLED=trueon all creation-serving replicas before creating packed tables. Seeserver/README.md.