Skip to content

A Datahike backend for the modular SPI - #81

Draft
whilo wants to merge 3 commits into
theronic:fix/audit-root-causes-datascriptfrom
whilo:feat/datahike-backend
Draft

A Datahike backend for the modular SPI#81
whilo wants to merge 3 commits into
theronic:fix/audit-root-causes-datascriptfrom
whilo:feat/datahike-backend

Conversation

@whilo

@whilo whilo commented Jul 30, 2026

Copy link
Copy Markdown

Adds modules/eacl-datahike, implementing the six-fn backend SPI over
datahike. Opened for discussion rather than as a
merge request — happy to reshape it to whatever fits your plans for the
modular branch.

eacl.datahike.contract-test runs the shared eacl.contract-support
suite — the same 23 assertions eacl.datascript.contract-test answers —
so the new backend is held to your definition of a backend rather than to
tests of its own. Full suite across all four modules: 61 tests, 951
assertions, 0 failures
.

Why DataScript was the template, not Datomic

Datahike and DataScript order tuple index bounds the same way, and
Datomic is the outlier: a partial tuple is not a valid seek bound.
Measured on datahike 0.8.1759 —

seek [:room :owner]        → [:kb :reader :party]   ; bound IGNORED
seek [:room :owner nil]    → [:room :owner :party]  ; correct
seek [:room :owner :party] → [:room :owner :party]  ; correct

So db/seek-tuple-prefix keeps your DataScript approach verbatim: pad to
full arity with nil, then take the matching prefix. Correct and
O(log n).

The suite runs twice, because datahike has two attribute representations

Datahike reports a datom's :a as the attribute keyword by default, and
as a numeric ref under :attribute-refs? true — Datomic's
representation, and a creation-time choice per database. The second mode
is where things break quietly, in three ways:

  1. Composite tuples need datahike >= 0.8.1759
    (replikativ/datahike#921,
    which came out of this work). Before that fix they were silently never
    derived — and since the tuples are the v7 engine, every permission
    check denied.
  2. index-range's :attrid is the one accessor that rejects the
    attribute keyword under :attribute-refs?. It raises, so this one was
    loud.
  3. Comparing a datom's :a against a set of keywords matches nothing.
    For the cache-eviction listener that fails open: can? keeps
    answering from pre-change permission paths and grants what the schema
    has just revoked.

Running the contract in both modes is what catches (3) — it is invisible
in the default mode.

One thing worth flagging about the shared contract

The contract goes through make-client, which serves relation
definitions from a prebuilt schema catalog (a full index scan). So it
never exercises the tuple seek at all: deleting the nil padding leaves
the contract suite green.
The seek is reached only by the bare-db path
(impl/can? with no options) and by the relation-retraction guard.

eacl.datahike.backend-test covers those directly. Both mechanisms were
verified by breaking them: no padding fails 12 assertions in both modes,
a raw :a comparison fails exactly 1, only under :attribute-refs?.
This likely applies to eacl-datascript too, if you want the same
coverage there.

Shape

Four namespaces where DataScript has three. eacl.datahike.db holds the
adapter primitives — entid (datahike has none), attribute
representation, and the three index accessors — so every divergence sits
in one file and the rest reads like its template. PORTING.md documents
the full API mapping.

The module is JVM-only (.clj): datahike's ClojureScript API is
asynchronous and the SPI is synchronous.

whilo and others added 3 commits July 30, 2026 00:23
Sets up `modules/eacl-datahike` against the modular SPI: module deps, root
deps wiring (paths, dev/test/nrepl extra-paths, a :build-eacl-datahike alias),
the shared-contract test, and PORTING.md. The three source files
(core/impl/schema) are still to be ported from `modules/eacl-datascript`.

DataScript is the template rather than Datomic, and that is the substantive
finding here: both DataScript and Datahike reject a PARTIAL tuple as an
`:avet` seek bound, where Datomic accepts one as a lower bound. Measured on
datahike main in BOTH `:attribute-refs?` modes:

  seek [:room :owner]        → [:kb :reader :party]    bound ignored
  seek [:room :owner nil]    → [:room :owner :party]   correct
  seek [:room :owner :party] → [:room :owner :party]   correct

So the DataScript adapter's nil-padding is the correct approach on datahike
too — and it is O(log n), not a workaround. An earlier attempt here treated
the partial bound as a datahike compatibility bug and emulated Datomic by
scanning the attribute segment; that was wrong on both counts.

Requires replikativ/datahike#921 (composite `:db/tupleAttrs` were silently
never derived, and tuple validation silently skipped, under
`:attribute-refs?`). EACL's v7 schema is built on composite tuples, so before
that fix every relation and permission tuple was absent and every permission
denied. Merged to datahike main, not yet tagged, hence the :local/root — swap
for an :mvn/version once released.

The contract test is the definition of done: it runs the same
`eacl.contract-support` suite as the DataScript backend, so the two are held
to one definition rather than to tests that drifted apart.
Implements the six-fn backend SPI over datahike, ported from
modules/eacl-datascript. DataScript rather than Datomic is the template
because DataScript and datahike order tuple index bounds the same way.

eacl.datahike.contract-test runs the shared eacl.contract-support suite —
the same 23 assertions the DataScript backend answers — TWICE, once per
attribute representation. Datahike reports a datom's :a as the attribute
keyword by default and as a numeric ref under :attribute-refs? (Datomic's
representation), and the second mode is where things break quietly, in
three ways that all had to be handled:

  - composite tuples need datahike >= 0.8.1759 (replikativ/datahike#921).
    Before it they were silently never derived; since the tuples ARE the
    v7 engine, every permission check denied.
  - index-range's :attrid is the one accessor that rejects the attribute
    keyword under :attribute-refs?. It raises, so this one was loud.
  - comparing a datom's :a against a keyword set matches nothing, which
    for the cache-eviction listener fails OPEN: can? keeps answering from
    pre-change permission paths and grants what the schema just revoked.

Adds eacl.datahike.db as a fourth namespace, so every divergence from the
DataScript source (entid, attribute representation, the index accessors)
sits in one file and the rest reads like its template.

Adds eacl.datahike.backend-test for the paths the shared contract cannot
reach. Worth stating plainly: the contract goes through make-client, which
serves relation definitions from a prebuilt catalog, so it never exercises
the tuple seek — deleting the nil padding leaves the contract suite green.
Both mechanisms were verified by breaking them: no padding fails 12
assertions in both modes, a raw :a comparison fails exactly 1, only under
:attribute-refs?.

Module is JVM-only: datahike's CLJS API is async, the SPI is synchronous.

Full suite: 61 tests, 951 assertions, 0 failures (all four modules).
The module deps.edn still carried :local/root "../../../datahike" and a
comment saying the fix was unreleased, while the root deps.edn had already
moved to 0.8.1759. A consumer building this module alone would have
resolved a sibling checkout on whatever branch it happened to sit on —
exactly the failure mode the comment warned about.
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