eldenmoon commented on code in PR #69761:
URL: https://github.com/apache/doris/pull/69761#discussion_r4438597383
##########
fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/info/IndexDefinition.java:
##########
@@ -562,6 +572,66 @@ public Map<String, String> getProperties() {
return properties;
}
+ public boolean isVariantValuesIndex() {
+ return indexType == IndexType.INVERTED &&
InvertedIndexProperties.isVariantValuesIndex(properties);
+ }
+
+ private void validateVariantValuesIndex(DataType columnType,
+ TInvertedIndexFileStorageFormat invertedIndexFileStorageFormat) {
+ String scope =
properties.get(InvertedIndexUtil.VARIANT_INDEX_SCOPE_KEY);
+ if (scope == null) {
+ return;
+ }
+ if (!isVariantValuesIndex()) {
+ throw new AnalysisException("variant_index_scope must be values,
found: " + scope);
+ }
+ if (!columnType.isVariantType()) {
+ throw new AnalysisException(
+ "variant_index_scope=values can only be used on VARIANT
columns");
+ }
+ // Declared Decimal256 can be stored, but full-root assembly cannot
decode it.
+ // Reject this combination before accepting a table whose legal
inserts would fail.
+ VariantType variantType = (VariantType) columnType;
+ if (variantType.getEnableNestedGroup()) {
+ throw new AnalysisException("VARIANT values index does not support
NestedGroup");
+ }
+ if (variantType.getPredefinedFields().stream()
+ .anyMatch(field -> containsDecimal256(field.getDataType()))) {
+ throw new AnalysisException("VARIANT values index does not support
declared Decimal256 paths");
+ }
+ if (invertedIndexFileStorageFormat !=
TInvertedIndexFileStorageFormat.SNII) {
+ throw new AnalysisException(
+ "VARIANT values index requires
inverted_index_storage_format=SNII");
+ }
+ if
("true".equals(properties.get(InvertedIndexUtil.INVERTED_INDEX_SUPPORT_PHRASE_KEY)))
{
+ throw new AnalysisException("VARIANT values index does not support
support_phrase=true");
+ }
+ String analyzer =
properties.getOrDefault(InvertedIndexUtil.INVERTED_INDEX_ANALYZER_NAME_KEY,
"").trim();
+ if
(InvertedIndexProperties.INVERTED_INDEX_PARSER_NONE.equalsIgnoreCase(
+ InvertedIndexProperties.getInvertedIndexParser(properties))
+ && (analyzer.isEmpty()
+ ||
InvertedIndexProperties.INVERTED_INDEX_PARSER_NONE.equalsIgnoreCase(analyzer)))
{
+ throw new AnalysisException("VARIANT values index requires a
parser or analyzer with tokenization");
+ }
+ if (!analyzer.isEmpty()
+ &&
!IndexPolicy.BUILTIN_ANALYZERS.contains(InvertedIndexUtil.resolveAnalyzerName(analyzer)))
{
+ throw new AnalysisException("VARIANT values index supports only
built-in parsers and analyzers");
+ }
+ if
(properties.containsKey(InvertedIndexUtil.INVERTED_INDEX_PARSER_FIELD_PATTERN_KEY))
{
+ throw new AnalysisException("VARIANT values index does not support
field_pattern");
+ }
+ }
+
+ private static boolean containsDecimal256(DataType type) {
+ if (type instanceof DecimalV3Type) {
+ return ((DecimalV3Type) type).getPrecision() > 38;
+ }
+ if (type instanceof ArrayType) {
Review Comment:
Not reachable. VARIANT predefined fields accept only scalar types and one
level of ARRAY over them (`VariantType.supportSubType`,
`fe/fe-type/.../VariantType.java:215`, enforced at DDL by
`DataType.validateNestedType`). `VARIANT<'obj':STRUCT<...>>`,
`VARIANT<'m':MAP<...>>`, `ARRAY<MAP<...>>` and a nested `VARIANT<...>` are all
rejected with `VARIANT unsupported sub-type` before the index is checked, so
`containsDecimal256()` already covers every legal declared type (direct and
array).
##########
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:
Resolved at this head. On a table with an English values index, `ALTER TABLE
t ADD INDEX path_idx(v) USING INVERTED PROPERTIES("parser"="english")`
succeeds, and `v['s'] MATCH_ANY` uses it for new rowsets, the same as on a
table without a values index (older rowsets need BUILD INDEX in both cases).
##########
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:
Resolved at this head. On an English values-indexed table, `v MATCH_ANY
'hello' USING ANALYZER ENGLISH` (and `English`) is accepted in both WHERE and
the projection and returns the same rows as the lower-case spelling.
##########
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:
Fixed. CREATE TABLE now rejects a values index on a VARIANT with declared
Decimal256 paths, direct or inside ARRAY (`VARIANT values index does not
support declared Decimal256 paths`, `IndexDefinition.java:599`), so INSERT can
no longer fail after the DDL succeeded.
##########
fe/fe-core/src/main/java/org/apache/doris/nereids/rules/analysis/ExpressionAnalyzer.java:
##########
@@ -1019,12 +1025,75 @@ 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()
+ || (currentPlan != null && isScalarVariantMatchInput(left,
currentPlan, new HashMap<>()))
+ || (match.op() != Operator.MATCH_ANY && match.op() !=
Operator.MATCH_ALL))) {
+ left = TypeCoercionUtils.castIfNotSameType(left,
StringType.INSTANCE);
}
+
return match.withChildren(left, right);
}
+ // Alias and set-operation outputs can have neither a subpath nor a source
column.
+ // Prove that every producer is scalar before using STRING semantics. An
unknown
+ // producer or any actual root keeps VARIANT semantics and must bind an
analyzer.
+ private boolean isScalarVariantMatchInput(Expression input, Plan plan,
Map<Slot, Boolean> scalarCteOutputs) {
+ // Set-operation coercion can cast both root columns to a common
VARIANT
+ // layout. That changes storage properties, not root/subpath lineage.
+ if (input instanceof Cast && input.getDataType().isVariantType()
+ && input.child(0).getDataType().isVariantType()) {
+ return isScalarVariantMatchInput(input.child(0), plan,
scalarCteOutputs);
+ }
+ if (!(input instanceof SlotReference)) {
+ return true;
+ }
+ SlotReference slot = (SlotReference) input;
+ if (slot.hasSubColPath()) {
+ return true;
+ }
+ if (plan instanceof LogicalCTEConsumer) {
+ LogicalCTEConsumer consumer = (LogicalCTEConsumer) plan;
+ Slot producerSlot = consumer.getProducerSlot(slot);
+ Boolean scalar = scalarCteOutputs.get(producerSlot);
+ if (scalar != null) {
+ return scalar;
+ }
+ LogicalCTEProducer<?> producer =
getCascadesContext().getStatementContext()
+ .getCteProducerByCteId(consumer.getCteId());
+ // Shared CTE chains form a DAG; do not expand the same producer
at every reference.
+ scalar = isScalarVariantMatchInput(producerSlot, producer.child(),
scalarCteOutputs);
+ scalarCteOutputs.put(producerSlot, scalar);
+ return scalar;
+ }
+ if (plan instanceof LogicalSetOperation) {
+ LogicalSetOperation setOperation = (LogicalSetOperation) plan;
+ int index = setOperation.getOutput().indexOf(slot);
+ if (index >= 0) {
+ for (int i = 0; i < setOperation.arity(); i++) {
+ if
(!isScalarVariantMatchInput(setOperation.getRegularChildrenOutputs().get(i).get(index),
+ setOperation.child(i), scalarCteOutputs)) {
+ return false;
+ }
+ }
+ return true;
+ }
+ }
+ for (Expression expression : plan.getExpressions()) {
+ if (expression instanceof Alias && ((Alias)
expression).getExprId().equals(slot.getExprId())) {
+ return isScalarVariantMatchInput(expression.child(0), plan,
scalarCteOutputs);
+ }
+ }
+ for (Plan child : plan.children()) {
+ if (child.getOutputSet().contains(slot)) {
+ return isScalarVariantMatchInput(slot, child,
scalarCteOutputs);
+ }
+ }
+ return false;
Review Comment:
Resolved at this head. With `enable_match_without_inverted_index=true` and a
values-indexed `v` holding `["hello"]` / `["other"]`, `SELECT element MATCH_ANY
'hello' FROM t LATERAL VIEW explode_variant_array(v) e AS element` returns 1 /
0: the generated slot keeps the scalar STRING MATCH.
##########
be/src/storage/compaction/compaction.cpp:
##########
@@ -135,6 +136,135 @@ namespace {
constexpr size_t kSniiCompactionReadAheadBudgetBytes = 64ULL << 20;
+using SniiSourceFileReaders = std::map<std::string,
std::unique_ptr<IndexFileReader>>;
+
+Result<IndexFileReader*> open_snii_compaction_source(const io::FileSystemSPtr&
fs,
+ const std::string& prefix,
+ const
InvertedIndexFileInfo& file_info,
+ int64_t tablet_id,
+ SniiSourceFileReaders&
readers) {
+ auto it = readers.find(prefix);
+ if (it == readers.end()) {
+ auto reader = std::make_unique<IndexFileReader>(
+ fs, prefix, InvertedIndexStorageFormatPB::SNII, file_info,
tablet_id);
+ const auto status =
reader->init(config::inverted_index_read_buffer_size);
+ if (status.is<ErrorCode::INVERTED_INDEX_FILE_NOT_FOUND>()) {
+ return ResultError(Status::Error<INVERTED_INDEX_NOT_SUPPORTED>(
+ "source SNII index file is unavailable"));
+ }
+ // Missing postings allow rebuild; actual IO failures and corrupt
containers abort.
+ RETURN_IF_ERROR_RESULT(status);
+ DBUG_EXECUTE_IF("Compaction::snii_eligibility_reader_initialized",
DBUG_RUN_CALLBACK());
+ it = readers.emplace(prefix, std::move(reader)).first;
+ }
+ return it->second.get();
+}
+
+Status validate_snii_merge_source(
+ const snii::reader::LogicalIndexReader& reader, const TabletIndex&
source_meta,
+ const TabletIndex& destination, size_t ordinal,
+ std::optional<snii::compaction::SniiCompactionEligibility>&
eligibility) {
+ if (!eligibility.has_value()) {
+ const std::array<snii::compaction::PlainT2CompactionSource, 1> source
= {
+ snii::compaction::PlainT2CompactionSource {.reader =
std::cref(reader),
+ .index_meta =
std::cref(source_meta)}};
+ snii::compaction::SniiCompactionEligibility result;
+
RETURN_IF_ERROR(snii::compaction::validate_snii_compaction_eligibility(source,
destination,
+
&result));
+ eligibility = result;
+ return Status::OK();
+ }
+ if (source_meta.index_id() != destination.index_id() ||
+ source_meta.get_index_suffix() != destination.get_index_suffix() ||
+ source_meta.properties() != destination.properties()) {
+ return Status::Error<INVERTED_INDEX_NOT_SUPPORTED>(
+ "source SNII index identity or properties differ from
destination");
+ }
+ return snii::compaction::validate_snii_source_eligibility(reader, ordinal,
*eligibility);
+}
+
+Result<uint64_t> check_snii_merge_sources(const std::vector<RowsetSharedPtr>&
rowsets,
+ const TabletIndex& destination,
int64_t tablet_id,
+ SniiSourceFileReaders& readers) {
+ size_t segment_count = 0;
+ for (const auto& rowset : rowsets) {
+ segment_count += rowset->num_segments();
+ }
+ if (segment_count == 0 ||
+ segment_count >
+ kSniiCompactionReadAheadBudgetBytes /
+
snii::compaction::SniiPlainT2MergePlan::kMinReadAheadBudgetPerSource) {
+ return ResultError(Status::Error<INVERTED_INDEX_NOT_SUPPORTED>(
+ "source index unavailable or read-ahead budget gate failed"));
+ }
+ const int32_t uid = destination.col_unique_ids().front();
+ std::optional<snii::compaction::SniiCompactionEligibility> eligibility;
+ size_t ordinal = 0;
+ // Guaranteed merge allocations: read-ahead plus one ubiquitous term's
docids/frequencies
+ // and the NULL docids from each source. Positions retain the existing
streamed accounting.
+ uint64_t estimated_bytes = kSniiCompactionReadAheadBudgetBytes;
+ for (const auto& rowset : rowsets) {
+ if
(static_cast<BetaRowset*>(rowset.get())->is_skip_index_compaction(uid)) {
+ return ResultError(Status::Error<INVERTED_INDEX_NOT_SUPPORTED>(
+ "source SNII index compaction is disabled"));
+ }
+ const auto source_metas =
rowset->tablet_schema()->inverted_indexs(uid);
+ const auto source_it = std::ranges::find_if(source_metas, [&](const
TabletIndex* index) {
+ return index->index_id() == destination.index_id();
+ });
+ const auto fs = rowset->rowset_meta()->fs();
+ if (source_it == source_metas.end() || fs == nullptr) {
+ return ResultError(Status::Error<INVERTED_INDEX_NOT_SUPPORTED>(
+ "source SNII index metadata or file system is
unavailable"));
+ }
+ for (auto segment : rowset->segments()) {
+ const auto path = segment.path();
+ if (!path.has_value()) {
+ return ResultError(Status::Error<INVERTED_INDEX_NOT_SUPPORTED>(
+ "source SNII segment path is unavailable"));
+ }
+ const std::string prefix
{InvertedIndexDescriptor::get_index_file_path_prefix(*path)};
+ const auto file_reader = open_snii_compaction_source(
+ fs, prefix, segment.inverted_index_file_info(), tablet_id,
readers);
+ if (!file_reader.has_value()) {
+ return ResultError(file_reader.error());
+ }
+ auto source =
+ (*file_reader)
+ ->open_snii_index(*source_it, nullptr,
+
snii::reader::LogicalIndexOpenMode::kCompaction);
+ if (!source.has_value() &&
+ source.error().is<ErrorCode::INVERTED_INDEX_FILE_NOT_FOUND>())
{
Review Comment:
Stale. Native merge eligibility now uses master's generic SNII check: a
source whose logical index cannot be opened (including
`INVERTED_INDEX_SNII_NOT_FOUND`) marks that index ineligible, and it is rebuilt
from the raw column (`compaction.cpp:1759`, `:1803`).
`construct_index_compaction_columns()` returns `void`, so this cannot abort
compaction.
--
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]