This is an automated email from the ASF dual-hosted git repository.
eldenmoon 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 1de2fd71be1 [refactor](variant) Sync NestedGroup provider interface
and reader guards (#60930)
1de2fd71be1 is described below
commit 1de2fd71be1e634abac2584ea47c96474354ba24
Author: lihangyu <[email protected]>
AuthorDate: Wed Mar 4 19:36:57 2026 +0800
[refactor](variant) Sync NestedGroup provider interface and reader guards
(#60930)
Add more interface and code for NestedGroup
---
be/src/common/config.cpp | 1 +
be/src/common/config.h | 4 +
.../rowset/segment_v2/variant/nested_group_path.h | 4 +
.../segment_v2/variant/nested_group_provider.cpp | 24 +++
.../segment_v2/variant/nested_group_provider.h | 11 ++
.../segment_v2/variant/nested_group_reader.h | 46 +++++
.../variant/nested_group_routing_plan.cpp | 186 +++++++++++++++++++
.../segment_v2/variant/nested_group_routing_plan.h | 82 +++++++++
.../segment_v2/variant/variant_column_reader.cpp | 89 +++++++--
.../segment_v2/variant/variant_column_reader.h | 26 ++-
.../variant/variant_column_writer_impl.cpp | 202 ++++++++++++++-------
.../variant/variant_column_writer_impl.h | 3 +
be/src/vec/columns/column_variant.cpp | 83 ++++++++-
13 files changed, 661 insertions(+), 100 deletions(-)
diff --git a/be/src/common/config.cpp b/be/src/common/config.cpp
index 36d465d2c77..29ad33ca3ef 100644
--- a/be/src/common/config.cpp
+++ b/be/src/common/config.cpp
@@ -1156,6 +1156,7 @@ DEFINE_mBool(enable_variant_doc_sparse_write_subcolumns,
"true");
// Reserved for future use when NestedGroup expansion moves to storage layer
// Deeper arrays will be stored as JSONB
DEFINE_mInt32(variant_nested_group_max_depth, "3");
+DEFINE_mBool(variant_nested_group_discard_scalar_on_conflict, "true");
DEFINE_Validator(variant_max_json_key_length,
[](const int config) -> bool { return config > 0 && config <=
65535; });
diff --git a/be/src/common/config.h b/be/src/common/config.h
index c1df9a11acb..e2e494af8a9 100644
--- a/be/src/common/config.h
+++ b/be/src/common/config.h
@@ -1418,6 +1418,10 @@
DECLARE_mBool(enable_variant_doc_sparse_write_subcolumns);
// Maximum depth of nested arrays to track with NestedGroup
// Reserved for future use when NestedGroup expansion moves to storage layer
DECLARE_mInt32(variant_nested_group_max_depth);
+// When true, discard scalar data that conflicts with NestedGroup array<object>
+// data at the same path. This simplifies compaction by always prioritizing
+// nested structure over scalar. When false, report an error on conflict.
+DECLARE_mBool(variant_nested_group_discard_scalar_on_conflict);
DECLARE_mBool(enable_merge_on_write_correctness_check);
// USED FOR DEBUGING
diff --git a/be/src/olap/rowset/segment_v2/variant/nested_group_path.h
b/be/src/olap/rowset/segment_v2/variant/nested_group_path.h
index a27dc4038d6..5c90d1441ad 100644
--- a/be/src/olap/rowset/segment_v2/variant/nested_group_path.h
+++ b/be/src/olap/rowset/segment_v2/variant/nested_group_path.h
@@ -26,6 +26,10 @@ inline constexpr std::string_view kNestedGroupMarker =
"__D0_ng__";
inline constexpr std::string_view kRootNestedGroupPath = "__D0_root__";
inline constexpr std::string_view kNestedGroupOffsetsSuffix = ".__offsets";
+inline bool is_root_nested_group_path(std::string_view path) {
+ return path == kRootNestedGroupPath;
+}
+
inline bool ends_with(std::string_view value, std::string_view suffix) {
return value.size() >= suffix.size() &&
value.compare(value.size() - suffix.size(), suffix.size(), suffix)
== 0;
diff --git a/be/src/olap/rowset/segment_v2/variant/nested_group_provider.cpp
b/be/src/olap/rowset/segment_v2/variant/nested_group_provider.cpp
index 9b664810e3a..91ffb3a2efb 100644
--- a/be/src/olap/rowset/segment_v2/variant/nested_group_provider.cpp
+++ b/be/src/olap/rowset/segment_v2/variant/nested_group_provider.cpp
@@ -19,6 +19,8 @@
#include <string>
+#include "olap/rowset/segment_v2/variant/nested_group_routing_plan.h"
+
namespace doris::segment_v2 {
namespace {
@@ -95,6 +97,17 @@ public:
return Status::NotSupported("NestedGroup element access is not
available in this build");
}
+ Status create_root_merge_iterator(ColumnIteratorUPtr base_iterator,
+ const NestedGroupReaders& /*readers*/,
+ const StorageReadOptions* /*opt*/,
+ ColumnIteratorUPtr* out) override {
+ if (out == nullptr) {
+ return Status::InvalidArgument("out is null");
+ }
+ *out = std::move(base_iterator);
+ return Status::OK();
+ }
+
Status map_elements_to_parent_ords(const std::vector<const
NestedGroupReader*>& /*group_chain*/,
const ColumnIteratorOptions& /*opts*/,
const roaring::Roaring&
/*element_bitmap*/,
@@ -112,6 +125,17 @@ NestedGroupPathMatch find_in_nested_groups(const
NestedGroupReaders& readers,
return {};
}
+Status collect_nested_group_routing_paths_from_variant_jsonb(
+ const vectorized::ColumnVariant& /*variant*/,
std::vector<std::string>* out_ng_paths,
+ std::vector<std::string>* out_conflict_paths) {
+ if (out_ng_paths == nullptr || out_conflict_paths == nullptr) {
+ return Status::InvalidArgument("out_ng_paths or out_conflict_paths is
null");
+ }
+ out_ng_paths->clear();
+ out_conflict_paths->clear();
+ return Status::OK();
+}
+
std::unique_ptr<NestedGroupWriteProvider> create_nested_group_write_provider()
{
return std::make_unique<DefaultNestedGroupWriteProvider>();
}
diff --git a/be/src/olap/rowset/segment_v2/variant/nested_group_provider.h
b/be/src/olap/rowset/segment_v2/variant/nested_group_provider.h
index c0877be2dd1..b9cef3c2aa1 100644
--- a/be/src/olap/rowset/segment_v2/variant/nested_group_provider.h
+++ b/be/src/olap/rowset/segment_v2/variant/nested_group_provider.h
@@ -23,11 +23,14 @@
#include <memory>
#include <optional>
#include <string>
+#include <unordered_map>
#include <unordered_set>
#include <vector>
#include "common/status.h"
#include "olap/rowset/segment_v2/column_reader.h"
+#include "olap/rowset/segment_v2/variant/nested_group_reader.h"
+#include "vec/columns/column.h"
#include "vec/data_types/data_type.h"
#include "vec/json/path_in_data.h"
@@ -214,6 +217,14 @@ public:
uint64_t* total_elements) const = 0;
// Map element-level bitmap to row-level bitmap through the nested group
chain.
+ // Create an iterator that wraps |base_iterator| with root-level NG merge
logic.
+ // For root variant reads, top-level NestedGroup arrays must be merged
back into
+ // the reconstructed variant. CE no-op returns base_iterator unchanged.
+ virtual Status create_root_merge_iterator(ColumnIteratorUPtr base_iterator,
+ const NestedGroupReaders&
readers,
+ const StorageReadOptions* opt,
+ ColumnIteratorUPtr* out) = 0;
+
virtual Status map_elements_to_parent_ords(
const std::vector<const NestedGroupReader*>& group_chain,
const ColumnIteratorOptions& opts, const roaring::Roaring&
element_bitmap,
diff --git a/be/src/olap/rowset/segment_v2/variant/nested_group_reader.h
b/be/src/olap/rowset/segment_v2/variant/nested_group_reader.h
new file mode 100644
index 00000000000..b1b1d62b03f
--- /dev/null
+++ b/be/src/olap/rowset/segment_v2/variant/nested_group_reader.h
@@ -0,0 +1,46 @@
+// 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.
+
+#pragma once
+
+#include <cstddef>
+#include <memory>
+#include <string>
+#include <unordered_map>
+
+namespace doris::segment_v2 {
+
+class ColumnReader;
+class NestedOffsetsMappingIndex;
+
+struct NestedGroupReader;
+using NestedGroupReaders = std::unordered_map<std::string,
std::unique_ptr<NestedGroupReader>>;
+
+// Holds readers for a single NestedGroup (offsets + child columns + nested
groups)
+struct NestedGroupReader {
+ std::string array_path;
+ size_t depth = 1; // Nesting depth (1 = first level)
+ std::shared_ptr<ColumnReader> offsets_reader;
+ std::shared_ptr<NestedOffsetsMappingIndex> offsets_mapping_index;
+ std::unordered_map<std::string, std::shared_ptr<ColumnReader>>
child_readers;
+ // Nested groups within this group (for multi-level nesting)
+ NestedGroupReaders nested_group_readers;
+
+ bool is_valid() const { return offsets_reader != nullptr; }
+};
+
+} // namespace doris::segment_v2
diff --git
a/be/src/olap/rowset/segment_v2/variant/nested_group_routing_plan.cpp
b/be/src/olap/rowset/segment_v2/variant/nested_group_routing_plan.cpp
new file mode 100644
index 00000000000..9ab078d17a5
--- /dev/null
+++ b/be/src/olap/rowset/segment_v2/variant/nested_group_routing_plan.cpp
@@ -0,0 +1,186 @@
+// 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 "olap/rowset/segment_v2/variant/nested_group_routing_plan.h"
+
+#include <algorithm>
+#include <string_view>
+#include <unordered_set>
+
+#include "common/config.h"
+#include "olap/rowset/segment_v2/variant/nested_group_path.h"
+#include "olap/rowset/segment_v2/variant/nested_group_provider.h"
+#include "vec/columns/column_variant.h"
+#include "vec/common/variant_util.h"
+#include "vec/data_types/data_type_nullable.h"
+#include "vec/json/path_in_data.h"
+
+namespace doris::segment_v2 {
+
+// --------------------------------------------------------------------------
+// Path prefix utilities
+// --------------------------------------------------------------------------
+
+static bool _path_has_prefix(std::string_view path, std::string_view prefix) {
+ return path == prefix ||
+ (path.size() > prefix.size() && path.starts_with(prefix) &&
path[prefix.size()] == '.');
+}
+
+static bool _is_excluded_by_prefixes(std::string_view path,
+ const std::vector<std::string>&
excluded_prefixes,
+ bool exclude_all_paths) {
+ if (exclude_all_paths) {
+ return true;
+ }
+ for (const auto& prefix : excluded_prefixes) {
+ if (_path_has_prefix(path, prefix)) {
+ return true;
+ }
+ }
+ return false;
+}
+
+bool NestedGroupRoutingPlan::is_excluded_subcolumn(const std::string& path)
const {
+ return _is_excluded_by_prefixes(path, ng_only_prefixes,
exclude_all_subcolumns);
+}
+
+// --------------------------------------------------------------------------
+// Routing plan builder helpers
+// --------------------------------------------------------------------------
+
+static std::vector<std::string> _compact_prefixes(std::vector<std::string>
prefixes) {
+ std::sort(prefixes.begin(), prefixes.end());
+ prefixes.erase(std::unique(prefixes.begin(), prefixes.end()),
prefixes.end());
+ std::sort(prefixes.begin(), prefixes.end(), [](const std::string& lhs,
const std::string& rhs) {
+ return lhs.size() < rhs.size();
+ });
+ std::vector<std::string> compacted;
+ for (auto& p : prefixes) {
+ bool redundant = false;
+ for (const auto& c : compacted) {
+ if (_path_has_prefix(p, c)) {
+ redundant = true;
+ break;
+ }
+ }
+ if (!redundant) {
+ compacted.push_back(std::move(p));
+ }
+ }
+ return compacted;
+}
+
+static bool _is_array_variant_type(const vectorized::DataTypePtr& type) {
+ if (!type) return false;
+ auto base_type = vectorized::variant_util::get_base_type_of_array(type);
+ return base_type != nullptr &&
vectorized::remove_nullable(base_type)->get_primitive_type() ==
+ PrimitiveType::TYPE_VARIANT;
+}
+// Routing builder: only NON-conflict NG paths go into ng_only_prefixes.
+// Conflict paths are NOT excluded from subcolumn writes so compaction/write
+// can still preserve conflict-path payload in regular subcolumns.
+static Status _build_ng_routing_from_columns(
+ const vectorized::ColumnVariant& variant,
+ const std::vector<std::string>& ng_candidate_paths,
+ const std::vector<std::string>& conflict_candidate_paths,
+ std::vector<std::string>* ng_only_prefixes, bool*
exclude_all_subcolumns,
+ NestedGroupConflictPolicy* conflict_policy, bool* has_conflict_paths) {
+ if (ng_only_prefixes == nullptr || exclude_all_subcolumns == nullptr ||
+ conflict_policy == nullptr || has_conflict_paths == nullptr) {
+ return Status::InvalidArgument("output argument is null");
+ }
+
+ ng_only_prefixes->clear();
+ *exclude_all_subcolumns = false;
+ *conflict_policy = get_nested_group_conflict_policy();
+ *has_conflict_paths = !conflict_candidate_paths.empty();
+
+ if (ng_candidate_paths.empty()) {
+ return Status::OK();
+ }
+
+ // Under ERROR policy, reject any conflicts immediately.
+ if (*conflict_policy == NestedGroupConflictPolicy::ERROR &&
!conflict_candidate_paths.empty()) {
+ std::string paths_str;
+ for (const auto& p : conflict_candidate_paths) {
+ if (!paths_str.empty()) paths_str += ", ";
+ paths_str += p;
+ }
+ return Status::InvalidArgument("NestedGroup conflict detected
(policy=ERROR) at paths: {}",
+ paths_str);
+ }
+
+ // Build the conflict set for quick lookup.
+ std::unordered_set<std::string>
conflict_set(conflict_candidate_paths.begin(),
+
conflict_candidate_paths.end());
+
+ // Log conflict paths under DISCARD_SCALAR policy.
+ if (!conflict_candidate_paths.empty()) {
+ for (const auto& p : conflict_candidate_paths) {
+ LOG(WARNING) << "NestedGroup conflict at path '" << p
+ << "': policy=DISCARD_SCALAR (prefer nested data;
scalar payload on this "
+ "path may be dropped). The path remains in regular
subcolumns for "
+ "cross-segment compatibility.";
+ }
+ }
+
+ // Only NON-conflict NG paths go into ng_only_prefixes.
+ // Conflict paths are kept as regular subcolumns to avoid data loss.
+ for (const auto& path : ng_candidate_paths) {
+ if (conflict_set.contains(path)) {
+ continue; // Skip conflict paths — they stay as regular subcolumns.
+ }
+ ng_only_prefixes->emplace_back(path);
+ // For root path that is purely array<variant>, exclude all subcolumns.
+ if (is_root_nested_group_path(path) &&
_is_array_variant_type(variant.get_root_type())) {
+ *exclude_all_subcolumns = true;
+ }
+ }
+
+ *ng_only_prefixes = _compact_prefixes(std::move(*ng_only_prefixes));
+ return Status::OK();
+}
+
+// --------------------------------------------------------------------------
+// Public API
+// --------------------------------------------------------------------------
+
+Status build_nested_group_routing_plan(const vectorized::ColumnVariant&
variant,
+ NestedGroupRoutingPlan* plan) {
+ if (plan == nullptr) {
+ return Status::InvalidArgument("plan is null");
+ }
+ *plan = NestedGroupRoutingPlan {};
+
+ std::vector<std::string> ng_candidate_paths;
+ std::vector<std::string> conflict_candidate_paths;
+ RETURN_IF_ERROR(collect_nested_group_routing_paths_from_variant_jsonb(
+ variant, &ng_candidate_paths, &conflict_candidate_paths));
+ RETURN_IF_ERROR(_build_ng_routing_from_columns(
+ variant, ng_candidate_paths, conflict_candidate_paths,
&plan->ng_only_prefixes,
+ &plan->exclude_all_subcolumns, &plan->conflict_policy,
&plan->has_conflict_paths));
+ return Status::OK();
+}
+
+NestedGroupConflictPolicy get_nested_group_conflict_policy() {
+ if (config::variant_nested_group_discard_scalar_on_conflict) {
+ return NestedGroupConflictPolicy::DISCARD_SCALAR;
+ }
+ return NestedGroupConflictPolicy::ERROR;
+}
+
+} // namespace doris::segment_v2
diff --git a/be/src/olap/rowset/segment_v2/variant/nested_group_routing_plan.h
b/be/src/olap/rowset/segment_v2/variant/nested_group_routing_plan.h
new file mode 100644
index 00000000000..482099c1df5
--- /dev/null
+++ b/be/src/olap/rowset/segment_v2/variant/nested_group_routing_plan.h
@@ -0,0 +1,82 @@
+// 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.
+
+#pragma once
+
+#include <string>
+#include <vector>
+
+#include "common/status.h"
+
+namespace doris::vectorized {
+class ColumnVariant;
+} // namespace doris::vectorized
+
+namespace doris::segment_v2 {
+
+// Policy for handling NestedGroup vs scalar conflicts.
+// When the same path has both array<object> and scalar data:
+// DISCARD_SCALAR: silently drop scalar data, keep nested data (default)
+// ERROR: report an error when conflict is detected
+enum class NestedGroupConflictPolicy {
+ DISCARD_SCALAR = 0,
+ ERROR = 1,
+};
+
+// Routing plan for NestedGroup write path. Controls which subcolumn paths
+// are excluded from regular writes because they are handled by NestedGroup.
+//
+// Simplified model:
+// - Only NON-conflict NG paths go into ng_only_prefixes.
+// - Conflict paths stay in regular subcolumns (not excluded), so routing can
+// remain compatible with cross-segment compaction where NG payload may
+// become non-JSONB after merge.
+struct NestedGroupRoutingPlan {
+ bool exclude_all_subcolumns = false;
+ bool has_conflict_paths = false;
+ std::vector<std::string> ng_only_prefixes;
+ NestedGroupConflictPolicy conflict_policy =
NestedGroupConflictPolicy::DISCARD_SCALAR;
+
+ // Returns true if |path| should be excluded from regular subcolumn writes.
+ bool is_excluded_subcolumn(const std::string& path) const;
+
+ // Returns true if the plan has any active exclusions (NG paths found).
+ bool has_exclusions() const { return exclude_all_subcolumns ||
!ng_only_prefixes.empty(); }
+
+ // Returns true if root JSONB can be safely replaced with empty defaults.
+ // Only safe when there are NG exclusions AND no conflict paths.
+ // With conflicts, root JSONB may carry data needed by the NG provider.
+ bool can_remove_root_jsonb() const { return has_exclusions() &&
!has_conflict_paths; }
+};
+
+// Build NG routing plan from variant content. Scans the variant for
+// array<object> paths, detects conflicts, and populates the plan.
+Status build_nested_group_routing_plan(const vectorized::ColumnVariant&
variant,
+ NestedGroupRoutingPlan* plan);
+
+// Collect NG routing metadata from variant content:
+// - out_ng_paths: all NG candidate paths
+// - out_conflict_paths: NG paths that have ARRAY<OBJECT> vs non-array
structural conflicts
+// Both outputs are de-duplicated and sorted.
+Status collect_nested_group_routing_paths_from_variant_jsonb(
+ const vectorized::ColumnVariant& variant, std::vector<std::string>*
out_ng_paths,
+ std::vector<std::string>* out_conflict_paths);
+
+// Get the current global conflict policy (driven by config).
+NestedGroupConflictPolicy get_nested_group_conflict_policy();
+
+} // namespace doris::segment_v2
diff --git a/be/src/olap/rowset/segment_v2/variant/variant_column_reader.cpp
b/be/src/olap/rowset/segment_v2/variant/variant_column_reader.cpp
index 6524a22d902..72748a79895 100644
--- a/be/src/olap/rowset/segment_v2/variant/variant_column_reader.cpp
+++ b/be/src/olap/rowset/segment_v2/variant/variant_column_reader.cpp
@@ -25,6 +25,7 @@
#include <roaring/roaring.hh>
#include <string>
#include <utility>
+#include <vector>
#include "binary_column_extract_iterator.h"
#include "binary_column_reader.h"
@@ -59,6 +60,15 @@ namespace doris::segment_v2 {
#include "common/compile_check_begin.h"
+namespace {
+
+bool is_compaction_or_checksum_reader(const StorageReadOptions* opts) {
+ return opts != nullptr &&
(ColumnReader::is_compaction_reader_type(opts->io_ctx.reader_type) ||
+ opts->io_ctx.reader_type ==
ReaderType::READER_CHECKSUM);
+}
+
+} // namespace
+
const SubcolumnColumnMetaInfo::Node*
VariantColumnReader::get_subcolumn_meta_by_path(
const vectorized::PathInData& relative_path) const {
std::shared_lock<std::shared_mutex> lock(_subcolumns_meta_mutex);
@@ -316,10 +326,17 @@ Status VariantColumnReader::_build_read_plan_flat_leaves(
std::shared_lock<std::shared_mutex> lock(_subcolumns_meta_mutex);
DCHECK(opts != nullptr);
+ int32_t col_uid =
+ target_col.unique_id() >= 0 ? target_col.unique_id() :
target_col.parent_unique_id();
auto relative_path = target_col.path_info_ptr()->copy_pop_front();
- // compaction need to read flat leaves nodes data to prevent from
amplification
const auto* node =
target_col.has_path_info() ?
_subcolumns_meta_info->find_leaf(relative_path) : nullptr;
+ if (!relative_path.empty() && _can_use_nested_group_read_path() &&
+ _try_fill_nested_group_plan(plan, target_col, opts, col_uid,
relative_path)) {
+ return Status::OK();
+ }
+
+ // compaction need to read flat leaves nodes data to prevent from
amplification
if (!node) {
// Handle sparse column reads in flat-leaf compaction.
const std::string rel = relative_path.get_path();
@@ -458,8 +475,12 @@ bool VariantColumnReader::_need_read_flat_leaves(const
StorageReadOptions* opts)
return opts != nullptr && opts->tablet_schema != nullptr &&
std::ranges::any_of(opts->tablet_schema->columns(),
[](const auto& column) { return
column->is_extracted_column(); }) &&
- (is_compaction_reader_type(opts->io_ctx.reader_type) ||
- opts->io_ctx.reader_type == ReaderType::READER_CHECKSUM);
+ is_compaction_or_checksum_reader(opts);
+}
+
+bool VariantColumnReader::_can_use_nested_group_read_path() const {
+ return _nested_group_read_provider != nullptr &&
+ _nested_group_read_provider->should_enable_nested_group_read_path();
}
Status VariantColumnReader::_validate_access_paths_debug(
@@ -587,21 +608,11 @@ Status VariantColumnReader::_validate_access_paths_debug(
return Status::OK();
}
-bool VariantColumnReader::_try_build_nested_group_plan(
+bool VariantColumnReader::_try_fill_nested_group_plan(
ReadPlan* plan, const TabletColumn& target_col, const
StorageReadOptions* opt,
int32_t col_uid, const vectorized::PathInData& relative_path) const {
- if (_nested_group_read_provider == nullptr ||
- !_nested_group_read_provider->should_enable_nested_group_read_path()) {
- return false;
- }
- if (_need_read_flat_leaves(opt)) {
- return false;
- }
- // compaction skip read NestedGroup
- if (is_compaction_reader_type(opt->io_ctx.reader_type) ||
- opt->io_ctx.reader_type == ReaderType::READER_CHECKSUM) {
- return false;
- }
+ DCHECK(_nested_group_read_provider != nullptr);
+
bool is_whole = false;
vectorized::DataTypePtr out_type;
vectorized::PathInData out_relative_path;
@@ -626,6 +637,26 @@ bool VariantColumnReader::_try_build_nested_group_plan(
return true;
}
+bool VariantColumnReader::_try_build_nested_group_plan(
+ ReadPlan* plan, const TabletColumn& target_col, const
StorageReadOptions* opt,
+ int32_t col_uid, const vectorized::PathInData& relative_path) const {
+ const bool is_compaction_or_checksum =
is_compaction_or_checksum_reader(opt);
+
+ // Root path in compaction/checksum must reconstruct full Variant rows for
re-write.
+ // Query root reads can still use NestedGroup whole read for top-level
array shape.
+ if (relative_path.empty() && is_compaction_or_checksum) {
+ return false;
+ }
+ if (!_can_use_nested_group_read_path()) {
+ return false;
+ }
+
+ if (_need_read_flat_leaves(opt)) {
+ return false;
+ }
+ return _try_fill_nested_group_plan(plan, target_col, opt, col_uid,
relative_path);
+}
+
Status VariantColumnReader::_try_build_leaf_plan(ReadPlan* plan, int32_t
col_uid,
const vectorized::PathInData&
relative_path,
const
SubcolumnColumnMetaInfo::Node* node,
@@ -705,6 +736,12 @@ Status VariantColumnReader::_build_read_plan(ReadPlan*
plan, const TabletColumn&
node = _subcolumns_meta_info->find_exact(relative_path);
}
+ // NestedGroup path resolution must happen before doc/sparse/hierarchical
fallbacks.
+ // This keeps query/compaction behavior consistent for array<object> paths.
+ if (_try_build_nested_group_plan(plan, target_col, opt, col_uid,
relative_path)) {
+ return Status::OK();
+ }
+
// read root: from doc value column
if (root->path == relative_path &&
_statistics->has_doc_value_column_non_null_size()) {
plan->kind = ReadKind::HIERARCHICAL_DOC;
@@ -838,9 +875,23 @@ Status VariantColumnReader::_create_iterator_from_plan(
case ReadKind::HIERARCHICAL: {
int32_t col_uid = target_col.unique_id() >= 0 ? target_col.unique_id()
:
target_col.parent_unique_id();
+ ColumnIteratorUPtr base_iterator;
RETURN_IF_ERROR(_create_hierarchical_reader(
- iterator, col_uid, plan.relative_path, plan.node, plan.root,
column_reader_cache,
- opt->stats,
HierarchicalDataIterator::ReadType::SUBCOLUMNS_AND_SPARSE));
+ &base_iterator, col_uid, plan.relative_path, plan.node,
plan.root,
+ column_reader_cache, opt->stats,
+ HierarchicalDataIterator::ReadType::SUBCOLUMNS_AND_SPARSE));
+
+ // Root variant reconstruction needs to merge top-level NestedGroup
arrays, because NG leaf
+ // columns are not row-aligned and are skipped by the generic
hierarchical reader.
+ if (plan.relative_path.empty() && _nested_group_read_provider !=
nullptr &&
+ !_nested_group_readers.empty()) {
+ ColumnIteratorUPtr merged_iterator;
+
RETURN_IF_ERROR(_nested_group_read_provider->create_root_merge_iterator(
+ std::move(base_iterator), _nested_group_readers, opt,
&merged_iterator));
+ *iterator = std::move(merged_iterator);
+ return Status::OK();
+ }
+ *iterator = std::move(base_iterator);
return Status::OK();
}
case ReadKind::LEAF: {
@@ -1167,7 +1218,7 @@ Status VariantColumnReader::init(const
ColumnReaderOptions& opts, ColumnMetaAcce
// NestedGroup initialization is provider-driven. Disabled providers keep
fallback behavior,
// while enabled providers populate nested group readers from segment
footer.
- if (_nested_group_read_provider->should_enable_nested_group_read_path()) {
+ if (_can_use_nested_group_read_path()) {
RETURN_IF_ERROR(_nested_group_read_provider->init_readers(opts,
footer, file_reader,
num_rows,
_nested_group_readers));
}
diff --git a/be/src/olap/rowset/segment_v2/variant/variant_column_reader.h
b/be/src/olap/rowset/segment_v2/variant/variant_column_reader.h
index 1318fc9402c..038764684c8 100644
--- a/be/src/olap/rowset/segment_v2/variant/variant_column_reader.h
+++ b/be/src/olap/rowset/segment_v2/variant/variant_column_reader.h
@@ -29,6 +29,7 @@
#include <vector>
#include "nested_group_provider.h"
+#include "nested_group_reader.h"
#include "olap/rowset/segment_v2/column_reader.h"
#include "olap/rowset/segment_v2/indexed_column_reader.h"
#include "olap/rowset/segment_v2/page_handle.h"
@@ -179,21 +180,7 @@ using BinaryColumnCacheSPtr =
std::shared_ptr<BinaryColumnCache>;
using PathToBinaryColumnCache = std::unordered_map<std::string,
BinaryColumnCacheSPtr>;
using PathToBinaryColumnCacheUPtr = std::unique_ptr<PathToBinaryColumnCache>;
-// Forward declaration
-struct NestedGroupReader;
-
-// Holds readers for a single NestedGroup (offsets + child columns + nested
groups)
-struct NestedGroupReader {
- std::string array_path;
- size_t depth = 1; // Nesting depth (1 = first level)
- std::shared_ptr<ColumnReader> offsets_reader;
- std::shared_ptr<NestedOffsetsMappingIndex> offsets_mapping_index;
- std::unordered_map<std::string, std::shared_ptr<ColumnReader>>
child_readers;
- // Nested groups within this group (for multi-level nesting)
- NestedGroupReaders nested_group_readers;
-
- bool is_valid() const { return offsets_reader != nullptr; }
-};
+// NestedGroupReader is defined in nested_group_reader.h
class VariantColumnReader : public ColumnReader {
public:
@@ -219,6 +206,11 @@ public:
const VariantStatistics* get_stats() const { return _statistics.get(); }
+ // Expose raw root column reader for test assertions (e.g., dedup checks).
+ const std::shared_ptr<ColumnReader>& get_root_column_reader() const {
+ return _root_column_reader;
+ }
+
int64_t get_metadata_size() const override;
// Return shared_ptr to ensure the lifetime of TabletIndex objects
@@ -361,9 +353,13 @@ private:
PathToBinaryColumnCache* binary_column_cache_ptr);
static bool _need_read_flat_leaves(const StorageReadOptions* opts);
+ bool _can_use_nested_group_read_path() const;
Status _validate_access_paths_debug(const TabletColumn& target_col,
const StorageReadOptions* opt, int32_t
col_uid,
const vectorized::PathInData&
relative_path) const;
+ bool _try_fill_nested_group_plan(ReadPlan* plan, const TabletColumn&
target_col,
+ const StorageReadOptions* opt, int32_t
col_uid,
+ const vectorized::PathInData&
relative_path) const;
bool _try_build_nested_group_plan(ReadPlan* plan, const TabletColumn&
target_col,
const StorageReadOptions* opt, int32_t
col_uid,
const vectorized::PathInData&
relative_path) const;
diff --git
a/be/src/olap/rowset/segment_v2/variant/variant_column_writer_impl.cpp
b/be/src/olap/rowset/segment_v2/variant/variant_column_writer_impl.cpp
index d50fc75e434..f5de9c1e651 100644
--- a/be/src/olap/rowset/segment_v2/variant/variant_column_writer_impl.cpp
+++ b/be/src/olap/rowset/segment_v2/variant/variant_column_writer_impl.cpp
@@ -19,23 +19,41 @@
#include <gen_cpp/segment_v2.pb.h>
#include <algorithm>
+#include <iostream>
#include <memory>
+#include <string_view>
+#include <unordered_map>
+#include <unordered_set>
+#include "common/cast_set.h"
#include "common/status.h"
#include "olap/olap_common.h"
#include "olap/olap_define.h"
#include "olap/rowset/rowset_writer_context.h"
#include "olap/rowset/segment_v2/column_writer.h"
#include "olap/rowset/segment_v2/indexed_column_writer.h"
+#include "olap/rowset/segment_v2/variant/nested_group_path.h"
+#include "olap/rowset/segment_v2/variant/nested_group_routing_plan.h"
#include "olap/tablet_schema.h"
#include "olap/types.h"
+#include "runtime/runtime_state.h"
#include "vec/columns/column.h"
+#include "vec/columns/column_const.h"
+#include "vec/columns/column_map.h"
#include "vec/columns/column_nullable.h"
+#include "vec/columns/column_string.h"
#include "vec/columns/column_variant.h"
#include "vec/common/variant_util.h"
+#include "vec/core/block.h"
+#include "vec/core/column_with_type_and_name.h"
#include "vec/data_types/data_type.h"
#include "vec/data_types/data_type_factory.hpp"
+#include "vec/data_types/data_type_jsonb.h"
#include "vec/data_types/data_type_nullable.h"
+#include "vec/data_types/data_type_string.h"
+#include "vec/data_types/data_type_variant.h"
+#include "vec/exprs/function_context.h"
+#include "vec/functions/simple_function_factory.h"
#include "vec/json/path_in_data.h"
#include "vec/olap/olap_data_convertor.h"
@@ -678,11 +696,15 @@ Status UnifiedSparseColumnWriter::append_single_sparse(
size_t limit = parent_column.variant_max_sparse_column_statistics_size();
for (size_t i = 0; i != paths->size(); ++i) {
auto k = paths->get_data_at(i);
- if (auto it = path_counts.find(k); it != path_counts.end())
+ if (auto it = path_counts.find(k); it != path_counts.end()) {
++it->second;
- else if (path_counts.size() < limit)
+ } else if (path_counts.size() < limit) {
path_counts.emplace(k, 1);
+ }
}
+
+ // Build path frequency statistics with upper bound limit to avoid
+ // large memory and metadata size. Persist to meta for readers.
segment_v2::VariantStatistics sparse_stats;
for (const auto& [k, cnt] : path_counts) {
sparse_stats.sparse_column_non_null_size.emplace(k.to_string(),
static_cast<uint32_t>(cnt));
@@ -1064,6 +1086,24 @@ Status
VariantColumnWriterImpl::_process_root_column(vectorized::ColumnVariant*
auto& nullable_column =
assert_cast<vectorized::ColumnNullable&>(*ptr->get_root()->assume_mutable());
auto root_column = nullable_column.get_nested_column_ptr();
+
+ // Simplified dedup logic:
+ // If we have NG paths that cover the root data, replace root JSONB with
+ // empty defaults — the actual data lives in NG columns.
+ // Conflict scalar data is discarded per the conflict policy.
+ if (_nested_group_routing_plan.can_remove_root_jsonb()) {
+ const bool has_root_ng = std::ranges::any_of(
+ _nested_group_routing_plan.ng_only_prefixes,
+ [](const std::string& p) { return
is_root_nested_group_path(p); });
+ if (has_root_ng) {
+ // Replace with empty JSONB defaults — the actual data is in NG
columns.
+ auto bare_jsonb_type =
std::make_shared<vectorized::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);
+ }
+ }
+
// If the root variant is nullable, then update the root column null
column with the outer null column.
if (_tablet_column->is_nullable()) {
// use outer null column as final null column
@@ -1093,66 +1133,59 @@ Status
VariantColumnWriterImpl::_process_root_column(vectorized::ColumnVariant*
Status VariantColumnWriterImpl::_process_subcolumns(vectorized::ColumnVariant*
ptr,
vectorized::OlapBlockDataConvertor* converter,
size_t num_rows, int&
column_id) {
- // generate column info by entry info
- auto generate_column_info = [&](const auto& entry) {
- const std::string& column_name =
- _tablet_column->name_lower_case() + "." +
entry->path.get_path();
- const vectorized::DataTypePtr& final_data_type_from_object =
- entry->data.get_least_common_type();
+ auto generate_column_info = [&](const vectorized::PathInData&
relative_path,
+ const vectorized::DataTypePtr&
final_data_type) {
+ const std::string column_name =
+ _tablet_column->name_lower_case() + "." +
relative_path.get_path();
vectorized::PathInData full_path;
- if (entry->path.has_nested_part()) {
+ if (relative_path.has_nested_part()) {
vectorized::PathInDataBuilder full_path_builder;
full_path =
full_path_builder.append(_tablet_column->name_lower_case(), false)
- .append(entry->path.get_parts(), false)
+ .append(relative_path.get_parts(), false)
.build();
} else {
full_path = vectorized::PathInData(column_name);
}
- // set unique_id and parent_unique_id, will use unique_id to get
iterator correct
- auto column = vectorized::variant_util::get_column_by_type(
- final_data_type_from_object, column_name,
+ return vectorized::variant_util::get_column_by_type(
+ final_data_type, column_name,
vectorized::variant_util::ExtraInfo {
.unique_id = -1,
.parent_unique_id = _tablet_column->unique_id(),
.path_info = full_path});
- return column;
};
_subcolumns_indexes.resize(ptr->get_subcolumns().size());
- // convert sub column data from engine format to storage layer format
- // NOTE: We only keep up to variant_max_subcolumns_count as extracted
columns; others are externalized.
- for (const auto& entry :
-
vectorized::variant_util::get_sorted_subcolumns(ptr->get_subcolumns())) {
- const auto& least_common_type = entry->data.get_least_common_type();
- if (vectorized::variant_util::get_base_type_of_array(least_common_type)
- ->get_primitive_type() == PrimitiveType::INVALID_TYPE) {
- continue;
- }
- if (entry->path.empty()) {
- // already handled
- continue;
- }
- CHECK(entry->data.is_finalized());
- // create subcolumn writer if under limit; otherwise externalize
ColumnMetaPB via IndexedColumn
+ auto write_one_subcolumn =
+ [&](const std::string& current_path, const vectorized::PathInData&
relative_path,
+ const vectorized::DataTypePtr& current_type,
+ const vectorized::ColumnPtr& current_column, size_t
non_null_count,
+ bool check_storage_type, bool use_existing_subcolumn_info) ->
Status {
int current_column_id = column_id++;
+ if (_subcolumns_indexes.size() <= cast_set<size_t>(current_column_id))
{
+ _subcolumns_indexes.resize(cast_set<size_t>(current_column_id) +
1);
+ }
+
TabletColumn tablet_column;
- int64_t none_null_value_size = entry->data.get_non_null_value_size();
- vectorized::ColumnPtr current_column =
entry->data.get_finalized_column_ptr()->get_ptr();
- vectorized::DataTypePtr current_type =
entry->data.get_least_common_type();
- if (auto current_path = entry->path.get_path();
- _subcolumns_info.find(current_path) != _subcolumns_info.end()) {
- tablet_column = std::move(_subcolumns_info[current_path].column);
- _subcolumns_indexes[current_column_id] =
- std::move(_subcolumns_info[current_path].indexes);
- if (auto storage_type =
-
vectorized::DataTypeFactory::instance().create_data_type(tablet_column);
- !storage_type->equals(*current_type)) {
- return Status::InvalidArgument("Storage type {} is not equal
to current type {}",
- storage_type->get_name(),
current_type->get_name());
+ if (use_existing_subcolumn_info) {
+ if (auto it = _subcolumns_info.find(current_path); it !=
_subcolumns_info.end()) {
+ tablet_column = it->second.column;
+ _subcolumns_indexes[current_column_id] = it->second.indexes;
+ if (check_storage_type) {
+ auto storage_type =
+
vectorized::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(), current_path);
+ }
+ }
+ } else {
+ tablet_column = generate_column_info(relative_path,
current_type);
}
} else {
- tablet_column = generate_column_info(entry);
+ tablet_column = generate_column_info(relative_path, current_type);
}
+
ColumnWriterOptions opts;
opts.meta = _opts.footer->add_columns();
opts.index_file_writer = _opts.index_file_writer;
@@ -1171,14 +1204,49 @@ Status
VariantColumnWriterImpl::_process_subcolumns(vectorized::ColumnVariant* p
RETURN_IF_ERROR(_create_column_writer(
current_column_id, tablet_column,
_opts.rowset_ctx->tablet_schema,
_opts.index_file_writer, &writer,
_subcolumns_indexes[current_column_id], &opts,
- none_null_value_size, need_record_none_null_value_size));
+ non_null_count, need_record_none_null_value_size));
_subcolumn_writers.push_back(std::move(writer));
_subcolumn_opts.push_back(opts);
- _subcolumn_opts[current_column_id - 1].meta->set_num_rows(num_rows);
+ _subcolumn_opts.back().meta->set_num_rows(num_rows);
RETURN_IF_ERROR(convert_and_write_column(converter, tablet_column,
current_type,
-
_subcolumn_writers[current_column_id - 1].get(),
- current_column, ptr->rows(),
current_column_id));
+
_subcolumn_writers.back().get(), current_column,
+ ptr->rows(),
current_column_id));
+ return Status::OK();
+ };
+
+ // convert sub column data from engine format to storage layer format
+ // NOTE: We only keep up to variant_max_subcolumns_count as extracted
columns; others are externalized.
+ for (const auto& entry :
+
vectorized::variant_util::get_sorted_subcolumns(ptr->get_subcolumns())) {
+ if (entry->path.empty()) {
+ continue;
+ }
+ const auto& least_common_type = entry->data.get_least_common_type();
+ if (least_common_type == nullptr) {
+ continue;
+ }
+ auto base_type =
vectorized::variant_util::get_base_type_of_array(least_common_type);
+ if (base_type != nullptr &&
+ base_type->get_primitive_type() == PrimitiveType::INVALID_TYPE) {
+ continue;
+ }
+ // Skip Array(Variant) subcolumns — these represent NG (nested group)
data
+ // that should be handled by the NG writer, not as regular subcolumns.
+ if (base_type != nullptr &&
+ typeid_cast<const vectorized::DataTypeVariant*>(base_type.get())
!= nullptr) {
+ continue;
+ }
+ const std::string current_path = entry->path.get_path();
+ if (_nested_group_routing_plan.is_excluded_subcolumn(current_path)) {
+ continue;
+ }
+ CHECK(entry->data.is_finalized());
+ RETURN_IF_ERROR(write_one_subcolumn(current_path, entry->path,
least_common_type,
+
entry->data.get_finalized_column_ptr()->get_ptr(),
+
entry->data.get_non_null_value_size(),
+ true /* check_storage_type */,
+ true /*
use_existing_subcolumn_info */));
}
return Status::OK();
}
@@ -1204,7 +1272,9 @@ Status VariantColumnWriterImpl::_process_binary_column(
Status VariantColumnWriterImpl::finalize() {
auto* ptr = _column.get();
ptr->set_max_subcolumns_count(_tablet_column->variant_max_subcolumns_count());
+
ptr->finalize(vectorized::ColumnVariant::FinalizeMode::WRITE_MODE);
+
// convert each subcolumns to storage format and add data to sub columns
writers buffer
auto olap_data_convertor =
std::make_unique<vectorized::OlapBlockDataConvertor>();
@@ -1229,6 +1299,21 @@ Status VariantColumnWriterImpl::finalize() {
}
RETURN_IF_ERROR(ptr->convert_typed_path_to_storage_type(_subcolumns_info));
+ _nested_group_routing_plan = NestedGroupRoutingPlan {};
+
+ const int current_variant_uid = _tablet_column->unique_id();
+ const bool has_extracted_columns = std::ranges::any_of(
+ _opts.rowset_ctx->tablet_schema->columns(),
[current_variant_uid](const auto& column) {
+ return column->is_extracted_column() &&
+ column->parent_unique_id() == current_variant_uid;
+ });
+ if (!has_extracted_columns) {
+ RETURN_IF_ERROR(build_nested_group_routing_plan(*ptr,
&_nested_group_routing_plan));
+
+ // Root NG dedup is handled in _process_root_column() — see the
+ // has_root_ng check there. We intentionally do NOT modify the
in-memory
+ // root data here because _nested_group_provider->prepare() needs it.
+ }
RETURN_IF_ERROR(ptr->pick_subcolumns_to_sparse_column(
_subcolumns_info,
_tablet_column->variant_enable_typed_paths_to_sparse()));
@@ -1243,12 +1328,7 @@ Status VariantColumnWriterImpl::finalize() {
// convert root column data from engine format to storage layer format
RETURN_IF_ERROR(_process_root_column(ptr, olap_data_convertor.get(),
num_rows, column_id));
- auto has_extracted_columns = [this]() {
- return std::ranges::any_of(
- _opts.rowset_ctx->tablet_schema->columns(),
- [](const auto& column) { return column->is_extracted_column();
});
- };
- if (!has_extracted_columns()) {
+ if (!has_extracted_columns) {
if (!_tablet_column->variant_enable_doc_mode()) {
// process and append each subcolumns to sub columns writers buffer
RETURN_IF_ERROR(
@@ -1258,16 +1338,16 @@ Status VariantColumnWriterImpl::finalize() {
// process sparse column and append to sparse writer buffer
RETURN_IF_ERROR(
_process_binary_column(ptr, olap_data_convertor.get(),
num_rows, column_id));
+ }
- // NestedGroup write behavior is determined by the injected provider
implementation.
- RETURN_IF_ERROR(_nested_group_provider->prepare(
- *ptr, /*include_jsonb_subcolumns=*/true, _tablet_column, _opts,
- olap_data_convertor.get(), num_rows, &column_id,
&_statistics));
- if (_binary_writer) {
- _binary_writer->merge_stats_to(&_statistics);
- }
- _statistics.to_pb(_opts.meta->mutable_variant_statistics());
+ // NestedGroup write behavior is determined by the injected provider
implementation.
+ RETURN_IF_ERROR(_nested_group_provider->prepare(
+ *ptr, /*include_jsonb_subcolumns=*/true, _tablet_column, _opts,
+ olap_data_convertor.get(), num_rows, &column_id, &_statistics));
+ if (_binary_writer) {
+ _binary_writer->merge_stats_to(&_statistics);
}
+ _statistics.to_pb(_opts.meta->mutable_variant_statistics());
_is_finalized = true;
return Status::OK();
diff --git a/be/src/olap/rowset/segment_v2/variant/variant_column_writer_impl.h
b/be/src/olap/rowset/segment_v2/variant/variant_column_writer_impl.h
index f135105da18..824e9a5ed44 100644
--- a/be/src/olap/rowset/segment_v2/variant/variant_column_writer_impl.h
+++ b/be/src/olap/rowset/segment_v2/variant/variant_column_writer_impl.h
@@ -21,12 +21,14 @@
#include <functional>
#include <unordered_map>
+#include <unordered_set>
#include <vector>
#include "common/status.h"
#include "olap/rowset/segment_v2/column_writer.h"
#include "olap/rowset/segment_v2/indexed_column_writer.h"
#include "olap/rowset/segment_v2/variant/nested_group_provider.h"
+#include "olap/rowset/segment_v2/variant/nested_group_routing_plan.h"
#include "olap/rowset/segment_v2/variant/variant_statistics.h"
#include "olap/tablet_schema.h"
#include "vec/columns/column.h"
@@ -216,6 +218,7 @@ private:
std::unordered_map<std::string, TabletSchema::SubColumnInfo>
_subcolumns_info;
std::unique_ptr<NestedGroupWriteProvider> _nested_group_provider;
VariantStatistics _statistics;
+ NestedGroupRoutingPlan _nested_group_routing_plan;
};
class VariantDocCompactWriter : public ColumnWriter {
diff --git a/be/src/vec/columns/column_variant.cpp
b/be/src/vec/columns/column_variant.cpp
index b46025c1021..bd07e84b5fe 100644
--- a/be/src/vec/columns/column_variant.cpp
+++ b/be/src/vec/columns/column_variant.cpp
@@ -40,6 +40,7 @@
#include <vector>
#include "common/compiler_util.h" // IWYU pragma: keep
+#include "common/config.h"
#include "common/exception.h"
#include "common/logging.h"
#include "common/status.h"
@@ -129,6 +130,45 @@ size_t get_number_of_dimensions(const IDataType& type) {
}
return num_dimensions;
}
+
+// ============================================================================
+// NestedGroup (NG) type-conflict helpers
+// ============================================================================
+// These helpers encapsulate the NG-specific logic that must run inside
+// Subcolumn::insert_range_from and Subcolumn::finalize. Keeping them here
+// avoids spreading NG semantics throughout the generic column code.
+
+// Returns true if the base element type of `type` is DataTypeVariant,
+// which indicates NG-originated array<object> data.
+bool is_nested_group_type(const DataTypePtr& type) {
+ auto base = get_base_type_of_array(type);
+ return typeid_cast<const DataTypeVariant*>(base.get()) != nullptr;
+}
+
+// Resolve a type conflict between dst (current LCT) and src types when one
+// side is an NG type (Array<Variant>) and the other is a scalar type.
+//
+// Under DISCARD_SCALAR policy: returns the NG side's type (NG wins).
+// Under ERROR policy: throws an exception.
+//
+// Returns nullptr if neither side is an NG type (caller should fall through
+// to the normal get_least_supertype_jsonb path).
+DataTypePtr resolve_ng_type_conflict(const DataTypePtr& dst_type, const
DataTypePtr& src_type) {
+ bool dst_is_ng = is_nested_group_type(dst_type);
+ bool src_is_ng = is_nested_group_type(src_type);
+ if (!dst_is_ng && !src_is_ng) {
+ return nullptr; // Not an NG conflict — use normal type resolution.
+ }
+ if (!config::variant_nested_group_discard_scalar_on_conflict) {
+ throw doris::Exception(ErrorCode::INVALID_ARGUMENT,
+ "NestedGroup type conflict: cannot merge
Array<Variant> with "
+ "scalar type. dst={}, src={}",
+ dst_type->get_name(), src_type->get_name());
+ }
+ // NG wins: keep whichever side is the NG type.
+ return dst_is_ng ? dst_type : src_type;
+}
+
} // namespace
// current nested level is 2, inside column object
@@ -324,9 +364,14 @@ void ColumnVariant::Subcolumn::insert_range_from(const
Subcolumn& src, size_t st
if (data.empty()) {
add_new_column_part(src.get_least_common_type());
} else if (!least_common_type.get()->equals(*src.get_least_common_type()))
{
- DataTypePtr new_least_common_type;
- get_least_supertype_jsonb(DataTypes {least_common_type.get(),
src.get_least_common_type()},
- &new_least_common_type);
+ DataTypePtr new_least_common_type =
+ resolve_ng_type_conflict(least_common_type.get(),
src.get_least_common_type());
+ if (new_least_common_type == nullptr) {
+ // Normal (non-NG) type promotion.
+ get_least_supertype_jsonb(
+ DataTypes {least_common_type.get(),
src.get_least_common_type()},
+ &new_least_common_type);
+ }
if (!new_least_common_type->equals(*least_common_type.get())) {
add_new_column_part(std::move(new_least_common_type));
}
@@ -350,6 +395,18 @@ void ColumnVariant::Subcolumn::insert_range_from(const
Subcolumn& src, size_t st
data.back()->insert_range_from(*column, from, n);
return;
}
+ // When LCT is Array<Variant> (NG data) and the part is scalar, cast
+ // would crash. Under DISCARD_SCALAR the scalar part becomes defaults.
+ if (is_nested_group_type(least_common_type.get())) {
+ if (!config::variant_nested_group_discard_scalar_on_conflict) {
+ throw doris::Exception(ErrorCode::INVALID_ARGUMENT,
+ "NestedGroup type conflict: cannot cast
scalar type {} to "
+ "Array<Variant>",
+ column_type->get_name());
+ }
+ data.back()->insert_many_defaults(n);
+ return;
+ }
/// If we need to insert large range, there is no sense to cut part of
column and cast it.
/// Casting of all column and inserting from it can be faster.
/// Threshold is just a guess.
@@ -488,6 +545,19 @@ void ColumnVariant::Subcolumn::finalize(FinalizeMode mode)
{
part = part->convert_to_full_column_if_const();
size_t part_size = part->size();
if (!from_type->equals(*to_type)) {
+ // NG vs scalar mismatch: casting Array(Variant) ↔ scalar is not
+ // supported. Under DISCARD_SCALAR the non-NG part becomes
defaults.
+ if (is_nested_group_type(to_type) !=
is_nested_group_type(from_type)) {
+ if (!config::variant_nested_group_discard_scalar_on_conflict) {
+ throw doris::Exception(
+ ErrorCode::INVALID_ARGUMENT,
+ "NestedGroup type conflict in finalize: cannot
cast {} to {}",
+ from_type->get_name(), to_type->get_name());
+ }
+ result_column->insert_many_defaults(part_size);
+ continue;
+ }
+
ColumnPtr ptr;
Status st = variant_util::cast_column({part, from_type, ""},
to_type, &ptr);
if (!st.ok()) {
@@ -1820,6 +1890,8 @@ Status ColumnVariant::serialize_sparse_columns(
return Status::OK();
}
+/// @deprecated This function is deprecated. Array<Variant> subcolumns are now
handled
+/// directly as NestedGroup data by the writer (VariantColumnWriterImpl).
void ColumnVariant::unnest(Subcolumns::NodePtr& entry, Subcolumns&
res_subcolumns) const {
entry->data.finalize();
auto nested_column =
entry->data.get_finalized_column_ptr()->assume_mutable();
@@ -2006,9 +2078,10 @@ void ColumnVariant::finalize(FinalizeMode mode) {
continue;
}
- // unnest all nested columns, add them to new_subcolumns
+ // [DEPRECATED] unnest path - Array<Variant> subcolumns are now handled
+ // directly as NestedGroup by the writer. This branch exists only for
+ // backward compatibility with the exact NESTED_TYPE wrapper.
if (mode == FinalizeMode::WRITE_MODE &&
least_common_type->equals(*NESTED_TYPE)) {
- // reset counter
unnest(entry, new_subcolumns);
continue;
}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]