github-actions[bot] commented on code in PR #69761:
URL: https://github.com/apache/doris/pull/69761#discussion_r4400052215
##########
fe/fe-core/src/main/java/org/apache/doris/nereids/rules/analysis/ExpressionAnalyzer.java:
##########
@@ -1019,9 +1020,14 @@ public Expression visitMatch(Match match,
ExpressionRewriteContext context) {
right.toSql(), match.toSql(), right.getDataType()));
}
- if (left.getDataType().isVariantType()) {
- left = new Cast(left, right.getDataType());
+ // Only direct roots use leaf-token semantics. Ordinary object
subpaths keep their
+ // existing JSON-to-STRING scalar semantics, including when pruning is
disabled.
+ if (left.getDataType().isVariantType()
+ && (!(left instanceof SlotReference) || ((SlotReference)
left).hasSubColPath()
Review Comment:
[P1] Resolve subpath aliases before choosing root MATCH semantics
A bare VARIANT slot is not sufficient evidence of a direct root. For a table
with an English values index and an ordinary `parser=none` path index, consider
this analyzed plan:
```text
Project(o#2 MATCH_ANY 'hello')
Project(v#1['obj'] AS o#2)
Scan(t.v#1)
```
`Alias.toSlot()` gives this derived `o#2` no subpath, so this condition
omits its STRING cast. `MergeProjectable`/`PlanUtils.mergeProjections` then
substitutes `ElementAt(v#1,'obj')` without rerunning coercion. With
nested-column pruning and index queries disabled, the translator sees the input
root slot `v#1`, binds its English values index, and BE recursively matches the
object's leaves. For `v={"obj":{"s":"hello"}}`, the derived query returns true,
while direct `v['obj'] MATCH_ANY 'hello'` receives the required STRING cast and
keyword JSON semantics, returning false. Preserve verified root/subpath lineage
across aliases, or perform this classification after resolving them.
##########
fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/CheckMatchExpression.java:
##########
@@ -56,9 +57,22 @@ private Plan checkChildren(LogicalFilter<? extends Plan>
filter) {
+ " right operand is Literal. But meet expression %s",
matchExpression));
}
if (slotReference.getDataType().isVariantType() &&
!slotReference.hasSubColPath()) {
- throw new AnalysisException(String.format("VARIANT root column
does not support MATCH predicates. "
- + "Please query a subcolumn instead, for
example %s['field'] MATCH 'xxx'",
- slotReference.getName()));
+ if (matchExpression.getAnalyzer().isPresent()
+ &&
!"english".equals(matchExpression.getAnalyzer().get())) {
Review Comment:
[P2] Resolve analyzer names before enforcing English-only MATCH
On a valid English values-indexed table, `SELECT id FROM t WHERE v MATCH_ANY
'hello' USING ANALYZER ENGLISH` is rejected here. The parser preserves the
identifier spelling and `Match` only trims it, so this comparison sees
`ENGLISH`. The normal binding path later calls
`InvertedIndexUtil.resolveAnalyzerName`, which resolves this built-in to
`english` case-insensitively when no legacy custom policy shadows that exact
name; a root projection with the same spelling therefore succeeds. Apply the
same canonical resolution before enforcing the restriction and test
uppercase/mixed-case built-in names in filters as well as projections.
##########
be/src/exec/common/variant_util.cpp:
##########
@@ -852,6 +855,78 @@ Status
VariantCompactionUtil::aggregate_variant_extended_info(
return Status::OK();
}
+Status VariantCompactionUtil::root_index_plan_preserves_tokens(const
VariantExtendedInfo& info,
+ const
TabletSchema& plan,
+ int32_t
column_uid) {
+ const auto& parent = plan.column_by_uid(column_uid);
+ if (parent.variant_enable_nested_group()) {
+ return Status::NotSupported(
+ "root index native merge does not support nested-group
conversion");
+ }
+ // Sparse paths have no complete type metadata. A newly materialized
sparse path can
+ // change boolean/numeric or floating text; keep the whole value for the
rebuild writer.
+ if (!info.sparse_paths.empty() || !parent.get_sub_columns().empty()) {
+ return Status::NotSupported("root index conversion includes sparse or
declared paths");
+ }
+ // The VARIANT root may itself hold promoted scalar/array values, even
when there is
+ // no extracted column. Include that runtime merge in the proof as well.
+ if (!parent.variant_enable_doc_mode()) {
+ const auto root = info.path_to_data_types.find(PathInData());
+ if (root != info.path_to_data_types.end() && !root->second.empty()) {
+ const auto first = remove_nullable(root->second.front());
+ for (const auto& type : root->second) {
+ if (!first->equals(*remove_nullable(type))) {
+ return Status::NotSupported(
+ "root scalar conversion is not proven token
preserving");
+ }
+ }
+ }
+ }
+ for (const auto& column : plan.columns()) {
+ if (!column->is_extracted_column() || column->parent_unique_id() !=
column_uid ||
Review Comment:
[P1] Prove visibility when materialized paths move into sparse storage
Checking only the output extracted-column types misses a layout change that
alters the logical leaves. With ordinary storage and
`variant_max_subcolumns_count=2`, let R0 contain `{"a":"needle","a.b":[]}` and
`{"a.b":[1]}`. Both paths are materialized; the assembler ignores materialized
empty arrays, so row 0 exposes `a="needle"` and its source index correctly
contains `needle`. Let R1 contain at least three rows `{"c":1,"d":2}`. All
sources have no sparse paths, but the final top-two plan extracts `c,d` and
moves `a,a.b` into sparse storage. This proof checks `c,d`, skips the sparse
map and permits native merge. `SparseColumnMergeIterator` serializes the
non-null `[]`; as a sparse cell it becomes a visible descendant and suppresses
`a`, leaving `{"a":{"b":[]}}` while the merged index still matches `needle`.
Row counts and path statistics remain equal, so publication checks pass.
Reject/rebuild such transitions or preserve their logical visibility. This
starts with valid source pos
tings and is separate from indexing already-hidden ancestors.
##########
be/src/storage/segment/variant/v2/variant_shredder.cpp:
##########
@@ -613,6 +617,88 @@ struct VariantShredder::Impl {
return publish_materialized(selection, candidate_builders, result);
}
+ // Typed paths are indexed only after the same conversion used by storage.
Merge compact
+ // rowids instead of scanning every path for every row or reconstructing
complete objects.
+ //
NOLINTNEXTLINE(readability-function-size,readability-function-cognitive-complexity,readability-make-member-function-const):
keep encoded leaf ownership and synchronous writer consumption in one row
scope; the root index writers are mutated.
+ Status write_converted_root_indexes(const VariantShreddedColumns& result,
+ const
DorisVector<VariantPathBuilder*>& builders) {
+ using namespace variant_v2::variant_assembler_detail;
+ using Position = std::pair<size_t, size_t>;
+ std::priority_queue<Position, DorisVector<Position>, std::greater<>>
pending;
+ DorisVector<PreparedMaterializedColumn> values;
+ DorisVector<size_t> positions(builders.size(), 0);
+ values.reserve(builders.size());
+ for (size_t index = 0; index < builders.size() &&
+ options.physical_layout !=
VariantShredderPhysicalLayout::DOC;
+ ++index) {
+ const auto* builder = builders[index];
+ if (builder->rowids().empty()) {
+ values.emplace_back();
+ continue;
+ }
+ values.push_back(prepare_materialized_column(builder->type(),
builder->column().get(),
+
builder->rowids().size()));
+ pending.emplace(builder->rowids().front(), index);
+ }
+ const auto& roots = assert_cast<const
ColumnString&>(*result.root_jsonb);
+ DorisVector<size_t> row_paths;
+ row_paths.reserve(builders.size() + 1);
+ for (size_t row = 0; row < rows; ++row) {
+ row_paths.clear();
+ VariantBatchBuilder encoded;
+ if (!index_nulls[row] && !roots.get_data_at(row).empty()) {
+ auto output = encoded.begin_row();
+ jsonb_to_variant(roots.get_data_at(row), output, 0);
+ output.finish();
+ row_paths.push_back(builders.size());
+ }
+ if (!index_nulls[row] &&
+ options.physical_layout == VariantShredderPhysicalLayout::DOC)
{
+ // DOC root reads the finalized binary buckets, not converted
typed sidecars.
+ for (const auto& bucket : result.binary_buckets) {
+ const auto& map = assert_cast<const
ColumnMap&>(*bucket.column);
+ const auto& cells = assert_cast<const
ColumnString&>(map.get_values());
+ const auto& offsets = map.get_offsets();
+ const size_t begin = row == 0 ? 0 : offsets[row - 1];
+ for (size_t cell = begin; cell < offsets[row]; ++cell) {
+ auto output = encoded.begin_row();
+
RETURN_IF_ERROR(variant_v2::append_v1_storage_cell(cells.get_data_at(cell),
+
output, 0));
+ output.finish();
+ row_paths.push_back(builders.size());
+ }
+ }
+ }
+ while (!pending.empty() && pending.top().first == row) {
+ const size_t index = pending.top().second;
+ pending.pop();
+ const size_t position = positions[index]++;
+ DCHECK(!index_nulls[row]);
+ auto output = encoded.begin_row();
+ RETURN_IF_ERROR(append_materialized_value(values[index],
position, output, 0));
Review Comment:
[P2] Handle declared Decimal256 paths before accepting the values index
With `enable_decimal256=true` and ordinary storage, CREATE TABLE accepts `v
VARIANT<'wide':DECIMAL(76,2)>` together with an English values index. Inserting
`parse_to_variant('{"wide":"123.45","text":"hello"}')` then converts the
declared builder to Decimal256 and fails at this new mandatory call:
`append_materialized_value` explicitly returns NotSupported for
TYPE_DECIMAL256. Existing `variant_decimal256_storage_error.groovy` verifies
that these declared schemas can INSERT successfully without a values index. Its
full-root SELECT is deliberately unsupported already; this is a new ingestion
failure after successful DDL, rather than an old MATCH regression. Handle these
no-token leaf/storage types without breaking ingestion, or reject the
incompatible values-index schema during DDL instead of accepting a table whose
legal inserts fail.
##########
be/src/storage/segment/variant/v2/variant_shredder.cpp:
##########
@@ -613,6 +617,88 @@ struct VariantShredder::Impl {
return publish_materialized(selection, candidate_builders, result);
}
+ // Typed paths are indexed only after the same conversion used by storage.
Merge compact
+ // rowids instead of scanning every path for every row or reconstructing
complete objects.
+ //
NOLINTNEXTLINE(readability-function-size,readability-function-cognitive-complexity,readability-make-member-function-const):
keep encoded leaf ownership and synchronous writer consumption in one row
scope; the root index writers are mutated.
+ Status write_converted_root_indexes(const VariantShreddedColumns& result,
+ const
DorisVector<VariantPathBuilder*>& builders) {
+ using namespace variant_v2::variant_assembler_detail;
+ using Position = std::pair<size_t, size_t>;
+ std::priority_queue<Position, DorisVector<Position>, std::greater<>>
pending;
+ DorisVector<PreparedMaterializedColumn> values;
+ DorisVector<size_t> positions(builders.size(), 0);
+ values.reserve(builders.size());
+ for (size_t index = 0; index < builders.size() &&
+ options.physical_layout !=
VariantShredderPhysicalLayout::DOC;
+ ++index) {
+ const auto* builder = builders[index];
+ if (builder->rowids().empty()) {
+ values.emplace_back();
+ continue;
+ }
+ values.push_back(prepare_materialized_column(builder->type(),
builder->column().get(),
+
builder->rowids().size()));
+ pending.emplace(builder->rowids().front(), index);
+ }
+ const auto& roots = assert_cast<const
ColumnString&>(*result.root_jsonb);
+ DorisVector<size_t> row_paths;
+ row_paths.reserve(builders.size() + 1);
+ for (size_t row = 0; row < rows; ++row) {
+ row_paths.clear();
+ VariantBatchBuilder encoded;
+ if (!index_nulls[row] && !roots.get_data_at(row).empty()) {
+ auto output = encoded.begin_row();
+ jsonb_to_variant(roots.get_data_at(row), output, 0);
+ output.finish();
+ row_paths.push_back(builders.size());
+ }
+ if (!index_nulls[row] &&
+ options.physical_layout == VariantShredderPhysicalLayout::DOC)
{
+ // DOC root reads the finalized binary buckets, not converted
typed sidecars.
+ for (const auto& bucket : result.binary_buckets) {
+ const auto& map = assert_cast<const
ColumnMap&>(*bucket.column);
+ const auto& cells = assert_cast<const
ColumnString&>(map.get_values());
+ const auto& offsets = map.get_offsets();
+ const size_t begin = row == 0 ? 0 : offsets[row - 1];
+ for (size_t cell = begin; cell < offsets[row]; ++cell) {
+ auto output = encoded.begin_row();
+
RETURN_IF_ERROR(variant_v2::append_v1_storage_cell(cells.get_data_at(cell),
+
output, 0));
+ output.finish();
+ row_paths.push_back(builders.size());
+ }
+ }
+ }
+ while (!pending.empty() && pending.top().first == row) {
+ const size_t index = pending.top().second;
+ pending.pop();
+ const size_t position = positions[index]++;
+ DCHECK(!index_nulls[row]);
+ auto output = encoded.begin_row();
+ RETURN_IF_ERROR(append_materialized_value(values[index],
position, output, 0));
+ output.finish();
+ row_paths.push_back(index);
+ if (positions[index] < builders[index]->rowids().size()) {
+
pending.emplace(builders[index]->rowids()[positions[index]], index);
+ }
+ }
+ // Root token values borrow bytes until end_document; retain all
encoded leaves
+ // for this row through synchronous SNII consumption.
+ auto batch = encoded.finish_batch();
+ for (auto* writer : options.root_index_writers) {
+ RETURN_IF_ERROR(writer->begin_document(index_nulls[row] != 0));
+ }
+ for (size_t leaf = 0; leaf < row_paths.size(); ++leaf) {
+
RETURN_IF_ERROR(append_variant_root_index_leaves(options.root_index_writers,
Review Comment:
[P1] Index only paths visible in the final logical row
For an ordinary values-indexed column, `{"a":"hidden","a.b":"visible"}`
creates distinct `a` and `a.b` builders, and this loop indexes both. Full-root
reads instead call `VariantAssembler::append_visible_merge_values`, which
suppresses `a` in favor of its descendant and reconstructs
`{"a":{"b":"visible"}}`. Consequently, `WHERE v MATCH_ANY 'hidden'` returns
this row with index queries enabled and excludes it on the scalar path; `NOT`
can lose the row instead. The DOC cell loop has the same problem. Apply the
assembler's final-row visibility rules before tokenizing, and cover
materialized/sparse and DOC ancestor collisions with index/scan parity cases.
##########
fe/fe-core/src/main/java/org/apache/doris/analysis/InvertedIndexUtil.java:
##########
@@ -504,9 +519,18 @@ public static boolean
canHaveMultipleInvertedIndexes(DataType colType, List<Inde
return false;
}
+ if
(indexDefs.stream().filter(IndexDefinition::isVariantRootIndex).count() > 1) {
+ return false;
+ }
+
Set<String> analyzerKeys = new HashSet<>();
Set<String> analyzerSelectors = new HashSet<>();
for (IndexDefinition indexDef : indexDefs) {
+ // Root and path indexes bind disjoint predicates. Only path
indexes share
+ // analyzer identities/selectors; the single-root constraint is
checked above.
+ if (indexDef.isVariantRootIndex()) {
Review Comment:
[P2] Apply root/path analyzer separation to ordinary index additions
This exemption only reaches CREATE TABLE. After creating `t` with an English
values index, `ALTER TABLE t ADD INDEX path_idx(v) USING INVERTED
PROPERTIES("parser"="english")` still reaches
`SchemaChangeHandler.checkDuplicateIndexes`, which compares against the root
index's analyzer selector and throws that the same selector already exists.
Declaring these two indexes together in CREATE TABLE succeeds. The added index
here is an ordinary path index, so the restriction on adding a values index
does not apply. Use the same root/path scope distinction in the ALTER duplicate
checker and cover this addition path.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]