Repository navigation
Fix traversal cycles, both() filter, statement reuse; WAL default; unmask CI - #19
Conversation
…ement reuse, WAL default Refs #10 #11 #12 #13 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UkPPxXPqpZDv12PSugDzm7
…atements - toArray(): without unique(), never revisit a node on the current path; without maxDepth(), follow each defined step once (integration specs); the start node is never returned or filtered - both(edge, nodeType): apply the type filter to both UNION branches - cache prepared statements per connection; head index instead of queue.shift(); unique() marks on enqueue; toPaths uses a parent chain Closes #10 Closes #11 Closes #12 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UkPPxXPqpZDv12PSugDzm7
Opt out with { wal: false }. In-memory and readonly databases unchanged.
Closes #13
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UkPPxXPqpZDv12PSugDzm7
Closes #15 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UkPPxXPqpZDv12PSugDzm7
Closes #14 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UkPPxXPqpZDv12PSugDzm7
Without this, minDepth(2) with one step returned [] and broke the documented example. Found by pre-PR verification. Refs #10 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UkPPxXPqpZDv12PSugDzm7
There was a problem hiding this comment.
Note
Copilot was unable to complete this review at the selected tier due to model policy or plan restrictions.
🟡 Changes recommended
A moderate, active issue remains because TraversalQuery.ts has inconsistent filter semantics between toPaths() and toArray(), potentially blocking traversal when the start node fails the predicate.
1 open issue
What changed in this PR
This pull request fixes traversal correctness issues (cycle termination, both() filtering), improves traversal performance via statement reuse and queue/path optimizations, switches file-backed SQLite DBs to WAL-by-default (opt-out supported), and updates CI/docs/benchmarks so failures are no longer masked.
Changes:
- Fix traversal termination on cycles; adjust default
toArray()traversal behavior whenmaxDepth()isn’t provided; improve traversal efficiency (statement caching, O(1) dequeue, path parent-chain). - Fix
both(edgeType, nodeType)sonodeTypefiltering applies across both UNION branches. - Default file DBs to
journal_mode=WALwithsynchronous=NORMAL(opt-out via{ wal: false }); add regression tests + scale benchmark; unmask CI failures.
Key review feedback to address:
toPaths()applies.filter(...)to the start node whiletoArray()skips it, creating inconsistent filter semantics across traversal APIs. (moderate)- Regression test monkey-patches
raw.preparewithouttry/finally, risking leaked state across tests on assertion failure. (moderate) - Minor nits: README hardcoded Jest counts may go stale; consider avoiding exact pass/fail totals. Also consider not retaining
prevback-references inunique()mode to reduce memory retention. (nits)
| File | Summary |
|---|---|
tests/unit/TraversalQuery-regressions.test.ts |
Adds regression coverage for cycle termination, both() nodeType filtering, statement reuse, and WAL defaults. |
src/types/index.ts |
Exposes DatabaseOptions.wal?: boolean to support WAL opt-out. |
src/query/TraversalQuery.ts |
Implements cycle-safe traversal semantics, fixes both() filtering, improves traversal performance, and caches prepared statements. |
src/core/Database.ts |
Defaults file-backed DBs to WAL + synchronous=NORMAL (unless opted out / readonly / in-memory). |
README.md |
Updates test/benchmark documentation and reported scale results. |
docs/API.md |
Documents updated toArray() depth/duplicates behavior and cycle termination expectations. |
benchmarks/scale-benchmark.js |
Adds a reproducible 1k–100k scale benchmark script. |
.github/workflows/ci.yml |
Removes ` |
This pull request requires changes.
Review effort: Balanced
| // Apply filter | ||
| if (this.filterPredicate && !this.filterPredicate(node)) { | ||
| continue; | ||
| } |
transaction() issued a bare BEGIN, so mergeNode()/mergeEdge() (which use transaction() internally) failed inside a caller's transaction. A nested call now opens a SAVEPOINT; commit releases it, rollback undoes only the inner work and the outer transaction continues. Closes #17 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UkPPxXPqpZDv12PSugDzm7
Two specs contradicted each other. Owner ruled to keep the existing behaviour (throw, Database.test.ts:746), so the TraversalQuery spec that expected [] now expects the same error. Closes #18 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UkPPxXPqpZDv12PSugDzm7

Summary
Fixes the defects and the CI mask found by a scale benchmark (1k to 100k nodes) and a Neo4j comparison.
toArray()withoutunique()never terminated on cycles and ran the test suite out of memory. A path now never revisits one of its own nodes. WithoutmaxDepth(), each defined step is followed once, astests/integration/graph-operations.test.tsspecifies.both(edge, nodeType)ignorednodeType. The filter now covers both UNION branches.queue.shift(). Statements are now cached per connection, the queue uses a head index,unique()marks nodes on enqueue, andtoPathsuses a parent chain instead of aSetcopy per path.synchronous=NORMAL;{ wal: false }opts out. In-memory and readonly databases are unchanged.transaction()inside an open transaction now uses a SAVEPOINT. Before, the bareBEGINmademergeNode()/mergeEdge()fail inside a caller's transaction. Commit releases the savepoint, and rollback undoes only the inner work.|| true#15 CI no longer masks test failures (|| trueremoved).benchmarks/scale-benchmark.jsis added.Evidence
|| trueboth()returned 2 extra nodes; 199 prepares; journaldeleteNo existing test file was changed. The new specs are in
tests/unit/TraversalQuery-regressions.test.ts.#18 ruling
TraversalQuery› traversal from a non-existent node contradictedDatabase.test.ts:746. The owner ruled on 2026-11-02 to keep the error, so that spec now expectsStart node with ID … not found(f4116be). This is the only existing test changed in this PR, and it changed by owner ruling.Behaviour changes to review
traverse(x).out('E').toArray()with nomaxDepth()now goes one hop per step, or down tominDepth()if that is deeper. Before, it walked without bound: on DAGs it returned every reachable node, and on cycles it never returned. On a 5-node chain,mainreturned c1 to c4 and this branch returns c1;minDepth(2)returned c2 to c4 and now returns c2. Callers that relied on deep walks must add.maxDepth(n). Documented indocs/API.md› toArray().toPaths()keeps its old unbounded default (it terminates thanks to its cycle detection) and ignoresminDepth, sotoPaths()andtoArray()now default differently.unique(), results grow exponentially with depth on dense graphs (complete 12-node graph,maxDepth(5): 64,471 results in 121ms). That is what "once per distinct path" means;unique()is the reachability mode.minDepth(n)greater thanmaxDepth(m)returns[]without an error.unique(), the start node is no longer returned when a cycle leads back to it.-wal/-shmside files appear, and a later open with{ wal: false }still reportswal. WAL is unsafe on network filesystems; use{ wal: false }there.Pre-PR verification
Adversarial verification returned PARTIAL twice. The first pass found one regression,
minDepth(n)withoutmaxDepthreturning[], which broke the documented example. That is fixed in 37d57c5, test first. Re-verification of 37d57c5 confirmed the fix: on the chain,minDepth(2)returned c2..c4 onmain,[]before the fix and c2 now, and 8 edge cases were checked.Also confirmed:
mainfirstmainshortestPath/allPaths/toPathsresults matchmainPARTIAL rather than PASS because the local second-opinion judge model was unstable (0.43 to 0.86 on the same evidence, then a timeout). That is noise in the checker, recorded here rather than counted as a pass. The README benchmark figures were not re-run by the verifier.
Not in this PR
Closes #10
Closes #11
Closes #12
Closes #13
Closes #14
Closes #15
Closes #17
Closes #18
🤖 Generated with Claude Code
https://claude.ai/code/session_01UkPPxXPqpZDv12PSugDzm7