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 a37fc36a0ff [opt](variant) Walk only non-null materialized slots per 
row in VariantAssembler (#69914)
a37fc36a0ff is described below

commit a37fc36a0ffdc6bc666d7069eb865038aa302857
Author: lihangyu <[email protected]>
AuthorDate: Tue Nov 3 12:31:52 2026 +0800

    [opt](variant) Walk only non-null materialized slots per row in 
VariantAssembler (#69914)
---
 be/benchmark/benchmark_main.cpp                    |   1 +
 be/benchmark/benchmark_variant_assembler.hpp       |  90 +++++++
 .../segment/variant/v2/variant_assembler.cpp       | 107 ++++++---
 .../segment/variant_assembler_legacy_test.cpp      | 258 +++++++++++++++++++++
 4 files changed, 426 insertions(+), 30 deletions(-)

diff --git a/be/benchmark/benchmark_main.cpp b/be/benchmark/benchmark_main.cpp
index 77ba90b09b2..00166122c8b 100644
--- a/be/benchmark/benchmark_main.cpp
+++ b/be/benchmark/benchmark_main.cpp
@@ -40,6 +40,7 @@
 #include "benchmark_hybrid_set.hpp"
 #include "benchmark_json_extract.hpp"
 #include "benchmark_relative_day.hpp"
+#include "benchmark_variant_assembler.hpp"
 #include "benchmark_zone_map_index.hpp"
 #include "binary_cast_benchmark.hpp"
 #include "parquet/benchmark_file_scanner_expr.hpp"
diff --git a/be/benchmark/benchmark_variant_assembler.hpp 
b/be/benchmark/benchmark_variant_assembler.hpp
new file mode 100644
index 00000000000..4dfe6e20162
--- /dev/null
+++ b/be/benchmark/benchmark_variant_assembler.hpp
@@ -0,0 +1,90 @@
+// 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 <benchmark/benchmark.h>
+
+#include <algorithm>
+#include <memory>
+#include <numeric>
+#include <random>
+#include <string>
+#include <utility>
+
+#include "core/column/column_nullable.h"
+#include "core/column/column_vector.h"
+#include "core/custom_allocator.h"
+#include "core/data_type/data_type_nullable.h"
+#include "core/data_type/data_type_number.h"
+#include "storage/segment/variant/v2/variant_assembler.h"
+
+namespace doris::variant_assembler_benchmark {
+
+static void BM_VariantAssembler(benchmark::State& state, size_t slots, size_t 
visible) {
+    constexpr size_t ROWS = 4096;
+    auto type = make_nullable(std::make_shared<DataTypeInt64>());
+    segment_v2::variant_v2::VariantAssemblerOptions options;
+    DorisVector<ColumnNullable::MutablePtr> columns;
+    DorisVector<const IColumn*> materialized;
+    for (size_t slot = 0; slot < slots; ++slot) {
+        options.materialized_paths.push_back(
+                {.path = PathInData("k" + std::to_string(slot)), .type = 
type});
+        columns.push_back(ColumnNullable::create(
+                ColumnInt64::create(ROWS, 1), ColumnUInt8::create(ROWS, 
visible == slots ? 0 : 1)));
+        materialized.push_back(columns.back().get());
+    }
+    if (visible != slots) {
+        std::mt19937 random(20261031);
+        DorisVector<size_t> slot_order(slots);
+        std::iota(slot_order.begin(), slot_order.end(), 0);
+        for (size_t row = 0; row < ROWS; ++row) {
+            std::shuffle(slot_order.begin(), slot_order.end(), random);
+            for (size_t index = 0; index < visible; ++index) {
+                columns[slot_order[index]]->get_null_map_data()[row] = 0;
+            }
+        }
+    }
+    auto result = 
segment_v2::variant_v2::VariantAssembler::create(std::move(options));
+    if (!result.has_value()) {
+        state.SkipWithError(result.error().to_string().c_str());
+        return;
+    }
+    auto assembler = std::move(result).value();
+    segment_v2::variant_v2::VariantAssemblerBatchView batch;
+    batch.num_rows = ROWS;
+    batch.materialized_columns = materialized;
+    for (auto _ : state) {
+        ColumnNullable::MutablePtr output;
+        const Status status = assembler->assemble(batch, &output);
+        if (!status.ok()) {
+            state.SkipWithError(status.to_string().c_str());
+            return;
+        }
+        benchmark::DoNotOptimize(output);
+    }
+    state.SetItemsProcessed(state.iterations() * ROWS);
+    state.counters["rows/s"] = 
benchmark::Counter(static_cast<double>(state.iterations()) * ROWS,
+                                                  benchmark::Counter::kIsRate);
+}
+
+BENCHMARK_CAPTURE(BM_VariantAssembler, WideSparse, 2048, 20);
+BENCHMARK_CAPTURE(BM_VariantAssembler, Dense, 200, 200);
+BENCHMARK_CAPTURE(BM_VariantAssembler, Narrow, 10, 10);
+BENCHMARK_CAPTURE(BM_VariantAssembler, MediumSparse, 200, 75);
+
+} // namespace doris::variant_assembler_benchmark
diff --git a/be/src/storage/segment/variant/v2/variant_assembler.cpp 
b/be/src/storage/segment/variant/v2/variant_assembler.cpp
index 20e5c2d2889..44833aa0e78 100644
--- a/be/src/storage/segment/variant/v2/variant_assembler.cpp
+++ b/be/src/storage/segment/variant/v2/variant_assembler.cpp
@@ -19,6 +19,8 @@
 
 #include <algorithm>
 #include <cstring>
+#include <limits>
+#include <numeric>
 #include <utility>
 #include <vector>
 
@@ -34,6 +36,7 @@
 #include "exprs/function/parse/variant_jsonb_parse.h"
 #include "storage/segment/variant/v2/variant_assembler_internal.h"
 #include "storage/segment/variant/v2/variant_storage_cell.h"
+#include "util/simd/bits.h"
 
 namespace doris::segment_v2::variant_v2 {
 namespace {
@@ -296,22 +299,17 @@ struct PreparedHierarchicalBatch {
     const ColumnString* root_values = nullptr;
     const uint8_t* root_nulls = nullptr;
     DorisVector<variant_assembler_detail::PreparedMaterializedColumn> 
materialized;
-};
+    DorisVector<size_t> candidate_offsets;
+    DorisVector<uint32_t> candidate_slots;
 
-bool has_materialized_value(
-        std::span<const variant_assembler_detail::PreparedMaterializedColumn> 
materialized,
-        std::span<const MaterializedSlot> materialized_slots, size_t row,
-        bool reads_whole_variant) {
-    DORIS_CHECK_EQ(materialized.size(), materialized_slots.size());
-    for (size_t index = 0; index < materialized.size(); ++index) {
-        if (variant_assembler_detail::is_materialized_value_visible(
-                    materialized[index], row,
-                    reads_whole_variant && 
materialized_slots[index].relative_path.empty())) {
-            return true;
-        }
+    std::span<const uint32_t> candidate_materialized_slots(size_t row) const {
+        const std::span<const uint32_t> slots(candidate_slots);
+        return candidate_offsets.empty()
+                       ? slots
+                       : slots.subspan(candidate_offsets[row],
+                                       candidate_offsets[row + 1] - 
candidate_offsets[row]);
     }
-    return false;
-}
+};
 
 PreparedHierarchicalBatch prepare_hierarchical_batch(
         StorageMapKind storage_map_kind, bool has_root,
@@ -336,9 +334,49 @@ PreparedHierarchicalBatch prepare_hierarchical_batch(
     }
 
     output.materialized.reserve(materialized_slots.size());
+    size_t candidate_count = 0;
     for (const MaterializedSlot& slot : materialized_slots) {
-        
output.materialized.push_back(variant_assembler_detail::prepare_materialized_column(
-                slot.type, batch.materialized_columns[slot.batch_index], 
batch.num_rows));
+        auto column = variant_assembler_detail::prepare_materialized_column(
+                slot.type, batch.materialized_columns[slot.batch_index], 
batch.num_rows);
+        candidate_count +=
+                column.nulls == nullptr
+                        ? batch.num_rows
+                        : simd::count_zero_num(reinterpret_cast<const 
int8_t*>(column.nulls),
+                                               batch.num_rows);
+        output.materialized.push_back(std::move(column));
+    }
+    DORIS_CHECK_LE(materialized_slots.size(), 
std::numeric_limits<uint32_t>::max());
+    // At 37.5% candidates, CSR was about 5% faster than identity in the 
4096-row benchmark.
+    if (candidate_count >= batch.num_rows * materialized_slots.size() / 2) {
+        output.candidate_slots.resize(materialized_slots.size());
+        std::iota(output.candidate_slots.begin(), 
output.candidate_slots.end(), uint32_t {0});
+    } else {
+        auto for_each_candidate = [&](auto&& visit) {
+            for (size_t index = 0; index < output.materialized.size(); 
++index) {
+                const auto* nulls = output.materialized[index].nulls;
+                for (size_t row = 0; row < batch.num_rows; ++row) {
+                    if (nulls != nullptr) {
+                        row = simd::find_byte<uint8_t>(nulls, row, 
batch.num_rows, 0);
+                        if (row == batch.num_rows) {
+                            break;
+                        }
+                    }
+                    visit(row, static_cast<uint32_t>(index));
+                }
+            }
+        };
+        output.candidate_offsets.resize(batch.num_rows + 1, 0);
+        for_each_candidate([&](size_t row, uint32_t) { 
++output.candidate_offsets[row + 1]; });
+        for (size_t row = 0; row < batch.num_rows; ++row) {
+            output.candidate_offsets[row + 1] += output.candidate_offsets[row];
+        }
+        output.candidate_slots.resize(candidate_count);
+        DorisVector<size_t> write_positions(output.candidate_offsets.begin(),
+                                            output.candidate_offsets.end() - 
1);
+        // Filling in sorted slot order keeps each row ordered, e.g. [a, a-, 
a.b].
+        for_each_candidate([&](size_t row, uint32_t index) {
+            output.candidate_slots[write_positions[row]++] = index;
+        });
     }
     if (storage_map_kind != StorageMapKind::NONE) {
         map_cursor->bind(batch.storage_map, batch.num_rows);
@@ -446,25 +484,30 @@ Status emit_doc_row(size_t row, StringRef requested_raw, 
bool reads_whole_varian
 
 Status emit_merged_row(
         std::span<const variant_assembler_detail::PreparedMaterializedColumn> 
materialized,
-        bool has_sparse, std::span<const MaterializedSlot> materialized_slots, 
size_t row,
-        StringRef requested_raw, bool reads_whole_variant, 
StorageMapRowCursor* sparse_cursor,
+        bool has_sparse, std::span<const MaterializedSlot> materialized_slots,
+        std::span<const uint32_t> candidate_slots, size_t row, StringRef 
requested_raw,
+        bool reads_whole_variant, StorageMapRowCursor* sparse_cursor,
         DorisVector<MergeValue>* pending, ObjectEmitter* emitter) {
     if (has_sparse) {
         RETURN_IF_ERROR(sparse_cursor->start_row(row, requested_raw, 
reads_whole_variant));
     }
     pending->clear();
+    size_t candidate_index = 0;
     size_t materialized_index = 0;
-    while (true) {
-        while (materialized_index < materialized.size()) {
+    auto advance_materialized = [&] {
+        while (candidate_index < candidate_slots.size()) {
+            materialized_index = candidate_slots[candidate_index++];
             if (variant_assembler_detail::is_materialized_value_visible(
                         materialized[materialized_index], row,
                         reads_whole_variant &&
                                 
materialized_slots[materialized_index].relative_path.empty())) {
-                break;
+                return true;
             }
-            ++materialized_index;
         }
-        const bool materialized_available = materialized_index < 
materialized_slots.size();
+        return false;
+    };
+    bool materialized_available = advance_materialized();
+    while (true) {
         StringRef materialized_raw_path;
         if (materialized_available) {
             const std::string& path =
@@ -490,7 +533,7 @@ Status emit_merged_row(
         if (!use_sparse) {
             
current.set_materialized(materialized_slots[materialized_index].relative_path,
                                      materialized_raw_path, 
&materialized[materialized_index]);
-            ++materialized_index;
+            materialized_available = advance_materialized();
         } else {
             current.set_cell(sparse_cursor->sort_key, sparse_cursor->cell,
                              sparse_cursor->is_direct_subtree_value, 
sparse_cursor->depth);
@@ -513,6 +556,7 @@ Status assemble_hierarchical_row(StorageMapKind 
storage_map_kind, bool has_root,
             batch.root_values->get_data_at(row_index).size != 0;
     const bool has_doc_row = has_doc && !map_cursor->row_empty(row_index);
     const bool reads_whole_variant = requested.empty();
+    const auto candidate_slots = batch.candidate_materialized_slots(row_index);
 
     // Physical row precedence:
     // 1. A non-empty doc row exclusively defines the row.
@@ -521,10 +565,13 @@ Status assemble_hierarchical_row(StorageMapKind 
storage_map_kind, bool has_root,
     // 4. An empty whole-Variant row is {}; an absent subtree is SQL NULL.
     if (has_root_sidecar_value && !has_doc_row &&
         (!has_sparse || map_cursor->row_empty(row_index))) {
-        // Only the root fast-path candidate needs this O(materialized-paths) 
scan. Ordinary object
-        // and doc rows are scanned once later by 
emit_merged_row()/emit_doc_row().
-        if (!has_materialized_value(batch.materialized, materialized_slots, 
row_index,
-                                    reads_whole_variant)) {
+        const bool has_materialized_value =
+                std::ranges::any_of(candidate_slots, [&](uint32_t index) {
+                    return 
variant_assembler_detail::is_materialized_value_visible(
+                            batch.materialized[index], row_index,
+                            reads_whole_variant && 
materialized_slots[index].relative_path.empty());
+                });
+        if (!has_materialized_value) {
             jsonb_to_variant(batch.root_values->get_data_at(row_index), *row, 
0, nullptr);
             return Status::OK();
         }
@@ -538,8 +585,8 @@ Status assemble_hierarchical_row(StorageMapKind 
storage_map_kind, bool has_root,
                                      pending, emitter));
     } else {
         RETURN_IF_ERROR(emit_merged_row(batch.materialized, has_sparse, 
materialized_slots,
-                                        row_index, requested_raw, 
reads_whole_variant, map_cursor,
-                                        pending, emitter));
+                                        candidate_slots, row_index, 
requested_raw,
+                                        reads_whole_variant, map_cursor, 
pending, emitter));
     }
     if (!emitter->emitted) {
         if (reads_whole_variant && has_root && storage_map_kind == 
StorageMapKind::NONE &&
diff --git a/be/test/storage/segment/variant_assembler_legacy_test.cpp 
b/be/test/storage/segment/variant_assembler_legacy_test.cpp
index 2be5ba56f86..f65f2a404d6 100644
--- a/be/test/storage/segment/variant_assembler_legacy_test.cpp
+++ b/be/test/storage/segment/variant_assembler_legacy_test.cpp
@@ -160,6 +160,19 @@ std::unique_ptr<VariantAssembler> 
create_assembler(VariantAssemblerOptions optio
     return std::move(result).value();
 }
 
+void expect_assembled_rows(VariantAssembler& assembler, const 
VariantAssemblerBatchView& batch,
+                           const std::vector<std::string_view>& expected,
+                           const std::vector<uint8_t>& outer_nulls = {}) {
+    ColumnNullable::MutablePtr output;
+    const Status status = assembler.assemble(batch, &output);
+    ASSERT_TRUE(status.ok()) << status;
+    ASSERT_EQ(output->size(), expected.size());
+    for (size_t row = 0; row < expected.size(); ++row) {
+        EXPECT_EQ(json_at(assembled_values(output), row), expected[row]) << 
"row=" << row;
+        EXPECT_EQ(output->get_null_map_data()[row], outer_nulls.empty() ? 0 : 
outer_nulls[row]);
+    }
+}
+
 Status assemble_binary_cells(std::span<const StringRef> cells, 
ColumnNullable::MutablePtr* output,
                              std::span<const uint8_t> outer_nulls = {},
                              std::span<const uint8_t> missing = {}) {
@@ -819,6 +832,251 @@ TEST(VariantAssemblerLegacyTest, 
MaterializedArraysPreserveExplicitValues) {
     EXPECT_EQ(subtree_output->get_null_map_data(), (PaddedPODArray<uint8_t> 
{1, 0, 0, 0, 0, 0, 1}));
 }
 
+TEST(VariantAssemblerLegacyTest, MaterializedVisibilityMatrix) {
+    auto check = [](const DataTypePtr& type, const IColumn* column, const 
PathInData& path,
+                    const PathInData& requested, const 
std::vector<std::string_view>& expected,
+                    const std::vector<uint8_t>& outer_nulls = {}) {
+        auto absent_type = make_nullable(std::make_shared<DataTypeInt32>());
+        auto absent = 
ColumnNullable::create(ColumnInt32::create(column->size(), 0),
+                                             
ColumnUInt8::create(column->size(), 255));
+        for (size_t padding : {0, 8}) {
+            VariantAssemblerOptions options;
+            options.requested_path = requested;
+            options.materialized_paths = {{.path = path, .type = type}};
+            DorisVector<const IColumn*> materialized {column};
+            for (size_t index = 0; index < padding; ++index) {
+                const std::string prefix = requested.empty() ? "" : 
requested.get_path() + ".";
+                options.materialized_paths.push_back(
+                        {.path = PathInData(prefix + "absent" + 
std::to_string(index)),
+                         .type = absent_type});
+                materialized.push_back(absent.get());
+            }
+            auto assembler = create_assembler(std::move(options));
+            VariantAssemblerBatchView batch;
+            batch.num_rows = column->size();
+            batch.materialized_columns = materialized;
+            expect_assembled_rows(*assembler, batch, expected, outer_nulls);
+        }
+    };
+
+    auto int_type = std::make_shared<DataTypeInt32>();
+    auto defaults = ColumnInt32::create(3, 0);
+    check(int_type, defaults.get(), PathInData("a"), {},
+          {R"({"a":0})", R"({"a":0})", R"({"a":0})"});
+    auto nulls = ColumnUInt8::create();
+    for (uint8_t value : {0, 1, 255}) {
+        nulls->insert_value(value);
+    }
+    auto nullable = ColumnNullable::create(std::move(defaults), 
std::move(nulls));
+    for (bool typed : {false, true}) {
+        check(make_nullable(int_type), nullable.get(), PathInData("a", typed), 
{},
+              {R"({"a":0})", "{}", "{}"});
+    }
+
+    auto jsonb = ColumnString::create();
+    for (std::string_view text : {"null", "{}", "[]"}) {
+        auto value = jsonb_column(text);
+        jsonb->insert_range_from(*value, 0, 1);
+    }
+    check(std::make_shared<DataTypeJsonb>(), jsonb.get(), PathInData("a"), {},
+          {R"({"a":null})", R"({"a":{}})", R"({"a":[]})"});
+
+    auto elements = ColumnString::create();
+    elements->insert_default();
+    auto empty_array = jsonb_column("[]");
+    elements->insert_range_from(*empty_array, 0, 1);
+    auto element_nulls = ColumnUInt8::create();
+    element_nulls->insert_value(1);
+    element_nulls->insert_value(0);
+    auto offsets = ColumnArray::ColumnOffsets::create();
+    auto array_nulls = ColumnUInt8::create();
+    for (uint8_t value : {1, 0, 0, 0, 255}) {
+        array_nulls->insert_value(value);
+    }
+    for (size_t offset : {0, 0, 1, 2, 2}) {
+        offsets->insert_value(offset);
+    }
+    auto arrays = ColumnNullable::create(
+            ColumnArray::create(
+                    ColumnNullable::create(std::move(elements), 
std::move(element_nulls)),
+                    std::move(offsets)),
+            std::move(array_nulls));
+    auto array_type = make_nullable(
+            
std::make_shared<DataTypeArray>(make_nullable(std::make_shared<DataTypeJsonb>())));
+    check(array_type, arrays.get(), PathInData("a"), {},
+          {"{}", "{}", R"({"a":[null]})", R"({"a":[[]]})", "{}"});
+    check(array_type, arrays.get(), {}, {}, {"{}", "[]", "[null]", "[[]]", 
"{}"});
+    check(array_type, arrays.get(), PathInData("a"), PathInData("a"),
+          {"null", "null", "[null]", "[[]]", "null"}, {1, 1, 0, 0, 1});
+}
+
+TEST(VariantAssemblerLegacyTest, 
MaterializedSparseMergeKeepsTiesAndPrefixOrdering) {
+    struct Case {
+        std::string_view path;
+        std::vector<std::pair<std::string, int32_t>> sparse;
+        std::string_view expected;
+    };
+    const std::vector<Case> cases {
+            {.path = "a", .sparse = {{"a", 20}}, .expected = ""},
+            {.path = "a",
+             .sparse = {{"a-", 20}, {"a.b", 30}},
+             .expected = R"({"a":{"b":30},"a-":20})"},
+            {.path = "a.b",
+             .sparse = {{"a", 20}, {"a-", 30}},
+             .expected = R"({"a":{"b":10},"a-":30})"},
+            {.path = "a-",
+             .sparse = {{"a", 20}, {"a.b", 30}},
+             .expected = R"({"a":{"b":30},"a-":10})"},
+            {.path = "a",
+             .sparse = {{"a", 20}, {"a-", 30}, {"a.b", 40}},
+             .expected = R"({"a":{"b":40},"a-":30})"},
+    };
+    auto check = [](const Status& status, const ColumnNullable::MutablePtr& 
output,
+                    std::string_view expected) {
+        if (expected.empty()) {
+            EXPECT_TRUE(status.is<ErrorCode::INVALID_ARGUMENT>()) << status;
+            EXPECT_NE(status.to_string().find("Variant object value must be 
preceded by add_key"),
+                      std::string::npos);
+            EXPECT_EQ(output.get(), nullptr);
+            return;
+        }
+        ASSERT_TRUE(status.ok()) << status;
+        EXPECT_EQ(json_at(assembled_values(output), 0), expected);
+    };
+    for (const auto& test : cases) {
+        SCOPED_TRACE(test.path);
+        MapEntries entries;
+        for (const auto& [path, value] : test.sparse) {
+            entries.emplace_back(
+                    path, 
fixed_storage_cell<int32_t>(FieldType::OLAP_FIELD_TYPE_INT, value));
+        }
+        auto sparse = map_column(entries);
+        for (size_t padding : {0, 8}) {
+            VariantAssemblerOptions options;
+            options.materialized_paths = {
+                    {.path = PathInData(test.path), .type = 
std::make_shared<DataTypeInt32>()}};
+            options.storage_map_kind = StorageMapKind::SPARSE;
+            auto values = ColumnInt32::create(1, 10);
+            auto absent =
+                    ColumnNullable::create(ColumnInt32::create(1, 0), 
ColumnUInt8::create(1, 255));
+            DorisVector<const IColumn*> materialized {values.get()};
+            for (size_t index = 0; index < padding; ++index) {
+                options.materialized_paths.push_back(
+                        {.path = PathInData("absent" + std::to_string(index)),
+                         .type = 
make_nullable(std::make_shared<DataTypeInt32>())});
+                materialized.push_back(absent.get());
+            }
+            auto assembler = create_assembler(std::move(options));
+            VariantAssemblerBatchView batch;
+            batch.num_rows = 1;
+            batch.materialized_columns = materialized;
+            batch.storage_map = sparse.get();
+            ColumnNullable::MutablePtr output;
+            const Status status = assembler->assemble(batch, &output);
+            check(status, output, test.expected);
+        }
+    }
+}
+
+TEST(VariantAssemblerLegacyTest, 
MixedDocRowsPreserveMaterializedAndRootPrecedence) {
+    auto root_value = jsonb_column(R"({"root":1})");
+    auto roots = ColumnString::create();
+    for (size_t row = 0; row < 4; ++row) {
+        roots->insert_range_from(*root_value, 0, 1);
+    }
+    auto root_nulls = ColumnUInt8::create(4, 0);
+    root_nulls->get_data()[3] = 1;
+    auto root = ColumnNullable::create(std::move(roots), 
std::move(root_nulls));
+    auto nulls = ColumnUInt8::create(4, 0);
+    nulls->get_data()[2] = 255;
+    auto values = ColumnNullable::create(ColumnInt32::create(4, 10), 
std::move(nulls));
+    auto array_type = 
std::make_shared<DataTypeArray>(std::make_shared<DataTypeInt32>());
+    auto empty_arrays = array_type->create_column();
+    empty_arrays->insert_many_defaults(4);
+    auto absent = ColumnNullable::create(ColumnInt32::create(4, 0), 
ColumnUInt8::create(4, 255));
+    const auto cell = 
fixed_storage_cell<int32_t>(FieldType::OLAP_FIELD_TYPE_INT, 20);
+    auto doc = map_column_rows({{{"d", cell}}, {}, {}, {{"d", cell}}});
+    for (size_t padding : {0, 8}) {
+        for (bool subtree : {false, true}) {
+            VariantAssemblerOptions options;
+            options.storage_map_kind = StorageMapKind::DOC;
+            options.has_root = !subtree;
+            options.requested_path = subtree ? PathInData("a") : PathInData();
+            options.materialized_paths = {
+                    {.path = PathInData("a"),
+                     .type = make_nullable(std::make_shared<DataTypeInt32>())},
+                    {.path = PathInData("a.empty"), .type = array_type}};
+            DorisVector<const IColumn*> materialized {values.get(), 
empty_arrays.get()};
+            for (size_t index = 0; index < padding; ++index) {
+                options.materialized_paths.push_back(
+                        {.path = PathInData("a.absent" + 
std::to_string(index)),
+                         .type = 
make_nullable(std::make_shared<DataTypeInt32>())});
+                materialized.push_back(absent.get());
+            }
+            auto assembler = create_assembler(std::move(options));
+            VariantAssemblerBatchView batch;
+            batch.num_rows = 4;
+            batch.root_jsonb = subtree ? nullptr : root.get();
+            batch.materialized_columns = materialized;
+            batch.storage_map = doc.get();
+            if (subtree) {
+                expect_assembled_rows(*assembler, batch, {"null", "10", 
"null", "null"},
+                                      {1, 0, 1, 1});
+            } else {
+                expect_assembled_rows(*assembler, batch,
+                                      {R"({"d":20})", R"({"a":10})", 
R"({"root":1})", "null"},
+                                      {0, 0, 0, 1});
+            }
+        }
+    }
+}
+
+TEST(VariantAssemblerLegacyTest, VisibleSlotsResetAcrossBatchBoundaries) {
+    auto type = make_nullable(std::make_shared<DataTypeInt32>());
+    VariantAssemblerOptions options;
+    options.materialized_paths = {{.path = PathInData("z"), .type = type},
+                                  {.path = PathInData("a"), .type = type}};
+    auto assembler = create_assembler(std::move(options));
+    VariantAssemblerOptions empty_options;
+    empty_options.storage_map_kind = StorageMapKind::SPARSE;
+    auto empty_assembler = create_assembler(std::move(empty_options));
+    for (size_t rows : {15, 16, 17, 33, 0}) {
+        for (size_t pattern = 0; pattern < 3; ++pattern) {
+            std::array<ColumnNullable::MutablePtr, 2> columns;
+            std::array<const IColumn*, 2> materialized;
+            for (size_t slot = 0; slot < columns.size(); ++slot) {
+                auto nulls = ColumnUInt8::create();
+                for (size_t row = 0; row < rows; ++row) {
+                    constexpr std::array<uint8_t, 8> NULL_BYTES {0, 1, 255, 1, 
255, 1, 255, 1};
+                    nulls->insert_value(
+                            NULL_BYTES[pattern == 1 ? (row + slot) % 
NULL_BYTES.size() : pattern]);
+                }
+                columns[slot] =
+                        ColumnNullable::create(ColumnInt32::create(rows, 0), 
std::move(nulls));
+                materialized[slot] = columns[slot].get();
+            }
+            VariantAssemblerBatchView batch;
+            batch.num_rows = rows;
+            batch.materialized_columns = materialized;
+            constexpr std::array<std::string_view, 4> EXPECTED_JSON {
+                    "{}", R"({"a":0})", R"({"z":0})", R"({"a":0,"z":0})"};
+            std::vector<std::string_view> expected;
+            for (size_t row = 0; row < rows; ++row) {
+                const bool a = columns[1]->get_null_map_data()[row] == 0;
+                const bool z = columns[0]->get_null_map_data()[row] == 0;
+                expected.push_back(EXPECTED_JSON[a + 2 * z]);
+            }
+            expect_assembled_rows(*assembler, batch, expected);
+        }
+        auto empty_map = map_column_rows(std::vector<MapEntries>(rows));
+        VariantAssemblerBatchView empty_batch;
+        empty_batch.num_rows = rows;
+        empty_batch.storage_map = empty_map.get();
+        expect_assembled_rows(*empty_assembler, empty_batch,
+                              std::vector<std::string_view>(rows, "{}"));
+    }
+}
+
 TEST(VariantAssemblerLegacyTest, 
HierarchicalMaterializedLegacyDateTimeAndDecimalV2) {
     LegacyCells source;
     VariantAssemblerOptions options;


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to