From 429e5bad16c8c608cd55e2c3efc0b91b77429792 Mon Sep 17 00:00:00 2001 From: Mikhail Koviazin Date: Wed, 8 Jul 2026 11:45:16 +0200 Subject: [PATCH 1/2] Fix crash merging _part_offset projection on row-combining engines A projection that carries the parent _part_offset can only be safely merged when the parent merge preserves rows 1:1. Row-combining engines (Summing/Aggregating/Graphite/Coalescing) collapse rows with equal sorting keys, so the source-to-merged offset mapping becomes incomplete and the projection must be rebuilt instead. These modes were missing from merge_may_reduce_rows, so such projections took the merge path and remapped offsets through a mapping that had no entry for the collapsed rows, reading out of bounds and aborting with "Logical error: 'page_pos < pages.size()'" (e.g. OPTIMIZE FINAL on a CoalescingMergeTree table with a _part_offset projection and deduplicate_merge_projection_mode = 'rebuild'). Add the four row-combining modes to merge_may_reduce_rows so these projections are rebuilt, and add a regression test. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/Storages/MergeTree/MergeTask.cpp | 6 +- ...ction_part_offset_reducing_merge.reference | 3 + ..._projection_part_offset_reducing_merge.sql | 67 +++++++++++++++++++ 3 files changed, 75 insertions(+), 1 deletion(-) create mode 100644 tests/queries/0_stateless/03786_projection_part_offset_reducing_merge.reference create mode 100644 tests/queries/0_stateless/03786_projection_part_offset_reducing_merge.sql diff --git a/src/Storages/MergeTree/MergeTask.cpp b/src/Storages/MergeTree/MergeTask.cpp index 5b8df976d475..5bce2c36bb44 100644 --- a/src/Storages/MergeTree/MergeTask.cpp +++ b/src/Storages/MergeTree/MergeTask.cpp @@ -534,7 +534,11 @@ bool MergeTask::ExecuteAndFinalizeHorizontalPart::prepare() const global_ctx->deduplicate || global_ctx->merging_params.mode == MergeTreeData::MergingParams::Collapsing || global_ctx->merging_params.mode == MergeTreeData::MergingParams::Replacing || - global_ctx->merging_params.mode == MergeTreeData::MergingParams::VersionedCollapsing; + global_ctx->merging_params.mode == MergeTreeData::MergingParams::VersionedCollapsing || + global_ctx->merging_params.mode == MergeTreeData::MergingParams::Summing || + global_ctx->merging_params.mode == MergeTreeData::MergingParams::Aggregating || + global_ctx->merging_params.mode == MergeTreeData::MergingParams::Graphite || + global_ctx->merging_params.mode == MergeTreeData::MergingParams::Coalescing; prepareProjectionsToMergeAndRebuild(); diff --git a/tests/queries/0_stateless/03786_projection_part_offset_reducing_merge.reference b/tests/queries/0_stateless/03786_projection_part_offset_reducing_merge.reference new file mode 100644 index 000000000000..5d778a7b1b43 --- /dev/null +++ b/tests/queries/0_stateless/03786_projection_part_offset_reducing_merge.reference @@ -0,0 +1,3 @@ +coalescing 1 +summing 1 +aggregating 1 diff --git a/tests/queries/0_stateless/03786_projection_part_offset_reducing_merge.sql b/tests/queries/0_stateless/03786_projection_part_offset_reducing_merge.sql new file mode 100644 index 000000000000..0c88a200237c --- /dev/null +++ b/tests/queries/0_stateless/03786_projection_part_offset_reducing_merge.sql @@ -0,0 +1,67 @@ +-- Regression test for a crash (Logical error: 'page_pos < pages.size()') that happened when +-- OPTIMIZE-ing a table whose engine combines rows (Coalescing/Summing/Aggregating/...) and that +-- has a projection carrying the parent _part_offset. +-- +-- Such merges reduce rows, so the source-to-merged offset mapping is incomplete and the projection +-- must be rebuilt rather than merged. Before the fix these engines were missing from +-- merge_may_reduce_rows, so the projection was merged and the offset remap read out of bounds. +-- +-- The self-join below checks that every rebuilt projection row points back to the base row with the +-- same key, i.e. its stored _parent_part_offset equals the base _part_offset. + +DROP TABLE IF EXISTS t_coalescing; +CREATE TABLE t_coalescing +( + a Int32, + v Nullable(Int64), + PROJECTION p (SELECT a, _part_offset ORDER BY a) +) +ENGINE = CoalescingMergeTree +ORDER BY a +SETTINGS deduplicate_merge_projection_mode = 'rebuild'; + +INSERT INTO t_coalescing SELECT number, number FROM numbers(1000); +INSERT INTO t_coalescing SELECT number, NULL FROM numbers(1000); +OPTIMIZE TABLE t_coalescing FINAL; +SELECT 'coalescing', count() = sum(l._part_offset = r._parent_part_offset) +FROM t_coalescing l JOIN mergeTreeProjection(currentDatabase(), t_coalescing, p) r USING (a) +SETTINGS enable_analyzer = 1; +DROP TABLE t_coalescing; + +DROP TABLE IF EXISTS t_summing; +CREATE TABLE t_summing +( + a Int32, + v Int64, + PROJECTION p (SELECT a, _part_offset ORDER BY a) +) +ENGINE = SummingMergeTree +ORDER BY a +SETTINGS deduplicate_merge_projection_mode = 'rebuild'; + +INSERT INTO t_summing SELECT number, 1 FROM numbers(1000); +INSERT INTO t_summing SELECT number, 10 FROM numbers(1000); +OPTIMIZE TABLE t_summing FINAL; +SELECT 'summing', count() = sum(l._part_offset = r._parent_part_offset) +FROM t_summing l JOIN mergeTreeProjection(currentDatabase(), t_summing, p) r USING (a) +SETTINGS enable_analyzer = 1; +DROP TABLE t_summing; + +DROP TABLE IF EXISTS t_aggregating; +CREATE TABLE t_aggregating +( + a Int32, + v SimpleAggregateFunction(sum, Int64), + PROJECTION p (SELECT a, _part_offset ORDER BY a) +) +ENGINE = AggregatingMergeTree +ORDER BY a +SETTINGS deduplicate_merge_projection_mode = 'rebuild'; + +INSERT INTO t_aggregating SELECT number, 1 FROM numbers(1000); +INSERT INTO t_aggregating SELECT number, 10 FROM numbers(1000); +OPTIMIZE TABLE t_aggregating FINAL; +SELECT 'aggregating', count() = sum(l._part_offset = r._parent_part_offset) +FROM t_aggregating l JOIN mergeTreeProjection(currentDatabase(), t_aggregating, p) r USING (a) +SETTINGS enable_analyzer = 1; +DROP TABLE t_aggregating; From 3437ebdde0bfab9be72636fe87306affe77ab4d4 Mon Sep 17 00:00:00 2001 From: Mikhail Koviazin Date: Wed, 8 Jul 2026 12:20:26 +0200 Subject: [PATCH 2/2] Flip the comparison to better align with upstream --- src/Storages/MergeTree/MergeTask.cpp | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/src/Storages/MergeTree/MergeTask.cpp b/src/Storages/MergeTree/MergeTask.cpp index 5bce2c36bb44..1c543f2f5491 100644 --- a/src/Storages/MergeTree/MergeTask.cpp +++ b/src/Storages/MergeTree/MergeTask.cpp @@ -532,13 +532,7 @@ bool MergeTask::ExecuteAndFinalizeHorizontalPart::prepare() const !patch_parts.empty() || global_ctx->cleanup || global_ctx->deduplicate || - global_ctx->merging_params.mode == MergeTreeData::MergingParams::Collapsing || - global_ctx->merging_params.mode == MergeTreeData::MergingParams::Replacing || - global_ctx->merging_params.mode == MergeTreeData::MergingParams::VersionedCollapsing || - global_ctx->merging_params.mode == MergeTreeData::MergingParams::Summing || - global_ctx->merging_params.mode == MergeTreeData::MergingParams::Aggregating || - global_ctx->merging_params.mode == MergeTreeData::MergingParams::Graphite || - global_ctx->merging_params.mode == MergeTreeData::MergingParams::Coalescing; + global_ctx->merging_params.mode != MergeTreeData::MergingParams::Ordinary; prepareProjectionsToMergeAndRebuild();