Skip to content

Bind entity-bound vars in pattern value position; widen Instant to int64 with cljs transit reps; fix multival 2-vector disambiguation - #17

Merged
tiensonqin merged 5 commits into
mainfrom
devin/query-engine-fixes-3
Sep 22, 2026
Merged

tiensonqin merged 5 commits into
mainfrom
devin/query-engine-fixes-3

Conversation

@tiensonqin

@tiensonqin tiensonqin commented Sep 22, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Four engine/storage bugs hit by the logseq db-worker port.

Bug A + B — bound vars dropped in pattern value position (one root cause). When a variable bound to an entity id lands in a pattern's value position — (parent ?p ?c) inside a recursive rule body, or :in ?x where ?x was passed as Arg_scalar (Result_entity e) — the constraint was silently discarded. query_value_term only handled QValue; a var bound to an entity substitutes to QEntity, which fell through to None, so pattern_datoms's v-probe scanned every datom of the attribute and the relation fast path returned children of every parent. Now:

let query_value_term = function
  | QValue value -> Some value
  | QEntity entity_id -> Some (Ref entity_id)  (* a bound entity IS the ref value *)
  | _ -> None

(parent 1 ?c) over two disjoint trees now returns {2,3} instead of {2,3,6,7}; Arg_scalar (Result_entity 1) to :in ?x binds identically to Result_value (Ref 1).

Bug C — Instant/Uuid transit encoding incompatible with cljs. melange/datascript_melange_storage.ml (and its near-identical copies in sqlite/datascript_sqlite_codec.ml and examples/logseq_sqlite_storage.ml) wrote Transit.Tagged ("m", Transit.Int v) / Tagged ("u", String v):

  • transit-js emitEncoded calls getVerboseHandler() for tagged reps under preferStrings → TypeError crash on every Instant write (bootstrap transact stores :db/txInstant, file/created-at);
  • wire format ~#m/~#u tagged maps is not cljs's ~t/~u, so kvs blobs aren't byte-compatible;
  • Instant of int truncates epoch ms on melange where int is 32-bit.

