Five independent findings turned up while fixing #6266 (PR #6291, merged as 11763ff1a). 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.
All references are against 11763ff1a.
1. PairHashJoinOp.buildWithViews ignores the label filter it is handed, so one CSR count push-down path can over-count
engine/src/main/java/com/arcadedb/query/opencypher/executor/steps/PairHashJoinOp.java:331
The method takes arm2Buckets - the per-hop bucket sets built from arm2IntermediateLabels - and never reads it. Every sibling branch does:
| branch |
line |
applies arm2Buckets? |
buildAndProbeInline |
142 |
yes (arm2Filter0/arm2Filter1) |
buildWithViews |
148 |
no |
buildWithArm1View |
150 |
yes (via walkArmWithViews) |
generic walkArm fallback |
153 |
yes |
OLTP executeOLTP |
194 |
yes (via walkArmOLTP) |
Reachability. buildWithViews runs when both arms are single-hop and both have a NeighborView, and the inline fast path did not return - which happens when the probe edge type has no NeighborView of its own. arm2Buckets is non-null whenever any entry of arm2IntermediateLabels is non-null (buildValidBucketSets, line 439), and for a single-hop arm that array has length 1 holding the label on the arm's endpoint. So a labelled single-hop arm2 reaches this path with a filter that is then dropped.
Why it matters. The dropped filter is not a performance detail - it decides which endpoints are counted. The same query would then return a different number depending on whether a Graph Analytical View happens to cover the probe edge type, because the OLTP fallback applies the labels and this path does not.
Codacy flagged the neighbouring unused provider parameter during #6291 and I removed that one, but deliberately left arm2Buckets in place so the symptom would still be visible here rather than being tidied away.
Wanted first: a test that reaches this branch with a labelled arm2 and asserts the count against the OLTP path. If it turns out the branch is unreachable with a non-null arm2Buckets, the fix is a precondition making that explicit, not a filter.
2. A TIMEOUT n RETURN statement is still bounded only between batches
engine/src/main/java/com/arcadedb/query/sql/executor/StatementTimeouts.java:79
StatementTimeouts.publish deliberately declines to pin a deadline for the RETURN failure strategy:
if (context == null || Timeout.RETURN.equals(clause.getFailureStrategy()))
return;
The reason is sound - a WorkGuard can only throw, and RETURN asks for the rows produced so far, so pinning would silently convert a documented "return what you have" into a failure. The consequence is that this one clause shape keeps exactly the granularity hole #6266 was about: a filter that rejects every record still scans to the end inside a single hasNext(), and only the between-batches check applies.
Note this became reachable because #6291 fixed SQLASTBuilder.visitTimeout, which had never read the EXCEPTION | RETURN token the grammar accepts - before that, failureStrategy was always null and every clause behaved as EXCEPTION.
Suggested shape. The guard needs a second signal meaning stop and yield what you have: a distinct exception thrown by WorkGuard when the pinned bound is a RETURN-strategy one, which the owning timeout step catches and converts into the partial/empty result it already produces in fail(). That keeps the abort cooperative and leaves the in-loop check as the only place that knows how to stop early.
3. SELECT ... TIMEOUT n and UPDATE ... TIMEOUT n mean two different things
AccumulatingTimeoutStep (SELECT) charges only time spent inside the pipeline: totalTime.addAndGet(System.nanoTime() - begin) around each hasNext()/next(). A consumer that pauses between fetches is not billed for the pause.
TimeoutStep (UPDATE) sets expiryTime once on its first pull and compares wall clock against it.
Same syntax, two semantics, and nothing documents either. A client streaming a large SELECT slowly gets a very different bound from the same number written on an UPDATE.
PR #6291 preserved both faithfully rather than unify them mid-change (AccumulatingTimeoutStep republishes its instant every batch as now + (clause - accumulated) precisely so the in-loop guards inherit its accounting instead of quietly becoming wall-clock). But a user cannot currently predict which of the two they get.
Options: document the difference where the clause is documented, or unify on one meaning. If unifying, wall-clock is the one users expect from the word "timeout", and the accumulating variant should then be a distinct opt-in rather than a per-statement-kind accident.
4. SQL MATCH and TRAVERSE accept no TIMEOUT clause at all
engine/src/main/antlr4/com/arcadedb/query/sql/grammar/SQLParser.g4:223 (traverseStatement), :237 (matchStatement)
Neither production has a timeout?, while selectStatement (:202) and updateStatement do. So SELECT ... TIMEOUT 100 parses and MATCH {...} RETURN ... TIMEOUT 100 is a syntax error, even though Statement.timeout exists on the shared base class and MatchStatement sets it internally for profiled timeouts.
Both statements now honour the global arcadedb.command.timeout (that was #6266), so this is a syntax-consistency gap rather than a safety one - a user who wants to bound one expensive MATCH has to change a database-wide setting instead.
5. The regex deadline still multiplies by the number of parallel scan workers
engine/src/main/java/com/arcadedb/query/sql/executor/CommandContext.java:73
getOrComputeRegexDeadline's own javadoc records the gap:
A type scanned in parallel across N buckets is therefore bounded by N * timeoutMillis overall, not one shared budget.
The cause is that the deadline lives in cachedValues, and BasicCommandContext.copy() does not copy that map - so each syncPullParallel worker computes its own. The javadoc argues the exposure is small (bucket count is a schema property, not attacker-controlled), which is fair.
What has changed is that a fix pattern now exists: #6291 made copy() resolve the command deadline before copying it, precisely so that copying an unresolved value would not hand every worker a fresh budget. The same treatment - resolve arcadedb.command.regexTimeout into the parent before copying, or carry the resolved instant on the context rather than in the opaque cache - would close this for free and remove a documented caveat.
Suggested triage
Item 1 is the only possible wrong answer and is the one worth looking at first. Item 2 is a real remaining hole in what #6266 set out to close. Items 3-5 are consistency and documentation debt.
Five independent findings turned up while fixing #6266 (PR #6291, merged as
11763ff1a). 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.All references are against
11763ff1a.1.
PairHashJoinOp.buildWithViewsignores the label filter it is handed, so one CSR count push-down path can over-countengine/src/main/java/com/arcadedb/query/opencypher/executor/steps/PairHashJoinOp.java:331The method takes
arm2Buckets- the per-hop bucket sets built fromarm2IntermediateLabels- and never reads it. Every sibling branch does:arm2Buckets?buildAndProbeInlinearm2Filter0/arm2Filter1)buildWithViewsbuildWithArm1ViewwalkArmWithViews)walkArmfallbackexecuteOLTPwalkArmOLTP)Reachability.
buildWithViewsruns when both arms are single-hop and both have aNeighborView, and the inline fast path did not return - which happens when the probe edge type has noNeighborViewof its own.arm2Bucketsis non-null whenever any entry ofarm2IntermediateLabelsis non-null (buildValidBucketSets, line 439), and for a single-hop arm that array has length 1 holding the label on the arm's endpoint. So a labelled single-hop arm2 reaches this path with a filter that is then dropped.Why it matters. The dropped filter is not a performance detail - it decides which endpoints are counted. The same query would then return a different number depending on whether a Graph Analytical View happens to cover the probe edge type, because the OLTP fallback applies the labels and this path does not.
Codacy flagged the neighbouring unused
providerparameter during #6291 and I removed that one, but deliberately leftarm2Bucketsin place so the symptom would still be visible here rather than being tidied away.Wanted first: a test that reaches this branch with a labelled arm2 and asserts the count against the OLTP path. If it turns out the branch is unreachable with a non-null
arm2Buckets, the fix is a precondition making that explicit, not a filter.2. A
TIMEOUT n RETURNstatement is still bounded only between batchesengine/src/main/java/com/arcadedb/query/sql/executor/StatementTimeouts.java:79StatementTimeouts.publishdeliberately declines to pin a deadline for theRETURNfailure strategy:The reason is sound - a
WorkGuardcan only throw, andRETURNasks for the rows produced so far, so pinning would silently convert a documented "return what you have" into a failure. The consequence is that this one clause shape keeps exactly the granularity hole #6266 was about: a filter that rejects every record still scans to the end inside a singlehasNext(), and only the between-batches check applies.Note this became reachable because #6291 fixed
SQLASTBuilder.visitTimeout, which had never read theEXCEPTION | RETURNtoken the grammar accepts - before that,failureStrategywas always null and every clause behaved asEXCEPTION.Suggested shape. The guard needs a second signal meaning stop and yield what you have: a distinct exception thrown by
WorkGuardwhen the pinned bound is a RETURN-strategy one, which the owning timeout step catches and converts into the partial/empty result it already produces infail(). That keeps the abort cooperative and leaves the in-loop check as the only place that knows how to stop early.3.
SELECT ... TIMEOUT nandUPDATE ... TIMEOUT nmean two different thingsAccumulatingTimeoutStep(SELECT) charges only time spent inside the pipeline:totalTime.addAndGet(System.nanoTime() - begin)around eachhasNext()/next(). A consumer that pauses between fetches is not billed for the pause.TimeoutStep(UPDATE) setsexpiryTimeonce on its first pull and compares wall clock against it.Same syntax, two semantics, and nothing documents either. A client streaming a large
SELECTslowly gets a very different bound from the same number written on anUPDATE.PR #6291 preserved both faithfully rather than unify them mid-change (
AccumulatingTimeoutSteprepublishes its instant every batch asnow + (clause - accumulated)precisely so the in-loop guards inherit its accounting instead of quietly becoming wall-clock). But a user cannot currently predict which of the two they get.Options: document the difference where the clause is documented, or unify on one meaning. If unifying, wall-clock is the one users expect from the word "timeout", and the accumulating variant should then be a distinct opt-in rather than a per-statement-kind accident.
4. SQL
MATCHandTRAVERSEaccept noTIMEOUTclause at allengine/src/main/antlr4/com/arcadedb/query/sql/grammar/SQLParser.g4:223(traverseStatement),:237(matchStatement)Neither production has a
timeout?, whileselectStatement(:202) andupdateStatementdo. SoSELECT ... TIMEOUT 100parses andMATCH {...} RETURN ... TIMEOUT 100is a syntax error, even thoughStatement.timeoutexists on the shared base class andMatchStatementsets it internally for profiled timeouts.Both statements now honour the global
arcadedb.command.timeout(that was #6266), so this is a syntax-consistency gap rather than a safety one - a user who wants to bound one expensiveMATCHhas to change a database-wide setting instead.5. The regex deadline still multiplies by the number of parallel scan workers
engine/src/main/java/com/arcadedb/query/sql/executor/CommandContext.java:73getOrComputeRegexDeadline's own javadoc records the gap:The cause is that the deadline lives in
cachedValues, andBasicCommandContext.copy()does not copy that map - so eachsyncPullParallelworker computes its own. The javadoc argues the exposure is small (bucket count is a schema property, not attacker-controlled), which is fair.What has changed is that a fix pattern now exists: #6291 made
copy()resolve the command deadline before copying it, precisely so that copying an unresolved value would not hand every worker a fresh budget. The same treatment - resolvearcadedb.command.regexTimeoutinto the parent before copying, or carry the resolved instant on the context rather than in the opaque cache - would close this for free and remove a documented caveat.Suggested triage
Item 1 is the only possible wrong answer and is the one worth looking at first. Item 2 is a real remaining hole in what #6266 set out to close. Items 3-5 are consistency and documentation debt.