Skip to content

refactor: value Int of int -> Int64 of int64 - #19

Merged
RCmerci merged 3 commits into
mainfrom
devin/int64-value
Sep 26, 2026
Merged

RCmerci merged 3 commits into
mainfrom
devin/int64-value

Conversation

@RCmerci

@RCmerci RCmerci commented Sep 26, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Datascript.value's plain-integer constructor changes from Int of int to Int64 of int64. Melange/JS int is 31-bit, so epoch-millis and large counts overflowed; the old code worked around this by stuffing big ints into Instant of int64, which conflated integers with real dates. This PR makes Int64 the single ordinary-integer representation and restores Instant to mean only db.type/instant / #inst / Date values.

This branch also fixes two pre-existing test failures on main (see "Test fixes" below).

Semantics

  • Int64 of int64: every ordinary integer — counts, journal days, epoch-ms numerics, integer literals. entity_id/tx stay OCaml int internally; values produced from them are Int64.
  • Float of float: true floats only.
  • Instant of int64: only real instants — from #inst EDN, Transit.Date/~m tags, db.type/instant schema attrs, squuid-related paths. Never chosen by magnitude.
  • value_equal: Int64 1 <> Float 1.0; Int64 x <> Instant x (never equal despite same payload). compare_value: Int64/Float/Ref order numerically; Instant↔numeric comparisons order by magnitude (upstream valueOf behavior) but with distinct type rank.
  • Hashing: Int64 hashes via murmur3_hash_long; number?/integer? reject Instant; even?/odd?/zero?/pos?/neg? still accept it (upstream compat).
  • Arithmetic stays exact in the int64 domain when no Float operand is present ((+ 1 2) → Int64, not Float); count/count-distinct return Int64.

Wire / codec

  • Encode: Int64 → Transit.Int64 unconditionally (no "fits in int" downgrade); Instant → Transit.Date; Ref → Transit.Int.
  • Decode (back-compat): Transit.Int | Int64 | Big_int → Int64; Transit.Date and ~m/Tagged("m") → Instant.
  • Yojson facade: `Int/ `Intlit → Int64; Int64 → `Intlit `` unconditionally.
  • Conversions to OCaml int (entity ids, pull limits, rand-int bounds) go through Util.int64_to_int/int64_to_int_exn — explicit, range-checked, never silent truncation.

Storage compatibility / migration

No format version bump needed. On-disk datoms decode Int64/Int/Big_int uniformly to Int64. normalize_stored_datom (schema-aware, runs on restore) migrates legacy values:

  • Instant under a non-db.type/instant attr → Int64 (the historical mis-encoding of plain ints).
  • Int64 under a db.type/instant attr → Instant.
  • Instant under db.type/instant → preserved.
  • Int64 under db.type/ref attr → Ref (range-checked).

Test fixes (pre-existing main failures)

  • AVET validation (test_datascript "AVET datoms reject untyped ref-valued attrs"): upstream -datoms/-seek-datoms/-rseek-datoms/-index-range/find-datom all run validate-indexed — an :avet lookup on an attr that isn't a ref, unique, or :db/index true raises "Attribute :x should be marked as :db/index true". Db.datoms/datoms_list/fold_datoms/find_datom skipped that check (a stale comment claimed d/datoms is ISearch/-search; it isn't). datoms now validates; internal -search mirrors (entity/pull reverse-attr lookups, query fast paths) use unvalidated search_datoms and follow upstream's fallback of an aevt scan filtered by value when the attr isn't indexed.
  • Entity scan counter (test_entity "full entity materialization may scan all datoms once"): stale expectation. Since 9284149, entity_attrs materializes forward attrs only via the per-entity eavt lookup — matching upstream touch (-search db [eid]) — so the all-datoms counter stays at 0.
  • Shared-tempid ordering (test_datascript "value-position tempids share the entity-id tempid's allocation", masked behind the AVET failure): the expected list was written in insertion order, but datoms :eavt returns attr-sorted results (friend < name < parent).

Tests

New test/test_int64.ml covers: 1_700_000_000_000L roundtrip (would overflow 32-bit), int64 min/max boundaries, epoch-ms not becoming Instant, #inst↔Instant both directions, Int64↔Float equality/order/hash semantics, number?/integer?/zero/even/odd predicates, query range filters/max/lookup-ref on int64, transit codec encode/decode both directions incl. legacy tags, and end-to-end kvs restore of the legacy-Instant→Int64 migration. Native + jsoo/melange smoke + upstream-cljs cross-runtime parity all pass; dune runtest is fully green on this branch.

Build/test: dune build @install; UPSTREAM_DATASCRIPT_REPO=... UPSTREAM_DATASCRIPT_JS=... dune runtest (see README/blueprint for the upstream bundle build).

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

Ordinary integers (entity ids in value position, tx ids, counts,
journal days, epoch-ms numerics) now use Int64 of int64 instead of
Int of int, eliminating 32-bit int overflow on Melange and removing
the misuse of Instant for plain integers.

- type: Int of int -> Int64 of int64; QueryFormInt of int -> int64
- equality/compare: Int64 vs Float numeric ordering, Int64 vs Instant
  comparable by magnitude but never equal (distinct semantics)
- hashing: murmur3_hash_long for Int64 payloads
- built-ins: exact int64 arithmetic; number?/integer? reject Instant;
  range emits int64 lists
- parser: EDN ints parse via Int64.of_string; #inst -> Instant
- codecs (sqlite + melange): Int64 -> Transit.Int64 unconditionally,
  Instant -> Transit.Date; decode accepts Int/Int64/Big_int and ~m
  tags for backwards compatibility
- storage: normalize_stored_datom migrates legacy Instant values to
  Int64 unless the attr is db.type/instant (real dates preserved)
- js facade: Yojson ints/Intlit -> Int64; Instant still prints as
  #inst/Intlit
@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

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 26, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-09-26T08:02:14.852650Z c39cad1 PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: c39cad18a2

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread impl/storage.ml
Comment on lines +73 to +76
| _, Instant millis ->
(* older databases stored plain ints as Instant; only db.type/instant
attrs are real dates *)
{ datom with v = Int64 millis }

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Preserve untyped instants during restore

When a stored database contains a genuine #inst/Instant value on an untyped attribute, this catch-all arm silently rewrites it to Int64. Such values are accepted by transactions, and the new codecs encode them as Transit.Date, but after decoding, every subsequent restore still changes their type; a database written by this commit therefore cannot round-trip dates unless every date attribute has an explicit db.type/instant schema. Preserve decoded dates and gate any legacy magnitude-based migration with storage-format metadata rather than attribute typing.

AGENTS.md reference: AGENTS.md:L4-L5

Useful? React with 👍 / 👎.

Comment thread impl/built_ins.ml
Comment on lines +199 to +204
let result = List.fold_left (fun acc item -> acc /. float_of_numeric item) (float_of_numeric first) rest in
let integral = Float.is_integer result in
Some (numeric_result (has_float || not integral) result)
if has_float || not integral then
Some (Float result)
else
Some (Int64 (Int64.of_float result))

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Divide large integers before converting to float

When all operands are newly supported Int64 values above the IEEE-754 exact range, this converts them to floats before deciding whether the quotient is integral, losing bits and potentially returning the wrong value and constructor. For example, (/ 9007199254740993 3) operates on the rounded 9007199254740992.0 and returns a fractional Float instead of Int64 3002399751580331; perform exact int64 division when the integer operands divide evenly, falling back to floating-point only for fractional results.

Useful? React with 👍 / 👎.

Comment thread js/datascript_js.ml
let rec json_of_value = function
| Nil -> `Null
| Int value -> `Int value
| Int64 value -> `Intlit (Int64.to_string value)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Avoid routing int64 results through JSON numbers