Fix: Ds.Instant widened to int64 (~30 use sites updated: parse_instant_millis now computes in Int64, murmur3_hash_long takes the int64 directly, Int64.to_string for display, `Intlit in the JS JSON binding), and all three codecs write Transit.Date value / Transit.Uuid value — the transit lib's cljs-compatible reps emitting ~t<iso> verbose / ~m<ms> normal and ~u. Legacy ~#m/~#u tagged blobs still decode via the kept read arms.

Bug D — a 2-vector in a cardinality-many ref attr was always treated as a lookup ref. ref_lookup_collection_value classified ANY 2-element List/Vector headed by an attr name as a lookup ref, so {:block/tags [:logseq.class/Page :logseq.class/Page]} raised Lookup ref attribute should be marked as :db/unique. Upstream maybe-wrap-multival (db.cljc) only keeps a 2-element collection as a lookup ref when (first vs) names a :db.unique/identity attr; otherwise it expands into individual values. Now:

let ref_lookup_collection_value context db = function
  | List [ attr; _ ] | Vector [ attr; _ ] ->
    (match attr_name_of_value attr with
     | Some attr -> context.is_unique_identity db attr  (* was: entity_ref_of_ref_attr_value value <> None *)
     | None -> false)
  | _ -> false

[:logseq.class/Page :logseq.class/Task] now expands into two ident-ref datoms (idempotent adds collapse); [:block/uuid u] still resolves as a lookup ref in both single- and multi-value attrs.

Schema validation regression fix (main's 1013dcf): the per-attr mid-tx refresh_schema now sees a partially-installed schema spec (db.type/tuple lands before db/tupleTypes), so validate_schema raised inside schema_from_transaction_datoms. schema_from_transaction_datoms gained ?(validate = true); the mid-tx refresh_schema calls it with ~validate:false (upstream never revalidates the whole schema mid-transaction), while the strict end-of-tx recompute still validates.

CI: workflow moved to OCaml 5.5.1 (required by lg/melange), datascript-ocaml-lg.opam gained pin-depends for lg (logseq/lg#main) so opam install --deps-only can resolve it, and the opam switch is cached keyed on OS/compiler/opam hashes.

Tests

test_query_engine_fixes:

  • recursive_rule_bound_head_arg — logseq's (parent ?p ?c) rule over disjoint trees, literal and :in-bound head arg (both returned children of every parent before);
  • in_scalar_result_entity — entity vs ref scalar inputs bind identically;
  • many_ref_vector_of_idents_expands / many_ref_vector_of_same_ident_idempotent — vector of ident keywords in a many-ref attr expands into one datom per ident (dup collapses);
  • many_ref_vector_with_unique_head_is_lookup_ref / entity_map_lookup_ref_vector_form — [:block/uuid u] still resolves as a lookup ref in One and Many attrs.

All verified fail-before/pass-after (Invalid_argument "Lookup ref attribute should be marked as :db/unique: [:logseq.class/Page :logseq.class/Task]" before).

test_sqlite_storage: sqlite_codec_instant_uuid_transit_reps asserts Instant/Uuid encode as Transit.Date/Transit.Uuid (verbose wire bytes contain ~t/~u), round-trip a >int32 millisecond value exactly, and still decode legacy ~#m/~#u blobs.

Full dune runtest green, including upstream cljs parity (identical datom hash) and the js_of_ocaml cross-runtime check.

Link to Devin session: https://app.devin.ai/sessions/c82925b6976a4a6da2688c410e65011a
Open in Devin Desktop: https://app.devin.ai/desktop/session/c82925b6976a4a6da2688c410e65011a?variant=devin
Requested by: @tiensonqin

Copilot AI lite review requested due to automatic review settings September 22, 2026 07:21
@devin-ai-integration

Copy link
Copy Markdown

I'll fix CI failures and address comments from users with write access. I'll skip comments containing "(aside)".

  • Disable automatic comment, CI, and merge conflict monitoring

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@devin-ai-integration

Copy link
Copy Markdown

Note on CI: the OCaml 5.2.1 check fails at opam install . --deps-only --with-test — datascript-ocaml-lg requires the lg package, which is unknown to the solver. That dep was added in 4c1ea15 (already on main; the same failure is on PR16). It's unrelated to this change — two things would be needed to fix it: a pin-depends for lg (git+https://github.com/logseq/lg.git), and lg requires ocaml >= 5.5 + melange >= 6.0, while the CI switch is on 5.2.1.

Locally on OCaml 5.5.1: dune build clean, full dune runtest green including upstream cljs parity.

…h cljs transit reps

- query_value_term now maps a variable substituted to QEntity into the Ref
  value of the datom, so bound vars flowing into pattern value position
  (recursive rule calls, :in scalars holding entity results) actually
  constrain the v-probe instead of scanning the whole attribute.
- Instant is int64: epoch-millisecond timestamps exceed int32 on melange.
- melange, sqlite, and example storage codecs write Instant/Uuid through
  Transit's date/uuid reps (~t/~u wire-compatible with cljs transit) instead
  of ~#m/~#u tagged maps; legacy tagged blobs still decode.
datascript-ocaml-lg depends on lg (logseq/lg), which requires
ocaml >= 5.5 — bump the CI compiler matrix and add the lg git
pin-depends so opam can resolve it.
Restore/save ~/.opam keyed on runner, compiler, and the opam
files hash, so dependency builds (including git-pinned deps)
are reused across runs.
1013dcf refreshes the schema per schema-attr datom inside entity
maps, so refresh_schema now sees partially-installed specs — e.g.
db.type/tuple landing before db/tupleTypes — and validate_schema
raised 'tuple value type requires tuple attrs or tuple types'.
Upstream only validates the schema when it is set explicitly, not
during a transaction; pass ~validate:false on the incremental path
while the final strict recompute still validates.
@devin-ai-integration
devin-ai-integration Bot force-pushed the devin/query-engine-fixes-3 branch from 650c46a to ad6f88c Compare September 22, 2026 08:16
… unique attr

Upstream maybe-wrap-multival treats a 2-element collection inside a
multival context as a lookup ref only when its first element names a
:db.unique/identity attribute; any other collection expands into
individual values. The port treated any 2-element list/vector headed by
an attr name as a lookup ref, so a many-ref value like
[:logseq.class/Page :logseq.class/Page] (two ident refs) raised
"Lookup ref attribute should be marked as :db/unique".

Gate ref_lookup_collection_value on context.is_unique_identity of the
head attr, matching upstream exactly. Vectors of ident keywords now
expand into one ref datom per ident (idempotent adds collapse), while
[:block/uuid u] still resolves as a lookup ref in both single- and
multi-value attrs.
@devin-ai-integration devin-ai-integration Bot changed the title Bind entity-bound vars in pattern value position; widen Instant to int64 with cljs transit reps Bind entity-bound vars in pattern value position; widen Instant to int64 with cljs transit reps; fix multival 2-vector disambiguation Sep 22, 2026
@tiensonqin
tiensonqin merged commit e72915c into main Sep 22, 2026
1 check passed
@tiensonqin
tiensonqin deleted the devin/query-engine-fixes-3 branch September 22, 2026 08:34
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.

2 participants