8000 feat(query): lazy cursor over the streaming engine (#544, PR 3) by adityamukho · Pull Request #591 · project-minigraf/minigraf · GitHub
[go: up one dir, main page]

Skip to content

feat(query): lazy cursor over the streaming engine (#544, PR 3) - #591

Merged
adityamukho merged 4 commits into
v3from
feat/544-lazy-cursor
Oct 14, 2026
Merged

adityamukho merged 4 commits into
v3from
feat/544-lazy-cursor

Conversation

@adityamukho
Copy link
Copy Markdown
Collaborator

#544 PR 3 of 3 (plan steps 10–12 in docs/superpowers/plans/2026-10-14-544-operator-core.md). Part of #473; no closing keywords.

What changes

  • Lazy cursor. Cursor holds the engine's operator tree (engine::Stream, owning its Snapshot) and pulls one internal batch (≤ 1,024 rows) when its buffered rows run out. close and drop stop all work. DatalogExecutor::open returns an Answer: the stream for an engine query, a whole result for everything else. execute drains the same stream.
  • Operators. Operator trait (next_batch, close, counters); Scan and Apply (Filter/Bind/pseudo step) implement it. Counters (rows in/out, batches) per operator, shown in the cursor's Debug; the profiler (post-1.0: build query profiler #185) reads them later.
  • Errors mid-stream. Rows gathered before a read error are returned first, the error on the next call, then None. The iterator does the same.
  • Per-pull DICT cache. IdSpace kept every committed UUID it projected, so cursor memory grew with the answer (189 MiB at 10M in the first commit). DICT lookups are now cached for one pull; provisional ids stay for the whole query.
  • The function registry is read-locked per pull (predicates are never replaced, INT-040), not for the whole query.

Tests (+8; 1,479 passing, 10 ignored)

  • cursor::tests: one pull reads one internal batch (scan counters), buffered rows need no pull, a closed stream reads nothing more (spec §10), filter counters chain, the answer is unchanged by writes and by checkpoints that free leaves the cursor has yet to read (spec §10, file-backed), and by later writes (in memory).
  • query::engine::tests: read error after gathered rows (failing test operator); DICT cache empty after each pull.
  • query::engine::ids: provisional ids survive a pull.
  • cargo test --features test-oracle passes. tests/cursor_test.rs and binding suites unchanged.

Measurement (#394 envelope, A/B, two swapped rounds per side, NUMA-pinned on an idle server)

100K 1M 10M
attribute_scan p50, v3 → PR 24.0 → 23.2 ms 268 → 248 ms 3.12 → 2.59 s
attribute_scan peak RSS, v3 → PR 11 → 6 MiB 58 → 7 MiB 458 → 7 MiB
point / :as-of / :valid-at / hot entity unchanged unchanged unchanged

Executor-only shapes (joins, aggregates, rules) unchanged. Details in docs/BENCHMARKS.md.

Codex review

  • max_results not applied on the engine path: not a bug, it limits rule derivation only (bug: a rule query fails with INT-020 on any database with 1,000,000 or more live facts #528) and the engine runs no rules.
  • Registry lock held during a pull: was held for the whole engine run before; now one batch.
  • The oracle compares a second, drained stream, not the cursor's: by design (the cursor may be closed early); between-pull behaviour is covered by the new cursor tests.
  • IdSpace fix: no findings.

Docs

CHANGELOG, CLAUDE.md, ROADMAP.md, README, TEST_COVERAGE, BENCHMARKS. Docs site: project-minigraf/minigraf-docs PR (streaming-engine section under @since v3.0.0).

🤖 Generated with Claude Code

https://claude.ai/code/session_01HyLscCUuJ1GQpdAeX79EdR

adityamukho and others added 4 commits October 14, 2026 15:39
Cursor pulls one internal batch at a time from the engine's operators
(Operator trait: next_batch, close, counters); close and drop stop all
work. Rows gathered before a read error are returned first, the error
by the next call. DatalogExecutor::open gives cursors an Answer: a
whole result, or an engine Stream.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HyLscCUuJ1GQpdAeX79EdR
IdSpace kept every committed UUID and ident it projected, so a cursor's
memory grew with the rows it returned (189 MiB for an attribute scan
over 10M facts). DICT ids never change, so their lookups are now
dropped after each pull; provisional ids stay for the whole query.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HyLscCUuJ1GQpdAeX79EdR
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HyLscCUuJ1GQpdAeX79EdR
@codecov
codecov Bot commented Oct 14, 2026
Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 96.66667% with 7 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
src/query/engine/mod.rs 92.42% 5 Missing ⚠️
src/cursor.rs 97.91% 1 Missing ⚠️
src/query/datalog/executor.rs 94.73% 1 Missing ⚠️

📢 Thoughts on this report? Let us know!

@adityamukho
adityamukho merged commit d843697 into v3 Oct 14, 2026
25 checks passed
@adityamukho
adityamukho deleted the feat/544-lazy-cursor branch October 14, 2026 12:09
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.

1 participant

0