Skip to content

fix(server): filter index results before input ordering - #3182

Open
contrueCT wants to merge 4 commits into
apache:masterfrom
contrueCT:fix-issue-3180
Open

fix(server): filter index results before input ordering#3182
contrueCT wants to merge 4 commits into
apache:masterfrom
contrueCT:fix-issue-3180

Conversation

@contrueCT

@contrueCT contrueCT commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

Purpose of the PR

Root cause

HStore does not sort these backend results by input IDs. HugeGraph therefore restores input
order with InputOrderIterator, which may prefetch the next flattened subquery. That prefetch
updates the shared origin query's resultsFilter. Previously, record matching happened after
input-order restoration, so rows from subquery A could be checked with subquery B's filter.

HStore query filter ordering before and after the fix

CI follow-up

Count-query fallbacks wrap the original ConditionQuery in an IdQuery. A transaction-cache
hit could therefore return index candidates before the original condition was applied. The
cache eligibility check now follows the origin-query chain, and both vertex and edge caches are
bypassed whenever any query in that chain still needs post-filtering.

Main Changes

  • Apply index-result matching to each backend subquery before restoring input order.
  • Keep undefined-label diagnostics and hidden/deleting-label filtering at the public graph-query
    boundary.
  • Skip vertex and edge transaction caches for queries whose origin chain still requires
    post-filtering, including search indexes, INDEX_FILTER, SORT_KEYS, and INDEX queries
    without a label.
  • Make the existing vertex regression deterministic by committing before querying, and add an
    equivalent edge regression plus cache-classification coverage.

Verifying these changes

  • Trivial rework / code cleanup without any test coverage. (No Need)
  • Already covered by existing tests, such as (please modify tests here).
  • Need tests and can be verified as follows:
    • mvn test -pl hugegraph-server/hugegraph-test -am -P unit-test
      • 682 tests, 0 failures, 0 errors, 1 skipped
    • Focused cache-classification unit test after the CI follow-up
      • 1 test, 0 failures, 0 errors
    • Focused core-test,hstore regression set
      • 4 tests, 0 failures, 0 errors on a freshly initialized PD/Store
    • Focused core-test,memory regression set
      • 4 tests, 0 failures, 0 errors
    • Focused core-test,rocksdb regression set
      • 4 tests, 0 failures, 0 errors
    • mvn clean compile -Dmaven.javadoc.skip=true
      • all 38 reactor modules compiled successfully
    • mvn editorconfig:format
      • no files required formatting

Does this PR potentially affect the following parts?

Documentation Status

  • Doc - TODO
  • Doc - Done
  • Doc - No Need

@codecov

codecov Bot commented Sep 1, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 52.00000% with 12 lines in your changes missing coverage. Please review.
✅ Project coverage is 36.98%. Comparing base (98477f0) to head (1019e61).

Files with missing lines Patch % Lines
...ugegraph/backend/cache/CachedGraphTransaction.java 36.84% 4 Missing and 8 partials ⚠️
Additional details and impacted files
@@             Coverage Diff              @@
##             master    #3182      +/-   ##
============================================
- Coverage     37.78%   36.98%   -0.81%     
+ Complexity     6556     6348     -208     
============================================
  Files           800      785      -15     
  Lines         68929    66845    -2084     
  Branches       9157     8892     -265     
============================================
- Hits          26046    24720    -1326     
+ Misses        39824    39189     -635     
+ Partials       3059     2936     -123     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@contrueCT
contrueCT marked this pull request as draft September 1, 2026 07:14
@contrueCT contrueCT changed the title fix(core): filter index results before input ordering fix(server): filter index results before input ordering Sep 1, 2026
@contrueCT
contrueCT marked this pull request as ready for review September 1, 2026 15:39

@bitflicker64 bitflicker64 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Blocking: no. Summary: The reordering is sound: moving index-candidate matching upstream of keepInputOrderIfNeeded means each record is tested while the shared resultsFilter still belongs to its own flattened sub-query, and the new cache guard compensates for filterUnmatchedRecords no longer running in queryVertices/queryEdges. Below are one hardening item, one question about a behavior change that looks incidental, and three maintenance items. Evidence: reviewed at head via git diff $(git merge-base FETCH_HEAD origin/master) FETCH_HEAD; queryNeedsPostFilter checked against every branch of rightResultFromIndexQuery (GraphTransaction.java:1915-1979); CI green at head apart from the codecov/project coverage threshold.

One finding has no line to anchor to, so it goes here. .github/images/issue-3180-query-filter-order.png adds 1,033,655 bytes to the repository. origin/master has no image files anywhere in the tree and no .github/images/ directory, so this introduces both a new directory convention and the repository's first binary asset. The PR body already loads the diagram from raw.githubusercontent.com on a fork SHA, so the description renders whether or not the file lands in apache/hugegraph. Please upload it through GitHub's own attachment upload on the PR description and drop the file from the diff.

}

