Repository navigation
fix(server): reject incomplete cross-label index coverage #3243
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
ae90ad2
f435a1f
0de33ed
7f5a917
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -480,7 +480,7 @@ private IdHolderList queryByUserprop(ConditionQuery query) { | |
| } | ||
| } | ||
| } | ||
| Set<MatchedIndex> indexes = this.collectMatchedIndexes(query); | ||
| Set<MatchedIndex> indexes = this.collectMatchedIndexes(query, true); | ||
| if (indexes.isEmpty()) { | ||
| Id label = query.condition(HugeKeys.LABEL); | ||
| throw noIndexException(this.graph(), query, label); | ||
|
|
@@ -492,16 +492,20 @@ private IdHolderList queryByUserprop(ConditionQuery query) { | |
| return IdHolderList.empty(paging); | ||
| } | ||
|
|
||
| // Do index query | ||
| IdHolderList holders = new IdHolderList(paging); | ||
| // Validate the whole plan before opening any backend iterators. A later | ||
| // label's unavailable index must not leave earlier iterators unowned. | ||
| for (MatchedIndex index : indexes) { | ||
| for (IndexLabel il : index.indexLabels()) { | ||
| validateIndexLabel(il); | ||
| } | ||
| if (paging && index.indexLabels().size() > 1) { | ||
| throw new NotSupportException("joint index query in paging"); | ||
| } | ||
| } | ||
|
|
||
| // Do index query | ||
| IdHolderList holders = new IdHolderList(paging); | ||
| for (MatchedIndex index : indexes) { | ||
| if (index.containsSearchIndex()) { | ||
| // Do search-index query | ||
| holders.addAll(this.doSearchIndex(query, index)); | ||
|
|
@@ -765,8 +769,13 @@ private PageIds doIndexQueryOnce(IndexLabel indexLabel, | |
| } | ||
| } | ||
|
|
||
| @Watched(prefix = "index") | ||
| private Set<MatchedIndex> collectMatchedIndexes(ConditionQuery query) { | ||
| return this.collectMatchedIndexes(query, false); | ||
| } | ||
|
|
||
| @Watched(prefix = "index") | ||
| private Set<MatchedIndex> collectMatchedIndexes(ConditionQuery query, | ||
| boolean requireCompleteCoverage) { | ||
| ISchemaTransaction schema = this.params().schemaTransaction(); | ||
| Id label = query.condition(HugeKeys.LABEL); | ||
|
|
||
|
|
@@ -797,6 +806,18 @@ private Set<MatchedIndex> collectMatchedIndexes(ConditionQuery query) { | |
| } | ||
| } | ||
|
|
||
| if (requireCompleteCoverage) { | ||
| // Match the public result visibility, including flattened queries. | ||
| // Removal jobs drop indexes before data/schema; those labels must | ||
| // neither require coverage nor contribute unavailable index plans. | ||
| // Cleanup callers still need to inspect invisible labels' indexes. | ||
| Query visibility = query.rootOriginQuery(); | ||
| schemaLabels = schemaLabels.stream().filter(schemaLabel -> | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| (visibility.showHidden() || !schemaLabel.hidden()) && | ||
| (visibility.showDeleting() || !schemaLabel.status().deleting())) | ||
| .collect(Collectors.toList()); | ||
| } | ||
|
|
||
| // Collect MatchedIndex for each SchemaLabel | ||
| Set<MatchedIndex> matchedIndexes = InsertionOrderUtil.newSet(); | ||
| for (SchemaLabel schemaLabel : schemaLabels) { | ||
|
|
@@ -805,9 +826,57 @@ private Set<MatchedIndex> collectMatchedIndexes(ConditionQuery query) { | |
| matchedIndexes.add(index); | ||
| } | ||
| } | ||
| // An invalid value returns no results after an index is found. Keep | ||
| // that result before deciding whether index coverage is complete. | ||
| if (requireCompleteCoverage && !matchedIndexes.isEmpty() && | ||
| validQueryConditionValues(this.graph(), query)) { | ||
| this.checkIndexCoverage(query, schemaLabels, matchedIndexes); | ||
|
contrueCT marked this conversation as resolved.
|
||
| } | ||
| return matchedIndexes; | ||
| } | ||
|
|
||
| private void checkIndexCoverage(ConditionQuery query, | ||
| List<? extends SchemaLabel> labels, | ||
| Set<MatchedIndex> indexes) { | ||
| // Check at execution, before creating any lazy index iterator. Cleanup | ||
| // callers only need matching indexes and must not require full coverage. | ||
| Set<Id> requiredProperties = query.userpropKeys(); | ||
| if (this.graph().readMode().showOlap()) { | ||
| // OLAP properties are shared rather than declared on every label. | ||
| requiredProperties.removeIf(id -> this.graph().propertyKey(id).olap()); | ||
|
contrueCT marked this conversation as resolved.
contrueCT marked this conversation as resolved.
|
||
| } | ||
| if (requiredProperties.isEmpty()) { | ||
| // A shared OLAP index can match several labels, while MatchedIndex | ||
| // deduplicates those matches by index labels alone. | ||
| return; | ||
| } | ||
| Set<Id> covered = new HashSet<>(); | ||
| for (MatchedIndex index : indexes) { | ||
| covered.add(index.schemaLabel().id()); | ||
| } | ||
| for (SchemaLabel label : labels) { | ||
|
contrueCT marked this conversation as resolved.
|
||
| if (covered.contains(label.id()) || | ||
| !label.properties().containsAll(requiredProperties)) { | ||
| continue; | ||
| } | ||
| if (label instanceof EdgeLabel) { | ||
| EdgeLabel edgeLabel = (EdgeLabel) label; | ||
| // Base labels have no direct edges; their property indexes | ||
| // include edges of their sub-labels (see updateEdgeIndex()). | ||
| if (edgeLabel.isFather() || | ||
| edgeLabel.hasFather() && covered.contains(edgeLabel.fatherId())) { | ||
| continue; | ||
| } | ||
| } | ||
| // Authorization filters elements after the backend query. Do not | ||
| // disclose an implicitly discovered schema label in this error. | ||
| throw new NoIndexException("Incomplete index coverage for properties %s; " + | ||
|
contrueCT marked this conversation as resolved.
contrueCT marked this conversation as resolved.
|
||
| "specify a label or create compatible indexes " + | ||
| "for every candidate label", | ||
| this.graph().mapPkId2Name(query.userpropKeys())); | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Collect matched IndexLabel(s) in a SchemaLabel for a query | ||
| * | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.