Four independent findings turned up while fixing #6304 (PR #6308, merged as 383a82169). None of them belonged in that PR, and they are unrelated to each other beyond having been found in the same neighbourhood. Filed together for triage; each is self-contained and can be split out or closed on its own.
Line references are against bdf46f9cc. The item 1 repro below was run on that commit and reproduces exactly as tabulated.
1. A two-pattern MATCH returns no rows when both shared variables carry a label
The one possible wrong answer here, and the only item that is not about a fast path: it reproduces through the ordinary materialization pipeline, with no aggregate and no Graph Analytical View.
Two comma-separated patterns sharing their endpoint variables return 0 rows whenever both shared variables are labelled and at least one of those labels is written on the second pattern:
CREATE (p1:Person {k:'p1'}), (p2:Person {k:'p2'}), (b1:Bot {k:'b1'}),
(c1:Comment {k:'c1'}), (c2:Comment {k:'c2'});
MATCH (c1:Comment {k:'c1'}), (p1:Person {k:'p1'}) CREATE (c1)-[:AUTHORED]->(p1);
MATCH (c1:Comment {k:'c1'}), (p2:Person {k:'p2'}) CREATE (c1)-[:MENTIONS]->(p2);
MATCH (c2:Comment {k:'c2'}), (b1:Bot {k:'b1'}) CREATE (c2)-[:AUTHORED]->(b1);
MATCH (c2:Comment {k:'c2'}), (p2:Person {k:'p2'}) CREATE (c2)-[:MENTIONS]->(p2);
MATCH (p1:Person {k:'p1'}), (p2:Person {k:'p2'}) CREATE (p1)-[:KNOWS]->(p2);
MATCH (b1:Bot {k:'b1'}), (p2:Person {k:'p2'}) CREATE (b1)-[:KNOWS]->(p2);
| query |
rows |
expected |
MATCH (p1)-[:KNOWS]->(p2), (p1:Person)<-[:AUTHORED]-(c:Comment)-[:MENTIONS]->(p2:Person) RETURN c |
0 |
1 (c1) |
MATCH (p1)-[:KNOWS]->(p2:Person), (p1:Person)<-[:AUTHORED]-(c:Comment)-[:MENTIONS]->(p2) RETURN c |
0 |
1 |
MATCH (p1:Person)-[:KNOWS]->(p2:Person), (p1)<-[:AUTHORED]-(c:Comment)-[:MENTIONS]->(p2) RETURN c |
1 |
1 |
MATCH (p1)-[:KNOWS]->(p2), (p1)<-[:AUTHORED]-(c:Comment)-[:MENTIONS]->(p2:Person) RETURN c |
2 |
2 |
Both labels on the first pattern is correct; splitting them across the two patterns, or putting both on the second, empties the result. The label is not merely being ignored - it is rejecting rows that satisfy it, so it is not the same shape as the dropped-filter bugs #6304 fixed.
RETURN c rather than count(*) is deliberate: no count push-down is involved, so this is the materialization pipeline binding a variable that the other pattern already bound.
Impact. Silent under-count on an ordinary read, on a pattern shape that is idiomatic (MATCH (a)-[:R]->(b), (a)<-[:S]-(x)-[:T]->(b)), with no error and no warning.
Where this was noticed. CypherPairJoinLabelFilterIssue6304Test.bothArmEndpointLabelsAreAppliedTogether had to use the OLTP count as its reference instead of the pipeline, and says so in its javadoc - that comment should be reverted to the ordinary three-way cross-check once this is fixed.
2. Five more count push-downs read only the first of a node's labels
engine/src/main/java/com/arcadedb/query/opencypher/executor/CypherExecutionPlan.java
(a:A:B) keeps only what carries both and (a:A|B) keeps what carries either. Taking getLabels().get(0) turns each of them into (a:A), which is neither - so an operator built on that filter counts a set the pattern did not describe. #6304 fixed this for the pair-join detector by declining the push-down (hasPushDownRepresentableLabel), and tryCreateTypeCountOptimization already had the guard at :3820. These five do not:
| line |
method |
what the label feeds |
:3659 |
tryOptimizeMatchCountReturn |
the counted node's type |
:4178, :4179 |
nodeLabelsAreTypeDisjoint |
both nodes of a disjointness test |
:4627 |
tryDetectChainCountStar |
per-hop label array |
:4847 |
tryDetectStarCountStar |
the star's central label |
:5338 |
tryDetectAntiJoinChainCountStar |
per-hop label array |
nodeLabelsAreTypeDisjoint is the one worth looking at first, because it is the only one where reading the wrong label can flip a decision in either direction: it exists to prove two positions cannot be the same vertex, so a wrong "disjoint" verdict drops matches and a wrong "not disjoint" keeps a pattern that cannot match.
Suggested shape. Reuse hasPushDownRepresentableLabel - it is already in this file and already the convention at :3820. The conservative direction is to decline the push-down and let the ordinary pipeline answer, exactly as #6304 did; representing an intersection is not something these operators have room for.
Worth a regression test per detector along the lines of CypherPairJoinLabelFilterIssue6304Test.aMultiLabelledNodeDeclinesThePushDown, which asserts both that the plan declines and that the answer matches the pipeline.
3. COMMIT RETRY n retries a deadline that has already passed
engine/src/main/java/com/arcadedb/query/sql/executor/RetryStep.java:89
} catch (final NeedRetryException | TimeoutException ex) {
The comment there explains the intent, and it is sound for what it names: the usual source of a TimeoutException inside a commit is TransactionManager's file-lock timeout, which is transient contention and clears after a backoff. But the same catch now also takes a TimeoutException raised by a WorkGuard on the command deadline - arcadedb.command.timeout or a TIMEOUT clause - and that kind cannot be cured by waiting: the instant is fixed for the whole command (#6266), so every remaining attempt aborts on its first check. A COMMIT RETRY 10 block spends all ten attempts plus nine backoff sleeps re-reaching a deadline that expired before the first retry, then fails anyway - or, with no ELSE FAIL, returns an empty result set instead of the error.
Same futility this project already addressed for chunked reads in #6258: a retry whose precondition cannot change is not a retry.
Suggested shape. Distinguish the two at the catch: a TimeoutException whose deadline is the command's own (the guard's, which names the bound in its message and could carry a marker instead) should propagate rather than consume the retry budget, while a lock-acquisition timeout keeps retrying. The existing comment then documents a real distinction instead of a blanket policy.
Deliberately left alone in #6308 because the behaviour is documented in that comment and changing it is a decision, not a fix.
4. A failing CI lane does not say which test failed
.github/workflows/mvn-test.yml
When vector-unit-tests and ha-integration-tests both went red on #6308, the failing test names were not recoverable from GitHub at all:
- the
dorny/test-reporter step reports success, but no Vector Unit Tests Report check run is ever published - GET /commits/<sha>/check-runs?per_page=100 returns 28 entries and none of them is a reporter;
- no annotations on the failing check run, and
output.title/output.summary are null;
- the per-job log endpoint returns only runner setup and cleanup (~500 lines), omitting the Maven output entirely;
- the run-level log archive cannot be downloaded while any job of the run is still in flight;
- the uploaded artifact for these lanes is
**/jacoco*.xml only - coverage, not surefire.
The only way to tell a flake from a regression was to wait for the whole run to finish and re-run the failed jobs (gh run rerun <id> --failed). Both then passed on the identical commit - vector-unit-tests fail 21m0s → pass 11m10s, ha-integration-tests fail 1h0m51s → pass 1h1m29s - so both were flakes, but that took an hour to establish and would have taken the same hour if one had been a real regression.
Suggested shape. Upload **/surefire-reports/TEST*.xml as an artifact on failure for the lanes that run tests. check-test-results.py already parses exactly those files to produce the verdict, so the data exists at that point in the job; it is only discarded. That alone makes the failing test names readable without a re-run, independently of why the reporter check is missing.
Suggested triage
Item 1 is the only wrong answer and is the one worth looking at first. Item 2 is the same class of bug #6304 fixed in one place, in five more. Item 3 is a behaviour decision. Item 4 is CI ergonomics, and the cheapest of the four.
Four independent findings turned up while fixing #6304 (PR #6308, merged as
383a82169). None of them belonged in that PR, and they are unrelated to each other beyond having been found in the same neighbourhood. Filed together for triage; each is self-contained and can be split out or closed on its own.Line references are against
bdf46f9cc. The item 1 repro below was run on that commit and reproduces exactly as tabulated.1. A two-pattern
MATCHreturns no rows when both shared variables carry a labelThe one possible wrong answer here, and the only item that is not about a fast path: it reproduces through the ordinary materialization pipeline, with no aggregate and no Graph Analytical View.
Two comma-separated patterns sharing their endpoint variables return 0 rows whenever both shared variables are labelled and at least one of those labels is written on the second pattern:
MATCH (p1)-[:KNOWS]->(p2), (p1:Person)<-[:AUTHORED]-(c:Comment)-[:MENTIONS]->(p2:Person) RETURN cc1)MATCH (p1)-[:KNOWS]->(p2:Person), (p1:Person)<-[:AUTHORED]-(c:Comment)-[:MENTIONS]->(p2) RETURN cMATCH (p1:Person)-[:KNOWS]->(p2:Person), (p1)<-[:AUTHORED]-(c:Comment)-[:MENTIONS]->(p2) RETURN cMATCH (p1)-[:KNOWS]->(p2), (p1)<-[:AUTHORED]-(c:Comment)-[:MENTIONS]->(p2:Person) RETURN cBoth labels on the first pattern is correct; splitting them across the two patterns, or putting both on the second, empties the result. The label is not merely being ignored - it is rejecting rows that satisfy it, so it is not the same shape as the dropped-filter bugs #6304 fixed.
RETURN crather thancount(*)is deliberate: no count push-down is involved, so this is the materialization pipeline binding a variable that the other pattern already bound.Impact. Silent under-count on an ordinary read, on a pattern shape that is idiomatic (
MATCH (a)-[:R]->(b), (a)<-[:S]-(x)-[:T]->(b)), with no error and no warning.Where this was noticed.
CypherPairJoinLabelFilterIssue6304Test.bothArmEndpointLabelsAreAppliedTogetherhad to use the OLTP count as its reference instead of the pipeline, and says so in its javadoc - that comment should be reverted to the ordinary three-way cross-check once this is fixed.2. Five more count push-downs read only the first of a node's labels
engine/src/main/java/com/arcadedb/query/opencypher/executor/CypherExecutionPlan.java(a:A:B)keeps only what carries both and(a:A|B)keeps what carries either. TakinggetLabels().get(0)turns each of them into(a:A), which is neither - so an operator built on that filter counts a set the pattern did not describe. #6304 fixed this for the pair-join detector by declining the push-down (hasPushDownRepresentableLabel), andtryCreateTypeCountOptimizationalready had the guard at:3820. These five do not::3659tryOptimizeMatchCountReturn:4178,:4179nodeLabelsAreTypeDisjoint:4627tryDetectChainCountStar:4847tryDetectStarCountStar:5338tryDetectAntiJoinChainCountStarnodeLabelsAreTypeDisjointis the one worth looking at first, because it is the only one where reading the wrong label can flip a decision in either direction: it exists to prove two positions cannot be the same vertex, so a wrong "disjoint" verdict drops matches and a wrong "not disjoint" keeps a pattern that cannot match.Suggested shape. Reuse
hasPushDownRepresentableLabel- it is already in this file and already the convention at:3820. The conservative direction is to decline the push-down and let the ordinary pipeline answer, exactly as #6304 did; representing an intersection is not something these operators have room for.Worth a regression test per detector along the lines of
CypherPairJoinLabelFilterIssue6304Test.aMultiLabelledNodeDeclinesThePushDown, which asserts both that the plan declines and that the answer matches the pipeline.3.
COMMIT RETRY nretries a deadline that has already passedengine/src/main/java/com/arcadedb/query/sql/executor/RetryStep.java:89The comment there explains the intent, and it is sound for what it names: the usual source of a
TimeoutExceptioninside a commit isTransactionManager's file-lock timeout, which is transient contention and clears after a backoff. But the same catch now also takes aTimeoutExceptionraised by aWorkGuardon the command deadline -arcadedb.command.timeoutor aTIMEOUTclause - and that kind cannot be cured by waiting: the instant is fixed for the whole command (#6266), so every remaining attempt aborts on its first check. ACOMMIT RETRY 10block spends all ten attempts plus nine backoff sleeps re-reaching a deadline that expired before the first retry, then fails anyway - or, with noELSE FAIL, returns an empty result set instead of the error.Same futility this project already addressed for chunked reads in #6258: a retry whose precondition cannot change is not a retry.
Suggested shape. Distinguish the two at the catch: a
TimeoutExceptionwhose deadline is the command's own (the guard's, which names the bound in its message and could carry a marker instead) should propagate rather than consume the retry budget, while a lock-acquisition timeout keeps retrying. The existing comment then documents a real distinction instead of a blanket policy.Deliberately left alone in #6308 because the behaviour is documented in that comment and changing it is a decision, not a fix.
4. A failing CI lane does not say which test failed
.github/workflows/mvn-test.ymlWhen
vector-unit-testsandha-integration-testsboth went red on #6308, the failing test names were not recoverable from GitHub at all:dorny/test-reporterstep reports success, but noVector Unit Tests Reportcheck run is ever published -GET /commits/<sha>/check-runs?per_page=100returns 28 entries and none of them is a reporter;output.title/output.summaryare null;**/jacoco*.xmlonly - coverage, not surefire.The only way to tell a flake from a regression was to wait for the whole run to finish and re-run the failed jobs (
gh run rerun <id> --failed). Both then passed on the identical commit -vector-unit-testsfail 21m0s → pass 11m10s,ha-integration-testsfail 1h0m51s → pass 1h1m29s - so both were flakes, but that took an hour to establish and would have taken the same hour if one had been a real regression.Suggested shape. Upload
**/surefire-reports/TEST*.xmlas an artifact on failure for the lanes that run tests.check-test-results.pyalready parses exactly those files to produce the verdict, so the data exists at that point in the job; it is only discarded. That alone makes the failing test names readable without a re-run, independently of why the reporter check is missing.Suggested triage
Item 1 is the only wrong answer and is the one worth looking at first. Item 2 is the same class of bug #6304 fixed in one place, in five more. Item 3 is a behaviour decision. Item 4 is CI ergonomics, and the cheapest of the four.