Skip to content

fix(query): unify at the binding seam — base engine done, planner + rule heads remain - #927

Draft
whilo wants to merge 1 commit into
mainfrom
fix/binding-seam-unification
Draft

fix(query): unify at the binding seam — base engine done, planner + rule heads remain#927
whilo wants to merge 1 commit into
mainfrom
fix/binding-seam-unification

Conversation

@whilo

@whilo whilo commented Jul 31, 2026

Copy link
Copy Markdown
Member

Draft: CI will be red, and that is the point — five assertions fail, all of them new law tests naming work this PR does not yet do. They are left visible rather than skipped, because a skipped test for a known-broken semantic is exactly the silence this whole effort is removing.

The law

Binding a variable that is already bound is UNIFICATION, not assignment.

Datomic's semantics, and already datahike's for a variable repeated inside one clause (#912/#913) and for ordinary data patterns — [?e :name ?v] [?e :nick ?v] correctly selects entities whose nick equals their name. It was the clause forms that bind a value which each invented their own rule.

The base-engine bug is one zipmap

prod-rel builds its attr map with (zipmap (concat attrs1 attrs2) (range)). A variable in both relations therefore resolves to the second relation's column — the later binding silently wins:

[:find ?e ?v :where [?e :name ?v] [(get-else $ ?e :nick "zzz") ?v]]
;; asks for entities whose nick equals their name
;; base engine returned every entity, with ?v overwritten by the nick/default

unify-rel keeps one column for a shared variable and admits a tuple pair only when the values agree, falling through to prod-rel when nothing is shared (the common case). bind-by-fn uses it.

What this fixes, and what it doesn't

site base planner
pattern occurrence (reference case)
plain function output
tuple binding ✅ fixed ✅ fixed
collection binding
get-else output ✅ fixed
get-else vs an :in constant
repeated rule-head var

Remaining: the planner ignores the obligation rather than overwriting it, so it needs its own fix at the fused/optional merge sites; and a variable repeated in a rule head answers nil in the first position on both engines.

The result that makes this safe to build on

Full suite: 861 tests, 7188 assertions, 5 failures — all five are the new law tests. unify-rel changes behaviour at every function-binding site in the base engine, and no pre-existing test encoded the overwrite behaviour. Had one done so, that would have been the interesting finding: a test pinning a bug.

Why it matters beyond these four bugs

This is the entry that #924's known-shared-wrong allowlist exists for. When the planner half lands, the :output-var-rebind entry must be deleted — and #924's known-shared-wrong-is-still-needed test fails if it isn't, so the deletion is forced rather than remembered.

…ule heads remain)

ONE law: binding a variable that is already bound is UNIFICATION, not
assignment. Datomic's semantics, and already datahike's for a variable
repeated inside one clause (#912/#913) and for ordinary data patterns.
The clause forms that BIND a value each invented their own rule instead.

The whole overwrite bug in the base engine is one `zipmap`: `prod-rel`
builds its attr map with `(zipmap (concat attrs1 attrs2) (range))`, so a
variable present in both relations resolves to the SECOND relation's
column — the later binding silently wins. `[?e :name ?v]
[(get-else $ ?e :nick "zzz") ?v]` asks for the entities whose nick equals
their name; overwriting answers with every entity, asserting a ?v the
database never contained.

`unify-rel` keeps one column for a shared variable and admits the tuple
pair only when the two values agree; it falls through to `prod-rel` when
nothing is shared, which is the common case. `bind-by-fn` now uses it.

Fixed by this one change, on the base engine: get-else output, tuple
bindings, collection bindings, plain function outputs.

STILL FAILING, deliberately left visible (5 assertions in
query-binding-seam-test) rather than hidden behind a skip:

  * get-else under the PLANNER, both against a pattern-bound var and
    against an :in constant — the planner ignores the obligation instead
    of overwriting it, so it needs its own fix at the fused/optional
    merge sites;
  * a variable repeated in a rule HEAD, on BOTH engines, which answers
    nil in the first position.

Those failing tests are the specification for the remaining work.

Suite: 861 tests, 7188 assertions, 5 failures — all five are the new law
tests above. NO pre-existing test encoded the overwrite behaviour, which
is the result that makes this change safe to build on.
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