Add ALTER TABLE ... RECOMPRESS COLUMN - #109453
Conversation
Introduce a new `ALTER TABLE ... RECOMPRESS COLUMN col` statement that re-compresses the existing data of a column with the column's current compression codec. Changing a column's codec with `MODIFY COLUMN col CODEC(...)` is a metadata-only operation: the new codec applies to newly written data, while data already stored in existing parts keeps its old codec until the parts happen to be merged. `RECOMPRESS COLUMN` rewrites the data of `col` in existing parts so that it is compressed with the codec currently set in the table metadata. Because a compression codec does not change the serialized representation of a column, for `Wide` parts the recompression is performed without deserializing the values: each compressed block of every substream `.bin` is decompressed and re-compressed one-to-one with the new codec, which keeps the decompressed content and granule boundaries byte-identical, so the marks file only needs its compressed offsets remapped (the decompressed offsets, per-granule row counts, the primary index and skip indexes are preserved and hardlinked). The decompressed content is unchanged, so the `uncompressed_size`/`uncompressed_hash` checksums are carried over from the source part and only the on-disk `file_size`/`file_hash` are recomputed. `Compact` parts (and dynamic-subcolumn types) cannot recompress a single column in isolation, so they fall back to a normal whole-part re-serialization that writes every column with its current codec. Implemented as a mutation. Closes: #109432 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Preview deployment for your docs. Learn more about Mintlify Previews.
|
|
Workflow [PR], commit [0c9491a] Summary: ✅
AI ReviewSummaryThis PR adds Findings❌ Blockers
Final VerdictStatus: ❌ Block LLVM Coverage ReportMeasured on commit 0c9491a.
Changed lines: Changed C/C++ lines covered: 591/640 (92.34%) · Uncovered code |
Two issues found in review of ALTER TABLE ... RECOMPRESS COLUMN: 1. Heap-buffer-overflow (caught by the `amd_asan_ubsan` stress test). The wide-part fast path in `RawCompressedBlockReader::readBlock` sized the decompression buffer to exactly the decompressed size. LZ4 decompression (`wildCopyFromInput`) writes in 8-byte chunks and legitimately overruns the end of the output by up to `getAdditionalSizeAtTheEndOfBuffer` bytes, so a full ~1 MiB block wrote a few bytes past the `PODArray` end. Reserve that trailing slack (as `CompressedReadBuffer::nextImpl` does) while keeping the logical `size` at the exact decompressed length, which callers use as the block's decompressed size. 2. Inherited default codec silently ignored. A column without an explicit `CODEC(...)` was recompressed with the part's stored `default_codec`, which is not updated when the table's `default_compression_codec` setting changes, so `RECOMPRESS COLUMN` did nothing after such a change. Route those columns through the whole-part rewrite, which re-serializes every column with the current effective codec (`getCompressionCodecForPart`) and rewrites `default_compression_codec.txt`. The stateless test now also recompresses an LZ4-compressed source (exercising the decompress path that overflowed) and an inherited-default column after a `default_compression_codec` change. CI report: https://s3.amazonaws.com/clickhouse-test-reports/json.html?PR=109453&sha=ca89ad937baf9f199287b0e7dc09521a6890d62a&name_0=PR&name_1=Stress%20test%20%28amd_asan_ubsan%29 PR: #109453 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
On a table with a `UNIQUE KEY`, `RECOMPRESS COLUMN` of a `Compact` part (or of a column that inherits the table's default codec) goes through the whole-part rewrite path (`MutateAllPartColumnsTask`), which only hardlinks checksummed files. The `delete_bitmap_*.rbm` sidecars are not checksummed, so they would be dropped, resurrecting deleted rows. Reject the command universally on `UNIQUE KEY` tables in `MergeTreeData::checkMutationIsPossible` (the gate runs before per-part dispatch), mirroring the existing guards for `MATERIALIZE COLUMN` / `CLEAR COLUMN` on such tables. A sidecar-preserving implementation is left for a follow-up. PR: #109453 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Addressed the AI-review findings and the stress-test crash (pushed in
Regression coverage: The |
# Conflicts: # src/Storages/MergeTree/MergeTreeData.cpp
…t rewrite Addresses the AI-review blocker on the wide-part fast path: the `recompress_needs_full_rewrite` gate only checked `!column_desc->codec`, so a column with an *explicit* codec AST that references `Default` -- either directly (`CODEC(Default)`) or inside a pipeline (`CODEC(Delta, Default)`) -- kept using the wide in-place fast path. That path resolves the codec against the part's stored `default_codec`, which is not updated when the table's `default_compression_codec` setting changes, so `ALTER TABLE ... MODIFY SETTING default_compression_codec = 'ZSTD'` followed by `ALTER TABLE ... RECOMPRESS COLUMN x` was still a no-op on wide parts for those columns. `Default` resolves through the `current_default` argument in `CompressionCodecFactory::get`, exactly like a column with no explicit `CODEC`. New helper `codecDependsOnDefault` detects such codec ASTs (mirroring how the factory parses them) and routes those columns through the whole-part rewrite, which re-serializes every column with the current effective codec (`getCompressionCodecForPart`) and rewrites `default_compression_codec.txt`. Regression test `04506_recompress_column_default_codec_ast` covers `CODEC(Default)` and `CODEC(Delta, Default)` on wide parts after a `default_compression_codec` change. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Addressed the remaining AI-review blocker (default-dependent explicit codecs) in The Since Regression coverage: new |
`RECOMPRESS COLUMN` recompresses a column's data streams one column at a time (`recompressColumnStreams`). With `share_nested_offsets` enabled, `Nested` siblings such as `n.a` and `n.b` share a single on-disk offsets stream (`n.size0`), so recompressing both in one `ALTER` reached that shared `.bin`/marks pair once per sibling and rewrote it twice; whichever pass ran last decided the codec of the shared stream. The data stays readable -- the codec is self-describing and each pass writes a self-consistent block/marks pair, so `CHECK TABLE` passes -- but the shared stream was written more than once and its codec was non-deterministic. Share a `recompressed_streams` set across the per-column calls so that every on-disk stream is rewritten exactly once, by the first column that reaches it -- mirroring the wide-part writer, which also writes a shared offsets stream only once. New test `04600_recompress_column_nested_shared_offsets` recompresses two `Nested` siblings with different codecs (and, separately, only one sibling) on wide parts and validates the data, the rewritten marks, and CHECK TABLE. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Addressed the remaining AI-review blocker (
The data itself stayed readable — the compression codec is self-describing via the block header, and each pass always reads the pristine source stream and writes a self-consistent block/marks pair, so Regression coverage: new |
`ALTER TABLE ... RECOMPRESS COLUMN` of `Nested` siblings that share a single offsets stream (`share_nested_offsets = 1`) already rewrote that shared `n.size0` stream exactly once (via the `recompressed_streams` dedup), but the sibling whose codec won was taken from `columns_to_recompress`, a `NameSet`. Its iteration order is `std::unordered_set` hash-bucket order, so for siblings with different codecs the shared stream got an arbitrary codec from run to run -- neither the `ALTER` query order nor any stable schema order. Build `ctx->columns_to_recompress` by iterating the part's stored-column order and filtering by the recompress set, so the first sibling in stored order deterministically owns the shared stream, matching how a fresh write of the part assigns it. Add `04601_recompress_column_shared_offsets_codec`, which asserts the shared `n.size0` stream takes the schema-first sibling's codec (`ZSTD`) regardless of the column order in the `ALTER`. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Addressed the remaining AI-review blocker (non-deterministic codec for the shared The earlier Fix: New test |
`ALTER TABLE ... RECOMPRESS COLUMN` is parsed straight into a `MutationCommand` and never goes through the `AlterCommands` validation, so an unknown / `ALIAS` / `EPHEMERAL` target used to slip into the per-part mutation path: on wide/full parts the ALTER silently did nothing (the column is simply absent from the part), and on compact/non-full parts it fell through to a whole-part rewrite of every unrelated physical column. Validate the target in `MergeTreeData::checkMutationIsPossible` (before per-part dispatch): reject an unknown name with `NO_SUCH_COLUMN_IN_TABLE` and an `ALIAS` / `EPHEMERAL` column with `BAD_ARGUMENTS`, since neither has an on-disk stream to recompress. Addresses the AI review "Request changes" verdict on the PR. Adds `04602_recompress_column_non_physical_guard`. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Addressed the AI review
Fix ( New test |
`forEachColumnStream` recomputed each data stream's on-disk file name from the table's *current* `replace_long_file_name_to_hash` / `max_file_name_length` settings. When those settings change after a part is written, the recomputed name no longer matches the file actually on disk: the wide fast path then treated the stream as absent and silently skipped it, so `RECOMPRESS COLUMN` became a no-op that left the column untouched. Resolve the stream name against the source part's recorded files (its checksums) via `IMergeTreeDataPart::getStreamNameForColumn`, which tries both the plain and the hashed name (and the alternative stream-file-name settings) and returns the one the part actually has. This subsumes the previous existence check. Addresses an AI review finding on `MergeTreeColumnRecompression.cpp`. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
`ALTER TABLE ... RECOMPRESS COLUMN` re-compresses a column's on-disk data streams. A `Memory` table keeps column data in RAM and has no on-disk streams, so `StorageMemory::mutate` dropped the command from its command list. Since `StorageMemory::checkMutationIsPossible` did no validation, the statement succeeded as a silent no-op, breaking the feature's contract (and the same for `MaterializedView` targets that forward mutations to a `Memory` table). Reject `RECOMPRESS_COLUMN` up front in `StorageMemory::checkMutationIsPossible` with `NOT_IMPLEMENTED`, so a bad `ALTER` fails loudly before any mutation is submitted (a combined `ALTER ... UPDATE ..., RECOMPRESS COLUMN ...` is rejected as a whole, and other supported mutations on `Memory` are unaffected). Addresses an AI review finding on `StorageMemory.cpp`. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Addressed the AI review verdict at `610205723d3`. Pushed `4f32ff3ab8e` + `e90b68d6d3a`. Blocker 1 — Major — Blocker 2 —
Happy to add an explicit guard rejecting the combination if you'd prefer defense-in-depth, but it looks unreachable today. |
|
Updating the branch to fix the |
…icitly The whole-part fallback of `RECOMPRESS COLUMN` was decided in `splitAndModifyMutationCommands`, but the task selection re-derived it from `MutationsInterpreter::isAffectingAllColumns`, which compares the interpreter's output columns against the table's physical columns. A wide part that predates an unrelated `ADD COLUMN` stores fewer columns than the metadata has, so the predicate was false and the mutation went through `MutateSomePartColumnsTask`, which keeps `source_part->default_codec`. A target that inherits the table codec (or uses `CODEC(Default)`) was then rewritten with the pre-`ALTER` default codec, making `RECOMPRESS COLUMN` a silent no-op on such parts. Carry the decision explicitly from `splitAndModifyMutationCommands` to `rewritesAllPartColumns` instead, so both always agree. Also allow `DROP COLUMN` while a `RECOMPRESS COLUMN` of the same column is still queued: the recompression only re-serializes that column's own streams and is skipped for every part once the column is gone. This makes the queued-then-dropped path deterministically testable; `RENAME COLUMN` stays blocked, because the recompression is carried over to the new name and is executed there. New test `05030_recompress_column_inherited_default_after_add_column`; `05023_recompress_column_dropped_target` now keeps the mutation queued with `SYSTEM STOP MERGES` instead of racing against a background pool that always executed it.
… behavior `ALTER TABLE ... DROP COLUMN` is a barrier command, so the drop of a queued recompression's target now always waits for that mutation instead of racing the background pool, and `05023_recompress_column_dropped_target` is deterministic. Also add `SET check_query_single_value_result = 1` to the new test so that `CHECK TABLE` prints a single value.
|
🕵 Pushed AI review Blocker ( New test All 12 red stateless jobs — the same failure, A pending Unrelated red: Local validation on this head: |
The `ALTER TABLE ... COLUMN` reference page is generated: its body lives inside the `AUTOGENERATED_START` / `AUTOGENERATED_END` region and is rendered from the statement registration in `ParserAlterQuery.cpp`. The `RECOMPRESS COLUMN` section was added to the generated page directly, so it would be overwritten by the next autogeneration pass, and the `No direct edits to generated or read-only docs` check of `Docs check (Mintlify)` failed on it. The section, the syntax line, the action list entry and the syntax summary are moved into the `ALTER TABLE ... COLUMN` registration, and the generated page is restored to what `master` has.
|
🕵 Pushed AI review Major /
@groeneai, could you confirm whether #116805 also covers the |
|
No, #116805 does not cover it, and no separate fix is needed: #114325 does. It is not a row-less chunk. What overflows is the residual mask. The fuzzed query carries SELECT count() FROM bf_l AS l LEFT JOIN bf_r AS r
ON and(l.y > r.y, h3EdgeLengthM(arrayJoin([0, 1, 2])), l.x < r.x);On master #114325 refuses such a condition at the sole writer of |
…SS COLUMN` The merge with master left a duplicate `#include <Parsers/ASTIdentifier.h>` in `MergeTreeData.cpp`, which the `arm_tidy` build rejects with `readability-duplicate-include` and which dropped every other CI job. Also address the review finding about the drop-and-re-add lifecycle: a queued `RECOMPRESS COLUMN` is now skipped for a part while the drop of its target is still a pending alter conversion for that part, not only after the drop has been applied. Previously the skip predicate only looked at whether the part still stores the column and whether the current metadata reports the name as physical, so a same-name `ADD COLUMN` could re-arm the older recompression on a part that still carries the pre-drop stream. The read path already substitutes the default value for such a stale stream, and the pending drop removes it anyway, so recompressing it is never useful.
|
🕵 Pushed 1.
2. AI-review Blocker (drop-and-re-add lifecycle of a queued Local validation: Standing rebuttals unchanged (re-point rather than re-implement): |
# Conflicts: # src/Storages/MergeTree/MutateTask.cpp
Covers both entry points added to the `ALTER RECOMPRESS COLUMN` privilege surface: the local `KILL MUTATION` (per-mutation check through `InterpreterAlterQuery::getRequiredAccessForCommand`) and `KILL MUTATION ON CLUSTER` (initiator allowlist in `InterpreterKillQueryQuery::getRequiredAccessForDDLOnCluster`). The initiator check requires the union of every mutation privilege globally, so the positive case holds the whole allowlist and the negative case holds everything except `ALTER RECOMPRESS COLUMN` and is rejected with a message naming the missing grant.
|
🕵 Status update (2026-09-04):
|
… `enable_sz3_codec` Master made `allow_experimental_codecs` obsolete and gates every experimental codec through its own setting: `CompressionCodecFactory` now looks up `getGateSettingName` for the codec family and throws `Codec SZ3 is experimental and not meant to be used in production. You can enable it with the 'enable_sz3_codec' setting` (`BAD_ARGUMENTS`). All twelve lossy `RECOMPRESS COLUMN` tests still enabled `SZ3` with the obsolete setting, so every `CREATE TABLE`/`MODIFY COLUMN` carrying `CODEC(SZ3(...))` was rejected. The two shell tests additionally hung for their whole 600 s budget: their table was never created, so the loops polling `system.mutations` for the expected failure reason never saw it. Replace `SET allow_experimental_codecs = 1` with `SET enable_sz3_codec = 1` (and the `--allow_experimental_codecs 1` client options with `--enable_sz3_codec 1`), matching the master tests `03202_sz3_codec` and `04791_lossy_codec_key_column`. All 12 tests pass locally against a server built from this branch. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
🕵 Merged All 10 red stateless jobs were the pull request's own lossy tests. Master made Fix: No source change was needed; No unresolved review threads remain, and the last AI review verdict was ✅ no blocking findings. Waiting for a fresh CI round on |
| res.column_name = getIdentifierName(command.column); | ||
| return res; | ||
| } | ||
| if (command.type == ASTAlterCommand::RECOMPRESS_COLUMN) |
There was a problem hiding this comment.
This turns RECOMPRESS COLUMN into a persisted mutation command, but the mutation entry format is still plain-text format version: 1 (ReplicatedMergeTreeMutationEntry::writeText / MergeTreeMutationEntry::writeText), and replay still parses it back through ParserAlterCommandList. On a rolling upgrade, a new replica can now write RECOMPRESS COLUMN x into /mutations, after which any older replica will fail to parse that entry and stop applying mutations for the table. The same pending mutation_*.txt also becomes unreadable after a downgrade/restart.
This needs a compatibility gate before merge: either reject RECOMPRESS COLUMN until every replica supports it, or version the persisted mutation representation and keep an older-safe encoding.
|
📊 Cloud Performance Report ✅ AI verdict: no significant changes detected. K_source=6 K_base=30 flagged=0/65 clickbench🟢 No significant changes tpch_adapted_1_official🟢 No significant changes Debug info
|
Closes: #109432
Introduce a new
ALTER TABLE ... RECOMPRESS COLUMN colstatement that re-compresses the existing data of a column with the column's current compression codec.Changing a column's codec with
MODIFY COLUMN col CODEC(...)is metadata-only: the new codec applies to newly written data, while data already stored in existing parts keeps its old codec until the parts happen to be merged.RECOMPRESS COLUMNrewrites the data ofcolin existing parts so that it is compressed with the codec currently set in the table metadata.Because a compression codec does not change the serialized representation of a column, for
Wideparts the recompression is done without deserializing the values: each compressed block of every substream.binis decompressed and re-compressed one-to-one with the new codec. This keeps the decompressed content and granule boundaries byte-identical, so the marks file only needs its compressed offsets remapped (the decompressed offsets, per-granule row counts, the primary index and skip indexes are preserved and hardlinked). The decompressed content is unchanged, so theuncompressed_size/uncompressed_hashchecksums are carried over from the source part and only the on-diskfile_size/file_hashare recomputed.Compactparts (and dynamic-subcolumn types) cannot recompress a single column in isolation, so they fall back to a normal whole-part re-serialization that writes every column with its current codec.Implemented as a mutation. A new
ALTER RECOMPRESS COLUMNgrant is added.The issue also asks to check
RECOMPRESSTTL mutations, which currently go through a full deserialize/re-serialize merge; that is left for a follow-up.Changelog category (leave one):
Changelog entry (a user-readable short description of the changes that goes into CHANGELOG.md):
Added
ALTER TABLE ... RECOMPRESS COLUMN col, which re-compresses a column's existing data with its current codec. For wide parts the data is recompressed without deserializing the column values.Workflow [PR]
Sync PR [sync-upstream/pr/109453]