Skip to content

Follow-ups from #6266: an ignored label filter in a count push-down, a TIMEOUT ... RETURN that is still coarse, and three consistency gaps #6304

Description

@lvca

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.

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

Milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions