blob: c70136282162020ef42e9d326f908e2fabe0eb12 [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/block_transform.h"
#include <limits>
#include <numeric>
#include <unordered_set>
#include "common/cast_set.h"
#include "common/logging.h"
#include "core/block/block.h"
#include "core/column/column_string.h"
#include "exec/common/variant_util.h"
#include "storage/partial_update_info.h"
#include "storage/rowset/rowset_writer_context.h"
#include "storage/tablet/tablet_schema.h"
#include "storage/transform/partial_update_fill.h"
#include "storage/transform/row_binlog_derive.h"
#include "util/jsonb/serialize.h"
namespace doris::segment_v2 {
namespace {
// Parses raw variant columns into subcolumn form, in place. The heavy variant
// work stays in the variant ColumnWriter; this stage only reshapes the block
// before conversion.
class VariantParseStage : public BlockTransform {
public:
Status apply(TransformExecContext& ctx, Block* block) const override {
const auto& schema = *ctx.tablet_schema;
if (schema.num_variant_columns() == 0) {
return Status::OK();
}
std::vector<uint32_t> column_ids(block->columns());
std::iota(column_ids.begin(), column_ids.end(), 0);
return variant_util::parse_and_materialize_variant_columns(*block, schema, column_ids);
}
std::string_view name() const override { return "VariantParse"; }
};
// Checks schema rules and block width for every block entering a seam. The
// horizontal writer keeps a transitional duplicate of the width check until a
// later change removes it: non-seam callers (compaction, index change) still
// rely on it.
class ValidateStage : public BlockTransform {
public:
Status apply(TransformExecContext& ctx, Block* block) const override {
const TabletSchema& schema = *ctx.tablet_schema;
if (schema.cluster_key_uids().empty()) {
DCHECK(schema.num_key_columns() >= schema.num_short_key_columns())
<< ", table_id=" << schema.table_id()
<< ", num_key_columns=" << schema.num_key_columns()
<< ", num_short_key_columns=" << schema.num_short_key_columns();
}
const auto* info = ctx.partial_update_info.get();
const bool is_partial_update_load = info != nullptr && info->is_partial_update() &&
ctx.write_type == DataWriteType::TYPE_DIRECT &&
!ctx.rowset_ctx->is_transient_rowset_writer;
if (!is_partial_update_load) {
if (block->columns() != schema.num_columns()) {
return Status::InvalidArgument(
"illegal block columns, block columns = {}, tablet_schema columns = {}",
block->dump_structure(), schema.dump_structure());
}
return Status::OK();
}
// No tablet context (e.g. the streaming BetaRowsetWriterV2) means this
// path can't do partial update: return a clear error instead of
// crashing in the probe.
if (ctx.tablet == nullptr || ctx.mow_context == nullptr) {
return Status::NotSupported(
"partial update is not supported on this write path (no tablet context)");
}
if (!(schema.keys_type() == UNIQUE_KEYS &&
ctx.rowset_ctx->enable_unique_key_merge_on_write)) {
auto msg = fmt::format(
"Can only do partial update on merge-on-write unique table, but found: "
"keys_type={}, enable_unique_key_merge_on_write={}, tablet_id={}",
schema.keys_type(), ctx.rowset_ctx->enable_unique_key_merge_on_write,
ctx.tablet->tablet_id());
DCHECK(false) << msg;
return Status::InternalError<false>(msg);
}
// partial update needs the segment id, which only flush_single_block sets
if (ctx.segment_id < 0) {
return Status::InternalError<false>(
"partial update blocks must be flushed through flush_single_block, "
"tablet_id={}",
ctx.tablet->tablet_id());
}
if (info->is_flexible_partial_update()) {
if (block->columns() != schema.num_columns()) {
return Status::InvalidArgument(
"illegal flexible partial update block columns, block columns = {}, "
"tablet_schema columns = {}",
block->dump_structure(), schema.dump_structure());
}
} else {
DCHECK(info->is_fixed_partial_update());
if (block->columns() < schema.num_key_columns() ||
block->columns() >= schema.num_columns()) {
return Status::InvalidArgument(fmt::format(
"illegal partial update block columns: {}, num key columns: {}, total "
"schema columns: {}",
block->columns(), schema.num_key_columns(), schema.num_columns()));
}
}
return Status::OK();
}
std::string_view name() const override { return "Validate"; }
};
// Generates the hidden row-store column (each row as JSONB). A
// DerivedColumnGenerator so the vertical writer can stream it in batches.
class RowStoreColumnGenerator : public DerivedColumnGenerator {
public:
RowStoreColumnGenerator(TabletSchemaSPtr schema, Block source_block)
: _schema(std::move(schema)),
_source_block(std::move(source_block)),
_serdes(create_data_type_serdes(_source_block.get_data_types())),
_row_store_cids(_schema->row_columns_uids().begin(),
_schema->row_columns_uids().end()) {}
size_t generate(const Block& block, size_t row_pos, size_t max_rows, size_t max_bytes,
IColumn* dst) const override {
// Rows are read from the COW snapshot, indexed by the caller's positions in `block`;
// the two must describe the same rows or the serialized cells silently mismatch.
DCHECK_EQ(_source_block.rows(), block.rows());
auto* dst_str = static_cast<ColumnString*>(dst);
return JsonbSerializeUtil::block_to_jsonb(*_schema, _source_block, *dst_str,
cast_set<int>(_schema->num_columns()), _serdes,
_row_store_cids, row_pos, max_rows, max_bytes);
}
private:
TabletSchemaSPtr _schema;
Block _source_block;
DataTypeSerDeSPtrs _serdes;
std::unordered_set<int32_t> _row_store_cids;
};
// Registers a row-store generator over a COW snapshot of the block at this
// exact stage. Variant parsing can change its JSONB representation, so the
// snapshot preserves the legacy writer's RowStore/Variant ordering while the
// vertical writer still materializes the column in bounded batches.
class RowStoreFillStage : public BlockTransform {
public:
Status apply(TransformExecContext& ctx, Block* block) const override {
if (block->rows() == 0) {
return Status::OK();
}
const auto& schema = *ctx.tablet_schema;
for (size_t i = 0; i < schema.num_columns(); ++i) {
if (!schema.column(i).is_row_store_column()) {
continue;
}
std::shared_ptr<const DerivedColumnGenerator> generator =
std::make_shared<RowStoreColumnGenerator>(ctx.tablet_schema, *block);
ctx.derived_column = std::make_pair(cast_set<uint32_t>(i), std::move(generator));
break;
}
return Status::OK();
}
std::string_view name() const override { return "RowStoreFill"; }
};
} // namespace
BlockTransformChain build_transform_chain(const RowsetWriterContext& context) {
if (context.write_type == DataWriteType::TYPE_COMPACTION) {
return BlockTransformChain {};
}
if (context.write_binlog_opt().enable) {
if (context.write_type != DataWriteType::TYPE_DIRECT) {
return BlockTransformChain {};
}
// binlog<row> sub-writer: only the source -> binlog-schema derivation.
// No parse or row-store stage is needed: FE rejects a VARIANT column on
// a binlog<row> table, and the hidden row-store column is a hidden
// non-key column, which the binlog schema drops. Plain (no probe) for
// DUP and no-BEFORE upserts; MoW (with probe) for partial update or the
// BEFORE image.
if (binlog_needs_historical_lookup(context)) {
return BlockTransformChain {{std::make_shared<MowRowBinlogDeriveStage>()}};
}
return BlockTransformChain {{std::make_shared<PlainRowBinlogDeriveStage>()}};
}
std::vector<std::shared_ptr<const BlockTransform>> stages;
stages.push_back(std::make_shared<ValidateStage>());
const bool is_partial_update_load = context.partial_update_info != nullptr &&
context.partial_update_info->is_partial_update() &&
context.write_type == DataWriteType::TYPE_DIRECT &&
!context.is_transient_rowset_writer;
const bool rebuild_row_store = context.write_type == DataWriteType::TYPE_DIRECT ||
context.write_type == DataWriteType::TYPE_SCHEMA_CHANGE;
if (is_partial_update_load) {
// A partial update load is always TYPE_DIRECT, so the row store is
// always rebuilt.
if (context.partial_update_info->is_fixed_partial_update()) {
stages.push_back(std::make_shared<FixedPartialUpdateFillStage>());
// The legacy fixed path parsed both provided and missing Variant
// columns before rebuilding RowStore.
stages.push_back(std::make_shared<VariantParseStage>());
stages.push_back(std::make_shared<RowStoreFillStage>());
} else {
stages.push_back(std::make_shared<FlexiblePartialUpdateFillStage>());
// The legacy flexible path rebuilt RowStore before parsing the
// filled Variant columns.
stages.push_back(std::make_shared<RowStoreFillStage>());
stages.push_back(std::make_shared<VariantParseStage>());
}
return BlockTransformChain {std::move(stages)};
}
// Direct and schema-change writers rebuilt RowStore from the raw Variant
// representation, then parsed Variant for its column writer.
if (rebuild_row_store) {
stages.push_back(std::make_shared<RowStoreFillStage>());
}
stages.push_back(std::make_shared<VariantParseStage>());
return BlockTransformChain {std::move(stages)};
}
Status materialize_derived_columns(const DerivedColumn& derived_column, Block* block) {
if (!derived_column.second) {
return Status::OK();
}
const auto& [cid, generator] = derived_column;
auto column_ptr = block->get_by_position(cid).column->clone_empty();
size_t num_rows = block->rows();
size_t pos = 0;
while (pos < num_rows) {
size_t rows = generator->generate(*block, pos, num_rows - pos,
std::numeric_limits<size_t>::max(), column_ptr.get());
DCHECK_GT(rows, 0);
pos += rows;
}
block->replace_by_position(cid, std::move(column_ptr));
return Status::OK();
}
} // namespace doris::segment_v2