blob: 25f2836fb846b3889a8e71e0c359b00009b4d031 [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/segment/variant/variant_writer_helpers.h"
#include <algorithm>
#include <memory>
#include <span>
#include <string>
#include <string_view>
#include <utility>
#include "common/cast_set.h"
#include "common/config.h"
#include "common/exception.h"
#include "core/assert_cast.h"
#include "core/column/column_nullable.h"
#include "core/column/column_variant.h"
#include "core/column/variant_v2/column_variant_v2.h"
#include "core/data_type/data_type_array.h"
#include "core/data_type/data_type_factory.hpp"
#include "core/data_type/data_type_nullable.h"
#include "exec/common/variant_util.h"
#include "storage/index/indexed_column_writer.h"
#include "storage/iterator/olap_data_convertor.h"
#include "storage/rowset/rowset_writer_context.h"
#include "storage/segment/encoding_info.h"
#include "storage/segment/variant/v2/variant_shredder.h"
#include "storage/types.h"
namespace doris::segment_v2::variant_writer_helpers {
bool has_extracted_variant_columns(const TabletSchema& tablet_schema, int parent_column_unique_id) {
return std::ranges::any_of(tablet_schema.columns(),
[parent_column_unique_id](const auto& column) {
return column->is_extracted_column() &&
column->parent_unique_id() == parent_column_unique_id;
});
}
Status validate_variant_v2_writer_layout(const TabletSchema& tablet_schema,
const TabletColumn& parent_column) {
if (parent_column.variant_enable_nested_group()) {
return Status::NotSupported("ColumnVariantV2 writer does not support nested-group layout");
}
if (tablet_schema.deprecated_variant_flatten_nested()) {
return Status::NotSupported(
"ColumnVariantV2 writer does not support deprecated flatten-nested layout");
}
DORIS_CHECK_GE(parent_column.variant_max_subcolumns_count(), 0);
return Status::OK();
}
Status make_variant_shredder_options(const TabletSchema& tablet_schema,
const TabletColumn& parent_column,
VariantShredderPhysicalLayout physical_layout,
VariantShredderOptions* options) {
DORIS_CHECK(options != nullptr);
RETURN_IF_ERROR(validate_variant_v2_writer_layout(tablet_schema, parent_column));
VariantShredderOptions result {
.tablet_schema = &tablet_schema,
.parent_column_unique_id = parent_column.unique_id(),
.physical_layout = physical_layout,
.max_subcolumns_count = cast_set<size_t>(parent_column.variant_max_subcolumns_count()),
.typed_paths_to_sparse = parent_column.variant_enable_typed_paths_to_sparse(),
.sparse_bucket_count = cast_set<uint32_t>(
std::max(1, parent_column.variant_sparse_hash_shard_count())),
.max_sparse_column_statistics_size =
cast_set<size_t>(parent_column.variant_max_sparse_column_statistics_size()),
.doc_bucket_count =
cast_set<uint32_t>(std::max(1, parent_column.variant_doc_hash_shard_count())),
.doc_materialization_min_rows = cast_set<size_t>(
std::max<int64_t>(0, parent_column.variant_doc_materialization_min_rows())),
.check_duplicate_json_path = config::variant_enable_duplicate_json_path_check,
};
*options = std::move(result);
return Status::OK();
}
Status classify_variant_writer_input(const VariantColumnData& column,
VariantWriterInputFormat current_format,
std::string_view writer_description,
VariantWriterInputFormat* input_format) {
DORIS_CHECK(input_format != nullptr);
if (column.column_data == nullptr) {
return Status::InvalidArgument("{} received null column data", writer_description);
}
const bool is_v1 = check_and_get_column<ColumnVariant>(*column.column_data) != nullptr;
const bool is_v2 = check_and_get_column<ColumnVariantV2>(*column.column_data) != nullptr;
if (!is_v1 && !is_v2) {
return Status::InvalidArgument("{} requires ColumnVariant or ColumnVariantV2, got {}",
writer_description, column.column_data->get_name());
}
const VariantWriterInputFormat detected_format =
is_v1 ? VariantWriterInputFormat::V1 : VariantWriterInputFormat::V2;
if (current_format != VariantWriterInputFormat::UNSET && current_format != detected_format) {
return Status::InvalidArgument("{} input representation changed within one segment",
writer_description);
}
*input_format = detected_format;
return Status::OK();
}
Status append_variant_v2_to_shredder(VariantShredder* shredder, const VariantColumnData& column,
size_t num_rows, std::span<const uint8_t> outer_nulls) {
DORIS_CHECK(shredder != nullptr);
DORIS_CHECK(column.column_data != nullptr);
const auto* source = check_and_get_column<ColumnVariantV2>(*column.column_data);
DORIS_CHECK(source != nullptr);
if (column.row_pos > source->size() || num_rows > source->size() - column.row_pos) {
return Status::InvalidArgument("ColumnVariantV2 writer range [{}, {}) exceeds {} rows",
column.row_pos, column.row_pos + num_rows, source->size());
}
if (!outer_nulls.empty() && outer_nulls.size() != num_rows) {
return Status::InvalidArgument("ColumnVariantV2 outer-null span has {} rows, expected {}",
outer_nulls.size(), num_rows);
}
const auto view = source->read_view();
if (!view.is_typed()) {
return shredder->append(view, column.row_pos, num_rows, outer_nulls);
}
auto encoded_batch = ColumnVariantV2::create();
RETURN_IF_CATCH_EXCEPTION(
{ encoded_batch->insert_range_from(*source, column.row_pos, num_rows); });
DORIS_CHECK(!encoded_batch->is_typed());
return shredder->append(encoded_batch->read_view(), 0, num_rows, outer_nulls);
}
void init_column_meta(ColumnMetaPB* meta, uint32_t column_id, const TabletColumn& column,
const ColumnWriterOptions& opts) {
meta->Clear();
meta->set_column_id(column_id);
meta->set_type(int(column.type()));
meta->set_length(column.length());
meta->set_encoding(EncodingInfo::resolve_default_encoding(opts.storage_format, column));
meta->set_compression(opts.compression_type);
meta->set_is_nullable(column.is_nullable());
meta->set_default_value(column.default_value());
meta->set_precision(column.precision());
meta->set_frac(column.frac());
if (column.has_path_info()) {
column.path_info_ptr()->to_protobuf(meta->mutable_column_path_info(),
column.parent_unique_id());
}
meta->set_unique_id(column.unique_id());
for (uint32_t i = 0; i < column.get_subtype_count(); ++i) {
init_column_meta(meta->add_children_columns(), column_id, column.get_sub_column(i), opts);
}
if (column.is_variant_type()) {
meta->set_variant_max_subcolumns_count(column.variant_max_subcolumns_count());
meta->set_variant_enable_doc_mode(column.variant_enable_doc_mode());
}
}
Status create_column_writer(uint32_t cid, const TabletColumn& column,
const TabletSchemaSPtr& tablet_schema,
IndexFileWriter* inverted_index_file_writer,
std::unique_ptr<ColumnWriter>* writer, TabletIndexes& subcolumn_indexes,
ColumnWriterOptions* opt, int64_t none_null_value_size,
bool need_record_none_null_value_size) {
init_column_meta(opt->meta, cid, column, *opt);
if (need_record_none_null_value_size) {
opt->meta->set_none_null_size(none_null_value_size);
}
opt->need_zone_map = tablet_schema->keys_type() != KeysType::AGG_KEYS;
opt->need_bloom_filter = column.is_bf_column();
if (opt->need_bloom_filter) {
opt->bf_options.fpp = tablet_schema->get_bloom_filter_fpp(column);
}
const auto& parent_index = tablet_schema->inverted_indexs(column.parent_unique_id());
if (segment_v2::IndexColumnWriter::check_support_inverted_index(column)) {
auto init_opt_inverted_index = [&]() {
DCHECK(!subcolumn_indexes.empty());
for (const auto& index : subcolumn_indexes) {
opt->inverted_indexes.push_back(index.get());
}
opt->need_inverted_index = true;
DCHECK(inverted_index_file_writer != nullptr);
opt->index_file_writer = inverted_index_file_writer;
};
if (!subcolumn_indexes.empty()) {
init_opt_inverted_index();
} else if (!parent_index.empty() &&
variant_util::inherit_index(parent_index, subcolumn_indexes, column)) {
init_opt_inverted_index();
} else {
opt->need_inverted_index = false;
}
}
#define DISABLE_INDEX_IF_FIELD_TYPE(TYPE) \
if (column.type() == FieldType::OLAP_FIELD_TYPE_##TYPE) { \
opt->need_zone_map = false; \
opt->need_bloom_filter = false; \
}
DISABLE_INDEX_IF_FIELD_TYPE(ARRAY)
DISABLE_INDEX_IF_FIELD_TYPE(JSONB)
DISABLE_INDEX_IF_FIELD_TYPE(VARIANT)
#undef DISABLE_INDEX_IF_FIELD_TYPE
RETURN_IF_ERROR(ColumnWriter::create(*opt, &column, opt->file_writer, writer));
RETURN_IF_ERROR((*writer)->init());
return Status::OK();
}
Status convert_and_write_column(OlapBlockDataConvertor* converter, const TabletColumn& column,
DataTypePtr data_type, ColumnWriter* writer,
const ColumnPtr& src_column, size_t num_rows, int column_id) {
converter->add_column_data_convertor(column);
RETURN_IF_ERROR(converter->set_source_content_with_specifid_column({src_column, data_type, ""},
0, num_rows, column_id));
auto [status, converted_column] = converter->convert_column_data(column_id);
RETURN_IF_ERROR(status);
RETURN_IF_ERROR(writer->append(converted_column->get_nullmap(), converted_column->get_data(),
num_rows));
converter->clear_source_content(column_id);
return Status::OK();
}
namespace {
Status append_sparse_array_column(const TabletColumn& tablet_column, ColumnWriter* writer,
OlapBlockDataConvertor* converter, int column_id,
const DataTypePtr& type, const ColumnPtr& values_column,
std::span<const uint32_t> rowids, size_t total_rows) {
// Example: values=[a,b], rowids=[1,4], total_rows=6 becomes
// [NULL,a,NULL,NULL,b,NULL]. ARRAY convertor output contains offsets/pointers rather than a
// fixed cell stride, so materialize only this path while it is being written. Scalar paths
// never allocate the full N-row representation.
MutableColumnPtr full_column = values_column->clone_empty();
full_column->reserve(total_rows);
size_t next_row = 0;
size_t value_index = 0;
while (value_index < rowids.size()) {
const size_t row = rowids[value_index];
DORIS_CHECK_GE(row, next_row);
full_column->insert_many_defaults(row - next_row);
size_t run_length = 1;
while (value_index + run_length < rowids.size() &&
rowids[value_index + run_length] == row + run_length) {
++run_length;
}
full_column->insert_range_from(*values_column, value_index, run_length);
value_index += run_length;
next_row = row + run_length;
}
DORIS_CHECK_LE(next_row, total_rows);
full_column->insert_many_defaults(total_rows - next_row);
RETURN_IF_ERROR(converter->set_source_content_with_specifid_column(
{full_column->get_ptr(), type, ""}, 0, total_rows, column_id));
auto [status, converted] = converter->convert_column_data(column_id);
RETURN_IF_ERROR(status);
const auto* data = reinterpret_cast<const uint8_t*>(converted->get_data());
RETURN_IF_ERROR(writer->append_nullable(converted->get_nullmap(), &data, total_rows));
converter->clear_source_content(column_id);
return Status::OK();
}
Status append_sparse_scalar_column(const TabletColumn& tablet_column, ColumnWriter* writer,
OlapBlockDataConvertor* converter, int column_id,
const DataTypePtr& type, const ColumnPtr& values_column,
std::span<const uint32_t> rowids, size_t total_rows) {
const size_t cell_size = field_type_size(writer->get_column()->type());
RETURN_IF_ERROR(converter->set_source_content_with_specifid_column({values_column, type, ""}, 0,
rowids.size(), column_id));
auto [status, converted] = converter->convert_column_data(column_id);
RETURN_IF_ERROR(status);
const uint8_t* nullmap = converted->get_nullmap();
const auto* data = reinterpret_cast<const uint8_t*>(converted->get_data());
auto append_gap = [&](size_t gap) -> Status {
if (gap == 0) {
return Status::OK();
}
DORIS_CHECK(tablet_column.is_nullable());
return writer->append_nulls(gap);
};
size_t next_row = 0;
size_t value_index = 0;
while (value_index < rowids.size()) {
const size_t row = rowids[value_index];
DORIS_CHECK_GE(row, next_row);
RETURN_IF_ERROR(append_gap(row - next_row));
size_t run_length = 1;
while (value_index + run_length < rowids.size() &&
rowids[value_index + run_length] == row + run_length) {
++run_length;
}
const uint8_t* run_nullmap = nullmap == nullptr ? nullptr : nullmap + value_index;
RETURN_IF_ERROR(writer->append(run_nullmap, data + cell_size * value_index, run_length));
value_index += run_length;
next_row = row + run_length;
}
RETURN_IF_ERROR(append_gap(total_rows - next_row));
converter->clear_source_content(column_id);
return Status::OK();
}
} // namespace
Status append_sparse_converted_column(const TabletColumn& tablet_column, ColumnWriter* writer,
OlapBlockDataConvertor* converter, int column_id,
const DataTypePtr& type, const ColumnPtr& values_column,
std::span<const uint32_t> rowids, size_t total_rows) {
DORIS_CHECK_EQ(values_column->size(), rowids.size());
if (!rowids.empty()) {
DORIS_CHECK_LT(rowids.back(), total_rows);
}
// Column ids and convertor slots advance together across physical paths. Reserve exactly one
// slot at the common entry point, including empty segments and paths whose forced cast removed
// every value; otherwise the next path is registered at this index but queried by the next id.
converter->add_column_data_convertor(tablet_column);
if (total_rows == 0) {
return Status::OK();
}
if (rowids.empty()) {
DORIS_CHECK(tablet_column.is_nullable());
return writer->append_nulls(total_rows);
}
const DataTypePtr base_type = remove_nullable(type);
if (base_type->get_primitive_type() == PrimitiveType::TYPE_ARRAY) {
return append_sparse_array_column(tablet_column, writer, converter, column_id, type,
values_column, rowids, total_rows);
}
return append_sparse_scalar_column(tablet_column, writer, converter, column_id, type,
values_column, rowids, total_rows);
}
void maybe_remove_root_jsonb_with_empty_defaults(MutableColumnPtr* root_column, size_t num_rows,
bool remove_root_jsonb) {
if (!remove_root_jsonb) {
return;
}
auto bare_jsonb_type = std::make_shared<ColumnVariant::MostCommonType>();
auto bare_jsonb_col = bare_jsonb_type->create_column();
bare_jsonb_col->insert_many_defaults(num_rows);
*root_column = std::move(bare_jsonb_col);
}
Status prepare_subcolumn_writer_target(
const ColumnWriterOptions& base_opts, const TabletColumn& parent_column,
int current_column_id, const PathInData& relative_path, const DataTypePtr& current_type,
int64_t none_null_value_size, size_t num_rows,
const TabletSchema::SubColumnInfo* existing_subcolumn_info, bool check_storage_type,
TabletIndexes* out_subcolumn_indexes, ColumnWriterOptions* out_subcolumn_opts,
std::unique_ptr<ColumnWriter>* out_writer, TabletColumn* out_tablet_column) {
if (out_subcolumn_indexes == nullptr || out_subcolumn_opts == nullptr ||
out_writer == nullptr || out_tablet_column == nullptr) {
return Status::InvalidArgument("subcolumn writer target output is null");
}
TabletColumn tablet_column;
TabletIndexes subcolumn_indexes;
bool resolved_from_schema = false;
if (existing_subcolumn_info != nullptr) {
tablet_column = existing_subcolumn_info->column;
subcolumn_indexes = existing_subcolumn_info->indexes;
resolved_from_schema = true;
} else {
TabletSchema::SubColumnInfo sub_column_info;
if (variant_util::generate_sub_column_info(*base_opts.rowset_ctx->tablet_schema,
parent_column.unique_id(),
relative_path.get_path(), &sub_column_info)) {
tablet_column = std::move(sub_column_info.column);
subcolumn_indexes = std::move(sub_column_info.indexes);
resolved_from_schema = true;
} else {
const std::string column_name =
parent_column.name_lower_case() + "." + relative_path.get_path();
PathInData full_path;
if (relative_path.has_nested_part()) {
PathInDataBuilder full_path_builder;
full_path = full_path_builder.append(parent_column.name_lower_case(), false)
.append(relative_path.get_parts(), false)
.build();
} else {
full_path = PathInData(column_name);
}
tablet_column = variant_util::get_column_by_type(
current_type, column_name,
variant_util::ExtraInfo {.unique_id = -1,
.parent_unique_id = parent_column.unique_id(),
.path_info = full_path});
const auto& indexes =
base_opts.rowset_ctx->tablet_schema->inverted_indexs(parent_column.unique_id());
variant_util::inherit_index(indexes, subcolumn_indexes, tablet_column);
}
}
if (resolved_from_schema && check_storage_type) {
auto storage_type = DataTypeFactory::instance().create_data_type(tablet_column);
if (!storage_type->equals(*current_type)) {
return Status::InvalidArgument(
"Storage type {} is not equal to current type {} for path {}",
storage_type->get_name(), current_type->get_name(), relative_path.get_path());
}
}
ColumnWriterOptions opts;
opts.meta = base_opts.footer->add_columns();
opts.index_file_writer = base_opts.index_file_writer;
opts.compression_type = base_opts.compression_type;
opts.rowset_ctx = base_opts.rowset_ctx;
opts.file_writer = base_opts.file_writer;
opts.storage_format = base_opts.storage_format;
variant_util::inherit_column_attributes(parent_column, tablet_column);
const bool need_record_none_null_value_size =
(!tablet_column.path_info_ptr()->get_is_typed() ||
parent_column.variant_enable_typed_paths_to_sparse()) &&
!tablet_column.path_info_ptr()->has_nested_part() &&
variant_util::should_record_variant_path_stats(parent_column);
std::unique_ptr<ColumnWriter> writer;
RETURN_IF_ERROR(create_column_writer(
current_column_id, tablet_column, base_opts.rowset_ctx->tablet_schema,
base_opts.index_file_writer, &writer, subcolumn_indexes, &opts, none_null_value_size,
need_record_none_null_value_size));
opts.meta->set_num_rows(num_rows);
*out_subcolumn_indexes = std::move(subcolumn_indexes);
*out_subcolumn_opts = opts;
*out_writer = std::move(writer);
*out_tablet_column = std::move(tablet_column);
return Status::OK();
}
} // namespace doris::segment_v2::variant_writer_helpers
namespace doris::segment_v2 {
void _init_column_meta(ColumnMetaPB* meta, uint32_t column_id, const TabletColumn& column,
const ColumnWriterOptions& opts) {
variant_writer_helpers::init_column_meta(meta, column_id, column, opts);
}
} // namespace doris::segment_v2