blob: 2f6a7933d24e9bd612475d3388f1f33ce188eb2c [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/v2/variant_shredder.h"
#include <algorithm>
#include <limits>
#include <numeric>
#include <optional>
#include <unordered_map>
#include <utility>
#include "common/exception.h"
#include "core/assert_cast.h"
#include "core/column/column_map.h"
#include "core/column/column_string.h"
#include "core/column/column_variant.h"
#include "core/column/column_vector.h"
#include "core/data_type/data_type_factory.hpp"
#include "exec/common/hash_table/phmap_fwd_decl.h"
#include "exec/common/variant_util.h"
#include "exprs/function/parse/variant_jsonb_parse.h"
#include "storage/tablet/tablet_schema.h"
#include "util/json/path_in_data.h"
#include "util/jsonb_writer.h"
namespace doris::segment_v2 {
namespace {
PathInData normalize_doc_publication_path(const PathInData& path) {
if (path.empty()) {
return path;
}
for (const PathInData::Part& part : path.get_parts()) {
if (part.is_nested || part.anonymous_array_level != 0) {
return path;
}
}
return PathInData(path.get_path(), path.get_is_typed());
}
} // namespace
struct VariantShredder::Impl {
enum class State : uint8_t { COLLECTING, FINISHED, FAILED };
using PathIndex = uint32_t;
using ParentFieldKey = uint64_t;
using ChildPathCache = doris::flat_hash_map<ParentFieldKey, PathIndex>;
static constexpr PathIndex UNRESOLVED_PATH = std::numeric_limits<PathIndex>::max();
static constexpr size_t MAX_BINARY_CELLS_PER_CHUNK = 1U << 20;
// Metadata bytes belong to the input ReadView. This cache never escapes one append call, so
// it can borrow the dictionary and retain only parent+field transitions observed in that
// batch. Canonical paths themselves remain owned by PathState across appends.
struct MetadataPathCache {
explicit MetadataPathCache(VariantMetadataRef metadata_) : metadata(metadata_) {}
VariantMetadataRef metadata;
ChildPathCache child_paths;
};
// Keep all state for one canonical dotted path together. This replaces three parallel
// containers (path plan, builders, and last-row markers), so a path has one index and one
// lifetime throughout shredding.
struct PathState {
explicit PathState(const PathInData& path_) : path(path_) {}
PathInData path;
std::optional<VariantPathBuilder> builder;
size_t last_row_marker = 0;
};
struct SparsePlan {
VariantPathBuilder* builder = nullptr;
uint32_t bucket = 0;
const std::string* path = nullptr;
bool track_statistics = false;
};
struct DocPlan {
VariantPathBuilder* builder = nullptr;
uint32_t bucket = 0;
const std::string* path = nullptr;
size_t candidate_index = 0;
};
explicit Impl(VariantShredderOptions options_) : options(std::move(options_)) {
paths.emplace_back(PathInData());
if (options.physical_layout == VariantShredderPhysicalLayout::ORDINARY &&
options.sparse_bucket_count == 0) {
failure = Status::InvalidArgument(
"Variant shredder sparse bucket count must be positive");
state = State::FAILED;
} else if (options.physical_layout == VariantShredderPhysicalLayout::DOC &&
options.doc_bucket_count == 0) {
failure = Status::InvalidArgument("Variant shredder doc bucket count must be positive");
state = State::FAILED;
} else if (options.tablet_schema != nullptr && options.parent_column_unique_id < 0) {
failure = Status::InvalidArgument(
"Variant shredder tablet schema requires a parent column unique id");
state = State::FAILED;
}
}
Status require_collecting() const {
if (state == State::FAILED) {
return failure;
}
if (state == State::FINISHED) {
return Status::InvalidArgument("Variant shredder is already finished");
}
return Status::OK();
}
Status fail(Status status) {
if (state != State::FAILED) {
failure = std::move(status);
state = State::FAILED;
}
return failure;
}
VariantPathBuilder* get_or_create_builder(PathIndex path_index) {
PathState& path_state = paths[path_index];
if (!path_state.builder.has_value()) {
path_state.builder.emplace(path_state.path, rows);
}
return &*path_state.builder;
}
Status validate_doc_path(PathIndex path_index) const {
if (options.physical_layout != VariantShredderPhysicalLayout::DOC) {
return Status::OK();
}
const auto& parts = paths[path_index].path.get_parts();
if (parts.empty()) {
return Status::Corruption("Variant doc path must not be empty");
}
return Status::OK();
}
Status append_leaf(VariantRef value, PathIndex path_index, size_t row) {
PathState& path_state = paths[path_index];
const size_t row_marker = row + 1;
if (path_state.last_row_marker == row_marker) {
if (!options.check_duplicate_json_path) {
return Status::InvalidArgument("may contains duplicated entry : {}",
path_state.path.get_path());
}
return Status::OK();
}
path_state.last_row_marker = row_marker;
if (value.is_null()) {
return Status::OK();
}
return get_or_create_builder(path_index)->append(value, row);
}
// Pack two uint32 values into one key without allocating a pair object for every transition.
static ParentFieldKey parent_field_key(PathIndex parent, uint32_t field) {
return (static_cast<uint64_t>(parent) << 32) | field;
}
PathIndex resolve_child_path(MetadataPathCache& metadata_cache, PathIndex parent,
uint32_t field) {
const ParentFieldKey cache_key = parent_field_key(parent, field);
if (const auto found = metadata_cache.child_paths.find(cache_key);
found != metadata_cache.child_paths.end()) {
return found->second;
}
PathInDataBuilder builder;
builder.append(paths[parent].path.get_parts(), false)
.append(metadata_cache.metadata.key_at(field).to_string_view(), false);
PathInData child = builder.build();
// V2 object traversal keeps arrays as leaves. Canonicalizing into the dotted on-disk
// namespace also makes {"a.b": 1} and {"a": {"b": 1}} share one path.
child = PathInData(child.get_path());
PathIndex child_index = 0;
if (const auto found = path_indices.find(child); found != path_indices.end()) {
child_index = found->second;
} else {
if (paths.size() >= UNRESOLVED_PATH) {
throw Exception(ErrorCode::INVALID_ARGUMENT,
"Variant path count exceeds uint32 limit");
}
child_index = static_cast<PathIndex>(paths.size());
path_indices.emplace(child, child_index);
paths.emplace_back(child);
}
metadata_cache.child_paths.emplace(cache_key, child_index);
return child_index;
}
Status visit(VariantRef value, MetadataPathCache& metadata_cache, PathIndex path_index,
size_t row) {
if (value.is_null()) {
return options.check_duplicate_json_path ? append_leaf(value, path_index, row)
: Status::OK();
}
if (value.basic_type() != VariantBasicType::OBJECT) {
return append_leaf(value, path_index, row);
}
const VariantRef::ObjectView object = value.object_view();
for (uint32_t index = 0; index < object.size(); ++index) {
uint32_t field = 0;
VariantRef child = object.value_at(index, &field);
const PathIndex child_path = resolve_child_path(metadata_cache, path_index, field);
RETURN_IF_ERROR(validate_doc_path(child_path));
RETURN_IF_ERROR(visit(child, metadata_cache, child_path, row));
}
return Status::OK();
}
Status complete_builder_rows(size_t completed_rows) {
for (PathState& path : paths) {
if (path.builder.has_value()) {
RETURN_IF_ERROR(path.builder->complete_rows(completed_rows));
}
}
return Status::OK();
}
void append_default_root() { root_values->insert_default(); }
Status append_root(VariantRef value) {
// V1 reconstructs objects exclusively from shredded paths, so JSON-null and unresolved
// leaves remain absent. Keep the root only for scalar and array values. The physical
// writer applies SQL NULL through the column's ordinary nullable map, so the shredder
// does not need a separate root-null state.
if (value.is_null() || value.basic_type() == VariantBasicType::OBJECT) {
append_default_root();
return Status::OK();
}
variant_to_jsonb(value, root_writer);
root_values->insert_data(root_writer.getOutput()->getBuffer(),
root_writer.getOutput()->getSize());
return Status::OK();
}
Status prepare_logical_candidates(DorisVector<VariantPathSelectionCandidate>* candidates,
DorisVector<VariantPathBuilder*>* candidate_builders,
DorisVector<DataTypePtr>* storage_types) {
candidates->reserve(paths.size());
candidate_builders->reserve(paths.size());
storage_types->reserve(paths.size());
for (PathState& path : paths) {
if (!path.builder.has_value()) {
continue;
}
VariantPathBuilder* builder = &*path.builder;
bool is_typed_path = false;
DataTypePtr storage_type;
if (options.tablet_schema != nullptr) {
TabletSchema::SubColumnInfo info;
is_typed_path = variant_util::generate_sub_column_info(
*options.tablet_schema, options.parent_column_unique_id,
builder->path().get_path(), &info);
if (is_typed_path) {
storage_type = DataTypeFactory::instance().create_data_type(info.column);
}
}
if (builder->non_null_rows() == 0) {
if (!is_typed_path || options.typed_paths_to_sparse) {
continue;
}
RETURN_IF_ERROR(builder->convert_to(storage_type));
} else {
RETURN_IF_ERROR(builder->convert_to(
normalize_variant_path_integer_widths(builder->type())));
}
if (builder->non_null_rows() != 0 &&
variant_path_type_contains_nothing(builder->type()) &&
(storage_type == nullptr || variant_path_type_contains_nothing(storage_type))) {
continue;
}
candidate_builders->push_back(builder);
candidates->push_back(VariantPathSelectionCandidate {.builder = builder,
.is_typed_path = is_typed_path});
storage_types->push_back(std::move(storage_type));
}
return Status::OK();
}
Status convert_typed_candidates(std::span<const size_t> selected,
const DorisVector<VariantPathBuilder*>& candidate_builders,
const DorisVector<DataTypePtr>& storage_types) const {
for (size_t index : selected) {
if (storage_types[index] != nullptr) {
RETURN_IF_ERROR(candidate_builders[index]->convert_to(storage_types[index]));
}
}
return Status::OK();
}
Status publish_materialized(const VariantPathSelection& selection,
const DorisVector<VariantPathBuilder*>& candidate_builders,
VariantShreddedColumns* result) const {
result->materialized.reserve(selection.materialized.size());
for (size_t selected : selection.materialized) {
VariantPathBuilder& builder = *candidate_builders[selected];
const PathInData& raw_path = builder.path();
PathInData publication_path =
options.physical_layout == VariantShredderPhysicalLayout::DOC
? normalize_doc_publication_path(raw_path)
: raw_path;
const std::span<const uint32_t> compact_rowids = builder.rowids();
DorisVector<uint32_t> rowids(compact_rowids.begin(), compact_rowids.end());
result->materialized.push_back({.path = publication_path,
.type = builder.type(),
.column = builder.column(),
.rowids = std::move(rowids)});
}
return Status::OK();
}
void publish_root(VariantShreddedColumns* result) {
result->num_rows = rows;
result->root_jsonb = std::move(root_values);
}
DorisVector<SparsePlan> build_sparse_plan(
const VariantPathSelection& selection,
const DorisVector<VariantPathBuilder*>& candidate_builders) const {
DorisVector<SparsePlan> sparse_plan;
sparse_plan.reserve(selection.sparse.size());
for (size_t selected : selection.sparse) {
VariantPathBuilder* builder = candidate_builders[selected];
const std::string& path = builder->path().get_path();
sparse_plan.push_back({.builder = builder,
.bucket = variant_util::variant_binary_shard_of(
{path.data(), path.size()}, options.sparse_bucket_count),
.path = &path,
.track_statistics = false});
}
return sparse_plan;
}
void select_sparse_statistics(DorisVector<SparsePlan>* sparse_plan) const {
if (options.max_sparse_column_statistics_size == 0) {
return;
}
DorisVector<size_t> encounter_order(sparse_plan->size());
std::iota(encounter_order.begin(), encounter_order.end(), 0);
std::ranges::sort(encounter_order, [&](size_t left, size_t right) {
const auto left_rowids = (*sparse_plan)[left].builder->rowids();
const auto right_rowids = (*sparse_plan)[right].builder->rowids();
DORIS_CHECK(!left_rowids.empty());
DORIS_CHECK(!right_rowids.empty());
if (left_rowids.front() != right_rowids.front()) {
return left_rowids.front() < right_rowids.front();
}
// The plan is path-sorted, matching the old inner loop's tie-break at a row.
return left < right;
});
DorisVector<size_t> tracked_paths_per_bucket(options.sparse_bucket_count, 0);
for (size_t index : encounter_order) {
SparsePlan& plan = (*sparse_plan)[index];
if (tracked_paths_per_bucket[plan.bucket] < options.max_sparse_column_statistics_size) {
plan.track_statistics = true;
++tracked_paths_per_bucket[plan.bucket];
}
}
}
template <typename BinaryPlan>
Status append_binary_rows(const DorisVector<BinaryPlan>& binary_plan,
const DorisVector<ColumnMap*>& maps) const {
DorisVector<size_t> bucket_cells(maps.size(), 0);
DorisVector<size_t> bucket_key_bytes(maps.size(), 0);
DorisVector<size_t> bucket_value_bytes(maps.size(), 0);
// Build a compact row index in two passes. Each path contributes only its
// present values, and paths are visited in publication order so cells
// within one row preserve path order.
DorisVector<size_t> row_offsets(rows + 1, 0);
for (const BinaryPlan& plan : binary_plan) {
DORIS_CHECK_LT(plan.bucket, maps.size());
const std::span<const uint32_t> rowids = plan.builder->rowids();
bucket_cells[plan.bucket] += rowids.size();
bucket_key_bytes[plan.bucket] += rowids.size() * plan.path->size();
const ColumnPtr column = plan.builder->column();
DORIS_CHECK(column);
bucket_value_bytes[plan.bucket] += column->byte_size();
for (uint32_t row : rowids) {
if (row >= rows) {
return Status::InternalError("Variant path {} row {} exceeds {} rows",
*plan.path, row, rows);
}
++row_offsets[row + 1];
}
}
for (size_t bucket = 0; bucket < maps.size(); ++bucket) {
auto& keys = assert_cast<ColumnString&>(maps[bucket]->get_keys());
auto& values = assert_cast<ColumnString&>(maps[bucket]->get_values());
maps[bucket]->get_offsets().reserve(rows);
keys.reserve(bucket_cells[bucket]);
keys.get_chars().reserve(bucket_key_bytes[bucket]);
values.reserve(bucket_cells[bucket]);
values.get_chars().reserve(bucket_value_bytes[bucket]);
}
if (binary_plan.size() > std::numeric_limits<uint32_t>::max()) {
return Status::InternalError("Variant binary path count {} exceeds uint32 limit",
binary_plan.size());
}
std::partial_sum(row_offsets.begin(), row_offsets.end(), row_offsets.begin());
// Transpose path-major builders into row-major maps in bounded chunks. A
// chunk always ends at a row boundary, so the path-sorted plan order
// remains the canonical key order within each row and bucket. The per-plan
// cursor also recovers value_index without storing it in every cell.
#if defined(BE_TEST) && !defined(BE_BENCHMARK)
const size_t max_binary_cells_per_chunk = binary_cells_per_chunk;
#else
constexpr size_t max_binary_cells_per_chunk = MAX_BINARY_CELLS_PER_CHUNK;
#endif
DorisVector<size_t> value_indices(binary_plan.size(), 0);
DorisVector<uint32_t> cells;
DorisVector<size_t> next_cell;
size_t row_begin = 0;
while (row_begin < rows) {
#if defined(BE_TEST) && !defined(BE_BENCHMARK)
++binary_chunk_count;
#endif
const size_t first_cell = row_offsets[row_begin];
size_t row_end = rows;
if (row_offsets.back() - first_cell > max_binary_cells_per_chunk) {
const auto first_too_large =
std::upper_bound(row_offsets.begin() + row_begin + 1, row_offsets.end(),
first_cell + max_binary_cells_per_chunk);
row_end = static_cast<size_t>(first_too_large - row_offsets.begin() - 1);
// One exceptionally wide row may exceed the bound, but must stay
// intact.
row_end = std::max(row_end, row_begin + 1);
}
cells.resize(row_offsets[row_end] - first_cell);
next_cell.resize(row_end - row_begin);
for (size_t row = row_begin; row < row_end; ++row) {
next_cell[row - row_begin] = row_offsets[row] - first_cell;
}
for (size_t plan_index = 0; plan_index < binary_plan.size(); ++plan_index) {
const std::span<const uint32_t> rowids = binary_plan[plan_index].builder->rowids();
size_t value_index = value_indices[plan_index];
DORIS_CHECK(value_index == rowids.size() || rowids[value_index] >= row_begin);
while (value_index < rowids.size() && rowids[value_index] < row_end) {
const uint32_t row = rowids[value_index++];
cells[next_cell[row - row_begin]++] = static_cast<uint32_t>(plan_index);
}
}
for (size_t row = row_begin; row < row_end; ++row) {
DORIS_CHECK_EQ(next_cell[row - row_begin], row_offsets[row + 1] - first_cell);
for (size_t cell_index = row_offsets[row] - first_cell;
cell_index < row_offsets[row + 1] - first_cell; ++cell_index) {
const uint32_t plan_index = cells[cell_index];
const BinaryPlan& plan = binary_plan[plan_index];
const size_t value_index = value_indices[plan_index]++;
auto& keys = assert_cast<ColumnString&>(maps[plan.bucket]->get_keys());
auto& values = assert_cast<ColumnString&>(maps[plan.bucket]->get_values());
keys.insert_data(plan.path->data(), plan.path->size());
RETURN_IF_ERROR(
plan.builder->write_sparse_cell(value_index, &values.get_chars()));
values.get_offsets().push_back(values.get_chars().size());
}
for (ColumnMap* map : maps) {
map->get_offsets().push_back(map->get_keys().size());
}
}
row_begin = row_end;
}
for (size_t plan_index = 0; plan_index < binary_plan.size(); ++plan_index) {
DORIS_CHECK_EQ(value_indices[plan_index],
binary_plan[plan_index].builder->rowids().size());
}
return Status::OK();
}
void publish_sparse_statistics(const DorisVector<SparsePlan>& sparse_plan,
DorisVector<VariantStatistics>* bucket_statistics,
VariantShreddedColumns* result) const {
for (const SparsePlan& plan : sparse_plan) {
if (!plan.track_statistics) {
continue;
}
const uint32_t count = plan.builder->non_null_rows();
(*bucket_statistics)[plan.bucket].sparse_column_non_null_size[*plan.path] = count;
result->statistics.sparse_column_non_null_size[*plan.path] += count;
}
}
Status publish_sparse(const VariantPathSelection& selection,
const DorisVector<VariantPathBuilder*>& candidate_builders,
VariantShreddedColumns* result) const {
DorisVector<MutableColumnPtr> sparse_owners;
DorisVector<ColumnMap*> sparse_maps;
sparse_owners.reserve(options.sparse_bucket_count);
sparse_maps.reserve(options.sparse_bucket_count);
result->binary_buckets.reserve(options.sparse_bucket_count);
for (uint32_t bucket = 0; bucket < options.sparse_bucket_count; ++bucket) {
MutableColumnPtr map = ColumnVariant::create_binary_column_fn();
sparse_maps.push_back(assert_cast<ColumnMap*>(map.get()));
sparse_owners.emplace_back(std::move(map));
}
DorisVector<SparsePlan> sparse_plan = build_sparse_plan(selection, candidate_builders);
select_sparse_statistics(&sparse_plan);
RETURN_IF_ERROR(append_binary_rows(sparse_plan, sparse_maps));
DorisVector<VariantStatistics> bucket_statistics(options.sparse_bucket_count);
publish_sparse_statistics(sparse_plan, &bucket_statistics, result);
for (uint32_t bucket = 0; bucket < options.sparse_bucket_count; ++bucket) {
result->binary_buckets.push_back({.column = std::move(sparse_owners[bucket]),
.statistics = std::move(bucket_statistics[bucket])});
}
return Status::OK();
}
Status build_doc_plan(const DorisVector<VariantPathBuilder*>& candidate_builders,
DorisVector<DocPlan>& plan) const {
plan.reserve(candidate_builders.size());
for (size_t index = 0; index < candidate_builders.size(); ++index) {
VariantPathBuilder* builder = candidate_builders[index];
const PathInData publication_path = normalize_doc_publication_path(builder->path());
if (publication_path.get_parts().empty()) {
return Status::Corruption("Variant doc path must not be empty");
}
for (const PathInData::Part& part : publication_path.get_parts()) {
if (part.key.find('.') != std::string_view::npos) {
return Status::Corruption("Variant doc path has an ambiguous dotted part");
}
}
const std::string& path = builder->path().get_path();
plan.push_back({.builder = builder,
.bucket = variant_util::variant_binary_shard_of(
{path.data(), path.size()}, options.doc_bucket_count),
.path = &path,
.candidate_index = index});
}
std::ranges::sort(plan, [](const DocPlan& left, const DocPlan& right) {
return *left.path < *right.path;
});
for (size_t index = 1; index < plan.size(); ++index) {
if (*plan[index - 1].path == *plan[index].path) {
return Status::Corruption("Variant structured doc paths collide at {}",
*plan[index].path);
}
}
return Status::OK();
}
Status publish_doc(const DorisVector<DocPlan>& plan, VariantShreddedColumns* result) const {
DorisVector<MutableColumnPtr> owners;
DorisVector<ColumnMap*> maps;
owners.reserve(options.doc_bucket_count);
maps.reserve(options.doc_bucket_count);
for (uint32_t bucket = 0; bucket < options.doc_bucket_count; ++bucket) {
MutableColumnPtr map = ColumnVariant::create_binary_column_fn();
maps.push_back(assert_cast<ColumnMap*>(map.get()));
owners.emplace_back(std::move(map));
}
RETURN_IF_ERROR(append_binary_rows(plan, maps));
DorisVector<VariantStatistics> statistics(options.doc_bucket_count);
for (const DocPlan& entry : plan) {
const uint32_t count = entry.builder->non_null_rows();
statistics[entry.bucket].doc_value_column_non_null_size[*entry.path] = count;
result->statistics.doc_value_column_non_null_size[*entry.path] += count;
}
result->binary_buckets.reserve(options.doc_bucket_count);
for (uint32_t bucket = 0; bucket < options.doc_bucket_count; ++bucket) {
result->binary_buckets.push_back({.column = std::move(owners[bucket]),
.statistics = std::move(statistics[bucket])});
}
return Status::OK();
}
Status finish_ordinary(const DorisVector<VariantPathSelectionCandidate>& candidates,
const DorisVector<VariantPathBuilder*>& candidate_builders,
const DorisVector<DataTypePtr>& storage_types,
VariantShreddedColumns* result) {
DorisVector<size_t> all(candidate_builders.size());
std::iota(all.begin(), all.end(), 0);
RETURN_IF_ERROR(convert_typed_candidates(all, candidate_builders, storage_types));
const VariantPathSelection selection = select_variant_paths(
candidates, options.max_subcolumns_count, options.typed_paths_to_sparse);
publish_root(result);
RETURN_IF_ERROR(publish_materialized(selection, candidate_builders, result));
RETURN_IF_ERROR(publish_sparse(selection, candidate_builders, result));
return Status::OK();
}
Status finish_doc(const DorisVector<VariantPathBuilder*>& candidate_builders,
const DorisVector<DataTypePtr>& storage_types,
VariantShreddedColumns* result) {
DorisVector<DocPlan> doc_plan;
RETURN_IF_ERROR(build_doc_plan(candidate_builders, doc_plan));
RETURN_IF_ERROR(publish_doc(doc_plan, result));
VariantPathSelection selection;
if (rows >= options.doc_materialization_min_rows) {
selection.materialized.reserve(doc_plan.size());
for (const DocPlan& entry : doc_plan) {
selection.materialized.push_back(entry.candidate_index);
}
RETURN_IF_ERROR(convert_typed_candidates(selection.materialized, candidate_builders,
storage_types));
}
publish_root(result);
return publish_materialized(selection, candidate_builders, result);
}
Status finish_impl(VariantShreddedColumns* result) {
RETURN_IF_ERROR(complete_builder_rows(rows));
DorisVector<VariantPathSelectionCandidate> candidates;
DorisVector<VariantPathBuilder*> candidate_builders;
DorisVector<DataTypePtr> storage_types;
RETURN_IF_ERROR(
prepare_logical_candidates(&candidates, &candidate_builders, &storage_types));
if (options.physical_layout == VariantShredderPhysicalLayout::DOC) {
RETURN_IF_ERROR(finish_doc(candidate_builders, storage_types, result));
} else {
RETURN_IF_ERROR(finish_ordinary(candidates, candidate_builders, storage_types, result));
}
return Status::OK();
}
VariantShredderOptions options;
State state = State::COLLECTING;
Status failure;
size_t rows = 0;
std::unordered_map<PathInData, PathIndex, PathInData::Hash> path_indices = {{PathInData(), 0}};
DorisVector<PathState> paths;
ColumnString::MutablePtr root_values = ColumnString::create();
JsonbWriter root_writer;
#if defined(BE_TEST) && !defined(BE_BENCHMARK)
size_t binary_cells_per_chunk = MAX_BINARY_CELLS_PER_CHUNK;
mutable size_t binary_chunk_count = 0;
#endif
};
VariantShredder::VariantShredder(VariantShredderOptions options)
: _impl(std::make_unique<Impl>(std::move(options))) {}
VariantShredder::~VariantShredder() = default;
VariantShredder::VariantShredder(VariantShredder&&) noexcept = default;
VariantShredder& VariantShredder::operator=(VariantShredder&&) noexcept = default;
Status VariantShredder::append(const ColumnVariantV2::ReadView& view, size_t begin, size_t length,
std::span<const uint8_t> outer_nulls) {
RETURN_IF_ERROR(_impl->require_collecting());
if (view.is_typed()) {
return _impl->fail(Status::InvalidArgument(
"Variant shredder requires encoded E-state input; caller must ensure_encoded"));
}
if (begin > view.size() || length > view.size() - begin) {
return _impl->fail(
Status::InvalidArgument("Variant shredder range [{}, {}) exceeds input size {}",
begin, begin + length, view.size()));
}
if (!outer_nulls.empty() && outer_nulls.size() != length) {
return _impl->fail(
Status::InvalidArgument("Variant shredder outer-null span has {} rows, expected {}",
outer_nulls.size(), length));
}
if (length > std::numeric_limits<size_t>::max() - _impl->rows) {
return _impl->fail(Status::InvalidArgument("Variant shredder row count overflows size_t"));
}
try {
// Dictionaries are usually few per batch. Borrow each dictionary for this append and
// cache only the parent+field transitions that are actually traversed; no metadata bytes
// or metadata-specific ids survive the call.
DorisVector<Impl::MetadataPathCache> metadata_caches;
metadata_caches.reserve(view.metadata_count());
for (size_t metadata_index = 0; metadata_index < view.metadata_count(); ++metadata_index) {
metadata_caches.emplace_back(view.metadata_at(static_cast<uint32_t>(metadata_index)));
}
for (size_t offset = 0; offset < length; ++offset) {
const bool outer_null = !outer_nulls.empty() && outer_nulls[offset] != 0;
if (outer_null) {
_impl->append_default_root();
++_impl->rows;
continue;
}
const size_t input_row = begin + offset;
const uint32_t metadata_index = view.metadata_id_at(input_row);
if (metadata_index >= metadata_caches.size()) {
return _impl->fail(
Status::Corruption("Variant row {} metadata index {} exceeds {} entries",
input_row, metadata_index, metadata_caches.size()));
}
VariantRef value = view.value_at(input_row);
Status status = _impl->append_root(value);
if (!status.ok()) {
return _impl->fail(std::move(status));
}
if (value.basic_type() == VariantBasicType::OBJECT) {
status = _impl->visit(value, metadata_caches[metadata_index], 0, _impl->rows);
if (!status.ok()) {
return _impl->fail(std::move(status));
}
}
++_impl->rows;
}
return Status::OK();
} catch (const Exception& exception) {
return _impl->fail(exception.to_status());
}
}
Status VariantShredder::finish(VariantShreddedColumns* output) {
RETURN_IF_ERROR(_impl->require_collecting());
if (output == nullptr) {
return _impl->fail(Status::InvalidArgument("Variant shredder output must not be null"));
}
try {
VariantShreddedColumns result;
Status status = _impl->finish_impl(&result);
if (!status.ok()) {
return _impl->fail(std::move(status));
}
*output = std::move(result);
_impl->state = Impl::State::FINISHED;
return Status::OK();
} catch (const Exception& exception) {
return _impl->fail(exception.to_status());
}
}
size_t VariantShredder::byte_size() const {
size_t size = sizeof(Impl);
size += _impl->path_indices.bucket_count() * sizeof(void*);
for (const auto& [path, index] : _impl->path_indices) {
static_cast<void>(index);
size += sizeof(std::pair<const PathInData, Impl::PathIndex>) + path_allocated_bytes(path);
}
size += _impl->paths.capacity() * sizeof(Impl::PathState);
for (const Impl::PathState& path : _impl->paths) {
size += path_allocated_bytes(path.path);
if (path.builder.has_value()) {
size += path.builder->byte_size();
}
}
if (_impl->root_values) {
size += _impl->root_values->allocated_bytes();
}
size += sizeof(JsonbOutStream) + _impl->root_writer.getOutput()->allocated_bytes();
return size;
}
#if defined(BE_TEST) && !defined(BE_BENCHMARK)
size_t VariantShredder::TestAccess::binary_chunk_count(const VariantShredder& shredder) {
return shredder._impl->binary_chunk_count;
}
void VariantShredder::TestAccess::set_binary_cells_per_chunk(VariantShredder& shredder,
size_t cells) {
DORIS_CHECK(cells > 0);
shredder._impl->binary_cells_per_chunk = cells;
}
#endif
} // namespace doris::segment_v2