When q, pull, or datoms returns an Int64 outside JavaScript's safe-integer range, Intlit is emitted as an unquoted JSON number and js_of_json immediately feeds it to JSON.parse, which silently rounds it. For example, an internal Int64 9007199254740993L reaches callers as the JavaScript number 9007199254740992, defeating the new exact representation at the JS API boundary; unsafe values need a non-Number representation such as BigInt or a lossless tagged/string encoding.

Useful? React with 👍 / 👎.

Comment thread impl/util.ml
let minute = Int64.to_int (Int64.div (Int64.rem rem 3600000L) 60000L) in
let second = Int64.to_int (Int64.div (Int64.rem rem 60000L) 1000L) in
let ms = Int64.to_int (Int64.rem rem 1000L) in
Printf.sprintf "%04d-%02d-%02dT%02d:%02d:%02d.%03dZ" year month day hour minute second ms

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Emit parseable extended-year instants

For valid instant values outside years 0000–9999, this formatter emits a variable-width unsigned year that the repository's own parse_instant_millis rejects because it requires the first hyphen at offset 4. For example, Instant 253402300800000L prints readably as #inst "10000-01-01T00:00:00.000Z", but feeding that form back to read_edn raises invalid #inst literal; negative years are malformed similarly. Format extended years in the signed six-digit ISO form and teach the reader to accept it, or reject unsupported values before claiming a readable representation.

AGENTS.md reference: AGENTS.md:L4-L5

Useful? React with 👍 / 👎.

Upstream -datoms/-seek-datoms/-rseek-datoms/-index-range/find-datom all
run validate-indexed: an :avet lookup on an attr that is not a ref, unique,
or :db/index true raises "Attribute :x should be marked as :db/index true".
Our datoms/datoms_list/fold_datoms/find_datom skipped the check (the stale
comment claimed d/datoms is ISearch/-search; it is not).

Internal paths that mirror -search keep the unvalidated search_datoms and
now also follow upstream's fallback: an attr+value pattern on a
non-indexed attr scans aevt filtered by value instead of probing avet.
- entity materialization reads only the entity's own eavt datoms since
  9284149; the scan counter assertion still expected one all-datoms scan.
- the shared-tempids test listed expected datoms in insertion order, but
  datoms :eavt returns attr-sorted results (friend < name < parent).
@RCmerci
RCmerci merged commit e439cba into main Sep 26, 2026
2 checks passed
@tiensonqin
tiensonqin deleted the devin/int64-value branch September 26, 2026 14:06
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