This is an automated email from the ASF dual-hosted git repository.
morningman pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/doris.git
The following commit(s) were added to refs/heads/master by this push:
new 86c78b92b1c [fix](binlog) Fix BE crash when APPEND_ONLY row-binlog
scan omits key columns (#66432)
86c78b92b1c is described below
commit 86c78b92b1c691b0720878059208ad5d9e678f36
Author: Mingyu Chen (Rayner) <[email protected]>
AuthorDate: Thu Aug 6 12:47:38 2026 +0800
[fix](binlog) Fix BE crash when APPEND_ONLY row-binlog scan omits key
columns (#66432)
### What problem does this PR solve?
Issue Number: close #66390
Related PR: #63850
Related Issue: #65418
Problem Summary:
A BE received SIGSEGV in `VMergeIteratorContext::compare()` while
`VMergeIterator::init()` was building its merge heap during a ROW-binlog
regression workload.
Root cause chain:
1. Any row-binlog scan (`binlog_scan_type != NONE`, including
`APPEND_ONLY`) forced a key-ordered merged read with
`read_orderby_key_num_prefix_columns == 0`.
2. With a zero prefix, `read_orderby_key_columns` stays null, so both
merge comparators (`VMergeIteratorContext::compare()` at the rowset
level and `VCollectIterator::LevelIteratorComparator` at the rowset-tree
level) fall back to comparing the first `num_key_columns` block
positions. `Schema::num_key_columns()` counts the key columns of the
**whole tablet schema**, independent of the projection, so the fallback
silently assumes the projection starts with the full ordered key prefix.
3. `MIN_DELTA` / `DETAIL` satisfy that contract because they widen the
storage projection with every key column. `APPEND_ONLY` takes the
`direct_mode` branch (`return_columns = SQL projection`), which may omit
some or all key columns (e.g. `SELECT v1 FROM t@incr(... "incrementType"
= "APPEND_ONLY")` on a table with two key columns).
4. `Block::compare_at()` has only DCHECK bounds checks, so in a release
build the first heap comparison reads past the block's column array and
kills the BE. This is reachable whenever a rowset has more than one
segment (`force_key_ordered_read` makes `is_merge_iterator()` true even
for non-overlapping segments) or rowsets overlap. A projection that has
enough columns but not the leading keys would instead be compared on the
wrong columns — a silent misordering.
Fix (three layers):
1. **Root cause (`olap_scanner.cpp`)**: only `MIN_DELTA` / `DETAIL` —
the modes that actually group by key and reconstruct BEFORE/AFTER rows,
and whose projection is widened with the full key prefix — force the
key-ordered merged read. `APPEND_ONLY` does no key grouping (it is a
plain op-filter + TSO-range stream), so it now reads unordered like a
plain scan, which also removes an unnecessary merge-heap cost from the
highest-throughput mode. ORDER BY / TopN pushdown params stay disabled
for all binlog scan types, as before.
2. **Defense in `VMergeIteratorContext::init()`**: validate the compare
contract once the first block is loaded — explicit compare columns must
point inside the block; the default key-prefix comparison requires the
projection to start with exactly the schema's key column ids in order;
the sequence tie-break position must be in range. Violations return an
`InternalError` carrying tablet/rowset/version/projection details
instead of an out-of-bounds read.
3. **Same defense in `VCollectIterator::Level1Iterator::init()`**:
validate every child's first block against the same contract before
anything is pushed into the merge heap.
---
be/src/exec/scan/olap_scanner.cpp | 33 ++++--
be/src/storage/iterator/vcollect_iterator.cpp | 57 +++++++++
be/src/storage/iterator/vcollect_iterator.h | 7 ++
be/src/storage/iterator/vgeneric_iterators.cpp | 53 +++++++++
be/src/storage/iterator/vgeneric_iterators.h | 6 +
be/test/exec/scan/vgeneric_iterators_test.cpp | 103 +++++++++++++++-
.../test_binlog_append_only_projection.groovy | 132 +++++++++++++++++++++
7 files changed, 379 insertions(+), 12 deletions(-)
diff --git a/be/src/exec/scan/olap_scanner.cpp
b/be/src/exec/scan/olap_scanner.cpp
index fee0f1b6199..14c5c02b9b5 100644
--- a/be/src/exec/scan/olap_scanner.cpp
+++ b/be/src/exec/scan/olap_scanner.cpp
@@ -476,13 +476,14 @@ Status OlapScanner::_init_tablet_reader_params(
}
};
- // For row-binlog scans that emit BEFORE/AFTER pairs (MIN_DELTA / DETAIL),
we must read
- // every key column, every requested value column, the binlog meta columns
(tso / op)
- // and their __BEFORE__ mirrors, so the BlockReader can reconstruct change
rows.
- const bool need_before_columns =
+ // MIN_DELTA / DETAIL row-binlog scans reconstruct change rows in
BlockReader through a
+ // key-ordered merge. They must read every key column, every requested
value column, the
+ // binlog meta columns (tso / op) and their __BEFORE__ mirrors.
APPEND_ONLY streams rows
+ // as-is and stays on the plain projection paths below.
+ const bool is_binlog_merge_scan =
_tablet_reader_params.binlog_scan_type ==
TBinlogScanType::MIN_DELTA ||
_tablet_reader_params.binlog_scan_type == TBinlogScanType::DETAIL;
- if (need_before_columns) {
+ if (is_binlog_merge_scan) {
for (size_t i = 0; i < tablet_schema->num_key_columns(); ++i) {
add_return_column_if_absent(static_cast<uint32_t>(i));
}
@@ -565,16 +566,26 @@ Status OlapScanner::_init_tablet_reader_params(
RETURN_IF_ERROR(_init_tso_pushdown());
- // For any row-binlog scan, force the storage layer to deliver rows
strictly in primary-key
- // order so the BlockReader can group consecutive same-key changes
(MIN_DELTA) or emit
- // BEFORE/AFTER pairs in deterministic order (DETAIL). Disable ORDER BY /
TopN pushdowns
- // and reset their related params, since they would otherwise re-order the
stream.
+ // Row-binlog scans must not be re-ordered or truncated by ORDER BY / TopN
pushdowns,
+ // so reset every reorder-related param for all binlog scan types.
+ //
+ // Only MIN_DELTA / DETAIL additionally force the storage layer to deliver
rows strictly
+ // in primary-key order, so the BlockReader can group consecutive same-key
changes
+ // (MIN_DELTA) or emit BEFORE/AFTER pairs in deterministic order (DETAIL).
Their storage
+ // projection is widened above with the full key prefix, which the
key-ordered merge
+ // comparator relies on: with read_orderby_key_num_prefix_columns == 0 the
comparator
+ // falls back to comparing the first num_key_columns block positions.
+ //
+ // APPEND_ONLY does no key grouping and keeps the raw SQL projection,
which may omit
+ // some or even all key columns. Forcing a key-ordered merge would make
the fallback
+ // comparator read key positions that do not exist in the projected blocks
and crash
+ // the BE (issue #66390), so it reads unordered like a plain scan.
if (_tablet_reader_params.binlog_scan_type != TBinlogScanType::NONE) {
- _tablet_reader_params.read_orderby_key = true;
+ _tablet_reader_params.read_orderby_key = is_binlog_merge_scan;
+ _tablet_reader_params.force_key_ordered_read = is_binlog_merge_scan;
_tablet_reader_params.read_orderby_key_reverse = false;
_tablet_reader_params.read_orderby_key_num_prefix_columns = 0;
_tablet_reader_params.read_orderby_key_limit = 0;
- _tablet_reader_params.force_key_ordered_read = true;
_tablet_reader_params.topn_filter_source_node_ids.clear();
}
diff --git a/be/src/storage/iterator/vcollect_iterator.cpp
b/be/src/storage/iterator/vcollect_iterator.cpp
index 8b2a786e1a6..51371b34e66 100644
--- a/be/src/storage/iterator/vcollect_iterator.cpp
+++ b/be/src/storage/iterator/vcollect_iterator.cpp
@@ -709,6 +709,8 @@ Status VCollectIterator::Level1Iterator::init(bool
get_data_by_ref) {
}
}
+ RETURN_IF_ERROR(_validate_merge_compare_contract(sequence_loc));
+
_heap = std::make_unique<MergeHeap>(LevelIteratorComparator(
sequence_loc, _is_reverse,
_reader->_reader_context.use_insert_order_when_same,
tso_col_id >= 0));
@@ -766,6 +768,61 @@ void
VCollectIterator::Level1Iterator::init_level0_iterators_for_union() {
}
}
+// LevelIteratorComparator reads block positions that carry no bounds checks
in release
+// builds: either the explicit compare-column positions, or positions
+// [0, tablet_schema().num_key_columns()) plus the sequence tie-break
position. A read
+// projection that omits or reorders the leading key columns would turn the
first heap
+// comparison into an out-of-bounds or semantically wrong positional access
(issue #66390).
+// Validate the contract against every child's first block before anything
enters _heap.
+Status VCollectIterator::Level1Iterator::_validate_merge_compare_contract(int
sequence_loc) const {
+ const auto& return_columns = _reader->_return_columns;
+ const size_t num_key_columns = _schema.num_key_columns();
+ for (const auto& child : _children) {
+ const IteratorRowRef* ref = child->current_row_ref();
+ if (ref->block == nullptr) {
+ continue;
+ }
+ const size_t block_columns = ref->block->columns();
+ auto contract_error = [&](const std::string& detail) {
+ std::string projected_ids;
+ for (auto cid : return_columns) {
+ if (!projected_ids.empty()) {
+ projected_ids += ',';
+ }
+ projected_ids += std::to_string(cid);
+ }
+ return Status::InternalError(
+ "merge heap compare contract violated: {}, tablet_id={},
block_columns={}, "
+ "num_key_columns={}, sequence_loc={}, return_columns=[{}]",
+ detail, _reader->_tablet->tablet_id(), block_columns,
num_key_columns,
+ sequence_loc, projected_ids);
+ };
+ if (_compare_columns != nullptr) {
+ for (uint32_t pos : *_compare_columns) {
+ if (pos >= block_columns) {
+ return contract_error(
+ fmt::format("compare column position {} out of
range", pos));
+ }
+ }
+ } else {
+ if (num_key_columns > return_columns.size() || num_key_columns >
block_columns) {
+ return contract_error("projection has fewer columns than the
key prefix");
+ }
+ for (size_t i = 0; i < num_key_columns; ++i) {
+ if (return_columns[i] != i) {
+ return contract_error(
+ fmt::format("position {} holds column id {}
instead of key column {}",
+ i, return_columns[i], i));
+ }
+ }
+ }
+ if (sequence_loc != -1 && static_cast<size_t>(sequence_loc) >=
block_columns) {
+ return contract_error("sequence column position out of range");
+ }
+ }
+ return Status::OK();
+}
+
Status VCollectIterator::Level1Iterator::_merge_next(IteratorRowRef* ref) {
auto res = _cur_child->next(ref);
if (LIKELY(res.ok())) {
diff --git a/be/src/storage/iterator/vcollect_iterator.h
b/be/src/storage/iterator/vcollect_iterator.h
index bf99a85fcac..9af05e0ced1 100644
--- a/be/src/storage/iterator/vcollect_iterator.h
+++ b/be/src/storage/iterator/vcollect_iterator.h
@@ -324,6 +324,13 @@ private:
bool collected_enough_rows(const MutableColumns& columns, int
rows_to_merge) const;
private:
+ // Validate that every block position LevelIteratorComparator may
touch exists in
+ // each child's current block and, for the default key-prefix
comparison, that the
+ // read projection really starts with the full ordered key prefix.
Called before
+ // any child is pushed into _heap, so a broken projection surfaces as
an error
+ // instead of an out-of-bounds positional access (issue #66390).
+ Status _validate_merge_compare_contract(int sequence_loc) const;
+
Status _merge_next(IteratorRowRef* ref);
Status _normal_next(IteratorRowRef* ref);
diff --git a/be/src/storage/iterator/vgeneric_iterators.cpp
b/be/src/storage/iterator/vgeneric_iterators.cpp
index b854776c6f8..13e6040b799 100644
--- a/be/src/storage/iterator/vgeneric_iterators.cpp
+++ b/be/src/storage/iterator/vgeneric_iterators.cpp
@@ -296,6 +296,7 @@ Status VMergeIteratorContext::init(const
StorageReadOptions& opts) {
_record_rowids = opts.record_rowids;
RETURN_IF_ERROR(_load_next_block());
if (valid()) {
+ RETURN_IF_ERROR(_validate_compare_contract(opts));
RETURN_IF_ERROR(advance());
}
_pre_ctx_same_bit.reserve(_block_row_max);
@@ -303,6 +304,58 @@ Status VMergeIteratorContext::init(const
StorageReadOptions& opts) {
return Status::OK();
}
+// compare() reads block positions that are only DCHECK-bounds-checked in
Block::compare_at(),
+// so in a release build a projection violating the merge contract turns into
an out-of-bounds
+// read inside std::push_heap and kills the BE (issue #66390). Verify the
contract once the
+// first block is loaded and surface a diagnosable error instead:
+// - explicit compare columns (_compare_columns) must all point inside the
block;
+// - otherwise the default comparison touches positions [0,
_num_key_columns), where
+// _num_key_columns counts the key columns of the WHOLE tablet schema. Key
columns always
+// occupy column ids [0, num_key_columns) of the tablet schema, so the
projection must
+// start with exactly those ids, in order, for the positional comparison
to be key order;
+// - the sequence tie-break column, when present, must point inside the
block as well.
+Status VMergeIteratorContext::_validate_compare_contract(const
StorageReadOptions& opts) const {
+ const size_t block_columns = _block->columns();
+ auto contract_error = [&](const std::string& detail) {
+ std::string projected_ids;
+ for (auto cid : _output_schema->column_ids()) {
+ if (!projected_ids.empty()) {
+ projected_ids += ',';
+ }
+ projected_ids += std::to_string(cid);
+ }
+ return Status::InternalError(
+ "merge iterator compare contract violated: {}, tablet_id={},
rowset_id={}, "
+ "version={}, block_columns={}, num_key_columns={},
sequence_id_idx={}, "
+ "projected_column_ids=[{}]",
+ detail, opts.tablet_id, opts.rowset_id.to_string(),
opts.version.to_string(),
+ block_columns, _num_key_columns, _sequence_id_idx,
projected_ids);
+ };
+ if (_compare_columns != nullptr) {
+ for (uint32_t pos : *_compare_columns) {
+ if (pos >= block_columns) {
+ return contract_error(fmt::format("compare column position {}
out of range", pos));
+ }
+ }
+ } else {
+ const auto num_key_columns = static_cast<size_t>(_num_key_columns);
+ if (num_key_columns > _output_schema->num_column_ids() ||
num_key_columns > block_columns) {
+ return contract_error("projection has fewer columns than the key
prefix");
+ }
+ for (size_t i = 0; i < num_key_columns; ++i) {
+ if (_output_schema->column_ids()[i] != static_cast<ColumnId>(i)) {
+ return contract_error(
+ fmt::format("position {} holds column id {} instead of
key column {}", i,
+ _output_schema->column_ids()[i], i));
+ }
+ }
+ }
+ if (_sequence_id_idx != -1 && static_cast<size_t>(_sequence_id_idx) >=
block_columns) {
+ return contract_error("sequence column position out of range");
+ }
+ return Status::OK();
+}
+
Status VMergeIteratorContext::advance() {
_skip = false;
_same = false;
diff --git a/be/src/storage/iterator/vgeneric_iterators.h
b/be/src/storage/iterator/vgeneric_iterators.h
index aacf8175457..38eee70cfae 100644
--- a/be/src/storage/iterator/vgeneric_iterators.h
+++ b/be/src/storage/iterator/vgeneric_iterators.h
@@ -185,6 +185,12 @@ private:
// Load next block into _block
Status _load_next_block();
+ // Validate that every block position compare() may touch actually exists
in _block
+ // and, for the default key-prefix comparison, that the projection really
starts with
+ // the full ordered key prefix. Returns an error instead of letting
compare() perform
+ // an out-of-bounds or semantically wrong positional access (issue #66390).
+ Status _validate_compare_contract(const StorageReadOptions& opts) const;
+
RowwiseIteratorUPtr _iter;
int _sequence_id_idx = -1;
diff --git a/be/test/exec/scan/vgeneric_iterators_test.cpp
b/be/test/exec/scan/vgeneric_iterators_test.cpp
index f8e3e09401a..d461512c4e0 100644
--- a/be/test/exec/scan/vgeneric_iterators_test.cpp
+++ b/be/test/exec/scan/vgeneric_iterators_test.cpp
@@ -42,7 +42,7 @@ public:
virtual ~VGenericIteratorsTest() {}
};
-static Schema create_schema() {
+static std::vector<TabletColumnPtr> create_col_schemas() {
std::vector<TabletColumnPtr> col_schemas;
auto c1 =
std::make_shared<TabletColumn>(FieldAggregationMethod::OLAP_FIELD_AGGREGATION_NONE,
FieldType::OLAP_FIELD_TYPE_SMALLINT, true);
@@ -57,6 +57,11 @@ static Schema create_schema() {
col_schemas.emplace_back(
std::make_shared<TabletColumn>(FieldAggregationMethod::OLAP_FIELD_AGGREGATION_SUM,
FieldType::OLAP_FIELD_TYPE_BIGINT,
true));
+ return col_schemas;
+}
+
+static Schema create_schema() {
+ std::vector<TabletColumnPtr> col_schemas = create_col_schemas();
std::vector<ColumnId> column_ids(col_schemas.size());
for (uint32_t cid = 0; cid < column_ids.size(); ++cid) {
@@ -402,4 +407,100 @@ TEST(VGenericIteratorsTest,
MergeWithSeqColumnSmallSeqFirst) {
EXPECT_EQ(0, actual_value);
}
+// Emits num_rows rows for a schema whose projection (col_ids) may be narrower
than the
+// full tablet schema. The filled block layout follows the projection,
matching how
+// VMergeIteratorContext::block_reset builds its block from the output schema.
+class ProjectedColumnsUtIterator : public RowwiseIterator {
+public:
+ ProjectedColumnsUtIterator(Schema schema, size_t num_rows)
+ : _schema(std::move(schema)), _num_rows(num_rows) {}
+ ~ProjectedColumnsUtIterator() override = default;
+
+ Status init(const StorageReadOptions& opts) override { return
Status::OK(); }
+
+ Status next_batch(Block* block) override {
+ if (_rows_returned >= _num_rows) {
+ return Status::EndOfFile("End of ProjectedColumnsUtIterator");
+ }
+ while (_rows_returned < _num_rows) {
+ for (size_t j = 0; j < _schema.num_column_ids(); ++j) {
+ ColumnWithTypeAndName& vc = block->get_by_position(j);
+ IColumn& vi = (IColumn&)(*vc.column);
+
+ char data[16] = {};
+ size_t data_len = 0;
+ const auto* col_schema = _schema.column(_schema.column_id(j));
+ switch (col_schema->type()) {
+ case FieldType::OLAP_FIELD_TYPE_SMALLINT:
+ *(int16_t*)data = static_cast<int16_t>(_rows_returned);
+ data_len = sizeof(int16_t);
+ break;
+ case FieldType::OLAP_FIELD_TYPE_INT:
+ *(int32_t*)data = static_cast<int32_t>(_rows_returned);
+ data_len = sizeof(int32_t);
+ break;
+ case FieldType::OLAP_FIELD_TYPE_BIGINT:
+ *(int64_t*)data = static_cast<int64_t>(_rows_returned);
+ data_len = sizeof(int64_t);
+ break;
+ default:
+ break;
+ }
+
+ vi.insert_data(data, data_len);
+ }
+ ++_rows_returned;
+ }
+ return Status::OK();
+ }
+
+ const Schema& schema() const override { return _schema; }
+
+private:
+ Schema _schema;
+ size_t _num_rows;
+ size_t _rows_returned = 0;
+};
+
+// The merge-heap comparator compares the first num_key_columns block
positions when no
+// explicit compare columns are given, and num_key_columns counts the key
columns of the
+// WHOLE tablet schema. A projection narrower than the key prefix must be
rejected at
+// init time with an error instead of crashing inside std::push_heap (issue
#66390: a
+// ROW-binlog APPEND_ONLY scan projected value columns only).
+TEST(VGenericIteratorsTest, MergeRejectsProjectionMissingKeyPrefix) {
+ // Full tablet schema: k0(smallint), k1(int), v2(bigint); project only v2.
+ Schema projected(create_col_schemas(), std::vector<ColumnId> {2});
+ auto output_schema = std::make_shared<Schema>(projected);
+
+ std::vector<RowwiseIteratorUPtr> inputs;
+ inputs.push_back(std::make_unique<ProjectedColumnsUtIterator>(projected,
10));
+ inputs.push_back(std::make_unique<ProjectedColumnsUtIterator>(projected,
10));
+
+ auto iter = new_merge_iterator(std::move(inputs), -1, false, false,
nullptr, output_schema);
+ StorageReadOptions opts;
+ auto st = iter->init(opts);
+ EXPECT_FALSE(st.ok());
+ EXPECT_TRUE(st.to_string().find("compare contract violated") !=
std::string::npos)
+ << st.to_string();
+}
+
+// Same as above, but the projection has enough columns while not starting
with the full
+// ordered key prefix (k0 is missing): position 0 would be compared as if it
were k0.
+TEST(VGenericIteratorsTest, MergeRejectsProjectionWithoutLeadingKey) {
+ // Full tablet schema: k0(smallint), k1(int), v2(bigint); project {k1, v2}.
+ Schema projected(create_col_schemas(), std::vector<ColumnId> {1, 2});
+ auto output_schema = std::make_shared<Schema>(projected);
+
+ std::vector<RowwiseIteratorUPtr> inputs;
+ inputs.push_back(std::make_unique<ProjectedColumnsUtIterator>(projected,
10));
+ inputs.push_back(std::make_unique<ProjectedColumnsUtIterator>(projected,
10));
+
+ auto iter = new_merge_iterator(std::move(inputs), -1, false, false,
nullptr, output_schema);
+ StorageReadOptions opts;
+ auto st = iter->init(opts);
+ EXPECT_FALSE(st.ok());
+ EXPECT_TRUE(st.to_string().find("compare contract violated") !=
std::string::npos)
+ << st.to_string();
+}
+
} // namespace doris
diff --git
a/regression-test/suites/row_binlog_p0/test_binlog_append_only_projection.groovy
b/regression-test/suites/row_binlog_p0/test_binlog_append_only_projection.groovy
new file mode 100644
index 00000000000..23369879bb9
--- /dev/null
+++
b/regression-test/suites/row_binlog_p0/test_binlog_append_only_projection.groovy
@@ -0,0 +1,132 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+// Regression test for issue #66390: a ROW-binlog APPEND_ONLY scan whose SQL
projection
+// contains fewer columns than the tablet key count (or omits the leading key
columns)
+// used to force a key-ordered merge in the storage layer. The merge
comparator then read
+// key positions that do not exist in the projected blocks and crashed the BE
with SIGSEGV
+// inside VMergeIterator::init / std::push_heap.
+//
+// The table has TWO key columns and the queries below project one value
column only
+// (0 key columns read), or a non-leading key subset, over overlapping
multi-rowset data,
+// which is exactly the shape that used to crash.
+suite("test_binlog_append_only_projection", "nonConcurrent") {
+ if (isCloudMode()) {
+ return
+ }
+ sql "DROP DATABASE IF EXISTS test_binlog_append_only_projection_db"
+ sql "CREATE DATABASE test_binlog_append_only_projection_db"
+ sql "USE test_binlog_append_only_projection_db"
+ sql "set enable_nereids_planner=true"
+ sql "set enable_fallback_to_original_planner=false"
+
+ def dupTable = "append_only_proj_dup"
+ def incrTimeFormat = new java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss")
+
+ try {
+ sql "DROP TABLE IF EXISTS ${dupTable}"
+
+ // Two key columns so that any single-column projection is narrower
than the
+ // key prefix the merge comparator would use.
+ sql """
+ CREATE TABLE ${dupTable} (
+ k1 BIGINT,
+ k2 INT,
+ v1 INT,
+ v2 VARCHAR(16) NULL
+ ) ENGINE=OLAP
+ DUPLICATE KEY(k1, k2)
+ DISTRIBUTED BY HASH(k1) BUCKETS 1
+ PROPERTIES (
+ "replication_num" = "1",
+ "binlog.enable" = "true",
+ "binlog.format" = "ROW"
+ )
+ """
+
+ sql "INSERT INTO ${dupTable} VALUES (1, 1, 10, 'seed1')"
+ sql "INSERT INTO ${dupTable} VALUES (2, 2, 20, 'seed2')"
+ sql "sync"
+ sleep(1200)
+ def t0 = incrTimeFormat.format(new Date())
+ sleep(1200)
+
+ // Each INSERT produces its own rowset; interleaved keys make the
rowsets'
+ // key ranges overlap, so a key-ordered read would need a real merge.
+ sql "INSERT INTO ${dupTable} VALUES (1, 3, 30, 'w1'), (9, 1, 31, 'w1')"
+ sql "INSERT INTO ${dupTable} VALUES (2, 4, 40, NULL), (8, 2, 41, 'w2')"
+ sql "INSERT INTO ${dupTable} VALUES (1, 5, 50, 'w3'), (9, 3, 51, NULL)"
+ sql "sync"
+ sleep(1200)
+ def t1 = incrTimeFormat.format(new Date())
+ sleep(1200)
+ sql "INSERT INTO ${dupTable} VALUES (7, 7, 70, 'late')"
+ sql "sync"
+
+ // 1. Project a single value column: 1 projected column < 2 key
columns.
+ // This is the exact shape that used to crash the BE.
+ assertEquals([[30], [31], [40], [41], [50], [51]],
+ sql("""SELECT v1
+ FROM ${dupTable}@incr('startTimestamp' = '${t0}',
+ "endTimestamp" = "${t1}",
+ "incrementType" = "APPEND_ONLY")
+ ORDER BY v1"""))
+
+ // 2. Project only the second key column: enough columns to compare,
but the
+ // leading key k1 is absent, so a positional key comparison would
have
+ // silently compared the wrong columns.
+ assertEquals([[1], [2], [3], [3], [4], [5]],
+ sql("""SELECT k2
+ FROM ${dupTable}@incr('startTimestamp' = '${t0}',
+ "endTimestamp" = "${t1}",
+ "incrementType" = "APPEND_ONLY")
+ ORDER BY k2"""))
+
+ // 3. Projection in reversed column order relative to the schema.
+ assertEquals([[30, 3L], [31, 1L], [40, 4L], [41, 2L], [50, 5L], [51,
3L]],
+ sql("""SELECT v1, CAST(k2 AS BIGINT)
+ FROM ${dupTable}@incr('startTimestamp' = '${t0}',
+ "endTimestamp" = "${t1}",
+ "incrementType" = "APPEND_ONLY")
+ ORDER BY v1"""))
+
+ // 4. Aggregate over a narrow projection.
+ assertEquals([[6L, 243L]],
+ sql("""SELECT count(*), sum(v1)
+ FROM ${dupTable}@incr('startTimestamp' = '${t0}',
+ "endTimestamp" = "${t1}",
+ "incrementType" = "APPEND_ONLY")"""))
+
+ // 5. DETAIL / MIN_DELTA keep the forced key-ordered merge but widen
the
+ // storage projection with the full key prefix internally; a narrow
SQL
+ // projection must still work and return the same rows for a dup
table.
+ assertEquals([[30], [31], [40], [41], [50], [51]],
+ sql("""SELECT v1
+ FROM ${dupTable}@incr('startTimestamp' = '${t0}',
+ "endTimestamp" = "${t1}",
+ "incrementType" = "DETAIL")
+ ORDER BY v1"""))
+ assertEquals([[30], [31], [40], [41], [50], [51]],
+ sql("""SELECT v1
+ FROM ${dupTable}@incr('startTimestamp' = '${t0}',
+ "endTimestamp" = "${t1}",
+ "incrementType" = "MIN_DELTA")
+ ORDER BY v1"""))
+ } finally {
+ sql "DROP DATABASE IF EXISTS test_binlog_append_only_projection_db"
+ }
+}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]