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 115d5c831b9 [fix](variant) Read exact sparse cells during path
promotion (#70212)
115d5c831b9 is described below
commit 115d5c831b9a7aa59fd1c82b00ca740db85c9590
Author: lihangyu <[email protected]>
AuthorDate: Fri Nov 6 11:07:59 2026 +0800
[fix](variant) Read exact sparse cells during path promotion (#70212)
---
.../variant/binary_column_extract_iterator.cpp | 32 +-
.../binary_column_extract_iterator_test.cpp | 217 ++++++-
.../data/variant_p0/sparse_ancestor_promotion.out | 673 +++++++++++++++++++++
.../sparse_ancestor_promotion_extended.out | 196 ++++++
.../variant_p0/sparse_ancestor_promotion.groovy | 92 +++
.../sparse_ancestor_promotion_extended.groovy | 140 +++++
6 files changed, 1339 insertions(+), 11 deletions(-)
diff --git a/be/src/storage/segment/variant/binary_column_extract_iterator.cpp
b/be/src/storage/segment/variant/binary_column_extract_iterator.cpp
index d908688fa3b..94c19b12c8d 100644
--- a/be/src/storage/segment/variant/binary_column_extract_iterator.cpp
+++ b/be/src/storage/segment/variant/binary_column_extract_iterator.cpp
@@ -20,9 +20,9 @@
#include "core/column/column_nullable.h"
#include "core/column/column_string.h"
#include "core/column/variant_column_utils.h"
-#include "storage/segment/variant/v2/variant_assembler.h"
+#include "core/custom_allocator.h"
#include "storage/segment/variant/v2/variant_column_reader.h"
-#include "util/json/path_in_data.h"
+#include "storage/segment/variant/v2/variant_storage_cell.h"
namespace doris::segment_v2 {
@@ -91,13 +91,29 @@ Status
BinaryColumnExtractIterator::_fill_variant_v2_path(MutableColumnPtr& dst,
return Status::Corruption("Variant sparse offsets consume {} of {}
cells",
map->get_offsets().back(), paths->size());
}
- variant_v2::VariantAssemblerOptions options;
- options.requested_path = PathInData(_path);
- options.storage_map_kind = variant_v2::StorageMapKind::SPARSE;
- auto assembler =
DORIS_TRY(variant_v2::VariantAssembler::create(std::move(options)));
+ // Compaction promotes one physical path. For a sparse row containing only
a.b,
+ // assembling the a subtree would invent a non-null a and break path
statistics.
+ DorisVector<StringRef> cells(num_rows);
+ DorisVector<uint8_t> missing(num_rows, 1);
+ const StringRef requested {_path.data(), _path.size()};
+ const auto& offsets = map->get_offsets();
+ for (size_t row = 0; row < num_rows; ++row) {
+ const size_t start = offsets[row - 1];
+ const size_t end = offsets[row];
+ // Corrupt offsets must not reach the binary search or cell decoder.
+ if (start > end || end > paths->size()) {
+ return Status::Corruption("Invalid Variant sparse offsets [{}, {})
for {} cells", start,
+ end, paths->size());
+ }
+ const size_t pos = find_variant_sparse_path_lower_bound(requested,
*paths, start, end);
+ // Prefix matches such as a.b belong to another physical path.
+ if (pos < end && paths->get_data_at(pos) == requested) {
+ cells[row] = values->get_data_at(pos);
+ missing[row] = 0;
+ }
+ }
ColumnNullable::MutablePtr assembled;
- RETURN_IF_ERROR(assembler->assemble(
- {.num_rows = num_rows, .materialized_columns = {}, .storage_map =
map}, &assembled));
+ RETURN_IF_ERROR(variant_v2::decode_v1_storage_cells(cells, {}, missing,
&assembled));
if (has_null != nullptr) {
*has_null = assembled->has_null();
}
diff --git a/be/test/storage/segment/binary_column_extract_iterator_test.cpp
b/be/test/storage/segment/binary_column_extract_iterator_test.cpp
index d8087d65d55..416b1a01f6e 100644
--- a/be/test/storage/segment/binary_column_extract_iterator_test.cpp
+++ b/be/test/storage/segment/binary_column_extract_iterator_test.cpp
@@ -25,6 +25,7 @@
#include <string>
#include <string_view>
#include <utility>
+#include <vector>
#include "core/column/column_nullable.h"
#include "core/column/column_string.h"
@@ -70,11 +71,12 @@ public:
Status next_batch(size_t* n, MutableColumnPtr& dst, bool* has_null)
override {
++_counters->next_batch;
- const size_t rows = std::min(*n, _source->size());
- if (rows == _source->size()) {
+ const size_t rows = std::min(*n, _source->size() - _ordinal);
+ if (_ordinal == 0 && rows == _source->size()) {
+ // Preserve deliberately invalid offsets in corruption tests.
dst = std::move(_source);
} else {
- dst->insert_range_from(*_source, 0, rows);
+ dst->insert_range_from(*_source, _ordinal, rows);
}
*n = rows;
_ordinal += rows;
@@ -410,6 +412,215 @@ TEST(BinaryColumnExtractIteratorV2Test,
SharedCacheReusesRowidBatchAcrossPaths)
EXPECT_EQ(counters->next_batch, 0);
}
+void check_exact_sparse_promotion_result(const IColumn& dst, const rowid_t*
rowids) {
+ ASSERT_EQ(dst.size(), 6);
+ const auto& nullable = assert_cast<const ColumnNullable&>(dst);
+ const auto& variant = assert_cast<const
ColumnVariantV2&>(nullable.get_nested_column());
+ constexpr std::string_view expected[] {"11", "", "", R"({"c":44})",
"null", ""};
+ for (size_t row = 0; row < 6; ++row) {
+ const size_t source_row = rowids != nullptr ? rowids[row] : row;
+ const bool missing = source_row == 1 || source_row == 2 || source_row
== 5;
+ EXPECT_EQ(nullable.is_null_at(row), missing) << source_row;
+ if (!missing) {
+ EXPECT_EQ(variant_v2_json_at(variant, row), expected[source_row]);
+ }
+ }
+}
+
+// Compaction promotes one physical path at a time. A row containing only a.b
must
+// stay NULL in the promoted a column; hierarchical query assembly reads a.b
separately.
+void check_exact_sparse_promotion(const std::string& path, bool by_rowids) {
+ SCOPED_TRACE(path + (by_rowids ? " rowids" : " batch"));
+ auto sparse = variant_util::create_variant_binary_column();
+ auto& map = assert_cast<ColumnMap&>(*sparse);
+ DataTypeInt64 int_type;
+ auto ints = ColumnInt64::create();
+ ints->insert_value(11);
+ ints->insert_value(22);
+ ints->insert_value(33);
+
+ append_storage_cell(map, path, int_type, *ints, 0);
+ map.get_offsets().push_back(1);
+ append_storage_cell(map, path + ".b", int_type, *ints, 1);
+ map.get_offsets().push_back(2);
+ append_storage_cell(map, path + "-", int_type, *ints, 2);
+ map.get_offsets().push_back(3);
+ // Objects stored at the exact path must keep their complete value.
+ append_raw_storage_cell(map, path, jsonb_storage_cell(R"({"c":44})"));
+ map.get_offsets().push_back(4);
+ const char none = static_cast<char>(FieldType::OLAP_FIELD_TYPE_NONE);
+ append_raw_storage_cell(map, path, std::string_view(&none, 1));
+ map.get_offsets().push_back(5);
+ map.get_offsets().push_back(5);
+
+ auto counters = std::make_shared<SparseReadCounters>();
+ auto cache = std::make_shared<BinaryColumnCache>(
+ std::make_unique<FixedSparseIterator>(std::move(sparse), counters),
+ variant_util::create_variant_binary_column());
+ OlapReaderStatistics stats;
+ StorageReadOptions read_options(stats);
+ BinaryColumnExtractIterator extract(path, std::move(cache), &read_options);
+ ColumnIteratorOptions iterator_options;
+ iterator_options.stats = &stats;
+ ASSERT_TRUE(extract.init(iterator_options).ok());
+ auto dst = make_v2_destination();
+ // Exercise reordered rows as well as the sequential compaction read.
+ const rowid_t rowids[] {3, 1, 0, 5, 4, 2};
+ if (by_rowids) {
+ ASSERT_TRUE(extract.read_by_rowids(rowids, std::size(rowids),
dst).ok());
+ } else {
+ ASSERT_TRUE(extract.seek_to_ordinal(0).ok());
+ size_t rows = 6;
+ bool has_null = false;
+ ASSERT_TRUE(extract.next_batch(&rows, dst, &has_null).ok());
+ ASSERT_EQ(rows, 6);
+ EXPECT_TRUE(has_null);
+ }
+ check_exact_sparse_promotion_result(*dst, by_rowids ? rowids : nullptr);
+}
+
+TEST(BinaryColumnExtractIteratorV2Test, PromotionReadsOnlyExactSparseCells) {
+ for (const std::string path : {"a", "p.a"}) {
+ for (const bool by_rowids : {false, true}) {
+ check_exact_sparse_promotion(path, by_rowids);
+ }
+ }
+}
+
+void check_sparse_payload_rows(const IColumn& destination, const
std::vector<std::string>& payloads,
+ const rowid_t* rowids, size_t rows) {
+ const auto& nullable = assert_cast<const ColumnNullable&>(destination);
+ const auto& variant = assert_cast<const
ColumnVariantV2&>(nullable.get_nested_column());
+ ASSERT_EQ(destination.size(), rows);
+ for (size_t i = 0; i < rows; ++i) {
+ const size_t source_row = rowids != nullptr ? rowids[i] : i;
+ EXPECT_EQ(nullable.is_null_at(i), source_row >= payloads.size());
+ if (source_row < payloads.size()) {
+ EXPECT_EQ(variant_v2_json_at(variant, i), payloads[source_row]);
+ }
+ }
+}
+
+void check_sparse_promotion_payloads(const std::string& path, bool by_rowids) {
+ SCOPED_TRACE(path + (by_rowids ? " rowids" : " batch"));
+ const std::vector<std::string> payloads {"{}", "[]",
R"([1,null,{"x":2}])", R"("")"};
+ auto sparse = variant_util::create_variant_binary_column();
+ auto& map = assert_cast<ColumnMap&>(*sparse);
+ for (const auto& payload : payloads) {
+ append_raw_storage_cell(map, path.substr(0, path.size() - 1),
jsonb_storage_cell("901"));
+ append_raw_storage_cell(map, path, jsonb_storage_cell(payload));
+ append_raw_storage_cell(map, path + "%", jsonb_storage_cell("902"));
+ append_raw_storage_cell(map, path + "-", jsonb_storage_cell("903"));
+ append_raw_storage_cell(map, path + ".child",
jsonb_storage_cell("904"));
+ append_raw_storage_cell(map, path + "z", jsonb_storage_cell("905"));
+ map.get_offsets().push_back(map.get_keys().size());
+ }
+ append_raw_storage_cell(map, path + ".child", jsonb_storage_cell("905"));
+ map.get_offsets().push_back(map.get_keys().size());
+ map.get_offsets().push_back(map.get_keys().size());
+ auto counters = std::make_shared<SparseReadCounters>();
+ auto cache = std::make_shared<BinaryColumnCache>(
+ std::make_unique<FixedSparseIterator>(std::move(sparse), counters),
+ variant_util::create_variant_binary_column());
+ OlapReaderStatistics stats;
+ StorageReadOptions read_options(stats);
+ BinaryColumnExtractIterator extract(path, cache, &read_options);
+ ColumnIteratorOptions options;
+ options.stats = &stats;
+ ASSERT_TRUE(extract.init(options).ok());
+ auto dst = make_v2_destination();
+ const rowid_t rowids[] {5, 2, 0, 4, 1, 3, 2};
+ size_t rows = payloads.size() + 2;
+ if (by_rowids) {
+ ASSERT_TRUE(extract.read_by_rowids(rowids, std::size(rowids),
dst).ok());
+ rows = std::size(rowids);
+ } else {
+ ASSERT_TRUE(extract.seek_to_ordinal(0).ok());
+ bool has_null = false;
+ ASSERT_TRUE(extract.next_batch(&rows, dst, &has_null).ok());
+ EXPECT_TRUE(has_null);
+ }
+ check_sparse_payload_rows(*dst, payloads, by_rowids ? rowids : nullptr,
rows);
+}
+
+TEST(BinaryColumnExtractIteratorV2Test,
PromotionReadsEncodedAndDeepPathsAmongPrefixNeighbors) {
+ // Encoded literal-dot names and structural paths must remain independent
even
+ // when a single sparse bucket contains their byte-order neighbors.
+ for (const std::string path : {"p.q.a", "a%2Eb", "a%252Eb"}) {
+ for (const bool by_rowids : {false, true}) {
+ check_sparse_promotion_payloads(path, by_rowids);
+ }
+ }
+}
+
+void check_cached_ancestor_rows(const IColumn& destination, size_t path_index)
{
+ const auto& nullable = assert_cast<const ColumnNullable&>(destination);
+ ASSERT_EQ(nullable.size(), 11);
+ const rowid_t expected_rows[] {0, 1, 2, 3, 2, 0, 1, 2, 3, 1, 2};
+ const size_t expected_counts[] {2, 3, 4};
+ size_t physical_count = 0;
+ for (size_t row = 0; row < nullable.size(); ++row) {
+ EXPECT_EQ(nullable.is_null_at(row), expected_rows[row] != path_index);
+ physical_count += !nullable.is_null_at(row);
+ }
+ EXPECT_EQ(physical_count, expected_counts[path_index]);
+}
+
+void read_cached_ancestor_batch(const
std::vector<BinaryColumnExtractIterator*>& readers,
+ std::vector<MutableColumnPtr>& destinations,
ordinal_t offset) {
+ for (size_t i = 0; i < readers.size(); ++i) {
+ ASSERT_TRUE(readers[i]->seek_to_ordinal(offset).ok());
+ size_t rows = 2;
+ ASSERT_TRUE(readers[i]->next_batch(&rows, destinations[i],
nullptr).ok());
+ EXPECT_EQ(rows, 2);
+ }
+}
+
+TEST(BinaryColumnExtractIteratorV2Test,
AncestorChainSharesCacheAcrossBatchesAndRowids) {
+ auto sparse = variant_util::create_variant_binary_column();
+ auto& map = assert_cast<ColumnMap&>(*sparse);
+ for (const auto& path : {"a", "a.b", "a.b.c"}) {
+ append_raw_storage_cell(map, path, jsonb_storage_cell("11"));
+ map.get_offsets().push_back(map.get_keys().size());
+ }
+ map.get_offsets().push_back(map.get_keys().size());
+ auto counters = std::make_shared<SparseReadCounters>();
+ auto cache = std::make_shared<BinaryColumnCache>(
+ std::make_unique<FixedSparseIterator>(std::move(sparse), counters),
+ variant_util::create_variant_binary_column());
+ OlapReaderStatistics stats;
+ StorageReadOptions read_options(stats);
+ BinaryColumnExtractIterator a("a", cache, &read_options);
+ BinaryColumnExtractIterator ab("a.b", cache, &read_options);
+ BinaryColumnExtractIterator abc("a.b.c", cache, &read_options);
+ const std::vector<BinaryColumnExtractIterator*> readers {&a, &ab, &abc};
+ ColumnIteratorOptions options;
+ options.stats = &stats;
+ for (auto* reader : readers) {
+ ASSERT_TRUE(reader->init(options).ok());
+ }
+ std::vector<MutableColumnPtr> destinations;
+ for (size_t i = 0; i < readers.size(); ++i) {
+ destinations.push_back(make_v2_destination());
+ }
+ // All readers consume the first batch before moving to the second batch.
+ read_cached_ancestor_batch(readers, destinations, 0);
+ read_cached_ancestor_batch(readers, destinations, 2);
+ EXPECT_EQ(counters->next_batch, 2);
+ const rowid_t rowids[] {2, 0, 1, 2, 3};
+ for (size_t i = 0; i < readers.size(); ++i) {
+ ASSERT_TRUE(readers[i]->read_by_rowids(rowids, std::size(rowids),
destinations[i]).ok());
+ }
+ EXPECT_EQ(counters->read_by_rowids, 1);
+ // Changing back from random reads must invalidate the shared cache.
+ read_cached_ancestor_batch(readers, destinations, 1);
+ for (size_t i = 0; i < readers.size(); ++i) {
+ check_cached_ancestor_rows(*destinations[i], i);
+ }
+ EXPECT_EQ(counters->next_batch, 3);
+ EXPECT_EQ(counters->init, 1);
+}
+
TEST(BinaryColumnExtractIteratorV2Test, RejectsInvalidOffsets) {
auto sparse = variant_util::create_variant_binary_column();
auto& map = assert_cast<ColumnMap&>(*sparse);
diff --git a/regression-test/data/variant_p0/sparse_ancestor_promotion.out
b/regression-test/data/variant_p0/sparse_ancestor_promotion.out
new file mode 100644
index 00000000000..59655fc08d5
--- /dev/null
+++ b/regression-test/data/variant_p0/sparse_ancestor_promotion.out
@@ -0,0 +1,673 @@
+-- This file is automatically generated. You should know what you did if you
want to edit this
+-- !before --
+1 1 \N {"a":1,"hot_0":1}
+2 2 \N {"a":2,"hot_0":2}
+3 \N \N {"hot_0":3}
+4 \N \N {"hot_0":4}
+5 {"b":5} 5 {"a":{"b":5},"hot_0":5}
+6 101 \N {"a":101,"hot_1":1}
+7 102 \N {"a":102,"hot_1":2}
+8 \N \N {"hot_1":3}
+9 \N \N {"hot_1":4}
+10 {"b":105} 105 {"a":{"b":105},"hot_1":5}
+11 201 \N {"a":201,"hot_2":1}
+12 202 \N {"a":202,"hot_2":2}
+13 \N \N {"hot_2":3}
+14 \N \N {"hot_2":4}
+15 {"b":205} 205 {"a":{"b":205},"hot_2":5}
+
+-- !full_status --
+[OK]
+
+-- !after_first --
+1 1 \N {"a":1,"hot_0":1}
+2 2 \N {"a":2,"hot_0":2}
+3 \N \N {"hot_0":3}
+4 \N \N {"hot_0":4}
+5 {"b":5} 5 {"a":{"b":5},"hot_0":5}
+6 101 \N {"a":101,"hot_1":1}
+7 102 \N {"a":102,"hot_1":2}
+8 \N \N {"hot_1":3}
+9 \N \N {"hot_1":4}
+10 {"b":105} 105 {"a":{"b":105},"hot_1":5}
+11 201 \N {"a":201,"hot_2":1}
+12 202 \N {"a":202,"hot_2":2}
+13 \N \N {"hot_2":3}
+14 \N \N {"hot_2":4}
+15 {"b":205} 205 {"a":{"b":205},"hot_2":5}
+
+-- !before_second --
+1 1 \N {"a":1,"hot_0":1}
+2 2 \N {"a":2,"hot_0":2}
+3 \N \N {"hot_0":3}
+4 \N \N {"hot_0":4}
+5 {"b":5} 5 {"a":{"b":5},"hot_0":5}
+6 101 \N {"a":101,"hot_1":1}
+7 102 \N {"a":102,"hot_1":2}
+8 \N \N {"hot_1":3}
+9 \N \N {"hot_1":4}
+10 {"b":105} 105 {"a":{"b":105},"hot_1":5}
+11 201 \N {"a":201,"hot_2":1}
+12 202 \N {"a":202,"hot_2":2}
+13 \N \N {"hot_2":3}
+14 \N \N {"hot_2":4}
+15 {"b":205} 205 {"a":{"b":205},"hot_2":5}
+16 301 \N {"a":301,"hot_3":1}
+17 302 \N {"a":302,"hot_3":2}
+18 \N \N {"hot_3":3}
+19 \N \N {"hot_3":4}
+20 {"b":305} 305 {"a":{"b":305},"hot_3":5}
+
+-- !full_status --
+[OK]
+
+-- !after_second --
+1 1 \N {"a":1,"hot_0":1}
+2 2 \N {"a":2,"hot_0":2}
+3 \N \N {"hot_0":3}
+4 \N \N {"hot_0":4}
+5 {"b":5} 5 {"a":{"b":5},"hot_0":5}
+6 101 \N {"a":101,"hot_1":1}
+7 102 \N {"a":102,"hot_1":2}
+8 \N \N {"hot_1":3}
+9 \N \N {"hot_1":4}
+10 {"b":105} 105 {"a":{"b":105},"hot_1":5}
+11 201 \N {"a":201,"hot_2":1}
+12 202 \N {"a":202,"hot_2":2}
+13 \N \N {"hot_2":3}
+14 \N \N {"hot_2":4}
+15 {"b":205} 205 {"a":{"b":205},"hot_2":5}
+16 301 \N {"a":301,"hot_3":1}
+17 302 \N {"a":302,"hot_3":2}
+18 \N \N {"hot_3":3}
+19 \N \N {"hot_3":4}
+20 {"b":305} 305 {"a":{"b":305},"hot_3":5}
+
+-- !before --
+1 1 \N {"a":1,"hot_0":1}
+2 2 \N {"a":2,"hot_0":2}
+3 \N \N {"hot_0":3}
+4 \N \N {"hot_0":4}
+5 {"b":5} 5 {"a":{"b":5},"hot_0":5}
+6 101 \N {"a":101,"hot_1":1}
+7 102 \N {"a":102,"hot_1":2}
+8 \N \N {"hot_1":3}
+9 \N \N {"hot_1":4}
+10 {"b":105} 105 {"a":{"b":105},"hot_1":5}
+11 201 \N {"a":201,"hot_2":1}
+12 202 \N {"a":202,"hot_2":2}
+13 \N \N {"hot_2":3}
+14 \N \N {"hot_2":4}
+15 {"b":205} 205 {"a":{"b":205},"hot_2":5}
+
+-- !full_status --
+[OK]
+
+-- !after_first --
+1 1 \N {"a":1,"hot_0":1}
+2 2 \N {"a":2,"hot_0":2}
+3 \N \N {"hot_0":3}
+4 \N \N {"hot_0":4}
+5 {"b":5} 5 {"a":{"b":5},"hot_0":5}
+6 101 \N {"a":101,"hot_1":1}
+7 102 \N {"a":102,"hot_1":2}
+8 \N \N {"hot_1":3}
+9 \N \N {"hot_1":4}
+10 {"b":105} 105 {"a":{"b":105},"hot_1":5}
+11 201 \N {"a":201,"hot_2":1}
+12 202 \N {"a":202,"hot_2":2}
+13 \N \N {"hot_2":3}
+14 \N \N {"hot_2":4}
+15 {"b":205} 205 {"a":{"b":205},"hot_2":5}
+
+-- !before_second --
+1 1 \N {"a":1,"hot_0":1}
+2 2 \N {"a":2,"hot_0":2}
+3 \N \N {"hot_0":3}
+4 \N \N {"hot_0":4}
+5 {"b":5} 5 {"a":{"b":5},"hot_0":5}
+6 101 \N {"a":101,"hot_1":1}
+7 102 \N {"a":102,"hot_1":2}
+8 \N \N {"hot_1":3}
+9 \N \N {"hot_1":4}
+10 {"b":105} 105 {"a":{"b":105},"hot_1":5}
+11 201 \N {"a":201,"hot_2":1}
+12 202 \N {"a":202,"hot_2":2}
+13 \N \N {"hot_2":3}
+14 \N \N {"hot_2":4}
+15 {"b":205} 205 {"a":{"b":205},"hot_2":5}
+16 301 \N {"a":301,"hot_3":1}
+17 302 \N {"a":302,"hot_3":2}
+18 \N \N {"hot_3":3}
+19 \N \N {"hot_3":4}
+20 {"b":305} 305 {"a":{"b":305},"hot_3":5}
+
+-- !full_status --
+[OK]
+
+-- !after_second --
+1 1 \N {"a":1,"hot_0":1}
+2 2 \N {"a":2,"hot_0":2}
+3 \N \N {"hot_0":3}
+4 \N \N {"hot_0":4}
+5 {"b":5} 5 {"a":{"b":5},"hot_0":5}
+6 101 \N {"a":101,"hot_1":1}
+7 102 \N {"a":102,"hot_1":2}
+8 \N \N {"hot_1":3}
+9 \N \N {"hot_1":4}
+10 {"b":105} 105 {"a":{"b":105},"hot_1":5}
+11 201 \N {"a":201,"hot_2":1}
+12 202 \N {"a":202,"hot_2":2}
+13 \N \N {"hot_2":3}
+14 \N \N {"hot_2":4}
+15 {"b":205} 205 {"a":{"b":205},"hot_2":5}
+16 301 \N {"a":301,"hot_3":1}
+17 302 \N {"a":302,"hot_3":2}
+18 \N \N {"hot_3":3}
+19 \N \N {"hot_3":4}
+20 {"b":305} 305 {"a":{"b":305},"hot_3":5}
+
+-- !before --
+1 1 \N {"a":1,"hot_0":1}
+2 2 \N {"a":2,"hot_0":2}
+3 \N \N {"hot_0":3}
+4 \N \N {"hot_0":4}
+5 {"b":5} 5 {"a":{"b":5},"hot_0":5}
+6 101 \N {"a":101,"hot_1":1}
+7 102 \N {"a":102,"hot_1":2}
+8 \N \N {"hot_1":3}
+9 \N \N {"hot_1":4}
+10 {"b":105} 105 {"a":{"b":105},"hot_1":5}
+11 201 \N {"a":201,"hot_2":1}
+12 202 \N {"a":202,"hot_2":2}
+13 \N \N {"hot_2":3}
+14 \N \N {"hot_2":4}
+15 {"b":205} 205 {"a":{"b":205},"hot_2":5}
+
+-- !full_status --
+[OK]
+
+-- !after_first --
+1 1 \N {"a":1,"hot_0":1}
+2 2 \N {"a":2,"hot_0":2}
+3 \N \N {"hot_0":3}
+4 \N \N {"hot_0":4}
+5 {"b":5} 5 {"a":{"b":5},"hot_0":5}
+6 101 \N {"a":101,"hot_1":1}
+7 102 \N {"a":102,"hot_1":2}
+8 \N \N {"hot_1":3}
+9 \N \N {"hot_1":4}
+10 {"b":105} 105 {"a":{"b":105},"hot_1":5}
+11 201 \N {"a":201,"hot_2":1}
+12 202 \N {"a":202,"hot_2":2}
+13 \N \N {"hot_2":3}
+14 \N \N {"hot_2":4}
+15 {"b":205} 205 {"a":{"b":205},"hot_2":5}
+
+-- !before_second --
+1 1 \N {"a":1,"hot_0":1}
+2 2 \N {"a":2,"hot_0":2}
+3 \N \N {"hot_0":3}
+4 \N \N {"hot_0":4}
+5 {"b":5} 5 {"a":{"b":5},"hot_0":5}
+6 101 \N {"a":101,"hot_1":1}
+7 102 \N {"a":102,"hot_1":2}
+8 \N \N {"hot_1":3}
+9 \N \N {"hot_1":4}
+10 {"b":105} 105 {"a":{"b":105},"hot_1":5}
+11 201 \N {"a":201,"hot_2":1}
+12 202 \N {"a":202,"hot_2":2}
+13 \N \N {"hot_2":3}
+14 \N \N {"hot_2":4}
+15 {"b":205} 205 {"a":{"b":205},"hot_2":5}
+16 301 \N {"a":301,"hot_3":1}
+17 302 \N {"a":302,"hot_3":2}
+18 \N \N {"hot_3":3}
+19 \N \N {"hot_3":4}
+20 {"b":305} 305 {"a":{"b":305},"hot_3":5}
+
+-- !full_status --
+[OK]
+
+-- !after_second --
+1 1 \N {"a":1,"hot_0":1}
+2 2 \N {"a":2,"hot_0":2}
+3 \N \N {"hot_0":3}
+4 \N \N {"hot_0":4}
+5 {"b":5} 5 {"a":{"b":5},"hot_0":5}
+6 101 \N {"a":101,"hot_1":1}
+7 102 \N {"a":102,"hot_1":2}
+8 \N \N {"hot_1":3}
+9 \N \N {"hot_1":4}
+10 {"b":105} 105 {"a":{"b":105},"hot_1":5}
+11 201 \N {"a":201,"hot_2":1}
+12 202 \N {"a":202,"hot_2":2}
+13 \N \N {"hot_2":3}
+14 \N \N {"hot_2":4}
+15 {"b":205} 205 {"a":{"b":205},"hot_2":5}
+16 301 \N {"a":301,"hot_3":1}
+17 302 \N {"a":302,"hot_3":2}
+18 \N \N {"hot_3":3}
+19 \N \N {"hot_3":4}
+20 {"b":305} 305 {"a":{"b":305},"hot_3":5}
+
+-- !before --
+1 1 \N {"a":1,"hot_0":1}
+2 2 \N {"a":2,"hot_0":2}
+3 \N \N {"hot_0":3}
+4 \N \N {"hot_0":4}
+5 {"b":5} 5 {"a":{"b":5},"hot_0":5}
+6 101 \N {"a":101,"hot_1":1}
+7 102 \N {"a":102,"hot_1":2}
+8 \N \N {"hot_1":3}
+9 \N \N {"hot_1":4}
+10 {"b":105} 105 {"a":{"b":105},"hot_1":5}
+11 201 \N {"a":201,"hot_2":1}
+12 202 \N {"a":202,"hot_2":2}
+13 \N \N {"hot_2":3}
+14 \N \N {"hot_2":4}
+15 {"b":205} 205 {"a":{"b":205},"hot_2":5}
+
+-- !full_status --
+[OK]
+
+-- !after_first --
+1 1 \N {"a":1,"hot_0":1}
+2 2 \N {"a":2,"hot_0":2}
+3 \N \N {"hot_0":3}
+4 \N \N {"hot_0":4}
+5 {"b":5} 5 {"a":{"b":5},"hot_0":5}
+6 101 \N {"a":101,"hot_1":1}
+7 102 \N {"a":102,"hot_1":2}
+8 \N \N {"hot_1":3}
+9 \N \N {"hot_1":4}
+10 {"b":105} 105 {"a":{"b":105},"hot_1":5}
+11 201 \N {"a":201,"hot_2":1}
+12 202 \N {"a":202,"hot_2":2}
+13 \N \N {"hot_2":3}
+14 \N \N {"hot_2":4}
+15 {"b":205} 205 {"a":{"b":205},"hot_2":5}
+
+-- !before_second --
+1 1 \N {"a":1,"hot_0":1}
+2 2 \N {"a":2,"hot_0":2}
+3 \N \N {"hot_0":3}
+4 \N \N {"hot_0":4}
+5 {"b":5} 5 {"a":{"b":5},"hot_0":5}
+6 101 \N {"a":101,"hot_1":1}
+7 102 \N {"a":102,"hot_1":2}
+8 \N \N {"hot_1":3}
+9 \N \N {"hot_1":4}
+10 {"b":105} 105 {"a":{"b":105},"hot_1":5}
+11 201 \N {"a":201,"hot_2":1}
+12 202 \N {"a":202,"hot_2":2}
+13 \N \N {"hot_2":3}
+14 \N \N {"hot_2":4}
+15 {"b":205} 205 {"a":{"b":205},"hot_2":5}
+16 301 \N {"a":301,"hot_3":1}
+17 302 \N {"a":302,"hot_3":2}
+18 \N \N {"hot_3":3}
+19 \N \N {"hot_3":4}
+20 {"b":305} 305 {"a":{"b":305},"hot_3":5}
+
+-- !full_status --
+[OK]
+
+-- !after_second --
+1 1 \N {"a":1,"hot_0":1}
+2 2 \N {"a":2,"hot_0":2}
+3 \N \N {"hot_0":3}
+4 \N \N {"hot_0":4}
+5 {"b":5} 5 {"a":{"b":5},"hot_0":5}
+6 101 \N {"a":101,"hot_1":1}
+7 102 \N {"a":102,"hot_1":2}
+8 \N \N {"hot_1":3}
+9 \N \N {"hot_1":4}
+10 {"b":105} 105 {"a":{"b":105},"hot_1":5}
+11 201 \N {"a":201,"hot_2":1}
+12 202 \N {"a":202,"hot_2":2}
+13 \N \N {"hot_2":3}
+14 \N \N {"hot_2":4}
+15 {"b":205} 205 {"a":{"b":205},"hot_2":5}
+16 301 \N {"a":301,"hot_3":1}
+17 302 \N {"a":302,"hot_3":2}
+18 \N \N {"hot_3":3}
+19 \N \N {"hot_3":4}
+20 {"b":305} 305 {"a":{"b":305},"hot_3":5}
+
+-- !before --
+1 1 \N {"a":1,"hot_0":1}
+2 2 \N {"a":2,"hot_0":2}
+3 \N \N {"hot_0":3}
+4 \N \N {"hot_0":4}
+5 {"b":5} 5 {"a":{"b":5},"hot_0":5}
+6 101 \N {"a":101,"hot_1":1}
+7 102 \N {"a":102,"hot_1":2}
+8 \N \N {"hot_1":3}
+9 \N \N {"hot_1":4}
+10 {"b":105} 105 {"a":{"b":105},"hot_1":5}
+11 201 \N {"a":201,"hot_2":1}
+12 202 \N {"a":202,"hot_2":2}
+13 \N \N {"hot_2":3}
+14 \N \N {"hot_2":4}
+15 {"b":205} 205 {"a":{"b":205},"hot_2":5}
+
+-- !full_status --
+[OK]
+
+-- !after_first --
+1 1 \N {"a":1,"hot_0":1}
+2 2 \N {"a":2,"hot_0":2}
+3 \N \N {"hot_0":3}
+4 \N \N {"hot_0":4}
+5 {"b":5} 5 {"a":{"b":5},"hot_0":5}
+6 101 \N {"a":101,"hot_1":1}
+7 102 \N {"a":102,"hot_1":2}
+8 \N \N {"hot_1":3}
+9 \N \N {"hot_1":4}
+10 {"b":105} 105 {"a":{"b":105},"hot_1":5}
+11 201 \N {"a":201,"hot_2":1}
+12 202 \N {"a":202,"hot_2":2}
+13 \N \N {"hot_2":3}
+14 \N \N {"hot_2":4}
+15 {"b":205} 205 {"a":{"b":205},"hot_2":5}
+
+-- !before_second --
+1 1 \N {"a":1,"hot_0":1}
+2 2 \N {"a":2,"hot_0":2}
+3 \N \N {"hot_0":3}
+4 \N \N {"hot_0":4}
+5 {"b":5} 5 {"a":{"b":5},"hot_0":5}
+6 101 \N {"a":101,"hot_1":1}
+7 102 \N {"a":102,"hot_1":2}
+8 \N \N {"hot_1":3}
+9 \N \N {"hot_1":4}
+10 {"b":105} 105 {"a":{"b":105},"hot_1":5}
+11 201 \N {"a":201,"hot_2":1}
+12 202 \N {"a":202,"hot_2":2}
+13 \N \N {"hot_2":3}
+14 \N \N {"hot_2":4}
+15 {"b":205} 205 {"a":{"b":205},"hot_2":5}
+16 301 \N {"a":301,"hot_3":1}
+17 302 \N {"a":302,"hot_3":2}
+18 \N \N {"hot_3":3}
+19 \N \N {"hot_3":4}
+20 {"b":305} 305 {"a":{"b":305},"hot_3":5}
+
+-- !full_status --
+[OK]
+
+-- !after_second --
+1 1 \N {"a":1,"hot_0":1}
+2 2 \N {"a":2,"hot_0":2}
+3 \N \N {"hot_0":3}
+4 \N \N {"hot_0":4}
+5 {"b":5} 5 {"a":{"b":5},"hot_0":5}
+6 101 \N {"a":101,"hot_1":1}
+7 102 \N {"a":102,"hot_1":2}
+8 \N \N {"hot_1":3}
+9 \N \N {"hot_1":4}
+10 {"b":105} 105 {"a":{"b":105},"hot_1":5}
+11 201 \N {"a":201,"hot_2":1}
+12 202 \N {"a":202,"hot_2":2}
+13 \N \N {"hot_2":3}
+14 \N \N {"hot_2":4}
+15 {"b":205} 205 {"a":{"b":205},"hot_2":5}
+16 301 \N {"a":301,"hot_3":1}
+17 302 \N {"a":302,"hot_3":2}
+18 \N \N {"hot_3":3}
+19 \N \N {"hot_3":4}
+20 {"b":305} 305 {"a":{"b":305},"hot_3":5}
+
+-- !before --
+1 1 \N {"a":1,"hot_0":1}
+2 2 \N {"a":2,"hot_0":2}
+3 \N \N {"hot_0":3}
+4 \N \N {"hot_0":4}
+5 {"b":5} 5 {"a":{"b":5},"hot_0":5}
+6 101 \N {"a":101,"hot_1":1}
+7 102 \N {"a":102,"hot_1":2}
+8 \N \N {"hot_1":3}
+9 \N \N {"hot_1":4}
+10 {"b":105} 105 {"a":{"b":105},"hot_1":5}
+11 201 \N {"a":201,"hot_2":1}
+12 202 \N {"a":202,"hot_2":2}
+13 \N \N {"hot_2":3}
+14 \N \N {"hot_2":4}
+15 {"b":205} 205 {"a":{"b":205},"hot_2":5}
+
+-- !full_status --
+[OK]
+
+-- !after_first --
+1 1 \N {"a":1,"hot_0":1}
+2 2 \N {"a":2,"hot_0":2}
+3 \N \N {"hot_0":3}
+4 \N \N {"hot_0":4}
+5 {"b":5} 5 {"a":{"b":5},"hot_0":5}
+6 101 \N {"a":101,"hot_1":1}
+7 102 \N {"a":102,"hot_1":2}
+8 \N \N {"hot_1":3}
+9 \N \N {"hot_1":4}
+10 {"b":105} 105 {"a":{"b":105},"hot_1":5}
+11 201 \N {"a":201,"hot_2":1}
+12 202 \N {"a":202,"hot_2":2}
+13 \N \N {"hot_2":3}
+14 \N \N {"hot_2":4}
+15 {"b":205} 205 {"a":{"b":205},"hot_2":5}
+
+-- !before_second --
+1 1 \N {"a":1,"hot_0":1}
+2 2 \N {"a":2,"hot_0":2}
+3 \N \N {"hot_0":3}
+4 \N \N {"hot_0":4}
+5 {"b":5} 5 {"a":{"b":5},"hot_0":5}
+6 101 \N {"a":101,"hot_1":1}
+7 102 \N {"a":102,"hot_1":2}
+8 \N \N {"hot_1":3}
+9 \N \N {"hot_1":4}
+10 {"b":105} 105 {"a":{"b":105},"hot_1":5}
+11 201 \N {"a":201,"hot_2":1}
+12 202 \N {"a":202,"hot_2":2}
+13 \N \N {"hot_2":3}
+14 \N \N {"hot_2":4}
+15 {"b":205} 205 {"a":{"b":205},"hot_2":5}
+16 301 \N {"a":301,"hot_3":1}
+17 302 \N {"a":302,"hot_3":2}
+18 \N \N {"hot_3":3}
+19 \N \N {"hot_3":4}
+20 {"b":305} 305 {"a":{"b":305},"hot_3":5}
+
+-- !full_status --
+[OK]
+
+-- !after_second --
+1 1 \N {"a":1,"hot_0":1}
+2 2 \N {"a":2,"hot_0":2}
+3 \N \N {"hot_0":3}
+4 \N \N {"hot_0":4}
+5 {"b":5} 5 {"a":{"b":5},"hot_0":5}
+6 101 \N {"a":101,"hot_1":1}
+7 102 \N {"a":102,"hot_1":2}
+8 \N \N {"hot_1":3}
+9 \N \N {"hot_1":4}
+10 {"b":105} 105 {"a":{"b":105},"hot_1":5}
+11 201 \N {"a":201,"hot_2":1}
+12 202 \N {"a":202,"hot_2":2}
+13 \N \N {"hot_2":3}
+14 \N \N {"hot_2":4}
+15 {"b":205} 205 {"a":{"b":205},"hot_2":5}
+16 301 \N {"a":301,"hot_3":1}
+17 302 \N {"a":302,"hot_3":2}
+18 \N \N {"hot_3":3}
+19 \N \N {"hot_3":4}
+20 {"b":305} 305 {"a":{"b":305},"hot_3":5}
+
+-- !before --
+1 1 \N {"a":1,"hot_0":1}
+2 2 \N {"a":2,"hot_0":2}
+3 \N \N {"hot_0":3}
+4 \N \N {"hot_0":4}
+5 {"b":5} 5 {"a":{"b":5},"hot_0":5}
+6 101 \N {"a":101,"hot_1":1}
+7 102 \N {"a":102,"hot_1":2}
+8 \N \N {"hot_1":3}
+9 \N \N {"hot_1":4}
+10 {"b":105} 105 {"a":{"b":105},"hot_1":5}
+11 201 \N {"a":201,"hot_2":1}
+12 202 \N {"a":202,"hot_2":2}
+13 \N \N {"hot_2":3}
+14 \N \N {"hot_2":4}
+15 {"b":205} 205 {"a":{"b":205},"hot_2":5}
+
+-- !full_status --
+[OK]
+
+-- !after_first --
+1 1 \N {"a":1,"hot_0":1}
+2 2 \N {"a":2,"hot_0":2}
+3 \N \N {"hot_0":3}
+4 \N \N {"hot_0":4}
+5 {"b":5} 5 {"a":{"b":5},"hot_0":5}
+6 101 \N {"a":101,"hot_1":1}
+7 102 \N {"a":102,"hot_1":2}
+8 \N \N {"hot_1":3}
+9 \N \N {"hot_1":4}
+10 {"b":105} 105 {"a":{"b":105},"hot_1":5}
+11 201 \N {"a":201,"hot_2":1}
+12 202 \N {"a":202,"hot_2":2}
+13 \N \N {"hot_2":3}
+14 \N \N {"hot_2":4}
+15 {"b":205} 205 {"a":{"b":205},"hot_2":5}
+
+-- !before_second --
+1 1 \N {"a":1,"hot_0":1}
+2 2 \N {"a":2,"hot_0":2}
+3 \N \N {"hot_0":3}
+4 \N \N {"hot_0":4}
+5 {"b":5} 5 {"a":{"b":5},"hot_0":5}
+6 101 \N {"a":101,"hot_1":1}
+7 102 \N {"a":102,"hot_1":2}
+8 \N \N {"hot_1":3}
+9 \N \N {"hot_1":4}
+10 {"b":105} 105 {"a":{"b":105},"hot_1":5}
+11 201 \N {"a":201,"hot_2":1}
+12 202 \N {"a":202,"hot_2":2}
+13 \N \N {"hot_2":3}
+14 \N \N {"hot_2":4}
+15 {"b":205} 205 {"a":{"b":205},"hot_2":5}
+16 301 \N {"a":301,"hot_3":1}
+17 302 \N {"a":302,"hot_3":2}
+18 \N \N {"hot_3":3}
+19 \N \N {"hot_3":4}
+20 {"b":305} 305 {"a":{"b":305},"hot_3":5}
+
+-- !full_status --
+[OK]
+
+-- !after_second --
+1 1 \N {"a":1,"hot_0":1}
+2 2 \N {"a":2,"hot_0":2}
+3 \N \N {"hot_0":3}
+4 \N \N {"hot_0":4}
+5 {"b":5} 5 {"a":{"b":5},"hot_0":5}
+6 101 \N {"a":101,"hot_1":1}
+7 102 \N {"a":102,"hot_1":2}
+8 \N \N {"hot_1":3}
+9 \N \N {"hot_1":4}
+10 {"b":105} 105 {"a":{"b":105},"hot_1":5}
+11 201 \N {"a":201,"hot_2":1}
+12 202 \N {"a":202,"hot_2":2}
+13 \N \N {"hot_2":3}
+14 \N \N {"hot_2":4}
+15 {"b":205} 205 {"a":{"b":205},"hot_2":5}
+16 301 \N {"a":301,"hot_3":1}
+17 302 \N {"a":302,"hot_3":2}
+18 \N \N {"hot_3":3}
+19 \N \N {"hot_3":4}
+20 {"b":305} 305 {"a":{"b":305},"hot_3":5}
+
+-- !before --
+1 1 \N {"a":1,"hot_0":1}
+2 2 \N {"a":2,"hot_0":2}
+3 \N \N {"hot_0":3}
+4 \N \N {"hot_0":4}
+5 {"b":5} 5 {"a":{"b":5},"hot_0":5}
+6 101 \N {"a":101,"hot_1":1}
+7 102 \N {"a":102,"hot_1":2}
+8 \N \N {"hot_1":3}
+9 \N \N {"hot_1":4}
+10 {"b":105} 105 {"a":{"b":105},"hot_1":5}
+11 201 \N {"a":201,"hot_2":1}
+12 202 \N {"a":202,"hot_2":2}
+13 \N \N {"hot_2":3}
+14 \N \N {"hot_2":4}
+15 {"b":205} 205 {"a":{"b":205},"hot_2":5}
+
+-- !full_status --
+[OK]
+
+-- !after_first --
+1 1 \N {"a":1,"hot_0":1}
+2 2 \N {"a":2,"hot_0":2}
+3 \N \N {"hot_0":3}
+4 \N \N {"hot_0":4}
+5 {"b":5} 5 {"a":{"b":5},"hot_0":5}
+6 101 \N {"a":101,"hot_1":1}
+7 102 \N {"a":102,"hot_1":2}
+8 \N \N {"hot_1":3}
+9 \N \N {"hot_1":4}
+10 {"b":105} 105 {"a":{"b":105},"hot_1":5}
+11 201 \N {"a":201,"hot_2":1}
+12 202 \N {"a":202,"hot_2":2}
+13 \N \N {"hot_2":3}
+14 \N \N {"hot_2":4}
+15 {"b":205} 205 {"a":{"b":205},"hot_2":5}
+
+-- !before_second --
+1 1 \N {"a":1,"hot_0":1}
+2 2 \N {"a":2,"hot_0":2}
+3 \N \N {"hot_0":3}
+4 \N \N {"hot_0":4}
+5 {"b":5} 5 {"a":{"b":5},"hot_0":5}
+6 101 \N {"a":101,"hot_1":1}
+7 102 \N {"a":102,"hot_1":2}
+8 \N \N {"hot_1":3}
+9 \N \N {"hot_1":4}
+10 {"b":105} 105 {"a":{"b":105},"hot_1":5}
+11 201 \N {"a":201,"hot_2":1}
+12 202 \N {"a":202,"hot_2":2}
+13 \N \N {"hot_2":3}
+14 \N \N {"hot_2":4}
+15 {"b":205} 205 {"a":{"b":205},"hot_2":5}
+16 301 \N {"a":301,"hot_3":1}
+17 302 \N {"a":302,"hot_3":2}
+18 \N \N {"hot_3":3}
+19 \N \N {"hot_3":4}
+20 {"b":305} 305 {"a":{"b":305},"hot_3":5}
+
+-- !full_status --
+[OK]
+
+-- !after_second --
+1 1 \N {"a":1,"hot_0":1}
+2 2 \N {"a":2,"hot_0":2}
+3 \N \N {"hot_0":3}
+4 \N \N {"hot_0":4}
+5 {"b":5} 5 {"a":{"b":5},"hot_0":5}
+6 101 \N {"a":101,"hot_1":1}
+7 102 \N {"a":102,"hot_1":2}
+8 \N \N {"hot_1":3}
+9 \N \N {"hot_1":4}
+10 {"b":105} 105 {"a":{"b":105},"hot_1":5}
+11 201 \N {"a":201,"hot_2":1}
+12 202 \N {"a":202,"hot_2":2}
+13 \N \N {"hot_2":3}
+14 \N \N {"hot_2":4}
+15 {"b":205} 205 {"a":{"b":205},"hot_2":5}
+16 301 \N {"a":301,"hot_3":1}
+17 302 \N {"a":302,"hot_3":2}
+18 \N \N {"hot_3":3}
+19 \N \N {"hot_3":4}
+20 {"b":305} 305 {"a":{"b":305},"hot_3":5}
+
diff --git
a/regression-test/data/variant_p0/sparse_ancestor_promotion_extended.out
b/regression-test/data/variant_p0/sparse_ancestor_promotion_extended.out
new file mode 100644
index 00000000000..8f810ae1237
--- /dev/null
+++ b/regression-test/data/variant_p0/sparse_ancestor_promotion_extended.out
@@ -0,0 +1,196 @@
+-- This file is automatically generated. You should know what you did if you
want to edit this
+-- !before --
+1 11 \N {"hot_0":0,"p":{"q":{"a":11}}}
+2 text \N {"hot_0":1,"p":{"q":{"a":"text"}}}
+3 1 \N {"hot_0":2,"p":{"q":{"a":true}}}
+4 {"child":{"leaf":100}} 100
{"hot_0":3,"p":{"q":{"a":{"child":{"leaf":100}}}}}
+5 \N \N {"a%2Eb":300,"a-":200,"hot_0":4}
+6 \N \N {"hot_0":5}
+7 [] \N {"hot_1":0,"p":{"q":{"a":[]}}}
+8 [1,null,{"x":2}] \N
{"hot_1":1,"p":{"q":{"a":[1,null,{"x":2}]}}}
+9 1.5 \N {"hot_1":2,"p":{"q":{"a":1.5}}}
+10 {"child":{"leaf":101}} 101
{"hot_1":3,"p":{"q":{"a":{"child":{"leaf":101}}}}}
+11 \N \N {"a%2Eb":301,"a-":201,"hot_1":4}
+12 \N \N {"hot_1":5}
+13 \N \N {"hot_2":0}
+14 \N \N {"hot_2":1}
+15 [{"x":3},{"x":4}] \N
{"hot_2":2,"p":{"q":{"a":[{"x":3},{"x":4}]}}}
+16 {"child":{"leaf":102}} 102
{"hot_2":3,"p":{"q":{"a":{"child":{"leaf":102}}}}}
+17 \N \N {"a%2Eb":302,"a-":202,"hot_2":4}
+18 \N \N {"hot_2":5}
+
+-- !compaction_status --
+[OK]
+
+-- !output_rows --
+1 1 18
+
+-- !promoted --
+1 11 \N {"hot_0":0,"p":{"q":{"a":11}}}
+2 text \N {"hot_0":1,"p":{"q":{"a":"text"}}}
+3 1 \N {"hot_0":2,"p":{"q":{"a":true}}}
+4 {"child":{"leaf":100}} 100
{"hot_0":3,"p":{"q":{"a":{"child":{"leaf":100}}}}}
+5 \N \N {"a%2Eb":300,"a-":200,"hot_0":4}
+6 \N \N {"hot_0":5}
+7 [] \N {"hot_1":0,"p":{"q":{"a":[]}}}
+8 [1,null,{"x":2}] \N
{"hot_1":1,"p":{"q":{"a":[1,null,{"x":2}]}}}
+9 1.5 \N {"hot_1":2,"p":{"q":{"a":1.5}}}
+10 {"child":{"leaf":101}} 101
{"hot_1":3,"p":{"q":{"a":{"child":{"leaf":101}}}}}
+11 \N \N {"a%2Eb":301,"a-":201,"hot_1":4}
+12 \N \N {"hot_1":5}
+13 \N \N {"hot_2":0}
+14 \N \N {"hot_2":1}
+15 [{"x":3},{"x":4}] \N
{"hot_2":2,"p":{"q":{"a":[{"x":3},{"x":4}]}}}
+16 {"child":{"leaf":102}} 102
{"hot_2":3,"p":{"q":{"a":{"child":{"leaf":102}}}}}
+17 \N \N {"a%2Eb":302,"a-":202,"hot_2":4}
+18 \N \N {"hot_2":5}
+
+-- !promoted_layout --
+id int Yes true \N
+v variant<PROPERTIES ("variant_max_subcolumns_count" =
"1","variant_enable_typed_paths_to_sparse" =
"false","variant_max_sparse_column_statistics_size" =
"10000","variant_sparse_hash_shard_count" = "1")> Yes false \N
NONE
+v.p.q.a json Yes false \N NONE
+
+-- !compaction_status --
+[OK]
+
+-- !output_rows --
+1 1 28
+
+-- !demoted --
+1 11 \N {"hot_0":0,"p":{"q":{"a":11}}}
+2 text \N {"hot_0":1,"p":{"q":{"a":"text"}}}
+3 1 \N {"hot_0":2,"p":{"q":{"a":true}}}
+4 {"child":{"leaf":100}} 100
{"hot_0":3,"p":{"q":{"a":{"child":{"leaf":100}}}}}
+5 \N \N {"a%2Eb":300,"a-":200,"hot_0":4}
+6 \N \N {"hot_0":5}
+7 [] \N {"hot_1":0,"p":{"q":{"a":[]}}}
+8 [1,null,{"x":2}] \N
{"hot_1":1,"p":{"q":{"a":[1,null,{"x":2}]}}}
+9 1.5 \N {"hot_1":2,"p":{"q":{"a":1.5}}}
+10 {"child":{"leaf":101}} 101
{"hot_1":3,"p":{"q":{"a":{"child":{"leaf":101}}}}}
+11 \N \N {"a%2Eb":301,"a-":201,"hot_1":4}
+12 \N \N {"hot_1":5}
+13 \N \N {"hot_2":0}
+14 \N \N {"hot_2":1}
+15 [{"x":3},{"x":4}] \N
{"hot_2":2,"p":{"q":{"a":[{"x":3},{"x":4}]}}}
+16 {"child":{"leaf":102}} 102
{"hot_2":3,"p":{"q":{"a":{"child":{"leaf":102}}}}}
+17 \N \N {"a%2Eb":302,"a-":202,"hot_2":4}
+18 \N \N {"hot_2":5}
+100 \N \N {"takeover":0}
+101 \N \N {"takeover":1}
+102 \N \N {"takeover":2}
+103 \N \N {"takeover":3}
+104 \N \N {"takeover":4}
+105 \N \N {"takeover":5}
+106 \N \N {"takeover":6}
+107 \N \N {"takeover":7}
+108 \N \N {"takeover":8}
+109 \N \N {"takeover":9}
+
+-- !demoted_layout --
+id int Yes true \N
+v variant<PROPERTIES ("variant_max_subcolumns_count" =
"1","variant_enable_typed_paths_to_sparse" =
"false","variant_max_sparse_column_statistics_size" =
"10000","variant_sparse_hash_shard_count" = "1")> Yes false \N
NONE
+v.takeover bigint Yes false \N NONE
+
+-- !compaction_status --
+[OK]
+
+-- !output_rows --
+1 1 46
+
+-- !repromoted --
+1 11 \N {"hot_0":0,"p":{"q":{"a":11}}}
+2 text \N {"hot_0":1,"p":{"q":{"a":"text"}}}
+3 1 \N {"hot_0":2,"p":{"q":{"a":true}}}
+4 {"child":{"leaf":100}} 100
{"hot_0":3,"p":{"q":{"a":{"child":{"leaf":100}}}}}
+5 \N \N {"a%2Eb":300,"a-":200,"hot_0":4}
+6 \N \N {"hot_0":5}
+7 [] \N {"hot_1":0,"p":{"q":{"a":[]}}}
+8 [1,null,{"x":2}] \N
{"hot_1":1,"p":{"q":{"a":[1,null,{"x":2}]}}}
+9 1.5 \N {"hot_1":2,"p":{"q":{"a":1.5}}}
+10 {"child":{"leaf":101}} 101
{"hot_1":3,"p":{"q":{"a":{"child":{"leaf":101}}}}}
+11 \N \N {"a%2Eb":301,"a-":201,"hot_1":4}
+12 \N \N {"hot_1":5}
+13 \N \N {"hot_2":0}
+14 \N \N {"hot_2":1}
+15 [{"x":3},{"x":4}] \N
{"hot_2":2,"p":{"q":{"a":[{"x":3},{"x":4}]}}}
+16 {"child":{"leaf":102}} 102
{"hot_2":3,"p":{"q":{"a":{"child":{"leaf":102}}}}}
+17 \N \N {"a%2Eb":302,"a-":202,"hot_2":4}
+18 \N \N {"hot_2":5}
+100 \N \N {"takeover":0}
+101 \N \N {"takeover":1}
+102 \N \N {"takeover":2}
+103 \N \N {"takeover":3}
+104 \N \N {"takeover":4}
+105 \N \N {"takeover":5}
+106 \N \N {"takeover":6}
+107 \N \N {"takeover":7}
+108 \N \N {"takeover":8}
+109 \N \N {"takeover":9}
+200 400 \N {"new_hot_0":0,"p":{"q":{"a":400}}}
+201 401 \N {"new_hot_0":1,"p":{"q":{"a":401}}}
+202 402 \N {"new_hot_0":2,"p":{"q":{"a":402}}}
+203 403 \N {"new_hot_0":3,"p":{"q":{"a":403}}}
+204 {"child":{"leaf":500}} 500
{"new_hot_0":4,"p":{"q":{"a":{"child":{"leaf":500}}}}}
+205 \N \N {"new_hot_0":5}
+206 410 \N {"new_hot_1":0,"p":{"q":{"a":410}}}
+207 411 \N {"new_hot_1":1,"p":{"q":{"a":411}}}
+208 412 \N {"new_hot_1":2,"p":{"q":{"a":412}}}
+209 413 \N {"new_hot_1":3,"p":{"q":{"a":413}}}
+210 {"child":{"leaf":501}} 501
{"new_hot_1":4,"p":{"q":{"a":{"child":{"leaf":501}}}}}
+211 \N \N {"new_hot_1":5}
+212 420 \N {"new_hot_2":0,"p":{"q":{"a":420}}}
+213 421 \N {"new_hot_2":1,"p":{"q":{"a":421}}}
+214 422 \N {"new_hot_2":2,"p":{"q":{"a":422}}}
+215 423 \N {"new_hot_2":3,"p":{"q":{"a":423}}}
+216 {"child":{"leaf":502}} 502
{"new_hot_2":4,"p":{"q":{"a":{"child":{"leaf":502}}}}}
+217 \N \N {"new_hot_2":5}
+
+-- !repromoted_layout --
+id int Yes true \N
+v variant<PROPERTIES ("variant_max_subcolumns_count" =
"1","variant_enable_typed_paths_to_sparse" =
"false","variant_max_sparse_column_statistics_size" =
"10000","variant_sparse_hash_shard_count" = "1")> Yes false \N
NONE
+v.p.q.a json Yes false \N NONE
+
+-- !compaction_status --
+[OK]
+
+-- !output_rows --
+1 1 15
+
+-- !mow_before_second --
+1 {"a":{"b":901}} {"b":901} 901 {"p":{"a":{"b":901}}}
+3 {"a":903} 903 \N {"p":{"a":903}}
+4 \N \N \N {"hot_0":4}
+5 \N \N \N {"hot_0":5}
+6 {"a":[]} [] \N {"p":{"a":[]}}
+7 {"a":21} 21 \N {"hot_1":2,"p":{"a":21}}
+8 \N \N \N {}
+9 \N \N \N {"hot_1":4}
+10 \N \N \N {"hot_1":5}
+11 {"a":12} 12 \N {"hot_2":1,"p":{"a":12}}
+12 {"a":22} 22 \N {"hot_2":2,"p":{"a":22}}
+13 {"a":{"b":32}} {"b":32} 32 {"hot_2":3,"p":{"a":{"b":32}}}
+14 \N \N \N {"hot_2":4}
+15 \N \N \N {"hot_2":5}
+
+-- !compaction_status --
+[OK]
+
+-- !output_rows --
+1 1 14
+
+-- !mow_second --
+1 {"a":{"b":901}} {"b":901} 901 {"p":{"a":{"b":901}}}
+3 {"a":903} 903 \N {"p":{"a":903}}
+4 \N \N \N {"hot_0":4}
+5 \N \N \N {"hot_0":5}
+6 {"a":[]} [] \N {"p":{"a":[]}}
+7 {"a":21} 21 \N {"hot_1":2,"p":{"a":21}}
+8 \N \N \N {}
+9 \N \N \N {"hot_1":4}
+10 \N \N \N {"hot_1":5}
+11 {"a":12} 12 \N {"hot_2":1,"p":{"a":12}}
+12 {"a":22} 22 \N {"hot_2":2,"p":{"a":22}}
+13 {"a":{"b":32}} {"b":32} 32 {"hot_2":3,"p":{"a":{"b":32}}}
+14 \N \N \N {"hot_2":4}
+15 \N \N \N {"hot_2":5}
+
diff --git a/regression-test/suites/variant_p0/sparse_ancestor_promotion.groovy
b/regression-test/suites/variant_p0/sparse_ancestor_promotion.groovy
new file mode 100644
index 00000000000..f489baa9cf6
--- /dev/null
+++ b/regression-test/suites/variant_p0/sparse_ancestor_promotion.groovy
@@ -0,0 +1,92 @@
+// 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.
+
+suite("sparse_ancestor_promotion", "p0") {
+ sql "SET default_variant_enable_doc_mode = false"
+ def backendIPs = [:]
+ def backendHttpPorts = [:]
+ getBackendIpHttpPort(backendIPs, backendHttpPorts)
+ def checkFullStatus = { String tableName ->
+ // Waiting for completion alone also accepts a failed compaction with
intact old
+ // rowsets. Record its status so unchanged query results cannot hide
that failure.
+ for (def tablet : sql_return_maparray("SHOW TABLETS FROM
${tableName}")) {
+ def (code, out, err) = be_show_tablet_status(
+ backendIPs["${tablet.BackendId}"],
backendHttpPorts["${tablet.BackendId}"], tablet.TabletId)
+ if (code != 0) {
+ throw new Exception("Unable to read compaction status: ${err}")
+ }
+ def status = parseJson(out)["last full
status"].toString().replace("'", "''")
+ qt_full_status "SELECT '${status}'"
+ }
+ }
+ for (def storageFormat : ["V2", "V3"]) {
+ for (def shardCount : [1, 3]) {
+ for (def keyModel : ["DUPLICATE", "UNIQUE"]) {
+ def tableName =
"sparse_ancestor_promotion_${storageFormat}_${shardCount}_${keyModel}"
+ def mowProperty = keyModel == "UNIQUE" ? ',
"enable_unique_key_merge_on_write"="true"' : ''
+ sql "DROP TABLE IF EXISTS ${tableName}"
+ sql """
+ CREATE TABLE ${tableName} (
+ id INT,
+ v VARIANT<PROPERTIES(
+ "variant_max_subcolumns_count"="1",
+ "variant_enable_nested_group"="false",
+ "variant_sparse_hash_shard_count"="${shardCount}"
+ )>
+ ) ${keyModel} KEY(id)
+ DISTRIBUTED BY HASH(id) BUCKETS 1
+ PROPERTIES("replication_num"="1",
"disable_auto_compaction"="true",
+
"storage_format"="${storageFormat}"${mowProperty})
+ """
+ // Each rowset materializes hot_i (5 rows), leaving a (2) and
a.b (1)
+ // sparse. Across rowsets a (6) wins over every hot_i (5) and
is promoted.
+ // Its physical count must stay 6: a.b must not become another
non-null a.
+ for (int batch = 0; batch < 3; ++batch) {
+ int id = batch * 5
+ int value = batch * 100
+ sql """INSERT INTO ${tableName} VALUES
+ (${id + 1}, parse_to_variant('{"a":${value +
1},"hot_${batch}":1}')),
+ (${id + 2}, parse_to_variant('{"a":${value +
2},"hot_${batch}":2}')),
+ (${id + 3}, parse_to_variant('{"hot_${batch}":3}')),
+ (${id + 4}, parse_to_variant('{"hot_${batch}":4}')),
+ (${id + 5}, parse_to_variant('{"a":{"b":${value +
5}},"hot_${batch}":5}'))
+ """
+ }
+ def projection = """SELECT id, CAST(v['a'] AS STRING),
v['a']['b'],
+ CAST(v AS STRING) FROM ${tableName} ORDER BY id"""
+ qt_before projection
+ trigger_and_wait_compaction(tableName, "full", 1800)
+ checkFullStatus(tableName)
+ qt_after_first projection
+
+ // Add another source with a sparse ancestor and compact
against the new
+ // materialized ancestor, exercising both sides of the
promotion boundary.
+ sql """INSERT INTO ${tableName} VALUES
+ (16, parse_to_variant('{"a":301,"hot_3":1}')),
+ (17, parse_to_variant('{"a":302,"hot_3":2}')),
+ (18, parse_to_variant('{"hot_3":3}')),
+ (19, parse_to_variant('{"hot_3":4}')),
+ (20, parse_to_variant('{"a":{"b":305},"hot_3":5}'))
+ """
+ qt_before_second projection
+ trigger_and_wait_compaction(tableName, "full", 1800)
+ checkFullStatus(tableName)
+ qt_after_second projection
+ }
+ }
+ }
+}
diff --git
a/regression-test/suites/variant_p0/sparse_ancestor_promotion_extended.groovy
b/regression-test/suites/variant_p0/sparse_ancestor_promotion_extended.groovy
new file mode 100644
index 00000000000..823438b66e7
--- /dev/null
+++
b/regression-test/suites/variant_p0/sparse_ancestor_promotion_extended.groovy
@@ -0,0 +1,140 @@
+// 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.
+
+import groovy.json.JsonOutput
+
+suite("sparse_ancestor_promotion_extended", "p0,nonConcurrent") {
+ sql "SET default_variant_enable_doc_mode = false"
+ sql "SET describe_extend_variant_column = true"
+ def backendIPs = [:]
+ def backendHttpPorts = [:]
+ getBackendIpHttpPort(backendIPs, backendHttpPorts)
+ def statusOf = { tablet ->
+ def (code, out, err) = be_show_tablet_status(
+ backendIPs["${tablet.BackendId}"],
backendHttpPorts["${tablet.BackendId}"], tablet.TabletId)
+ assert code == 0 : err
+ parseJson(out)
+ }
+ def compact = { String tableName ->
+ def tablet = sql_return_maparray("SHOW TABLETS FROM ${tableName}")[0]
+ def before = statusOf(tablet)
+ assert before.rowsets.size() > 1 : before
+ trigger_and_wait_compaction(tableName, "full", 300)
+ def after = statusOf(tablet)
+ // Query equality alone also passes when compaction fails and leaves
inputs intact.
+ qt_compaction_status "SELECT '${after['last full
status'].toString().replace("'", "''")}'"
+ assert after.rowsets.size() == 1 : after
+ assert after.rowsets.intersect(before.rowsets).isEmpty() : after
+ qt_output_rows """SELECT count(*), sum(NUM_SEGMENTS),
sum(ROWSET_NUM_ROWS)
+ FROM information_schema.rowsets WHERE TABLET_ID=${tablet.TabletId}
AND NUM_SEGMENTS>0"""
+ logger.info("${tableName}: replaced ${before.rowsets} with
${after.rowsets}")
+ }
+ // The existing sparse_ancestor_promotion suite covers the
format/shard/key-model
+ // matrix. Add one deep-path case with heterogeneous values and three
generations
+ // of promotion, demotion and promotion, all on the same tablet.
+ sql "DROP TABLE IF EXISTS sparse_ancestor_extended_deep"
+ sql """CREATE TABLE sparse_ancestor_extended_deep (id INT, v
VARIANT<PROPERTIES(
+ "variant_max_subcolumns_count"="1",
"variant_enable_nested_group"="false",
+ "variant_sparse_hash_shard_count"="1")>)
+ DUPLICATE KEY(id) DISTRIBUTED BY HASH(id) BUCKETS 1
+ PROPERTIES("replication_num"="1", "disable_auto_compaction"="true",
+ "storage_format"="V3")"""
+ def wrap = { value -> [q: [a: value]] }
+ def payloads = [[11, 'text', true], [[], [1, null, [x: 2]], 1.5],
+ [[:], null, [[x: 3], [x: 4]]]]
+ for (int batch = 0; batch < 3; ++batch) {
+ def rows = []
+ for (int row = 0; row < 6; ++row) {
+ def value = [("hot_${batch}".toString()): row]
+ if (row < 3) {
+ value['p'] = wrap(payloads[batch][row])
+ } else if (row == 3) {
+ value['p'] = wrap([child: [leaf: batch + 100]])
+ } else if (row == 4) {
+ value['a-'] = batch + 200
+ value['a%2Eb'] = batch + 300
+ }
+ rows.add("(${batch * 6 + row + 1},
parse_to_variant('${JsonOutput.toJson(value)}'))")
+ }
+ sql "INSERT INTO sparse_ancestor_extended_deep VALUES
${rows.join(',')}"
+ }
+ def projection = """SELECT id, CAST(v['p']['q']['a'] AS STRING),
v['p']['q']['a']['child']['leaf'],
+ CAST(v AS STRING) FROM sparse_ancestor_extended_deep ORDER BY id"""
+ qt_before projection
+ setBeConfigTemporary([enable_vertical_compaction:true,
+ enable_vertical_compact_variant_subcolumns:true,
+ vertical_compaction_num_columns_per_group:1]) {
+ compact('sparse_ancestor_extended_deep')
+ qt_promoted projection
+ order_qt_promoted_layout "DESC sparse_ancestor_extended_deep"
+ // Ten occurrences force the ancestor back into sparse storage.
+ sql """INSERT INTO sparse_ancestor_extended_deep SELECT 100 + number,
+ parse_to_variant(concat('{"takeover":', number, '}'))
+ FROM numbers("number"="10")"""
+ compact('sparse_ancestor_extended_deep')
+ qt_demoted projection
+ order_qt_demoted_layout "DESC sparse_ancestor_extended_deep"
+ // A third generation promotes the ancestor from both old sparse
+ // output and newly written rowsets, while descendants stay sparse.
+ for (int batch = 0; batch < 3; ++batch) {
+ def rows = []
+ for (int row = 0; row < 6; ++row) {
+ def value = [("new_hot_${batch}".toString()): row]
+ if (row < 4) {
+ value['p'] = wrap(batch * 10 + row + 400)
+ } else if (row == 4) {
+ value['p'] = wrap([child: [leaf: batch + 500]])
+ }
+ rows.add("(${200 + batch * 6 + row},
parse_to_variant('${JsonOutput.toJson(value)}'))")
+ }
+ sql "INSERT INTO sparse_ancestor_extended_deep VALUES
${rows.join(',')}"
+ }
+ compact('sparse_ancestor_extended_deep')
+ qt_repromoted projection
+ order_qt_repromoted_layout "DESC sparse_ancestor_extended_deep"
+ }
+
+ // Updates and delete predicates exercise paths where physical count
equality
+ // cannot replace checking the visible latest row and its complete root
value.
+ sql "DROP TABLE IF EXISTS sparse_ancestor_extended_mow"
+ sql """CREATE TABLE sparse_ancestor_extended_mow (id INT, v
VARIANT<PROPERTIES(
+ "variant_max_subcolumns_count"="1",
"variant_enable_nested_group"="false",
+ "variant_sparse_hash_shard_count"="1")>) UNIQUE KEY(id)
+ DISTRIBUTED BY HASH(id) BUCKETS 1 PROPERTIES("replication_num"="1",
+ "disable_auto_compaction"="true",
"enable_unique_key_merge_on_write"="true",
+ "storage_format"="V3")"""
+ for (int batch = 0; batch < 3; ++batch) {
+ sql """INSERT INTO sparse_ancestor_extended_mow VALUES
+
(${batch*5+1},parse_to_variant('{"p":{"a":${batch+10}},"hot_${batch}":1}')),
+
(${batch*5+2},parse_to_variant('{"p":{"a":${batch+20}},"hot_${batch}":2}')),
+
(${batch*5+3},parse_to_variant('{"p":{"a":{"b":${batch+30}}},"hot_${batch}":3}')),
+ (${batch*5+4},parse_to_variant('{"hot_${batch}":4}')),
+ (${batch*5+5},parse_to_variant('{"hot_${batch}":5}'))"""
+ }
+ def mowProjection = """SELECT id, CAST(v['p'] AS STRING), CAST(v['p']['a']
AS STRING),
+ v['p']['a']['b'], CAST(v AS STRING) FROM sparse_ancestor_extended_mow
ORDER BY id"""
+ compact('sparse_ancestor_extended_mow')
+ sql """INSERT INTO sparse_ancestor_extended_mow VALUES
+ (1,parse_to_variant('{"p":{"a":{"b":901}}}')),
+ (3,parse_to_variant('{"p":{"a":903}}')),
+ (6,parse_to_variant('{"p":{"a":[]}}')),
+ (8,parse_to_variant('{"p":{"a":null}}'))"""
+ sql "DELETE FROM sparse_ancestor_extended_mow WHERE id=2"
+ qt_mow_before_second mowProjection
+ compact('sparse_ancestor_extended_mow')
+ qt_mow_second mowProjection
+}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]