blob: b06db536f5eb15c7a4fd236e889a9e5d9fb1442c [file]
// 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/transform/row_binlog_derive.h"
#include <algorithm>
#include <optional>
#include <span>
#include "common/cast_set.h"
#include "core/assert_cast.h"
#include "core/block/block.h"
#include "core/block/column_with_type_and_name.h"
#include "core/column/column_nullable.h"
#include "core/column/column_vector.h"
#include "storage/binlog.h"
#include "storage/iterator/olap_data_convertor.h"
#include "storage/key/row_key_encoder.h"
#include "storage/mow/historical_row_fetcher.h"
#include "storage/partial_update_info.h"
#include "storage/rowset/rowset_writer_context.h"
#include "storage/segment/historical_row_retriever.h"
#include "storage/tablet/tablet_schema.h"
#include "storage/transform/transform_util.h"
#include "util/time.h"
namespace doris::segment_v2 {
namespace {
// Number of row-binlog system columns: TSO, LSN, op.
constexpr uint32_t BINLOG_COLNUM = 3;
// Runs the primary-key historical lookup over `block`'s source key (+seq)
// columns, leaving the planned reads and the op for each row in `retriever` for
// building AFTER/BEFORE. `seq_pos` is the seq column's position in the input
// block (-1 if absent). `convertor` must live longer than `retriever`: its
// accessors back the lookup plan.
Status setup_retriever_and_lookup(TransformExecContext& ctx, const SegmentWriteBinlogOptions& cfg,
const TabletSchemaSPtr& source_schema, const Block* block,
int32_t seq_pos, const Int8* delete_signs, size_t num_rows,
OlapBlockDataConvertor& convertor,
std::unique_ptr<PrimaryKeyModelRowRetriever>& retriever) {
retriever = std::make_unique<PrimaryKeyModelRowRetriever>();
// Row binlog lives in its own tablet, so ctx.tablet is that binlog tablet
// while the history to look up sits in the source tablet. Falling back to
// ctx.tablet would silently search a tablet that holds no source rows, so
// the caller has to name the source tablet -- every site that fills
// source.tablet_schema fills this too.
RETURN_IF_ERROR(retriever->init(HistoricalRowRetrieverContext {
.tablet = cfg.source.base_tablet,
.tablet_schema = source_schema,
.rowset_writer_ctx = ctx.rowset_ctx,
.partial_update_info = cfg.source.partial_update_info,
.is_transient_rowset_writer = cfg.source.is_transient_rowset_writer,
.write_type = cfg.source.source_write_type}));
// key (+seq) only conversion from the input block for the lookup
convertor.resize(source_schema->num_columns());
std::vector<IOlapColumnDataAccessor*> key_columns;
RETURN_IF_ERROR(convert_key_columns(convertor, *source_schema, *block, num_rows, key_columns));
IOlapColumnDataAccessor* seq_column = nullptr;
if (seq_pos != -1) {
RETURN_IF_ERROR(convert_seq_column(convertor, *source_schema, *block,
static_cast<size_t>(seq_pos), num_rows, seq_column));
}
RETURN_IF_ERROR(retriever->prepare_lookup_plan_from_source_columns(key_columns, seq_column,
cfg.source.mow_context));
RETURN_IF_ERROR(retriever->retrieve_historical_row(delete_signs, 0, num_rows));
return Status::OK();
}
// Fills the BEFORE value columns into `out` from the retriever's historical
// reads. No-op when the source schema has no value columns. The retriever is
// not const: reading BEFORE also loads the old delete signs it keeps.
Status fill_before_columns(Block& out, const TabletSchema& binlog_schema,
PrimaryKeyModelRowRetriever* retriever, uint32_t before_col_start,
const std::vector<uint32_t>& value_source_cids, size_t num_rows) {
size_t value_column_num = value_source_cids.size();
if (value_column_num == 0) {
return Status::OK();
}
std::vector<uint32_t> before_cids;
for (uint32_t cid = before_col_start;
cid < before_col_start + cast_set<uint32_t>(value_column_num); ++cid) {
before_cids.emplace_back(cid);
}
Block before_block = binlog_schema.create_storage_block(before_cids);
DCHECK(retriever != nullptr);
DCHECK_EQ(before_cids.size(), value_source_cids.size());
RETURN_IF_ERROR(retriever->build_before_block(&before_block, value_source_cids, 0, num_rows));
size_t col_pos_in_block = 0;
for (auto cid : before_cids) {
out.replace_by_position(cid, before_block.get_by_position(col_pos_in_block++).column);
}
return Status::OK();
}
// Fills the TSO / LSN / op columns into `out`. TSO is unknown until publish,
// LSN is the per-row auto-inc value, and op describes the row change.
void fill_binlog_system_columns(Block& out, const TabletSchema& binlog_schema,
uint32_t binlog_tso_cid, uint32_t binlog_lsn_cid,
uint32_t binlog_op_cid, const std::vector<int64_t>& lsn_ids,
const std::vector<int64_t>& operators, size_t num_rows) {
std::vector<uint32_t> binlog_cids = {binlog_tso_cid, binlog_lsn_cid, binlog_op_cid};
Block binlog_system_block = binlog_schema.create_storage_block(binlog_cids);
{
auto binlog_system_columns_guard = binlog_system_block.mutate_columns_scoped();
auto& binlog_system_columns = binlog_system_columns_guard.mutable_columns();
// The reader replaces this placeholder with the real commit_tso.
IColumn* tso_col_ptr = binlog_system_columns[0].get();
auto* tso_nullable_column = check_and_get_column<ColumnNullable>(tso_col_ptr);
DCHECK(tso_nullable_column != nullptr);
tso_nullable_column->insert_many_defaults(num_rows);
// Auto-inc LSN keeps row order until the rowset is published.
IColumn* lsn_col_ptr = binlog_system_columns[1].get();
for (size_t i = 0; i < num_rows; i++) {
assert_cast<ColumnInt64*>(lsn_col_ptr)->insert_value(lsn_ids.at(i));
}
// wrong op only happens under partial update, it is fixed by the
// delete bitmap at publish
const FieldType op_col_type = binlog_schema.column(binlog_op_cid).type();
IColumn* op_col_ptr = binlog_system_columns[2].get();
auto* op_nullable_column = check_and_get_column<ColumnNullable>(op_col_ptr);
IColumn* op_nested_column = op_nullable_column != nullptr
? &op_nullable_column->get_nested_column()
: op_col_ptr;
CHECK(operators.size() >= num_rows) << operators.size() << " vs " << num_rows;
CHECK(op_col_type == FieldType::OLAP_FIELD_TYPE_BIGINT)
<< "row binlog op column type must be BIGINT, actual="
<< static_cast<int>(op_col_type);
auto* op_int64_column = assert_cast<ColumnInt64*>(op_nested_column);
for (size_t i = 0; i < num_rows; i++) {
op_int64_column->insert_value(operators[i]);
}
if (op_nullable_column != nullptr) {
auto& op_null_map = op_nullable_column->get_null_map_data();
for (size_t i = 0; i < num_rows; i++) {
op_null_map.emplace_back(0);
}
}
}
size_t col_pos_in_block = 0;
for (auto cid : binlog_cids) {
out.replace_by_position(cid,
binlog_system_block.get_by_position(col_pos_in_block++).column);
}
}
// Shared setup step: work out the binlog schema layout and consume this
// segment's LSN range (exactly once).
Status resolve_binlog_context(TransformExecContext& ctx, const Block* block,
BinlogDeriveContext* c) {
c->binlog_schema = ctx.tablet_schema;
auto& cfg = ctx.rowset_ctx->write_binlog_opt().write_binlog_config();
c->source_schema = cfg.source.tablet_schema;
if (UNLIKELY(c->source_schema == nullptr)) {
return Status::InternalError(
"binlog<row> derive missing source_tablet_schema, tablet_id={}",
ctx.rowset_ctx->tablet_id);
}
if (UNLIKELY(cfg.source.base_tablet == nullptr &&
binlog_needs_historical_lookup(*ctx.rowset_ctx))) {
return Status::InternalError(
"binlog<row> derive needs the source tablet to look history up, tablet_id={}",
ctx.rowset_ctx->tablet_id);
}
c->num_rows = block->rows();
// the LSN range for each row was registered per segment id before this
// flush; consume it exactly once.
if (ctx.segment_id < 0) {
return Status::InternalError<false>(
"binlog<row> blocks must be flushed through flush_single_block");
}
c->lsn_ids = cfg.get_seg_lsn(ctx.segment_id);
cfg.remove_seg(ctx.segment_id);
CHECK(c->lsn_ids->size() >= c->num_rows) << c->lsn_ids->size() << " vs " << c->num_rows;
// Preserve the source writer's layout: system columns may be a prefix or
// suffix, and source hidden non-key columns are omitted while hidden keys
// remain part of the normal row image.
int tso_col_id = c->binlog_schema->binlog_tso_col_idx();
int lsn_col_id = c->binlog_schema->binlog_lsn_col_idx();
int op_col_id = c->binlog_schema->binlog_op_col_idx();
CHECK(tso_col_id >= 0) << "binlog<row> schema missing " << BINLOG_TSO_COL;
CHECK(lsn_col_id >= 0) << "binlog<row> schema missing " << BINLOG_LSN_COL;
CHECK(op_col_id >= 0) << "binlog<row> schema missing " << BINLOG_OP_COL;
c->binlog_tso_cid = cast_set<uint32_t>(tso_col_id);
c->binlog_lsn_cid = cast_set<uint32_t>(lsn_col_id);
c->binlog_op_cid = cast_set<uint32_t>(op_col_id);
c->normal_col_start = tso_col_id == 0 ? BINLOG_COLNUM : 0;
for (uint32_t cid = 0; cid < c->source_schema->num_columns(); ++cid) {
const auto& column = c->source_schema->column(cid);
if (column.visible() || column.is_key()) {
c->normal_source_cids.emplace_back(cid);
}
if (column.visible() && !column.is_key()) {
c->value_source_cids.emplace_back(cid);
}
}
c->before_col_start = c->normal_col_start + cast_set<uint32_t>(c->normal_source_cids.size());
c->write_before = cfg.write_before;
// The schema must carry exactly the BEFORE columns this flush fills: one per
// source value column when write_before is on, none otherwise. Too few and
// the BEFORE fill would write over the TSO/LSN columns; too many and those
// columns would stay empty in an otherwise full block, leaving the writer a
// block whose columns hold different row counts.
const size_t expected_before_columns = c->write_before ? c->value_source_cids.size() : 0;
const size_t expected_columns =
c->normal_source_cids.size() + expected_before_columns + BINLOG_COLNUM;
if (c->binlog_schema->num_columns() != expected_columns) {
return Status::InternalError<false>(
"binlog<row> schema does not match the derived block: num_columns={}, expected={} "
"(normal={}, before={}, system={}), write_before={}, tablet_id={}",
c->binlog_schema->num_columns(), expected_columns, c->normal_source_cids.size(),
expected_before_columns, BINLOG_COLNUM, c->write_before, ctx.rowset_ctx->tablet_id);
}
return Status::OK();
}
// A view over the delete-sign column, or nullopt when the schema has no
// delete-sign column or the column does not cover the requested rows.
std::optional<std::span<const Int8>> read_delete_signs(const Block* block, int32_t delete_sign_pos,
size_t num_rows) {
if (delete_sign_pos == -1) {
return std::nullopt;
}
const ColumnWithTypeAndName& delete_sign_column = block->get_by_position(delete_sign_pos);
const auto& delete_sign_col = assert_cast<const ColumnInt8&>(*(delete_sign_column.column));
if (delete_sign_col.size() >= num_rows) {
return std::span<const Int8>(delete_sign_col.get_data().data(), num_rows);
}
return std::nullopt;
}
// Shared final step: build the final binlog block -- AFTER columns (COW from
// after_src), optional BEFORE columns, the TSO/LSN/op columns -- and swap
// it into `block`.
Status emit_binlog_block(const BinlogDeriveContext& c, const Block* after_src,
PrimaryKeyModelRowRetriever* retriever,
const std::vector<int64_t>* plain_operators, Block* block) {
Block out = c.binlog_schema->create_storage_block();
// every index below is a cid, so the block has to carry every schema column
if (out.columns() != c.binlog_schema->num_columns()) {
return Status::InternalError<false>(
"binlog<row> block width {} does not match its schema's {} columns", out.columns(),
c.binlog_schema->num_columns());
}
// key + AFTER columns: COW pointers of the source-layout visible columns
for (size_t ordinal = 0; ordinal < c.normal_source_cids.size(); ++ordinal) {
const uint32_t source_cid = c.normal_source_cids[ordinal];
ColumnPtr after_column = after_src->get_by_position(source_cid).column;
// The binlog schema declares every AFTER value column nullable, so a NOT
// NULL source column has to be wrapped: the segment writer converts by
// the binlog schema. Keys keep their own nullability.
if (!c.source_schema->column(source_cid).is_key()) {
after_column = make_nullable(after_column);
}
out.replace_by_position(c.normal_col_start + ordinal, std::move(after_column));
}
if (c.write_before) {
RETURN_IF_ERROR(fill_before_columns(out, *c.binlog_schema, retriever, c.before_col_start,
c.value_source_cids, c.num_rows));
}
// An insert whose old row is a tombstone is an append, not an update. Run
// this after the BEFORE read, which already loaded the old delete signs.
if (retriever != nullptr) {
RETURN_IF_ERROR(retriever->revise_operators_by_old_delete_sign(c.num_rows));
}
const std::vector<int64_t>& operators =
retriever != nullptr ? retriever->get_operators() : *plain_operators;
fill_binlog_system_columns(out, *c.binlog_schema, c.binlog_tso_cid, c.binlog_lsn_cid,
c.binlog_op_cid, *c.lsn_ids, operators, c.num_rows);
block->swap(out);
return Status::OK();
}
} // namespace
bool binlog_needs_historical_lookup(const RowsetWriterContext& context) {
const auto& cfg = context.write_binlog_opt().write_binlog_config();
const bool is_source_direct_write =
cfg.source.source_write_type == DataWriteType::TYPE_DIRECT &&
!cfg.source.is_transient_rowset_writer;
// A direct partial update (fixed needs AFTER rebuilt; flexible is rejected
// inside the MoW stage) or a requested BEFORE image needs the probe.
const bool is_partial_update = cfg.source.partial_update_info != nullptr &&
cfg.source.partial_update_info->is_partial_update() &&
is_source_direct_write;
return is_partial_update || cfg.write_before;
}
Status RowBinlogDeriveStage::apply(TransformExecContext& ctx, Block* block) const {
BinlogDeriveContext c;
RETURN_IF_ERROR(resolve_binlog_context(ctx, block, &c));
return derive(ctx, block, c);
}
Status PlainRowBinlogDeriveStage::derive(TransformExecContext& /*ctx*/, Block* block,
const BinlogDeriveContext& c) const {
DCHECK(!c.write_before);
// No probe: op is APPEND, or DELETE when the row carries a delete sign. DUP
// tables have no delete-sign column, so every row is APPEND.
std::optional<std::span<const Int8>> delete_signs =
read_delete_signs(block, c.source_schema->delete_sign_idx(), c.num_rows);
std::vector<int64_t> operators;
operators.reserve(c.num_rows);
for (size_t pos = 0; pos < c.num_rows; ++pos) {
bool have_delete_sign = (delete_signs && (*delete_signs)[pos] != 0);
operators.emplace_back(have_delete_sign ? ROW_BINLOG_DELETE : ROW_BINLOG_APPEND);
}
return emit_binlog_block(c, /*after_src=*/block, /*retriever=*/nullptr, &operators, block);
}
Status MowRowBinlogDeriveStage::derive(TransformExecContext& ctx, Block* block,
const BinlogDeriveContext& c) const {
const auto& cfg = ctx.rowset_ctx->write_binlog_opt().write_binlog_config();
bool is_source_direct_write = cfg.source.source_write_type == DataWriteType::TYPE_DIRECT &&
!cfg.source.is_transient_rowset_writer;
if (cfg.source.partial_update_info && is_source_direct_write &&
cfg.source.partial_update_info->is_flexible_partial_update()) {
return Status::NotSupported("binlog<row> does not support flexible partial update");
}
bool is_partial_update = cfg.source.partial_update_info &&
cfg.source.partial_update_info->is_fixed_partial_update() &&
is_source_direct_write;
if (is_partial_update) {
if (block->columns() < c.source_schema->num_key_columns() ||
block->columns() >= c.source_schema->num_columns()) {
return Status::InvalidArgument(fmt::format(
"illegal partial update block columns: {}, num key columns: {}, total "
"schema columns: {}",
block->columns(), c.source_schema->num_key_columns(),
c.source_schema->num_columns()));
}
}
// find the delete sign and sequence columns in the input block (positions
// move in the narrow partial-update block)
int32_t delete_sign_pos = c.source_schema->delete_sign_idx();
int32_t seq_pos = c.source_schema->sequence_col_idx();
if (is_partial_update) {
delete_sign_pos = -1;
seq_pos = -1;
int32_t pos = 0;
for (auto& cid : cfg.source.partial_update_info->update_cids) {
if (cid == c.source_schema->delete_sign_idx()) {
delete_sign_pos = pos;
} else if (cid == c.source_schema->sequence_col_idx()) {
seq_pos = pos;
}
pos++;
}
}
std::optional<std::span<const Int8>> delete_signs =
read_delete_signs(block, delete_sign_pos, c.num_rows);
// probe history once: produces the AFTER-fill plan, the BEFORE-read plan,
// and the exact op for each row (APPEND / UPDATE / DELETE). retrieve_historical_row
// takes the raw delete-sign pointer (the codebase convention), so bridge here.
// declared first so it outlives the retriever: the lookup plan points into it
OlapBlockDataConvertor key_convertor;
std::unique_ptr<PrimaryKeyModelRowRetriever> retriever;
RETURN_IF_ERROR(setup_retriever_and_lookup(ctx, cfg, c.source_schema, block, seq_pos,
delete_signs ? delete_signs->data() : nullptr,
c.num_rows, key_convertor, retriever));
// Building AFTER: fixed partial update widens the narrow input and rebuilds
// missing columns from history; upserts are already source-schema shaped.
Block full_block;
const Block* after_src = block;
if (is_partial_update) {
full_block = widen_partial_update_block(
*c.source_schema, cfg.source.partial_update_info->update_cids, *block);
RETURN_IF_ERROR(retriever->build_after_block(&full_block, 0, c.num_rows));
after_src = &full_block;
}
return emit_binlog_block(c, after_src, retriever.get(), /*plain_operators=*/nullptr, block);
}
} // namespace doris::segment_v2