Conn is no longer an atom: derive the db value from the store - #6
Merged
Conversation
The store is the single source of truth, so the connection stops being a state container: `deref` derives the current database value by querying the latest transaction id (`q-max-tx`), and `Conn` only holds process-local context (db template with schema and caches, listeners). The schema deliberately stays in-memory. This fixes a double-commit bug: `-transact!` used to run the side-effecting, committing transaction inside `swap!`, so a concurrent update of the conn state (e.g. `reset-schema!`) could fail the CAS and make `swap!` re-run the transaction — committing it twice under two different tx ids. Writes are now serialized with `locking` and there is no retry path. Also removes the now-unused extend-clj dependency and updates the JS API to use the new listener/reset helpers. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
`hash-db`/`hash-fdb` computed a database's hash from all of its datoms and `equiv-db` compared databases datom by datom — full scans that would have to realize a potentially larger-than-memory database (the same reasoning that removed `clojure.data/diff` support). A db value is now identified by its storage connection, its basis (`:max-tx`) and its schema — O(1), like Datomic's (store, basis-t) identity. A filtered db is identified by the db it filters and its predicate (compared by identity). The `:hash` cache field disappears from both records, which also retires the stale-cached-hash bug when a snapshot was advanced via `assoc :max-tx`. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A defrecord always has value semantics — the only question is which ones. After removing content-based hashing, the honest shape is Datomic's: a database value is an opaque handle with reference identity, compared via its basis. `d/basis-tx` is the new public accessor for that; internal record-isms (keyword access, `assoc :max-tx`, `map->DB`) are replaced by `db/basis-tx`, `db/db-conn`, `db/with-max-tx` and plain constructors. Also removes machinery that only existed to support the record shape or the removed persistent-sorted-set backend: `defrecord-updatable` (and its prismatic helpers), `db-transient`/`db-persistent!`, `restore-db`, the unused `rollback-report!`, a duplicate `TxReport` defrecord, and FilteredDB's map-interface throw stubs. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- `query_v3` (975 lines): Datascript's abandoned query-engine experiment, never wired to the public API. - The datom comparators (`cmp-datoms-*`, `defcomp`, `combine-cmp`, `diff-sorted`, `cmp`, `value-cmp`, `cmp-attr-quick`): their last caller was `clojure.data/diff`. They also still compared UUID entity ids with `Integer/compare`, so they were broken anyway. - `case-tree`/`case-pick`/`vpred` and the unused bindings in `-search`: leftovers of the in-memory search implementation. - The mutable `idx` field on `Datom` (persistent-sorted-set storage support) and its protocol methods. - `nippy`: required, never used. - `persistent-sorted-set`: only its array helpers were still used; they are now the tiny `dbval.arrays` namespace, and the dependency that was Datascript's storage heart is gone from deps.edn. - `core/settings`: read the `:eavt` field that no longer exists. - Datomic-compat shims hollowed out by UUID ids: `tempid`, `resolve-tempid`, and the Datascript `squuid` implementation (tx ids use com.yetanalytics/colossal-squuid). - `empty-db` docstring: documented persistent-sorted-set options instead of `:db-file`. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The storage layer is SQLite via JDBC, so the CLJS variant (disabled since the SQLite port) cannot work without an entirely different backend; git history keeps it recoverable if one ever materializes. - Delete `dbval.js`, `deps.cljs`, `externs.js`, `release-js/`, `test/js/`, `dbval.test.cljs` and the cljs/js test+bench scripts. - Strip every `#?`/`#?@` reader conditional (keeping the :clj branches) and rename all `.cljc` sources and tests to `.clj`. - Remove the cross-platform macro layer that existed only for CLJS: `defn+` (now plain `defn`), `if-cljs`, `cljs-env?`, `patch-tag`. - Remove `conn/-transact!` and `conn/reset-conn!`, which survived only for the JS API. - Drop the :cljs alias and transit-cljs from deps.edn. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Remove `bench_datomic/`, `test_datomic/`, the :datomic alias and the datomic peer dependency, plus the wrapper scripts. `script/bench.sh` replaces bench_clj/bench_all; test_all.sh (a shell for the deleted cljs/js/datomic runs) is gone — CI runs `test_clj.sh` directly. - Rename the runner to `dbval.bench.run` (it benched dbval, not Datascript) and adapt it to the SQLite/UUID/strict-snapshot world: transaction benches replay against a fresh store per iteration with `*batch*` 1 over a 1k-people dataset; read benches build a 20k store once; entity lookups use sampled UUIDs / lookup refs instead of the old integer eids; freeze/thaw dropped with `d/serializable` (jsonista/cheshire removed from deps). - Bugfix found by the bench: storing an empty vector as a value NPEd in `tuple` (& rest args are nil for zero components, Tuple.addAll requires a List). With regression test. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A comment between `^java.util.List` and the `(or ...)` form left the `Tuple.addAll` call reflective; bind the argument with the hint instead. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This was referenced Jul 15, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Fixes the double-commit bug (#4 from the bug review):
-transact!ran the side-effecting, committing transaction insideswap!, so a concurrent update of the conn state (e.g.reset-schema!) could fail the CAS and makeswap!re-run the transaction — committing it twice under two different tx ids.Rather than patching the retry, this makes the bug unrepresentable: since all transacted state lives in the SQLite store, the connection stops being a state container (the atom-conn was a Datascript inheritance, where the atom is the database).
Connis a plaindeftypeimplementing onlyIDeref— noIAtom, noswap!/reset!.derefderives the current database value from the store (q-max-tx+ in-memory template). It takes the conn lock so it never observes another thread's uncommitted transaction through the shared JDBC connection.transact!= lock →withon a freshly derived value → commit → notify listeners. No CAS, no retry path. Composes with the strict snapshot check inwith(throws on stale snapshots): a value derived under the write lock always passes it.reset-schema!,listen!/unlisten!update conn-local fields; they can no longer interfere with a running transaction. Schema deliberately stays in-memory (per discussion).-transact!is kept as the no-notify variant used bytransact!and the JS API;reset-conn!added as a low-level helper for the JS API.extend-cljdependency; updatescreate-conn/transact!docstrings (dead[[reset-conn!]]/[[db]]links, atom wording).Regression tests (
dbval.test.conn)test-transact!-not-repeated-by-concurrent-conn-update— the Refactor entity and transaction IDs to UUIDs #4 race:reset-schema!during a slowtransact!must leave exactly one committed datom (previously two, under different tx ids).test-deref-derives-value-from-store—@connreflects adb-withthat bypassedtransact!.test-with-rejects-stale-snapshot— locks in the strictwithbehavior.test-conn-is-not-an-atom— conn no longer satisfiesIAtom;conn?is now a strict instance check.Against the old implementation these produce 3 failures + 1 error; with this branch they pass.
Known limitation (intentionally out of scope)
Deref freshness is scoped to values backed by the same storage connection. A second JDBC connection to the same db-file pins its WAL read snapshot (connections run with
autoCommit=false), so it does not see other connections' commits until its read transaction ends. Fixing that needs storage-layer work (ending read transactions on deref, or per-snapshot read connections) — same bucket as theBEGIN IMMEDIATEcross-process hardening discussed earlier.Follow-up in this PR: db identity instead of content hashing
hash-db/hash-fdb(full-scan content hash with a:hashcache field) and the datom-by-datomequiv-dbare removed — same reasoning as droppingclojure.data/diff: they would have to realize a potentially larger-than-memory database. A db value is now identified by (storage connection,:max-tx, schema) — O(1), like Datomic's (store, basis-t) identity. A filtered db is identified by (underlying db, predicate identity). Removing:hashalso retires the stale-cached-hash bug when advancing a snapshot viaassoc :max-tx.Semantic change: two databases with equal content but different stores are no longer
=(seetest-db-value-identity); filtered-db hashes are no longer content-based (see the updateddbval.test.filterhash test).Follow-up in this PR: DB and FilteredDB are deftypes (opaque handles)
A defrecord always has value semantics — the only question is which ones. After dropping content-based hashing, the honest shape is Datomic's:
DBandFilteredDBare nowdeftypes with plain reference identity. Consequences:d/basis-txis the new public accessor: compare two snapshots of the same store by comparing their basis, not by=.(= @conn @conn)is only guaranteed while the store hasn't moved (deref returns the same handle then).(:schema db)→d/schema,(:max-tx db)→d/basis-tx); internalassoc/map->DBrecord-isms are replaced bydb/with-max-tx/ plain constructors.defrecord-updatable+ its prismatic helpers,db-transient/db-persistent!,restore-db, the unusedrollback-report!, a duplicateTxReportdefrecord, and FilteredDB's map-interface throw stubs (~200 lines net deletion).test-db-value-identitynow pins the Datomic-style contract;test-protocolsasserts db values are not maps.Follow-up in this PR: minimalism pass
Dead code and dependencies removed (
d3d8d4d):query_v3(Datascript's abandoned experiment, ~1,000 lines), the datom comparators orphaned by theclojure.data/diffremoval (also the last UUID-brokenInteger/comparepaths), the mutableidxfield onDatom,case-tree/case-pick/vpredleftovers, the brokencore/settings, and the hollow Datomic-compat shimstempid/resolve-tempidplus the duplicate Datascriptsquuid. Dependencies dropped from deps.edn:nippy(never used) andpersistent-sorted-set(its array helpers are now the 20-linedbval.arraysnamespace).ClojureScript/JS variant removed (
0f466ab): the storage layer is SQLite via JDBC, so the disabled CLJS variant cannot work without a different backend. All#?reader conditionals stripped, every.cljcrenamed to.clj,dbval.js/deps.cljs/externs.js/release-js//test/js/deleted, the CLJS-only macro layer (defn+,if-cljs,patch-tag) removed, andconn/-transact!/conn/reset-conn!(kept only for the JS API) deleted. Recoverable from git history if a CLJS backend ever materializes.Benchmarks refactored, Datomic comparison dropped (
0029a77):bench_datomic/,test_datomic/, the:datomicalias and the wrapper scripts are gone;script/bench.shruns the renameddbval.bench.run(it benches dbval, not Datascript). The benches are adapted to the SQLite/UUID/strict-snapshot world — transaction benches replay against a fresh store per iteration (1k people, batch 1), read benches build a 20k store once, lookups use sampled UUIDs / lookup refs. Running them surfaced and fixed a real bug: storing an empty vector as a value NPEd intuple(regression test added).Test plan
./script/test_clj.sh: 159 tests, 1036 assertions, 0 failures (tests removed alongside deleted features:defrecord-updatable,query-v3,squuid), 0 failures, 0 errors.🤖 Generated with Claude Code
https://claude.ai/code/session_014dX8tTR4yFh5SyBGw3atpo