private <T extends HugeElement> Iterator<T> filterUnmatchedRecords(
private <T extends HugeElement> Iterator<T> filterInvalidRecords(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ HugeFactoryAuthProxy.java:273 registers the method being renamed here, "filterUnmatchedRecords", with Reflection.registerMethodsToFilter. The new sibling filterInvalidRecords is not registered, so it remains enumerable via getDeclaredMethods() while the method it was split out of is hidden. This affects enumeration only: filterInvalidRecords is still private and access control is unchanged.

Please add "filterInvalidRecords" to the Reflection.registerMethodsToFilter(GraphTransaction.class, ...) list next to "filterUnmatchedRecords".

return new ExtendableIterator<>(edges.iterator(), rs);
}

private static boolean queryNeedsPostFilter(Query query) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 This predicate has to track GraphTransaction.rightResultFromIndexQuery (GraphTransaction.java:1915-1979), which decides whether a record is actually dropped, but nothing links the two.

The pairing is safe today. rightResultFromIndexQuery returns true early for a non-ConditionQuery whose immediate origin is also not a ConditionQuery (1920-1925), for edge + LABEL + conditions().size() == 1, and for edge + LABEL + optimized() == INDEX; otherwise it falls through to optimized() == NONE || cq.test(elem) at 1948. queryNeedsPostFilter allows caching only for the INDEX and NONE shapes, so it is strictly more conservative. The risk is drift: the rule lives in two classes in two shapes, and editing one would silently let the caches serve unmatched records.

Please derive both from a single helper. Note the refactor also has to move CachedGraphTransactionTest.testQueryNeedsPostFilter, which calls Whitebox.invokeStatic(CachedGraphTransaction.class, ..., "queryNeedsPostFilter", ...).

Iterator<HugeVertex> vertices = new MapperIterator<>(entries,
this::parseEntry);
vertices = this.filterExpiredResultFromBackend(query, vertices);
vertices = this.filterUnmatchedRecords(vertices, query);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 On the non-paging edge branch the filter runs in queryEdgesFromBackendInternal(cq) with cq already flattened, and re-flattening yields exactly one sub-query (ConditionQueryFlatten.flattenRelations returns ImmutableList.of(cq)), so cq owns its resultsFilter. Paging and non-ConditionQuery edge queries skip that flatten and go straight to queryEdgesFromBackendInternal at line 1080.

The vertex path has no outer flatten at all: this.query(query) flattens internally and every sub-query pushes its filter onto this same shared query through ConditionQuery.updateResultsFilter (ConditionQuery.java:708-727), called from QueryList.java:223.

It is correct today only because FilterIterator.fetch() tests each element immediately and WrappedIterator.hasNext() returns the buffered element, so the advance into the next sub-query always follows the test. Please state that invariant here, or bind the filter to the sub-query the records came from.

}

Iterator<HugeEdge> rs = super.queryEdgesFromBackend(query);
if (queryNeedsPostFilter(query)) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 This reads as dead code: the same call ran at line 402 on the same object, and the comment does not say why the answer can change.

It is not dead. ConditionQuery.optimized() propagates up the originQuery chain (ConditionQuery.java:681-696) and ConditionQuery.copy() sets originQuery(this) (line 577), so the flattened children point back at this query. super.queryEdgesFromBackend(query) reaches GraphIndexTransaction.queryIndex (line 383), which sets INDEX at line 402, and INDEX_FILTER can be set at line 609. So query.optimized() can move off NONE between the two checks.

Please expand the comment to name that, for example: super.queryEdgesFromBackend() may promote query.optimized() via origin-chain propagation, so re-check before caching.

"confirmType", 3, "type", 1, "kid", 3);

this.mayCommitTx();
this.commitTx();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Making the regression deterministic is the right call, since the bug only reproduces once the rows are in the backend store. But mayCommitTx() is a coin flip (BaseCoreTest.java:141-146), so this test used to exercise the uncommitted-transaction path roughly half the time, and that coverage is now gone rather than merely made reliable.

The uncommitted path is not incidental to this change: ConditionQuery.test deliberately skips resultsFilter for fresh elements (ConditionQuery.java:625), so it is a different branch of the code this PR touches.

Please keep the deterministic committed assertions and restore the other half, either by repeating the three query blocks after a mayCommitTx() or by adding a sibling test that queries before committing.


Iterator<HugeVertex> results = this.queryVerticesFromBackend(query);
results = this.filterUnmatchedRecords(results, query);
results = this.filterInvalidRecords(results, query);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ The split also reverses the two predicates relative to each other, which looks incidental rather than intended.

Before this PR a single filter ran the undefined-label warning, then hidden, then deleting-label, then rightResultFromIndexQuery. Now filterUnmatchedRecords runs upstream (864 and 1104) and filterInvalidRecords runs downstream (here and 1015). Two consequences:

  • Hidden and deleting-label elements now reach rightResultFromIndexQuery, so this.indexTx.asyncRemoveIndexLeft(cq, elem) (1939, 1955, 1972) can be scheduled for elements whose schema label is already being deleted. Previously they were dropped before reaching that point.
  • The "Left record is found" warning (1882-1884) is now unreachable on index queries for any record filterUnmatchedRecords already dropped, which is the left-index case that warning exists for.

Please confirm the reordering is intended, and if it is, either restore the warning for left records on index queries or note why it is no longer needed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug] HStore combined range/search index test intermittently drops results

2 participants