Skip to content

Support recursive stable pagination with max-depth#69

Open
theronic wants to merge 7 commits into
eacl/v7from
codex/recursive-stable-cursor-max-depth
Open

Support recursive stable pagination with max-depth#69
theronic wants to merge 7 commits into
eacl/v7from
codex/recursive-stable-cursor-max-depth

Conversation

@theronic

Copy link
Copy Markdown
Owner

Summary

  • replace recursive full-closure materialization with a stable discovery-order frontier cursor
  • add :max-depth runtime guards with a default of 50
  • keep acyclic lookups on the existing fast static merge path
  • update recursive lookup/count semantics to exact deduplicated stable order instead of global eid order

Verification

  • eacl.datomic.schema-test
  • eacl.datomic.config-test
  • eacl.datomic.parser_test
  • eacl.datomic.impl.indexed-test
  • eacl.spice-test
  • non-recursive pagination benchmark remains within existing thresholds
  • recursive pagination benchmark shows the regression is repaired by a wide margin

Notes

  • recursive pagination order can change, but result membership remains exact and deduplicated
  • this unblocks 0tx shared subtree access on the new EACL SHA

theronic and others added 6 commits July 6, 2026 21:13
…ause fixes

15 REPL-verified bugs across 5 root-cause layers: schema-write pipeline
trust, keyword-bounded index scan, cache keying, nil-tolerant ID
resolution, and nil-on-failure cursor decoding. The OpenSpec change
(proposal/design/specs/tasks) records the fix strategy, including the
adversarially-reviewed schema-history-digest cache design and the
Datomic behaviors it was verified against.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Schema-write safety (audit 1, 8, 9, 10): write-schema! throws typed
errors on parse failures instead of silently retracting the entire
schema; // and /* */ comments supported; duplicate definitions/relations
and permission/relation name collisions rejected; empty-schema guard
with {:allow-empty-schema? true} escape; arrow targets validated against
ALL subject types (declaration-order independent); parenthesized unions
flatten, paren arrow bases rejected cleanly.

Relation lookup (audit 2): relation-datoms uses a seek-datoms prefix
scan instead of the :a..:z keyword range that hid uppercase, z-prefixed
and namespaced subject types from permission evaluation.

Strict object-ID resolution (audit 4, 5, 11, 12): reads with unknown IDs
return empty results (read-relationships no longer degrades to a global
scan); writes throw :eacl/unknown-object with existence verified by
datom presence; impl/tx-relationship requires {:allow-tempids? true} to
mint tempids; make-client honors the documented :entid->object-id key
and rejects unknown option keys.

Cache freshness (audit 3): permission-path/plan cache keys carry a
schema-history digest derived from the queried db value - programmatic
schema writes, other peers, and d/as-of views all resolve fresh paths
with no writer-side signal; unclassifiable views (filter/since/history)
and all failures degrade to cache misses, never staleness.

Cursor tokens (audit 7, 6-guard): invalid/expired tokens throw
:eacl/invalid-cursor instead of silently restarting at page one; TTL is
opt-in via :cursor-ttl-seconds (default: no expiry); cursors carry a
two-part schema/paths fingerprint that throws :eacl/stale-cursor when
the queried permission's paths changed mid-pagination while ignoring
unrelated schema changes.

API contract (audit 13, 16): write-relationship!/delete-relationship!
implemented (previously AbstractMethodError); typed ex-info errors
replace asserts (unsupported consistency, not-implemented); vacuous
count-resources assert removed.

Housekeeping (audit 14, 15, 16): dead v6 namespaces deleted
(rules*, datalog, base/Relationship - all referenced storage attrs
absent from the v7 schema); parser tests renamed to run under
clj -X:test; README ID-config and quickstart examples corrected with a
Breaking Changes section; test typos fixed.

Tests: 57 tests / 860 assertions from cold (was 35/376), including
pinned Datomic behavior facts for the digest design and seeded
differential property tests (lookup == can? ground truth == paginated
union == count, forward and reverse, both engines).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The schema-history digest (D8) recomputed per fresh db basis, i.e. after
every d/transact: unrelated relationship writes paid an O(schema-history)
scan for schema-change detection. That does not scale under write load.

Replace derivation with a signaled stamp: write-schema! asserts a fresh
:eacl/schema-version squuid on the schema singleton in the same transaction
as any definition change, and the path/plan caches key on
[(.id db) version resource-type permission-name]. Reading the stamp is one
AVET lookup — no history scans — and unrelated transactions leave every
cache key untouched.

Retained from the audit fixes:
- as-of correctness (§3): as-of views read their era's version datom, so
  historic bases get their own cache slots — no cross-base slot sharing.
- Cross-peer invalidation: the stamp lives in the db, so a write-schema!
  on any peer invalidates all peers (works without the local eviction,
  which remains as hygiene + manual recovery hatch).
- Positive view classification: filter/since/history views bypass the
  caches entirely; any stamp-read failure degrades to a miss, never a
  stale hit.
- Counter-elision safety: a squuid (not a counter) cannot be re-asserted
  to an unchanged value by a concurrent writer.
- Cursor fingerprints keep the {:s :p} shape (:s = version string) and
  survive restarts since the stamp is stored.

Consciously accepted (by design, per #74): programmatic relation/permission
datom edits (raw d/transact, d/with, excision) no longer invalidate caches
and may serve stale paths until the next write-schema! or manual
evict-permission-paths-cache!. Users must not manage EACL schema outside
the API.

write-schema! installs the :eacl/schema-version attribute just-in-time on
databases created before it existed, and skips the version bump when the
definition delta is empty (no-op rewrites keep caches and cursors hot).

Tests: schema_basis_test.clj rewritten to pin the new contract (unrelated
transacts hit the cache; invalidation works with eviction disabled; as-of
resolves historical paths; raw-edit staleness documented as intended).
Cold clj -X:test: 56 tests, 854 assertions, 0 failures.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…tion (#74)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Fix #74: only write-schema! busts the path cache (revert derived digest)
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