This is an automated email from the ASF dual-hosted git repository.
gavinchou 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 3175cc01d11 [fix](be) Suppress no-op updates in min delta scans
(#66849)
3175cc01d11 is described below
commit 3175cc01d113013e76d30290f22fb9ee6dcc65bc
Author: Gavin Chou <[email protected]>
AuthorDate: Mon Aug 24 16:44:11 2026 +0800
[fix](be) Suppress no-op updates in min delta scans (#66849)
Problem Summary:
MIN_DELTA row-binlog scans previously classified each primary-key window
only from its first and last operation. As a result, an update emitted
`UPDATE_BEFORE` and `UPDATE_AFTER` rows even when the complete row image
was unchanged, including multi-update chains that eventually returned to
their original values.
This change reads all AFTER/BEFORE value columns needed by MIN_DELTA,
compares the first BEFORE image with the final AFTER image, and converts
a net-zero update into `SKIP`. Comparison-only columns remain hidden
from the SQL projection. Missing, incompatible, or unsupported BEFORE
images preserve the existing UPDATE output conservatively.
The tests cover direct no-op updates, changes in unprojected columns,
multiple keys and columns, pending output across batch boundaries, and
complex insert/delete/update chains that either return to the original
image or finish changed.
### Release note
MIN_DELTA row-binlog queries no longer return update rows when the
complete row image is unchanged across the query window.
---
.../storage/iterator/binlog_block_reader_utils.h | 1 -
be/src/storage/iterator/block_reader.cpp | 50 ++
be/src/storage/iterator/block_reader.h | 5 +
be/src/storage/schema.cpp | 85 +++
be/src/storage/schema.h | 16 +
.../iterator/binlog_block_reader_utils_test.cpp | 29 -
.../block_reader_change_next_block_test.cpp | 583 ++++++++++++++++++++-
be/test/storage/read_schema_test.cpp | 52 ++
.../glue/translator/PhysicalPlanTranslator.java | 14 +-
.../translator/PhysicalPlanTranslatorTest.java | 5 +-
.../row_binlog_p0/test_binlog_changes_syntax.out | 3 +
.../test_binlog_changes_syntax.groovy | 19 +
12 files changed, 807 insertions(+), 55 deletions(-)
diff --git a/be/src/storage/iterator/binlog_block_reader_utils.h
b/be/src/storage/iterator/binlog_block_reader_utils.h
index 1dfc3520482..5b3bf53ce02 100644
--- a/be/src/storage/iterator/binlog_block_reader_utils.h
+++ b/be/src/storage/iterator/binlog_block_reader_utils.h
@@ -31,7 +31,6 @@ constexpr int64_t STREAM_CHANGE_INSERT = 0;
constexpr int64_t STREAM_CHANGE_DELETE = 1;
constexpr int64_t STREAM_CHANGE_UPDATE_BEFORE = 2;
constexpr int64_t STREAM_CHANGE_UPDATE_AFTER = 3;
-
enum class MinDeltaResultType { SKIP, INSERT, DELETE, UPDATE_BEFORE_AFTER };
// MIN_DELTA uses row binlog op codes as indices into a 2D lookup table, so we
guard the op layout here.
diff --git a/be/src/storage/iterator/block_reader.cpp
b/be/src/storage/iterator/block_reader.cpp
index 729682d038f..29aa476bd0e 100644
--- a/be/src/storage/iterator/block_reader.cpp
+++ b/be/src/storage/iterator/block_reader.cpp
@@ -31,6 +31,7 @@
#include "cloud/config.h"
#include "common/compiler_util.h" // IWYU pragma: keep
#include "common/config.h"
+#include "common/exception.h"
#include "common/status.h"
#include "core/block/column_with_type_and_name.h"
#include "core/column/column_nullable.h"
@@ -118,6 +119,42 @@ uint32_t
BlockReader::_resolve_source_column_ordinal(uint32_t ordinal, bool use_
return use_before ? _read_schema->before_column_ordinal(ordinal) : ordinal;
}
+bool BlockReader::_min_delta_values_equal(size_t last_row) {
+ const auto& value_column_pairs =
_read_schema->row_binlog_value_column_pairs();
+ if (!_read_schema->row_binlog_value_pairs_complete() ||
value_column_pairs.empty() ||
+ _min_delta_value_compare_unsupported) {
+ return false;
+ }
+ bool before_image_available = false;
+ for (const auto& [after_idx, before_idx] : value_column_pairs) {
+ const auto* before_column = _stored_data_columns[before_idx].get();
+ if (const auto* nullable =
check_and_get_column<ColumnNullable>(*before_column)) {
+ before_image_available |= !nullable->is_null_at(0);
+ } else {
+ before_image_available = true;
+ }
+ try {
+ if (_stored_data_columns[before_idx]->compare_at(
+ 0, last_row, *_stored_data_columns[after_idx], -1) !=
0) {
+ return false;
+ }
+ } catch (const Exception& e) {
+ if (e.code() != ErrorCode::NOT_IMPLEMENTED_ERROR) {
+ throw;
+ }
+ // Column types are stable for the lifetime of this reader. Once
one value column
+ // reports that compare_at is unsupported, no row can be proven to
be a no-op. Cache
+ // that capability result so BITMAP-like columns pay the exception
cost at most once.
+ _min_delta_value_compare_unsupported = true;
+ return false;
+ }
+ }
+ // Historical lookup and the compatibility writer both represent an
unavailable BEFORE image
+ // as an all-NULL row. Without a persisted validity bit, retaining the
UPDATE is the only safe
+ // choice; otherwise a missing-key DELETE followed by an all-NULL INSERT
would disappear.
+ return before_image_available;
+}
+
void BlockReader::_init_pending_row_columns(const Block& block) {
if (!_pending_row_columns.empty()) {
return;
@@ -243,6 +280,11 @@ Status BlockReader::_min_delta_next_block(Block* block,
bool* eof) {
output_row_count++;
break;
case
binlog::AggregateFunctionMinDelta::ResultType::UPDATE_BEFORE_AFTER:
+ if (binlog::is_valid_row_binlog_op(first_op) &&
+ binlog::is_valid_row_binlog_op(last_op) &&
+ _min_delta_values_equal(group_size - 1)) {
+ break;
+ }
for (size_t ordinal = 0; ordinal < target_columns.size();
++ordinal) {
if (static_cast<int32_t>(ordinal) == op_ordinal) {
RETURN_IF_ERROR(_write_binlog_op(*target_columns[ordinal],
@@ -498,6 +540,14 @@ Status BlockReader::init(const ReaderParams& read_params) {
_read_schema = std::move(read_schema);
}
+ if (read_params.binlog_scan_type == TBinlogScanType::MIN_DELTA ||
+ read_params.binlog_scan_type == TBinlogScanType::DETAIL) {
+ auto read_schema = std::make_shared<ReadSchema>(*_read_schema);
+ read_schema->init_row_binlog_column_mappings(*_tablet_schema);
+ _read_schema = std::move(read_schema);
+ _min_delta_value_compare_unsupported = false;
+ }
+
// Every merge and caller Block has the exact read-schema layout. Cache
only
// the non-key columns of AGG tables that go through the aggregate
machinery.
const auto num_block_columns =
static_cast<uint32_t>(_read_schema->num_block_columns());
diff --git a/be/src/storage/iterator/block_reader.h
b/be/src/storage/iterator/block_reader.h
index a93f96ee870..de1ad68fdcd 100644
--- a/be/src/storage/iterator/block_reader.h
+++ b/be/src/storage/iterator/block_reader.h
@@ -81,6 +81,8 @@ private:
uint32_t _resolve_source_column_ordinal(uint32_t ordinal, bool use_before)
const;
+ bool _min_delta_values_equal(size_t last_row);
+
void _init_pending_row_columns(const Block& block);
bool _emit_pending_row(MutableColumns& target_columns, size_t&
output_row_count);
@@ -153,6 +155,9 @@ private:
bool _is_rowsets_overlapping = true;
+ // Unsupported compare_at means equality cannot be proven, so MIN_DELTA
conservatively keeps
+ // UPDATE rows. Column types are stable within a reader; cache this after
the first exception.
+ bool _min_delta_value_compare_unsupported = false;
Arena _arena;
};
diff --git a/be/src/storage/schema.cpp b/be/src/storage/schema.cpp
index e1b7a7c9228..c3fd1af402b 100644
--- a/be/src/storage/schema.cpp
+++ b/be/src/storage/schema.cpp
@@ -29,6 +29,25 @@
namespace doris {
+namespace {
+
+bool row_binlog_value_columns_have_same_type(const TabletColumn& lhs, const
TabletColumn& rhs) {
+ if (lhs.type() != rhs.type() || lhs.is_nullable() != rhs.is_nullable() ||
+ lhs.length() != rhs.length() || lhs.precision() != rhs.precision() ||
+ lhs.frac() != rhs.frac() || lhs.get_subtype_count() !=
rhs.get_subtype_count()) {
+ return false;
+ }
+ for (uint32_t i = 0; i < lhs.get_subtype_count(); ++i) {
+ if (!row_binlog_value_columns_have_same_type(lhs.get_sub_column(i),
+ rhs.get_sub_column(i))) {
+ return false;
+ }
+ }
+ return true;
+}
+
+} // namespace
+
std::vector<TabletColumnPtr> project_columns_by_ordinal(
const std::vector<TabletColumnPtr>& columns,
const std::vector<ColumnId>& source_column_ordinals) {
@@ -99,6 +118,72 @@ void ReadSchema::_init_before_column_ordinals() {
}
}
+void ReadSchema::init_row_binlog_column_mappings(const TabletSchema&
tablet_schema) {
+ DORIS_CHECK_GE(_op_ordinal, 0);
+ DORIS_CHECK_EQ(_before_column_ordinals.size(), _num_block_columns);
+
+ _row_binlog_value_column_pairs.clear();
+ _row_binlog_value_pairs_complete = false;
+
+ std::vector<ColumnId> value_column_ids;
+ value_column_ids.reserve(tablet_schema.num_columns() -
tablet_schema.num_key_columns());
+ for (ColumnId cid = 0; cid < tablet_schema.num_columns(); ++cid) {
+ if (static_cast<int32_t>(cid) == tablet_schema.binlog_tso_col_idx() ||
+ static_cast<int32_t>(cid) == tablet_schema.binlog_lsn_col_idx() ||
+ static_cast<int32_t>(cid) == tablet_schema.binlog_op_col_idx() ||
+ tablet_schema.column(cid).is_key()) {
+ continue;
+ }
+ value_column_ids.push_back(cid);
+ }
+
+ if (value_column_ids.empty() || value_column_ids.size() % 2 != 0) {
+ return;
+ }
+
+ const size_t value_column_count = value_column_ids.size() / 2;
+ for (size_t i = 0; i < value_column_count; ++i) {
+ const auto& after = tablet_schema.column(value_column_ids[i]);
+ const auto& before = tablet_schema.column(value_column_ids[i +
value_column_count]);
+ if (before.name() != binlog::build_before_column_name(after.name()) ||
+ !row_binlog_value_columns_have_same_type(after, before)) {
+ return;
+ }
+ }
+
+ // A valid physical row-binlog layout starts from an identity mapping.
Only AFTER value
+ // columns map to their BEFORE companions; keys, metadata and BEFORE
columns map to themselves.
+ for (ColumnId ordinal = 0; ordinal < _num_block_columns; ++ordinal) {
+ _before_column_ordinals[ordinal] = ordinal;
+ }
+
+ bool complete = true;
+ _row_binlog_value_column_pairs.reserve(value_column_count);
+ for (size_t i = 0; i < value_column_count; ++i) {
+ const auto after_cid = value_column_ids[i];
+ const auto before_cid = value_column_ids[i + value_column_count];
+ const int32_t after_ordinal =
ordinal_by_uid(tablet_schema.column(after_cid).unique_id());
+ const int32_t before_ordinal =
ordinal_by_uid(tablet_schema.column(before_cid).unique_id());
+ if (after_ordinal < 0 || before_ordinal < 0 ||
+ static_cast<size_t>(after_ordinal) >= _num_block_columns ||
+ static_cast<size_t>(before_ordinal) >= _num_block_columns) {
+ complete = false;
+ continue;
+ }
+
+ const auto after = cast_set<ColumnId>(after_ordinal);
+ const auto before = cast_set<ColumnId>(before_ordinal);
+ _before_column_ordinals[after] = before;
+ if (!_read_types[after]->equals(*_read_types[before])) {
+ complete = false;
+ continue;
+ }
+ _row_binlog_value_column_pairs.emplace_back(after, before);
+ }
+ _row_binlog_value_pairs_complete =
+ complete && _row_binlog_value_column_pairs.size() ==
value_column_count;
+}
+
Block ReadSchema::create_read_block() const {
Block block;
for (size_t ordinal = 0; ordinal < _num_block_columns; ++ordinal) {
diff --git a/be/src/storage/schema.h b/be/src/storage/schema.h
index f26732cc67e..473e603a05a 100644
--- a/be/src/storage/schema.h
+++ b/be/src/storage/schema.h
@@ -25,6 +25,7 @@
#include <memory>
#include <string>
#include <unordered_map>
+#include <utility>
#include <vector>
#include "common/consts.h"
@@ -58,6 +59,7 @@ std::vector<TabletColumnPtr> project_columns_by_ordinal(
class ReadSchema {
public:
using SequenceMap = std::unordered_map<ColumnId, std::vector<ColumnId>>;
+ using RowBinlogValueColumnPairs = std::vector<std::pair<ColumnId,
ColumnId>>;
explicit ReadSchema(std::vector<TabletColumnPtr> columns);
@@ -87,6 +89,12 @@ public:
const SequenceMap& sequence_map() const { return _sequence_map; }
+ // Initialize all row-binlog column relationships from the physical tablet
schema and map
+ // them to this ReadSchema's dense ordinals. Physical pairing avoids
ambiguous column-name
+ // lookup, while schemas without a complete physical layout retain the
name-based BEFORE
+ // mapping initialized by the constructor.
+ void init_row_binlog_column_mappings(const TabletSchema& tablet_schema);
+
// Return the matching before-image ordinal for a Row Binlog value column.
For example, in
// [v1, v2, __BEFORE__v1__, __BEFORE__v2__], 0 maps to 2 and 1 maps to 3.
Columns without a
// before image, including TSO/LSN/OP, map to themselves.
@@ -95,6 +103,12 @@ public:
return _before_column_ordinals[ordinal];
}
+ const RowBinlogValueColumnPairs& row_binlog_value_column_pairs() const {
+ return _row_binlog_value_column_pairs;
+ }
+
+ bool row_binlog_value_pairs_complete() const { return
_row_binlog_value_pairs_complete; }
+
const TabletColumn* column(size_t ordinal) const { return
_read_columns[ordinal].get(); }
// Total columns used inside storage, including appended storage-only
columns.
@@ -224,6 +238,8 @@ private:
std::unordered_map<int32_t, int32_t> _uid_to_ordinal;
SequenceMap _sequence_map;
std::vector<ColumnId> _before_column_ordinals;
+ RowBinlogValueColumnPairs _row_binlog_value_column_pairs;
+ bool _row_binlog_value_pairs_complete = false;
};
} // namespace doris
diff --git a/be/test/storage/iterator/binlog_block_reader_utils_test.cpp
b/be/test/storage/iterator/binlog_block_reader_utils_test.cpp
deleted file mode 100644
index 9d099fe1c92..00000000000
--- a/be/test/storage/iterator/binlog_block_reader_utils_test.cpp
+++ /dev/null
@@ -1,29 +0,0 @@
-// 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.
-
-#include "storage/iterator/binlog_block_reader_utils.h"
-
-#include <gtest/gtest.h>
-
-namespace doris {
-
-class BinlogBlockReaderUtilsTest : public testing::Test {};
-TEST_F(BinlogBlockReaderUtilsTest, BuildBeforeColumnName) {
- EXPECT_EQ(binlog::build_before_column_name("v1"), "__BEFORE__v1__");
-}
-
-} // namespace doris
diff --git a/be/test/storage/iterator/block_reader_change_next_block_test.cpp
b/be/test/storage/iterator/block_reader_change_next_block_test.cpp
index d7fcd447152..a75bd4a6cfe 100644
--- a/be/test/storage/iterator/block_reader_change_next_block_test.cpp
+++ b/be/test/storage/iterator/block_reader_change_next_block_test.cpp
@@ -36,13 +36,20 @@
#include <memory>
#include <string>
+#include <string_view>
+#include <utility>
#include <vector>
#include "common/config.h"
+#include "common/exception.h"
#include "common/status.h"
#include "core/assert_cast.h"
#include "core/block/block.h"
+#include "core/column/column_dummy.h"
+#include "core/column/column_nullable.h"
#include "core/column/column_vector.h"
+#include "core/data_type/data_type_nothing.h"
+#include "core/data_type/data_type_nullable.h"
#include "core/data_type/data_type_number.h"
#include "storage/binlog.h"
#include "storage/iterator/binlog_block_reader_utils.h"
@@ -77,6 +84,70 @@ struct Row {
int64_t op;
};
+class ThrowOnCompareColumn final : public COWHelper<IColumnDummy,
ThrowOnCompareColumn> {
+private:
+ friend class COWHelper<IColumnDummy, ThrowOnCompareColumn>;
+
+ ThrowOnCompareColumn(size_t size, int error_code, std::shared_ptr<size_t>
compare_calls)
+ : _error_code(error_code),
_compare_calls(std::move(compare_calls)) {
+ s = size;
+ }
+ ThrowOnCompareColumn(const ThrowOnCompareColumn&) = default;
+
+public:
+ std::string get_name() const override { return "ThrowOnCompare"; }
+
+ MutableColumnPtr clone_dummy(size_t size) const override {
+ return ThrowOnCompareColumn::create(size, _error_code, _compare_calls);
+ }
+
+ bool structure_equals(const IColumn& rhs) const override {
+ return typeid(rhs) == typeid(ThrowOnCompareColumn);
+ }
+
+ int compare_at(size_t, size_t, const IColumn&, int) const override {
+ ++*_compare_calls;
+ throw Exception(_error_code, "injected compare_at failure");
+ }
+
+private:
+ int _error_code;
+ std::shared_ptr<size_t> _compare_calls;
+};
+
+TabletColumn make_test_column(std::string name, FieldType type, bool is_key,
bool is_nullable,
+ int32_t unique_id) {
+ TabletColumn column(FieldAggregationMethod::OLAP_FIELD_AGGREGATION_NONE,
type, is_nullable,
+ unique_id, sizeof(int64_t));
+ column.set_name(std::move(name));
+ column.set_is_key(is_key);
+ column.set_index_length(sizeof(int64_t));
+ return column;
+}
+
+TabletSchemaSPtr make_test_tablet_schema(
+ const std::vector<std::pair<std::string, FieldType>>& value_columns = {
+ {"val", FieldType::OLAP_FIELD_TYPE_BIGINT}}) {
+ auto schema = std::make_shared<TabletSchema>();
+ int32_t unique_id = 0;
+ schema->append_column(
+ make_test_column("key", FieldType::OLAP_FIELD_TYPE_BIGINT, true,
false, unique_id++));
+ for (const auto& [name, type] : value_columns) {
+ schema->append_column(make_test_column(name, type, false, true,
unique_id++));
+ }
+ for (const auto& [name, type] : value_columns) {
+
schema->append_column(make_test_column(binlog::build_before_column_name(name),
type, false,
+ true, unique_id++));
+ }
+ schema->append_column(make_test_column(BINLOG_TSO_COL,
FieldType::OLAP_FIELD_TYPE_BIGINT, false,
+ true, unique_id++));
+ schema->append_column(make_test_column(BINLOG_LSN_COL,
FieldType::OLAP_FIELD_TYPE_BIGINT, false,
+ false, unique_id++));
+ schema->append_column(make_test_column(BINLOG_OP_COL,
FieldType::OLAP_FIELD_TYPE_BIGINT, false,
+ false, unique_id));
+ return schema;
+}
+
std::shared_ptr<Block> make_source_block(const std::vector<Row>& rows) {
auto block = std::make_shared<Block>();
auto type = std::make_shared<DataTypeInt64>();
@@ -104,6 +175,118 @@ std::shared_ptr<Block> make_source_block(const
std::vector<Row>& rows) {
return block;
}
+std::shared_ptr<Block> make_all_null_source_block() {
+ auto block = std::make_shared<Block>();
+ auto int_type = std::make_shared<DataTypeInt64>();
+ auto nullable_int_type = make_nullable(int_type);
+ auto key_col = ColumnInt64::create();
+ auto val_col = nullable_int_type->create_column();
+ auto before_col = nullable_int_type->create_column();
+ auto tso_col = ColumnInt64::create();
+ auto lsn_col = ColumnInt64::create();
+ auto op_col = ColumnInt64::create();
+
+ key_col->insert_many_vals(1, 2);
+ val_col->insert_many_defaults(2);
+ before_col->insert_many_defaults(2);
+ tso_col->insert_value(1);
+ tso_col->insert_value(2);
+ lsn_col->insert_value(1);
+ lsn_col->insert_value(2);
+ op_col->insert_value(ROW_BINLOG_DELETE);
+ op_col->insert_value(ROW_BINLOG_APPEND);
+
+ block->insert({std::move(key_col), int_type, "key"});
+ block->insert({std::move(val_col), nullable_int_type, "val"});
+ block->insert(
+ {std::move(before_col), nullable_int_type,
binlog::build_before_column_name("val")});
+ block->insert({std::move(tso_col), int_type, BINLOG_TSO_COL});
+ block->insert({std::move(lsn_col), int_type, BINLOG_LSN_COL});
+ block->insert({std::move(op_col), int_type, BINLOG_OP_COL});
+ return block;
+}
+
+std::shared_ptr<Block> make_colliding_name_source_block(int64_t after_v,
int64_t after_collision,
+ int64_t before_v,
+ int64_t
before_collision) {
+ auto block = std::make_shared<Block>();
+ auto type = std::make_shared<DataTypeInt64>();
+ const std::vector<std::pair<std::string, int64_t>> values = {
+ {"key", 1},
+ {"v", after_v},
+ {"__BEFORE__v__", after_collision},
+ {binlog::build_before_column_name("v"), before_v},
+ {binlog::build_before_column_name("__BEFORE__v__"),
before_collision},
+ {BINLOG_TSO_COL, 1},
+ {BINLOG_LSN_COL, 1},
+ {BINLOG_OP_COL, ROW_BINLOG_UPDATE},
+ };
+ for (const auto& [name, value] : values) {
+ auto column = ColumnInt64::create();
+ column->insert_value(value);
+ block->insert({std::move(column), type, name});
+ }
+ return block;
+}
+
+std::shared_ptr<Block> make_signed_zero_source_block() {
+ auto block = std::make_shared<Block>();
+ auto int_type = std::make_shared<DataTypeInt64>();
+ auto float_type = std::make_shared<DataTypeFloat32>();
+ auto key_col = ColumnInt64::create();
+ auto val_col = ColumnFloat32::create();
+ auto before_col = ColumnFloat32::create();
+ auto tso_col = ColumnInt64::create();
+ auto lsn_col = ColumnInt64::create();
+ auto op_col = ColumnInt64::create();
+
+ key_col->insert_value(1);
+ val_col->insert_value(-0.0F);
+ before_col->insert_value(+0.0F);
+ tso_col->insert_value(1);
+ lsn_col->insert_value(1);
+ op_col->insert_value(ROW_BINLOG_UPDATE);
+
+ block->insert({std::move(key_col), int_type, "key"});
+ block->insert({std::move(val_col), float_type, "val"});
+ block->insert({std::move(before_col), float_type,
binlog::build_before_column_name("val")});
+ block->insert({std::move(tso_col), int_type, BINLOG_TSO_COL});
+ block->insert({std::move(lsn_col), int_type, BINLOG_LSN_COL});
+ block->insert({std::move(op_col), int_type, BINLOG_OP_COL});
+ return block;
+}
+
+std::shared_ptr<Block> make_unsupported_compare_source_block(
+ const std::shared_ptr<size_t>& compare_calls) {
+ auto block = std::make_shared<Block>();
+ auto int_type = std::make_shared<DataTypeInt64>();
+ auto nothing_type = std::make_shared<DataTypeNothing>();
+ auto key_col = ColumnInt64::create();
+ auto val_col = ThrowOnCompareColumn::create(0,
ErrorCode::NOT_IMPLEMENTED_ERROR, compare_calls);
+ auto before_col =
+ ThrowOnCompareColumn::create(0, ErrorCode::NOT_IMPLEMENTED_ERROR,
compare_calls);
+ auto tso_col = ColumnInt64::create();
+ auto lsn_col = ColumnInt64::create();
+ auto op_col = ColumnInt64::create();
+
+ for (int64_t key : {1, 2}) {
+ key_col->insert_value(key);
+ val_col->insert_default();
+ before_col->insert_default();
+ tso_col->insert_value(key);
+ lsn_col->insert_value(key);
+ op_col->insert_value(ROW_BINLOG_UPDATE);
+ }
+
+ block->insert({std::move(key_col), int_type, "key"});
+ block->insert({std::move(val_col), nothing_type, "val"});
+ block->insert({std::move(before_col), nothing_type,
binlog::build_before_column_name("val")});
+ block->insert({std::move(tso_col), int_type, BINLOG_TSO_COL});
+ block->insert({std::move(lsn_col), int_type, BINLOG_LSN_COL});
+ block->insert({std::move(op_col), int_type, BINLOG_OP_COL});
+ return block;
+}
+
// Fake merge iterator: hands out rows from `source` one at a time. `is_same`
is
// derived from primary-key equality with the previous emitted row, matching
the
// real merge iterator's contract that consecutive same-key rows are flagged.
@@ -137,7 +320,7 @@ public:
Status next(Block* /*block*/) override { return
Status::Error<END_OF_FILE>(""); }
- RowLocation current_row_location() override { return RowLocation(); }
+ RowLocation current_row_location() override { return {}; }
Status current_block_row_locations(std::vector<RowLocation>* /*loc*/)
override {
return Status::OK();
}
@@ -170,24 +353,26 @@ ReadSchemaSPtr make_read_schema(const
std::vector<std::string>& names = {
return std::make_shared<ReadSchema>(std::move(cols));
}
-TabletSchemaSPtr make_tablet_schema(const ReadSchemaSPtr& schema) {
- auto tablet_schema = std::make_shared<TabletSchema>();
- for (const auto& column : schema->columns()) {
- tablet_schema->append_column(*column);
- }
- return tablet_schema;
-}
-
// Wire a BlockReader as if init() had already completed for a row-binlog
change
// scan over the fixed 6-column schema, then plug in the fake merge iterator.
-void configure_reader(BlockReader& reader, std::shared_ptr<Block> source,
size_t batch_size) {
+void configure_reader(BlockReader& reader, std::shared_ptr<Block> source,
size_t batch_size,
+ TabletSchemaSPtr schema = make_test_tablet_schema()) {
config::enable_adaptive_batch_size = false;
reader._reader_context.batch_size = batch_size;
- // Must be set before constructing the fake LevelIterator: its base ctor
- // snapshots reader->_read_schema.
- reader._read_schema = make_read_schema();
- reader._tablet_schema = make_tablet_schema(reader._read_schema);
+ // The physical tablet schema supplies stable unique ids for AFTER/BEFORE
pairing. The fake
+ // source block supplies the materialized types used by this read schema.
+ reader._tablet_schema = std::move(schema);
+ ASSERT_EQ(reader._tablet_schema->num_columns(), source->columns());
+ std::vector<DataTypePtr> read_types;
+ read_types.reserve(source->columns());
+ for (size_t ordinal = 0; ordinal < source->columns(); ++ordinal) {
+ read_types.emplace_back(source->get_by_position(ordinal).type);
+ }
+ auto read_schema =
+ std::make_shared<ReadSchema>(reader._tablet_schema->columns(),
std::move(read_types));
+ read_schema->init_row_binlog_column_mappings(*reader._tablet_schema);
+ reader._read_schema = std::move(read_schema);
reader._next_row.block = source;
reader._next_row.row_pos = 0;
@@ -228,12 +413,17 @@ std::vector<OutRow> drain(BlockReader& reader, Status
(BlockReader::*fn)(Block*,
bool eof = false;
int guard = 0;
while (!eof) {
- Block block = make_output_block();
+ Block block = reader._read_schema->create_read_block();
Status st = (reader.*fn)(&block, &eof);
EXPECT_TRUE(st.ok()) << st;
+ const int32_t op_ordinal = reader._read_schema->op_ordinal();
+ if (op_ordinal < 0) {
+ ADD_FAILURE() << "row-binlog read schema has no op column";
+ return result;
+ }
for (size_t r = 0; r < block.rows(); ++r) {
result.push_back({out_i64(block, KEY_IDX, r), out_i64(block,
VAL_IDX, r),
- out_i64(block, OP_IDX, r)});
+ out_i64(block, op_ordinal, r)});
}
if (++guard >= 1000) {
ADD_FAILURE() << "drain did not terminate";
@@ -243,6 +433,16 @@ std::vector<OutRow> drain(BlockReader& reader, Status
(BlockReader::*fn)(Block*,
return result;
}
+void expect_out_rows(const std::vector<OutRow>& actual, const
std::vector<OutRow>& expected) {
+ ASSERT_EQ(actual.size(), expected.size());
+ for (size_t i = 0; i < expected.size(); ++i) {
+ SCOPED_TRACE(i);
+ EXPECT_EQ(actual[i].key, expected[i].key);
+ EXPECT_EQ(actual[i].val, expected[i].val);
+ EXPECT_EQ(actual[i].op, expected[i].op);
+ }
+}
+
} // namespace
class BlockReaderChangeNextBlockTest : public testing::Test {
@@ -333,6 +533,338 @@ TEST_F(BlockReaderChangeNextBlockTest,
MinDeltaUpdateBeforeAfter) {
EXPECT_EQ(out[1].val, 30); // after value from the last op
}
+// A physical UPDATE whose complete BEFORE and AFTER row values are equal has
no net delta.
+TEST_F(BlockReaderChangeNextBlockTest, MinDeltaNoOpUpdateIsSkipped) {
+ auto source = make_source_block({
+ {1, 20, 20, 1, 1, ROW_BINLOG_UPDATE},
+ });
+ BlockReader reader;
+ configure_reader(reader, source, 16);
+
+ auto out = drain(reader, &BlockReader::_min_delta_next_block);
+ EXPECT_TRUE(out.empty());
+}
+
+// An unavailable historical row is encoded as an all-NULL BEFORE image. It
must not cancel an
+// all-NULL row inserted later, because the net state changes from absent to
present.
+TEST_F(BlockReaderChangeNextBlockTest, MinDeltaAllNullBeforeImageIsRetained) {
+ auto source = make_all_null_source_block();
+ BlockReader reader;
+ configure_reader(reader, source, 16);
+
+ bool eof = false;
+ Block output = source->clone_empty();
+ ASSERT_TRUE(reader._min_delta_next_block(&output, &eof).ok());
+ ASSERT_EQ(output.rows(), 2);
+ EXPECT_EQ(out_i64(output, OP_IDX, 0), binlog::STREAM_CHANGE_UPDATE_BEFORE);
+ EXPECT_EQ(out_i64(output, OP_IDX, 1), binlog::STREAM_CHANGE_UPDATE_AFTER);
+ const auto& value_column =
+ assert_cast<const
ColumnNullable&>(*output.get_by_position(VAL_IDX).column);
+ EXPECT_TRUE(value_column.is_null_at(0));
+ EXPECT_TRUE(value_column.is_null_at(1));
+ EXPECT_TRUE(eof);
+}
+
+// A user column may have the same name as another column's generated BEFORE
mirror. Pairing by
+// schema ordinals must still recognize an unchanged complete row and suppress
the UPDATE.
+TEST_F(BlockReaderChangeNextBlockTest,
MinDeltaNoOpWithCollidingBeforeNameIsSkipped) {
+ auto source = make_colliding_name_source_block(/*after_v=*/10,
/*after_collision=*/20,
+ /*before_v=*/10,
/*before_collision=*/20);
+ auto schema = make_test_tablet_schema({{"v",
FieldType::OLAP_FIELD_TYPE_BIGINT},
+ {"__BEFORE__v__",
FieldType::OLAP_FIELD_TYPE_BIGINT}});
+ BlockReader reader;
+ configure_reader(reader, source, 16, std::move(schema));
+
+ bool eof = false;
+ Block output = source->clone_empty();
+ ASSERT_TRUE(reader._min_delta_next_block(&output, &eof).ok());
+ EXPECT_EQ(output.rows(), 0);
+ EXPECT_TRUE(eof);
+}
+
+// compare_at considers +0 and -0 equal, so they represent no net MIN_DELTA
change.
+TEST_F(BlockReaderChangeNextBlockTest, MinDeltaSignedZeroUpdateIsSkipped) {
+ auto source = make_signed_zero_source_block();
+ BlockReader reader;
+ configure_reader(reader, source, 16,
+ make_test_tablet_schema({{"val",
FieldType::OLAP_FIELD_TYPE_FLOAT}}));
+
+ bool eof = false;
+ Block output = source->clone_empty();
+ ASSERT_TRUE(reader._min_delta_next_block(&output, &eof).ok());
+ EXPECT_EQ(output.rows(), 0);
+ EXPECT_TRUE(eof);
+}
+
+TEST_F(BlockReaderChangeNextBlockTest,
MinDeltaUnsupportedCompareIsRetainedAndCached) {
+ auto compare_calls = std::make_shared<size_t>(0);
+ auto source = make_unsupported_compare_source_block(compare_calls);
+ BlockReader reader;
+ configure_reader(reader, source, 16);
+
+ bool eof = false;
+ Block output = source->clone_empty();
+ ASSERT_TRUE(reader._min_delta_next_block(&output, &eof).ok());
+
+ // Both equal-value updates must be retained. The second key uses the
cached unsupported
+ // capability, so the exception-based capability probe runs exactly once
per reader.
+ ASSERT_EQ(output.rows(), 4);
+ EXPECT_EQ(out_i64(output, OP_IDX, 0), binlog::STREAM_CHANGE_UPDATE_BEFORE);
+ EXPECT_EQ(out_i64(output, OP_IDX, 1), binlog::STREAM_CHANGE_UPDATE_AFTER);
+ EXPECT_EQ(out_i64(output, OP_IDX, 2), binlog::STREAM_CHANGE_UPDATE_BEFORE);
+ EXPECT_EQ(out_i64(output, OP_IDX, 3), binlog::STREAM_CHANGE_UPDATE_AFTER);
+ EXPECT_EQ(*compare_calls, 1);
+ EXPECT_TRUE(reader._min_delta_value_compare_unsupported);
+ EXPECT_TRUE(eof);
+}
+
+TEST_F(BlockReaderChangeNextBlockTest,
MinDeltaUnexpectedCompareExceptionIsNotSuppressed) {
+ auto source = make_source_block({{1, 20, 20, 1, 1, ROW_BINLOG_UPDATE}});
+ auto throwing_type = std::make_shared<DataTypeNothing>();
+ auto compare_calls = std::make_shared<size_t>(0);
+ source->get_by_position(VAL_IDX).column =
+ ThrowOnCompareColumn::create(1, ErrorCode::INTERNAL_ERROR,
compare_calls);
+ source->get_by_position(VAL_IDX).type = throwing_type;
+ source->get_by_position(VAL_IDX + 1).column =
+ ThrowOnCompareColumn::create(1, ErrorCode::INTERNAL_ERROR,
compare_calls);
+ source->get_by_position(VAL_IDX + 1).type = throwing_type;
+ BlockReader reader;
+ configure_reader(reader, source, 16);
+
+ reader._stored_data_columns = source->clone_empty_columns();
+ for (size_t i = 0; i < source->columns(); ++i) {
+
reader._stored_data_columns[i]->insert_from(*source->get_by_position(i).column,
0);
+ }
+
+ try {
+ static_cast<void>(reader._min_delta_values_equal(0));
+ FAIL() << "expected a non-NOT_IMPLEMENTED comparison exception";
+ } catch (const Exception& e) {
+ EXPECT_EQ(e.code(), ErrorCode::INTERNAL_ERROR);
+ }
+ EXPECT_EQ(*compare_calls, 1);
+ EXPECT_FALSE(reader._min_delta_value_compare_unsupported);
+}
+
+// The comparison is between the first BEFORE and last AFTER values, so A -> B
-> A also has no
+// net delta even though neither individual row-binlog UPDATE is a no-op.
+TEST_F(BlockReaderChangeNextBlockTest,
MinDeltaUpdatesReturningToOriginalAreSkipped) {
+ auto source = make_source_block({
+ {1, 20, 10, 1, 1, ROW_BINLOG_UPDATE},
+ {1, 10, 20, 2, 2, ROW_BINLOG_UPDATE},
+ });
+ BlockReader reader;
+ configure_reader(reader, source, 16);
+
+ auto out = drain(reader, &BlockReader::_min_delta_next_block);
+ EXPECT_TRUE(out.empty());
+}
+
+// Exercise long operation chains where intermediate rows repeatedly change
existence and value.
+// MIN_DELTA must preserve only the net state transition across the whole key
window.
+TEST_F(BlockReaderChangeNextBlockTest, MinDeltaComplexOperationChains) {
+ struct TestCase {
+ std::string_view name;
+ std::vector<Row> rows;
+ std::vector<OutRow> expected;
+ size_t batch_size;
+ };
+ const std::vector<TestCase> test_cases = {
+ {
+ "insert_delete_reinsert_update_back_delete",
+ {
+ {1, 1, 0, 1, 1, ROW_BINLOG_APPEND},
+ {1, 1, 1, 2, 2, ROW_BINLOG_DELETE},
+ {1, 1, 0, 3, 3, ROW_BINLOG_APPEND},
+ {1, 2, 1, 4, 4, ROW_BINLOG_UPDATE},
+ {1, 3, 2, 5, 5, ROW_BINLOG_UPDATE},
+ {1, 1, 3, 6, 6, ROW_BINLOG_UPDATE},
+ {1, 1, 1, 7, 7, ROW_BINLOG_DELETE},
+ },
+ {},
+ 1,
+ },
+ {
+ "existing_row_delete_reinsert_and_return_to_original",
+ {
+ {1, 2, 1, 1, 1, ROW_BINLOG_UPDATE},
+ {1, 2, 2, 2, 2, ROW_BINLOG_DELETE},
+ {1, 2, 0, 3, 3, ROW_BINLOG_APPEND},
+ {1, 3, 2, 4, 4, ROW_BINLOG_UPDATE},
+ {1, 1, 3, 5, 5, ROW_BINLOG_UPDATE},
+ },
+ {},
+ 2,
+ },
+ {
+ "existing_row_delete_reinsert_and_finish_changed",
+ {
+ {1, 2, 1, 1, 1, ROW_BINLOG_UPDATE},
+ {1, 2, 2, 2, 2, ROW_BINLOG_DELETE},
+ {1, 2, 0, 3, 3, ROW_BINLOG_APPEND},
+ {1, 3, 2, 4, 4, ROW_BINLOG_UPDATE},
+ {1, 4, 3, 5, 5, ROW_BINLOG_UPDATE},
+ },
+ {
+ {1, 1, binlog::STREAM_CHANGE_UPDATE_BEFORE},
+ {1, 4, binlog::STREAM_CHANGE_UPDATE_AFTER},
+ },
+ 1,
+ },
+ {
+ "new_row_temporarily_deleted_but_finishes_present",
+ {
+ {1, 1, 0, 1, 1, ROW_BINLOG_APPEND},
+ {1, 2, 1, 2, 2, ROW_BINLOG_UPDATE},
+ {1, 2, 2, 3, 3, ROW_BINLOG_DELETE},
+ {1, 5, 0, 4, 4, ROW_BINLOG_APPEND},
+ {1, 6, 5, 5, 5, ROW_BINLOG_UPDATE},
+ },
+ {
+ {1, 6, binlog::STREAM_CHANGE_INSERT},
+ },
+ 1,
+ },
+ {
+ "existing_row_temporarily_reinserted_but_finishes_deleted",
+ {
+ {1, 2, 1, 1, 1, ROW_BINLOG_UPDATE},
+ {1, 2, 2, 2, 2, ROW_BINLOG_DELETE},
+ {1, 3, 0, 3, 3, ROW_BINLOG_APPEND},
+ {1, 4, 3, 4, 4, ROW_BINLOG_UPDATE},
+ {1, 4, 4, 5, 5, ROW_BINLOG_DELETE},
+ },
+ {
+ {1, 1, binlog::STREAM_CHANGE_DELETE},
+ },
+ 1,
+ },
+ {
+ "delete_reinsert_update_and_return_to_original",
+ {
+ {1, 1, 1, 1, 1, ROW_BINLOG_DELETE},
+ {1, 1, 0, 2, 2, ROW_BINLOG_APPEND},
+ {1, 2, 1, 3, 3, ROW_BINLOG_UPDATE},
+ {1, 1, 2, 4, 4, ROW_BINLOG_UPDATE},
+ },
+ {},
+ 1,
+ },
+ };
+
+ for (const auto& test_case : test_cases) {
+ SCOPED_TRACE(test_case.name);
+ auto source = make_source_block(test_case.rows);
+ BlockReader reader;
+ configure_reader(reader, source, test_case.batch_size);
+
+ auto out = drain(reader, &BlockReader::_min_delta_next_block);
+ expect_out_rows(out, test_case.expected);
+ }
+}
+
+// Build a source block with a second value column that is present in the
physical MIN_DELTA
+// projection but absent from the SQL output projection.
+struct TwoValueRow {
+ int64_t key;
+ int64_t val1;
+ int64_t val2;
+ int64_t before_val1;
+ int64_t before_val2;
+ int64_t tso;
+ int64_t lsn;
+ int64_t op;
+};
+
+std::shared_ptr<Block> make_two_value_source_block(const
std::vector<TwoValueRow>& rows) {
+ auto block = std::make_shared<Block>();
+ auto type = std::make_shared<DataTypeInt64>();
+ auto key_col = ColumnInt64::create();
+ auto val1_col = ColumnInt64::create();
+ auto val2_col = ColumnInt64::create();
+ auto before_val1_col = ColumnInt64::create();
+ auto before_val2_col = ColumnInt64::create();
+ auto tso_col = ColumnInt64::create();
+ auto lsn_col = ColumnInt64::create();
+ auto op_col = ColumnInt64::create();
+ for (const auto& row : rows) {
+ key_col->insert_value(row.key);
+ val1_col->insert_value(row.val1);
+ val2_col->insert_value(row.val2);
+ before_val1_col->insert_value(row.before_val1);
+ before_val2_col->insert_value(row.before_val2);
+ tso_col->insert_value(row.tso);
+ lsn_col->insert_value(row.lsn);
+ op_col->insert_value(row.op);
+ }
+ block->insert({std::move(key_col), type, "key"});
+ block->insert({std::move(val1_col), type, "val"});
+ block->insert({std::move(val2_col), type, "val2"});
+ block->insert({std::move(before_val1_col), type,
binlog::build_before_column_name("val")});
+ block->insert({std::move(before_val2_col), type,
binlog::build_before_column_name("val2")});
+ block->insert({std::move(tso_col), type, BINLOG_TSO_COL});
+ block->insert({std::move(lsn_col), type, BINLOG_LSN_COL});
+ block->insert({std::move(op_col), type, BINLOG_OP_COL});
+ return block;
+}
+
+void configure_two_value_reader(BlockReader& reader, std::shared_ptr<Block>
source,
+ size_t batch_size = 16) {
+ configure_reader(reader, source, batch_size,
+ make_test_tablet_schema({{"val",
FieldType::OLAP_FIELD_TYPE_BIGINT},
+ {"val2",
FieldType::OLAP_FIELD_TYPE_BIGINT}}));
+}
+
+TEST_F(BlockReaderChangeNextBlockTest,
MinDeltaNoOpUpdateComparesAllValueColumns) {
+ auto source = make_two_value_source_block({
+ {/*key=*/1, /*val1=*/20, /*val2=*/30, /*before_val1=*/20,
/*before_val2=*/30,
+ /*tso=*/1, /*lsn=*/1, ROW_BINLOG_UPDATE},
+ });
+ BlockReader reader;
+ configure_two_value_reader(reader, source);
+
+ auto out = drain(reader, &BlockReader::_min_delta_next_block);
+ EXPECT_TRUE(out.empty());
+}
+
+TEST_F(BlockReaderChangeNextBlockTest,
MinDeltaRetainsChangeInUnprojectedValueColumn) {
+ auto source = make_two_value_source_block({
+ {/*key=*/1, /*val1=*/20, /*val2=*/31, /*before_val1=*/20,
/*before_val2=*/30,
+ /*tso=*/1, /*lsn=*/1, ROW_BINLOG_UPDATE},
+ });
+ BlockReader reader;
+ configure_two_value_reader(reader, source);
+
+ auto out = drain(reader, &BlockReader::_min_delta_next_block);
+ ASSERT_EQ(out.size(), 2);
+ EXPECT_EQ(out[0].op, binlog::STREAM_CHANGE_UPDATE_BEFORE);
+ EXPECT_EQ(out[1].op, binlog::STREAM_CHANGE_UPDATE_AFTER);
+ EXPECT_EQ(out[0].val, 20);
+ EXPECT_EQ(out[1].val, 20);
+}
+
+// key 1 changes both columns several times and returns to its complete
original row image, so it
+// disappears. key 2 returns only the projected value column to its original
value while the hidden
+// value column remains changed, so its UPDATE pair must survive. batch_size=1
also forces the pair
+// through the pending-row path after the skipped key.
+TEST_F(BlockReaderChangeNextBlockTest, MinDeltaComplexMultiColumnChains) {
+ auto source = make_two_value_source_block({
+ {1, 11, 100, 10, 100, 1, 1, ROW_BINLOG_UPDATE},
+ {1, 11, 101, 11, 100, 2, 2, ROW_BINLOG_UPDATE},
+ {1, 10, 100, 11, 101, 3, 3, ROW_BINLOG_UPDATE},
+ {2, 21, 200, 20, 200, 4, 4, ROW_BINLOG_UPDATE},
+ {2, 20, 201, 21, 200, 5, 5, ROW_BINLOG_UPDATE},
+ });
+ BlockReader reader;
+ configure_two_value_reader(reader, source, /*batch_size=*/1);
+
+ auto out = drain(reader, &BlockReader::_min_delta_next_block);
+ expect_out_rows(out, {
+ {2, 20, binlog::STREAM_CHANGE_UPDATE_BEFORE},
+ {2, 20, binlog::STREAM_CHANGE_UPDATE_AFTER},
+ });
+}
+
// Multiple distinct keys, each in its own group, are folded independently.
TEST_F(BlockReaderChangeNextBlockTest, MinDeltaMultipleKeys) {
auto source = make_source_block({
@@ -440,6 +972,25 @@ TEST_F(BlockReaderChangeNextBlockTest, DetailUpdatePair) {
EXPECT_EQ(out[1].val, 20); // after
}
+TEST_F(BlockReaderChangeNextBlockTest,
DetailUsesOrdinalBeforePairWhenNamesCollide) {
+ auto source = make_colliding_name_source_block(/*after_v=*/11,
/*after_collision=*/20,
+ /*before_v=*/10,
/*before_collision=*/20);
+ auto schema = make_test_tablet_schema({{"v",
FieldType::OLAP_FIELD_TYPE_BIGINT},
+ {"__BEFORE__v__",
FieldType::OLAP_FIELD_TYPE_BIGINT}});
+ BlockReader reader;
+ configure_reader(reader, source, 16, std::move(schema));
+
+ bool eof = false;
+ Block output = source->clone_empty();
+ ASSERT_TRUE(reader._detail_change_next_block(&output, &eof).ok());
+ ASSERT_EQ(output.rows(), 2);
+ EXPECT_EQ(out_i64(output, /*v=*/1, 0), 10);
+ EXPECT_EQ(out_i64(output, /*v=*/1, 1), 11);
+ EXPECT_EQ(out_i64(output, /*op=*/7, 0),
binlog::STREAM_CHANGE_UPDATE_BEFORE);
+ EXPECT_EQ(out_i64(output, /*op=*/7, 1),
binlog::STREAM_CHANGE_UPDATE_AFTER);
+ EXPECT_TRUE(eof);
+}
+
// Mixed ops emitted verbatim in order.
TEST_F(BlockReaderChangeNextBlockTest, DetailMixedOps) {
auto source = make_source_block({
diff --git a/be/test/storage/read_schema_test.cpp
b/be/test/storage/read_schema_test.cpp
index 6c41a522dba..5ee2abd5f0a 100644
--- a/be/test/storage/read_schema_test.cpp
+++ b/be/test/storage/read_schema_test.cpp
@@ -26,6 +26,7 @@
#include "core/block/block.h"
#include "core/data_type/data_type_number.h"
#include "core/data_type/data_type_struct.h"
+#include "storage/binlog.h"
#include "storage/schema.h"
namespace doris {
@@ -54,6 +55,20 @@ TabletColumnPtr create_struct_column(int32_t unique_id) {
return column;
}
+TabletSchemaSPtr create_row_binlog_schema_with_colliding_names() {
+ auto schema = std::make_shared<TabletSchema>();
+ schema->append_column(*create_int_column(10, "key", true));
+ schema->append_column(*create_int_column(11, "v"));
+ schema->append_column(*create_int_column(12, "__BEFORE__v__"));
+ schema->append_column(*create_int_column(13,
binlog::build_before_column_name("v")));
+ schema->append_column(
+ *create_int_column(14,
binlog::build_before_column_name("__BEFORE__v__")));
+ schema->append_column(*create_int_column(15, BINLOG_TSO_COL));
+ schema->append_column(*create_int_column(16, BINLOG_LSN_COL));
+ schema->append_column(*create_int_column(17, BINLOG_OP_COL));
+ return schema;
+}
+
TEST(ReadSchemaTest, DefaultColumnsAreVisible) {
std::vector<TabletColumnPtr> storage_columns {create_int_column(10, "k",
true),
create_int_column(11,
"dropped"),
@@ -125,5 +140,42 @@ TEST(ReadSchemaTest,
AppendedDroppedColumnDoesNotExtendReadBlock) {
EXPECT_EQ(2, read_schema.create_read_block().columns());
}
+TEST(ReadSchemaTest, RowBinlogMappingsUsePhysicalSchemaOrdinals) {
+ auto tablet_schema = create_row_binlog_schema_with_colliding_names();
+ // Reorder the two AFTER values and their BEFORE companions to verify that
ReadSchema stores
+ // dense read ordinals resolved by unique id, rather than physical tablet
column ids or names.
+ ReadSchema read_schema(project_columns_by_ordinal(
+ tablet_schema->columns(), std::vector<ColumnId> {0, 2, 1, 4, 3, 5,
6, 7}));
+
+ read_schema.init_row_binlog_column_mappings(*tablet_schema);
+
+ EXPECT_TRUE(read_schema.row_binlog_value_pairs_complete());
+ EXPECT_EQ(read_schema.row_binlog_value_column_pairs(),
+ (ReadSchema::RowBinlogValueColumnPairs {{2, 4}, {1, 3}}));
+ EXPECT_EQ(4, read_schema.before_column_ordinal(2));
+ EXPECT_EQ(3, read_schema.before_column_ordinal(1));
+ for (ColumnId ordinal : {0, 3, 4, 5, 6, 7}) {
+ EXPECT_EQ(ordinal, read_schema.before_column_ordinal(ordinal));
+ }
+}
+
+TEST(ReadSchemaTest, MalformedRowBinlogLayoutKeepsConservativeNameMapping) {
+ TabletSchema tablet_schema;
+ tablet_schema.append_column(*create_int_column(10, "key", true));
+ tablet_schema.append_column(*create_int_column(11, "v"));
+ tablet_schema.append_column(*create_int_column(12,
binlog::build_before_column_name("v")));
+ tablet_schema.append_column(*create_int_column(13, "orphan"));
+ tablet_schema.append_column(*create_int_column(14, BINLOG_TSO_COL));
+ tablet_schema.append_column(*create_int_column(15, BINLOG_LSN_COL));
+ tablet_schema.append_column(*create_int_column(16, BINLOG_OP_COL));
+ ReadSchema read_schema(tablet_schema.columns());
+
+ read_schema.init_row_binlog_column_mappings(tablet_schema);
+
+ EXPECT_FALSE(read_schema.row_binlog_value_pairs_complete());
+ EXPECT_TRUE(read_schema.row_binlog_value_column_pairs().empty());
+ EXPECT_EQ(2, read_schema.before_column_ordinal(1));
+}
+
} // namespace
} // namespace doris
diff --git
a/fe/fe-core/src/main/java/org/apache/doris/nereids/glue/translator/PhysicalPlanTranslator.java
b/fe/fe-core/src/main/java/org/apache/doris/nereids/glue/translator/PhysicalPlanTranslator.java
index 8c1395c9923..cf5ac2f5e18 100644
---
a/fe/fe-core/src/main/java/org/apache/doris/nereids/glue/translator/PhysicalPlanTranslator.java
+++
b/fe/fe-core/src/main/java/org/apache/doris/nereids/glue/translator/PhysicalPlanTranslator.java
@@ -3084,10 +3084,10 @@ public class PhysicalPlanTranslator extends
DefaultPlanVisitor<PlanFragment, Pla
/**
* An {@code @incr} read folds all changes to a row into one record: TSO
is the tie-break column
* ordering them, and the folded result is written back into OP as the
change kind the user
- * sees, so every scan type needs both slots present. {@code SELECT k, v
FROM
- * t@incr("incrementType" = "DETAIL")} also needs the keys to group by
and, when the table keeps
- * historical values, {@code __BEFORE__v__} to report what v was before
the change; APPEND_ONLY
- * never groups and needs neither.
+ * sees, so every scan type needs both slots present. MIN_DELTA compares
the complete first
+ * BEFORE and last AFTER row images, including value columns omitted by
the SQL projection.
+ * DETAIL needs BEFORE images only for projected values. APPEND_ONLY never
groups and needs
+ * neither keys nor BEFORE images.
*/
private void preserveRowBinlogSemanticSlots(OlapScanNode scanNode,
Set<SlotId> requiredSlotIds) {
RowBinlogTableWrapper wrapper = (RowBinlogTableWrapper)
scanNode.getOlapTable();
@@ -3121,12 +3121,14 @@ public class PhysicalPlanTranslator extends
DefaultPlanVisitor<PlanFragment, Pla
// are no before-image storage dependencies to preserve.
return;
}
+ boolean preserveCompleteRow = scanType == TBinlogScanType.MIN_DELTA;
for (SlotDescriptor slot : scanSlots) {
Column column = slot.getColumn();
- if (column == null || column.isKey() ||
!requiredSlotIds.contains(slot.getId())
- || isRowBinlogInternalColumn(column)) {
+ if (column == null || column.isKey() ||
isRowBinlogInternalColumn(column)
+ || (!preserveCompleteRow &&
!requiredSlotIds.contains(slot.getId()))) {
continue;
}
+ preserveStorageSlot(slot, requiredSlotIds);
preserveStorageSlot(slotByName.get(Column.generateBeforeColName(column.getName())),
requiredSlotIds);
}
diff --git
a/fe/fe-core/src/test/java/org/apache/doris/nereids/glue/translator/PhysicalPlanTranslatorTest.java
b/fe/fe-core/src/test/java/org/apache/doris/nereids/glue/translator/PhysicalPlanTranslatorTest.java
index eb885887ded..e6f1fd92bd0 100644
---
a/fe/fe-core/src/test/java/org/apache/doris/nereids/glue/translator/PhysicalPlanTranslatorTest.java
+++
b/fe/fe-core/src/test/java/org/apache/doris/nereids/glue/translator/PhysicalPlanTranslatorTest.java
@@ -237,10 +237,9 @@ public class PhysicalPlanTranslatorTest extends
TestWithFeService {
.collect(Collectors.toList());
Assertions.assertTrue(scanColumns.containsAll(ImmutableList.of(
- "k1", "k2", "v1", Column.generateBeforeColName("v1"),
+ "k1", "k2", "v1", "v2", Column.generateBeforeColName("v1"),
+ Column.generateBeforeColName("v2"),
Column.BINLOG_OPERATION_COL, Column.BINLOG_TSO_COL)));
- Assertions.assertFalse(scanColumns.contains("v2"));
-
Assertions.assertFalse(scanColumns.contains(Column.generateBeforeColName("v2")));
Assertions.assertFalse(scanColumns.contains(Column.BINLOG_LSN_COL));
Assertions.assertTrue(scanNode.getExtraKeyColumnSlotIds().isEmpty());
diff --git a/regression-test/data/row_binlog_p0/test_binlog_changes_syntax.out
b/regression-test/data/row_binlog_p0/test_binlog_changes_syntax.out
index 801a324eceb..7550bbab228 100644
--- a/regression-test/data/row_binlog_p0/test_binlog_changes_syntax.out
+++ b/regression-test/data/row_binlog_p0/test_binlog_changes_syntax.out
@@ -241,3 +241,6 @@
1 1,2 2
1 3,4 3
+-- !bitmap_equal_min_delta --
+1 3,4 2
+1 3,4 3
diff --git
a/regression-test/suites/row_binlog_p0/test_binlog_changes_syntax.groovy
b/regression-test/suites/row_binlog_p0/test_binlog_changes_syntax.groovy
index 2ce56948450..c7de72472d5 100644
--- a/regression-test/suites/row_binlog_p0/test_binlog_changes_syntax.groovy
+++ b/regression-test/suites/row_binlog_p0/test_binlog_changes_syntax.groovy
@@ -584,6 +584,25 @@ suite("test_binlog_changes_syntax", "nonConcurrent") {
"incrementType" = "MIN_DELTA")
ORDER BY __DORIS_BINLOG_OP__
"""
+
+ // BITMAP does not implement compare_at in production. MIN_DELTA must
conservatively
+ // retain an equal-value UPDATE instead of failing the query or
suppressing an unproven
+ // no-op.
+ sleep(1200)
+ def bitmapEqualT0 = incrTimeFormat.format(new Date())
+ sleep(1200)
+ sql "INSERT INTO ${mowBitmapTable} VALUES (1,
BITMAP_FROM_STRING('3,4'))"
+ sql "sync"
+ sleep(1200)
+ def bitmapEqualT1 = incrTimeFormat.format(new Date())
+
+ order_qt_bitmap_equal_min_delta """
+ SELECT id, BITMAP_TO_STRING(b), __DORIS_BINLOG_OP__
+ FROM ${mowBitmapTable}@incr('startTimestamp' = '${bitmapEqualT0}',
+ "endTimestamp" = "${bitmapEqualT1}",
+ "incrementType" = "MIN_DELTA")
+ ORDER BY __DORIS_BINLOG_OP__
+ """
} finally {
sql "DROP DATABASE IF EXISTS test_binlog_changes_syntax_db"
}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]