All notable changes to this project will be documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
Release candidate for 1.0.0. Highlights: Datalevin 1.0.0, Pathom-version agnosticism (2 and 3, no hard pathom dependency), read-your-writes saves, full-text search, and vector similarity search (hybrid keyword + semantic search ready).
:<entity>/similar resolvers)generate-resolvers emits a fourth resolver type: for every entity type
with at least one :vec attribute, a parameterized nearest-neighbor
resolver outputting {:<ns>/similar [{id-attr ...} ...]} — idents in
similarity order (ascending vector distance), with fields auto-filled by
the batched id-resolver:
[{(:account/similar {:vector embedding :top 10}) [:account/id :account/name]}].(:query-params env): :vector (required query embedding),
:attribute (optional narrowing to one :vec attribute), :top. Missing
:vector resolves to an empty list. Uses Datalevin's vec-neighbors with
{:display :refs+dists} + explicit ascending sort (Datalog set semantics
do not preserve engine rank order). Native-id entities supported.vec-conn-opts now also derives :metric-type from
:db.vec/metric-type in ::dlo/attribute-schema (stripped from the
schema alongside :db.vec/dimensions), so cosine/euclidean/etc. can be
declared per attribute.:<entity>/search resolver for hybrid
keyword + semantic search.::dlo/fulltext? declaration,
generated-resolver EQL, parameter reference (:query/:top/:limit/:offset),
phrase search + :index-position?, and a RAD report wiring example
(ro/source-attribute :account/search + a :query control).AccountSearchList report component.:<entity>/search resolvers — Phase 2generate-resolvers now emits a third resolver type: for every entity type
with at least one ::dlo/fulltext? attribute, a parameterized full-text
search resolver outputting {:<ns>/search [{id-attr ...} ...]} — idents in
relevance order (descending score), with fields auto-filled by the
existing batched id-resolver:
[{(:account/search {:query "fox"}) [:account/id :account/name]}].(:query-params env) (populated by both RAD parsers
from the EQL join params / a load's {:params ...}}): :query (string,
boolean expression vector, or {:phrase "..."}), :top, :limit,
:offset. Missing/blank :query resolves to an empty list.{:display :refs+scores} + explicit descending
sort (Datalog set semantics do not preserve engine rank order). Native-id
entities return the matched eid as the id; searches the entity's derived
domain(s), honoring user-specified :db.fulltext/domains/autoDomain.::dlo/fulltext?) — Phase 1::dlo/fulltext? (value true or a search-domain
options map such as {:index-position? true} for phrase/proximity search).:db/fulltext true plus a derived
:db.fulltext/domains [<entity-domain>] — one shared domain per entity
type, named after the attribute namespace. User-supplied native
:db.fulltext/domains / :db.fulltext/autoDomain (via
::dlo/attribute-schema) are respected and suppress derivation.search-conn-opts derives {:search-domains {...}} from map-valued
::dlo/fulltext? declarations; start-database! merges it (alongside
vec-conn-opts) into the d/get-conn options. merge-conn-opts now
deep-merges :search-domains like :vector-domains.:<entity>/search resolver is Phase 2 (see
mementum/knowledge/design/full-text-search.md)..github/workflows/ci.yml — on push/PR to main: runs the full kaocha
suite and a clj-kondo lint job (which regenerates third-party lint configs
from the classpath before linting, since .clj-kondo/imports/ is derived
and gitignored)..github/workflows/release.yml — pushing a vX.Y.Z (full release) or
vX.Y.Z-RCn (release candidate) tag runs the tests, builds the jar with
the version derived from the tag, deploys to Clojars
(us.whitford/fulcro-rad-datalevin), and creates a GitHub Release.
-alpha/-beta version suffixes are local-only and never deploy.build.clj (clojure -T:build clean|jar|install); version defaults to
1.0.0-RC1 and can be overridden with the VERSION env var. The published
pom is built from the root :deps only (no test-only deps such as pathom).:jar/:install depstar aliases removed; :deploy (deps-deploy) now reads
the pom from inside target/classes/ and still runs in an isolated
classpath.::dlo/databases is now {schema -> atom<db>} (was {schema -> db}).
wrap-env / pathom-plugin seed the atoms per request; save-form! and
delete-entity! reset! the relevant atom to the transaction report's
:db-after. As a result, a form/save-form mutation's output query resolves
the just-saved (or updated) entity within the same request — the mutation
returns the saved attributes, not just :tempids — matching the datomic/xtdb
adapters. Uses Datalevin's native :db-after (idiomatic; the report is already
in hand) for read-your-writes plus request-scoped snapshot consistency.db is still accepted), so
callers passing a plain db in a hand-built env continue to work.pathom_integration_test.clj drives the adapter through real RAD
parsers — pathom/new-parser (Pathom 2) and pathom3/new-processor
(Pathom 3) — with full CRUD parity: id-resolver read, all-ids read,
form/save-form new + update (asserting the returned entity → proves
read-your-writes), and form/delete-entity. com.wsscode/pathom (2) added as
a test-only dependency alongside pathom3.pathom3 moved
out of :deps into the :test/:run-tests aliases only. The main namespaces
load with no Pathom library on the classpath.generate-resolvers now emits Pathom-2-shape resolver maps (plain data
keyed by :com.wsscode.pathom.connect/{sym,input,output,batch?,resolve}). These
work directly in a Pathom 2 parser and are auto-converted by RAD's Pathom 3
processor (new-processor runs convert-resolvers), so Pathom 3 apps need
no changes. Batching survives the conversion.generate-resolvers-pathom3 — new convenience returning native Pathom 3
resolver records for callers that build a Pathom 3 index directly. Pathom 3 is
resolved lazily via requiring-resolve at call time, keeping it optional.:conn-opts pass-through in start-database! / start-databases — a
database config may now supply a :conn-opts map of native Datalevin
get-conn options, merged with the adapter-derived :vector-domains. This
unlocks v1.0 features without adapter changes, notably:
:auto-entity-time? — Datalevin maintains :db/created-at / :db/updated-at
(epoch-millis) per entity automatically. Free audit timestamps for RAD
entities.:validate-data? — runtime value-type validation on transact.:closed-schema? — reject attributes not defined in the schema.:wal?, :search-domains, :kv-opts, :idoc-domains, etc.::dlo/transact-options is now honored — passed as Datalevin tx-meta
(the third arg to transact!) for every save transaction, so it is visible in
tx reports and listen! callbacks (e.g. audit/user context).::dlo/transaction-timeout-ms is now honored — when present, each save
transaction runs inside a with-transaction with that per-transaction
:timeout-ms and is aborted if it exceeds it (v1.0). Preferred over the global
set-explicit-transaction-timeout! for per-request control.::dlo/max-batch-size is now honored — auto-generated id-resolvers read it
from the env to override the default (1000) batch limit (previously the option
was declared but ignored).pathom-plugin is now a Pathom 2 ::p/wrap-parser plugin (was a Pathom 3
wrap-root-run runner plugin). Pathom 3 apps should compose wrap-env
(unchanged (fn [env] env')) into their processor's env-middleware instead —
the idiomatic RAD Pathom 3 integration point.generate-resolvers return shape changed from native Pathom 3 records to
Pathom-2-shape maps (see Added). Code that introspected ::pco/* keys off the
returned resolvers must read :com.wsscode.pathom.connect/* instead (or call
generate-resolvers-pathom3).pathom3 is no longer a transitive dependency; consumers bring their own
Pathom (2 or 3).:unresolved-var exclude for
datalevin.core/with-transaction — Datalevin 1.0.0 re-exports it at runtime
via import-macro, which clj-kondo cannot statically resolve. Surfaced by
regenerating the derived lint cache; CI regeneration would hit it too.:output {:exclude-files [".clj-kondo/imports/"]} — imports/ is
derived third-party config regenerated from jars; guardrails 1.3.3 ships a
hook file that trips redundant-str-call in editors otherwise.delete-entity! now resolves the entity via d/entid (raw id for native-id
attributes, [pk id] lookup ref otherwise) instead of a manual d/q, and
handles native-id deletes correctly. Deleting a non-existent entity remains
an idempotent no-op.::dlo/max-retries — it was declared but never implemented, and blind
retries are inappropriate for embedded Datalevin (no transient network layer,
and permanent failures like :transact/attr-pred must not be retried).[{}], which Pathom 2 rejected as
an "Invalid resolve response". Resolvers now detect single-vs-batch input and
respond in kind. Surfaced by the new real-parser integration tests.delete-entity! now propagates transaction failures via ex-info
({:ident :schema}) instead of returning {} silently — mirroring the earlier
save-path fix.fix-numerics) — incoming values are now coerced
to match the RAD attribute's declared numeric type before transacting
(:int/:long → long, :double/:float → double, :bigdec → bigdec). This
handles JavaScript clients that transmit integers where doubles are expected
(and vice versa), avoiding :db.type mismatch errors. Non-numeric values and
types pass through unchanged. (PLAN #9)::dlo/wrap-resolve on identity resolvers — the previously-declared
::dlo/wrap-resolve attribute option is now covered by tests confirming it
wraps the generated id-resolver for custom pre/post processing
((fn [resolve]) => (fn [env input])). (PLAN #5)schema-problems / verify-schema!) — new functions
(re-exported from the datalevin facade) compare the RAD-derived expected
schema against the live Datalevin schema of a connection. schema-problems
returns :missing and :mismatch problem maps for the adapter-managed keys
(:db/valueType, :db/cardinality, :db/unique); verify-schema! throws
ex-info when problems exist and returns true otherwise. (PLAN #8):db.attr/preds — declare
database-enforced validation on an attribute by adding :db.attr/preds to its
::dlo/attribute-schema. The predicate(s) must be qualified symbols (the
schema is persisted and resolved via requiring-resolve), invoked as
(pred value) and must return strictly true; any other result aborts the
write with a :transact/attr-pred error. This follows the Datalevin-native
convention (and the Datomic adapter's "put native schema keys in
attribute-schema" philosophy) rather than introducing a new option key.
(defattr email :account/email :string
{::attr/identity? true
::dlo/attribute-schema {:db.attr/preds 'my.app/valid-email?}})
:db/ensure + ::dlo/raw-txn — new
env key ::dlo/raw-txn and helper append-to-raw-txn (re-exported from the
datalevin facade) let save middleware append native Datalevin transaction
forms to a save. The primary use is [:db/ensure pred & args] post-conditions,
which run against db-after and abort the transaction on any falsey result.
(dlo/append-to-raw-txn env [[:db/ensure `my.app/balance-non-negative? [:account/id id]]])
Note: in a multi-schema save the forms are appended to each affected schema's transaction.
:vec):vec now map to Datalevin's :db.type/vec
and initialize a vector (HNSW) index at connection time.:dimensions) lives on the connection,
not the schema. start-database now:
:db.vec/dimensions from the generated schema map (it is not a valid
Datalevin schema key).vec-attr-domain (mirrors datalevin.vector/attr-domain: / → _).:vector-domains {"domain" {:dimensions N}} to d/get-conn so
Datalevin can build the HNSW index.vec-conn-opts extracts these connection options from the
:vec attributes for a schema.init-vector-domains (storage layer), which
walks the schema for :db.type/vec attributes and merges per-domain config
from the :vector-domains connection option.0.10.5 → 1.0.0 — first stable Datalevin release. Notable
upstream additions now available to build on: :db/ensure transaction special
form (post-condition checks), :db.attr/preds attribute predicates,
datalog-kv, and search :limit/:offset.
:bytes is no longer a valid tuple
component). No impact on this adapter's current usage.3.9.2 → 3.9.31.6.20 → 1.6.233.159.0 → 3.169.1 — required by Datalevin 1.0.0's
nippy 3.7.0-beta1 (needs encore >= 3.160.1); resolves a version-conflict
load error.save-form! previously caught transaction exceptions, logged, and
returned {} — silently reporting success on a failed save. It now rethrows
via ex-info with {:schema :txn-data} context so attribute-predicate
(:transact/attr-pred), :db/ensure post-condition, and any other transaction
failures propagate to the caller/client.:db/ident format when saving:admin or #{:read :write} failed with:
clojure.lang.ExceptionInfo: Nothing found for entity id :admin
:db.type/ref, which expects entity references, not raw keywordsstart-databases code correctly creates :db/ident entities (e.g., :account.role/admin), but the save code wasn't converting raw values to matchconvert-enum-value function in wrap-datalevin-save that:
:db/ident format (e.g., :admin → :account.role/admin):status/active stays as-is)#{:read :write} → #{:account.permissions/read :account.permissions/write});; Before (failed):
;; Delta: {:account/role {:before nil :after :admin}}
;; Transaction attempted: {:db/id [...] :account/role :admin}
;; Error: Nothing found for entity id :admin
;; After (works):
;; Delta: {:account/role {:before nil :after :admin}}
;; Transaction: {:db/id [...] :account/role :account.role/admin}
;; Success: Datalevin resolves :account.role/admin to the correct entity ref
:person/id) used the query [:find ?e :where [?e _ _]] which returned ALL entities in the database[:person/all] to incorrectly return enums, other entity types, and all database entities instead of just person entities[?e _ _] pattern matches any entity with any attribute:person/name for :person/id) and queries for entities that have that specific attributeall-ids-resolver function signature to accept all-attributes parameter for finding sample attributes;; Before (broken):
;; Query: [:person/all]
;; Returns: [{:person/id 1} {:person/id 2} ... {:person/id 47}] ; All 47 entities including enums!
;; After (fixed):
;; Query: [:person/all]
;; Uses: [:find ?e :in $ ?attr :where [?e ?attr _]] with ?attr = :person/name
;; Returns: [{:person/id 1} {:person/id 2}] ; Only 2 actual person entities
native_id_all_resolver_test.clj:
native-id-all-resolver-excludes-other-entities - verifies only correct entity type is returnednative-id-all-resolver-returns-full-data - tests integration with id-resolvernative-id-all-resolver-with-no-attributes - handles edge case of entity with only identity attributenative-id-all-resolver-query-uses-correct-attribute - validates sample attribute selectioncom.fulcrologic/guardrails dependency for runtime validation>defn with specs for better error messagesautomatic-schema: validates schema-name (keyword) and attributes (::attr/attributes)delta->txn: validates env (map) and delta (map) inputsgenerate-resolvers: validates attributes and schema inputsdatalevin-result->pathom-result: validates key->attribute map and EQL queryid-resolver: validates all-attributes, id-attribute, and output-attributes:db/id identity attributes (following Datomic pattern)::dlo/native-id? option for identity attributes:db/id instead of a domain-specific attribute:db/id back to the identity attribute key in results(defattr id :person/id :long
{::attr/identity? true
::dlo/native-id? true ; ← Uses :db/id directly
::attr/schema :production})
native-id?: checks if an attribute uses native IDpathom-query->datalevin-query: converts Pathom EQL to Datalevin pull patterndatalevin-result->pathom-result: maps :db/id back to identity key::dlo/wrap-resolve option for identity attributes(defattr id :account/id :uuid
{::attr/identity? true
::dlo/wrap-resolve (fn [resolve]
(fn [env input]
;; Pre-processing
(let [result (resolve env input)]
;; Post-processing
result)))})
{:db/id 18}) instead of their :db/ident keyword valuesname on a mapreplace-ref-types function (following fulcro-rad-datomic pattern) that walks pull results and replaces enum entity references with their :db/ident values::attr/cardinality :one) and multi-valued (::attr/cardinality :many) enum attributes;; Before (broken):
{:account/id uuid
:account/name "Alice"
:account/role {:db/id 18}} ; ← Error: can't call (name) on a map
;; After (fixed):
{:account/id uuid
:account/name "Alice"
:account/role :account.role/admin} ; ← Correct: keyword value
:enum attribute type:db.type/ref in Datalevin (following Datomic pattern):db/ident#{:admin :user} → auto-generates :account.role/admin, :account.role/user#{:status/active :status/inactive} → uses as-is:one and :many cardinality for enum attributes(def account-role
{::attr/qualified-key :account/role
::attr/type :enum
::attr/schema :production
::attr/identities #{:account/id}
::attr/enumerated-values #{:admin :user :guest}
::attr/enumerated-labels {:admin "Administrator"
:user "Regular User"
:guest "Guest User"}})
d/pull, use a pull pattern with :db/ident:
;; For single-valued enum
(d/pull db [:account/id {:account/role [:db/ident]}] [:account/id id])
;; Returns: {:account/id uuid :account/role {:db/ident :account.role/admin}}
;; For many-valued enum
(d/pull db [:account/id {:account/permissions [:db/ident]}] [:account/id id])
;; Returns: {:account/id uuid :account/permissions [{:db/ident :read} {:db/ident :write}]}
get-metrics, reset-metrics!){:tempids {}} to match RAD expectationseclipse-collection->clojure from public API{:tempids {...}}, not full transaction resultssave-form! now returns only {:tempids {...}} map, not full transaction resultdelete-entity! now returns {}, not transaction resultwrap-datalevin-save and wrap-datalevin-delete ensure :tempids key is present in resultq, pull, pull-many) return raw Datalevin resultsseed-database! returns raw transaction result (not used in RAD operations)eclipse-collection?, convert-eclipse-collection, and eclipse-collection->clojure functionsclojure.walk dependencysave-middleware-returns-tempids - Tests save operations always include :tempidsdelete-middleware-returns-tempids - Tests delete operations always include :tempidseclipse-collection->clojure directly, remove those calls. Query and form operations now work without conversion.all-ids-resolver functionality that was removed during XTDB-style refactorgenerate-resolvers now creates two types of resolvers for each entity:
:account/id -> account data):all-accounts -> [{:account/id ...} ...]);; Query for a specific account by ID
[{:account/id some-uuid} [:account/name :account/email]]
;; Query for all account IDs
[:account/all-accounts] ;; Returns [{:account/id uuid-1} {:account/id uuid-2} ...]
:entity/all-entitys (e.g., :account/all-accounts, :item/all-items)test_utils.clj
utilities.clj: empty-db-connection, create-temp-database!, with-temp-database, seed-database!, mock-resolver-envtest_utils.clj: seed-database!, mock-resolver-envutilities.clj: Now only contains production query helpers (q, pull, pull-many)
datalevin.clj namespace for all public API functionsdatalevin.clj re-exports: Now directly re-exports from source namespaces
get-by-ids from generate-resolversdelta->txn, keys-in-delta, schemas-for-delta, save-form! from wrap-datalevin-saveutilities.cljstart-database!: Now correctly uses the :schema parameter instead of ignoring it
:default when calling automatic-schemawith-test-conn and with-test-conn-attrs now lint correctlyPLAN.md (was empty, not being used)DEDUPLICATION_SUMMARY.md (changes now documented in CHANGELOG)datalevin.clj (~900 lines) into focused modules:
datalevin/start_databases.clj - Database lifecycle and schema generationdatalevin/pathom_plugin.clj - Pathom3 plugin for database accessdatalevin/generate_resolvers.clj - Automatic resolver generationdatalevin/wrap_datalevin_save.clj - Save form middlewaredatalevin/wrap_datalevin_delete.clj - Delete form middlewaredatalevin/utilities.clj - Query helpers, delta processing, and test utilitiesdatalevin.clj now serves as a clean API facade, re-exporting all public functionsgenerate-resolvers:
(generate-resolvers attributes) or (generate-resolvers attributes schema)(generate-resolvers attributes schema) - schema requiredMiddleware:
((wrap-datalevin-save {:default-schema :main}) handler)(wrap-datalevin-save handler) or (wrap-datalevin-save) for terminal::attr/schema in attributesRemoved Features:
all-ids-resolver (not in XTDB)ref-resolvers (not in XTDB)id-resolver from public API (internal only)datalevin_test.cljdatalevin_core_test.clj, datalevin_middleware_test.clj, datalevin_new_entity_test.clj, datalevin_save_test.cljwith-temp-database macro location for proper importssave-form! function to be testable independently of middleware contextwrap-datalevin-save middleware to delegate to save-form!test_utils.clj for proper macro lintingwith-temp-database macro now available via:
(require '[us.whitford.fulcro.rad.database-adapters.datalevin.utilities :refer [with-temp-database]])First beta release of fulcro-rad-datalevin, a Datalevin database adapter for Fulcro RAD. This release includes full CRUD functionality, comprehensive error handling, and production-ready features.
Test Coverage: 56 tests, 269 assertions, 0 failures ✅
test_utils.clj - Shared utilities, fixtures, and test datadatalevin_core_test.clj - Core functionality tests (23 tests)datalevin_save_test.clj - Save/delete middleware tests (15 tests)datalevin_new_entity_test.clj - New entity creation tests (9 tests)datalevin_middleware_test.clj - Middleware composition tests (9 tests):tempids in the result map, even when saving existing entities:tempids was only added when there were actual tempid mappings, causing Pathom3 "attribute-unreachable" errors when RAD tried to query for :tempids after updating existing entities{:tempids {}} at minimum, allowing RAD's EQL queries to work correctlydatalevin_tempids_test.clj to prevent regressioncreate-temp-database! function returns cleanup function to prevent resource leakswith-temp-database macro automatically cleans up temporary databasesempty-db-connection now includes warning about cleanup responsibility*max-batch-size* dynamic var allows customization per-context::dlo/transaction-timeout-ms option key::dlo/max-retries option key::dlo/max-batch-size option key*max-batch-size**transaction-timeout-ms**max-retries*clojure.set importempty-db-connectioncreate-temp-database! or with-temp-databaseex-info with :schema and :available-schemas in ex-databinding to increase *max-batch-size* if needed, or paginate large queriesConnection Configuration: Ensure all schemas referenced in your attributes have corresponding connections in your configuration. Missing connections now throw exceptions instead of silently failing.
Large Batch Queries: If you query more than 1000 entities at once, either:
(binding [dl/*max-batch-size* 5000] ...)Temporary Databases: Update test code to properly clean up:
;; Old (leaks resources)
(let [conn (dl/empty-db-connection :test attrs)]
...)
;; New (automatic cleanup)
(dl/with-temp-database [conn :test attrs]
...)
;; Or manual cleanup
(let [{:keys [conn cleanup!]} (dl/create-temp-database! :test attrs)]
(try
...
(finally
(cleanup!))))
Error Handling: Transaction and connection errors are now thrown as ex-info exceptions. Wrap operations in try-catch if you need custom error handling:
(try
((middleware handler) env)
(catch clojure.lang.ExceptionInfo e
(log/error "Database operation failed:" (ex-data e))))
Can you improve this documentation?Edit on GitHub
cljdoc builds & hosts documentation for Clojure/Script libraries
| Ctrl+k | Jump to recent docs |
| ← | Move to previous article |
| → | Move to next article |
| Ctrl+/ | Jump to the search field |