Add deref value types: large values out of index keys - #13
Merged
Conversation
Attributes flagged with {:dbval/deref true} keep only a SHA-256 content
hash of their value in the index keys; the value's pr-str bytes are
stored once, content-addressed, in a new blob area of the store (SQLite:
dbval_blob table, SlateDB: ("blob", <hash>) keys with the bytes in the
KV value, memory: a map). Reads surface a BlobRef (IDeref) and the value
is only fetched and parsed on deref.
This keeps large values (e.g. several-hundred-KiB FabricJS models) out
of the index keys, which both fixes the SlateDB 64 KiB key cap and stops
scans from edn/read-string-ing large values they never needed.
Equality keeps working by comparing hashes (same content <=> same hash):
datom equality, transact no-op detection, retract-by-value, upserts and
:db/unique, CAS, and datalog equality joins. Blobs commit atomically
with the datom keys and a pending blob overlay provides read-your-writes
for transaction functions and dry-runs. Legacy inline datoms (written
before an attribute was flagged) keep reading and retracting correctly
without a data migration.
Values that serialize to more than 60 KB on a non-deref attribute now
fail with a descriptive error pointing at the flag, instead of an opaque
native error from SlateDB.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
java.util.HexFormat needs JDK 17+, but dbval's core test suite runs on JDK 11 (only store-slatedb requires a newer JDK for slatedb-uniffi). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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.
dbval encodes each datom — value included — into every index key (eavt/aevt/teav, plus avet when indexed). That breaks on SlateDB, which caps keys at 64 KiB, and it makes scans expensive for large values: every scanned datom
edn/read-strings the full value even when a filter discards it. Storrito's FabricJS model EDN strings regularly reach several hundred KiB.Deref value types
Attributes flagged with
{:dbval/deref true}store only a 32-byte SHA-256 of the value's pr-str UTF-8 bytes in the v-position of their index keys. The pr-str bytes themselves are stored once, content-addressed, in a new blob area of the store:dbval_blob (h blob primary key, v blob) WITHOUT ROWID, written in the same transaction as the keys("blob", <hash>)keys with the bytes in the KV value (values may be up to 4 GiB), in the sameWriteBatchITupleStoreaccordingly gains ablobsargument on-commit!and a new-get-blob.Reads surface a
BlobRef, aclojure.lang.IDeref/IPending: the value is only fetched and parsed on@, cached per instance. Printing shows#dbval/blob-ref "<hex>"and never fetches, so logging query results stays safe.Why a content hash (not a random blob id)
serialize-valuemust remain a pure function of the value:fsearch db [e a v](no-op detection, retract-by-value, cardinality-many), the upsert/:db/uniqueAVET scans, and thedatom=post-filter all reconstruct the v-component from a plain value. With a content hash, all those engine paths work unchanged — equality is hash equality (same content ⇔ same hash), so re-asserting an unchanged value is detected as redundant without fetching anything,:db/uniqueand upserts keep working, and datalog equality joins compare 32-byte hashes. Content-addressing also makes blob writes idempotent and deduplicating.Known caveat (documented in the code): Clojure printing is not canonical — two
=small maps built in different insertion orders can pr-str differently and get different hashes. Hash equality is therefore conservative: never false-equal, worst case an occasional redundant retract+assert.Details
BlobRef(resolve-pattern-v), so scan ranges and thedatom=filter both compare hashes.pending-blobsonDB, mirroring the pending key overlay) is committed atomically with the keys and gives transaction functions and chained dry-runs read-your-writes on values asserted earlier in the same transaction.BlobRefs that hash the inline string, so equality against blob-backed datoms of the same value keeps working. They re-serialize to their original inline form (inline-str), which keeps retraction keys adjacent to the assertion keys they cancel out. No data migration needed;find-exact-datomalso finds them where the hash-ranged search cannot.{:dbval/deref true}validates against:db/valueType/:db/tupleAttrscombinations;:db/uniqueand:db/indexremain supported.:transact/value-too-largewith a message pointing at the flag, instead of failing with an opaque native SlateDB error mid-commit.Testing
dbval.test.deref-value(13 tests): round-trip via query/entity/pull, laziness, print form, no-op re-assert, retract+assert on update, history, retract by value / by BlobRef / retractEntity, copy-without-fetch, equality join, unique upsert + lookup ref, cardinality-many, CAS, tx-fn read-your-writes, chained dry-runs, legacy inline datoms, oversize guard, SQLite reopen persistence.dbval.store.slatedb-testgains a test that round-trips a ~300 KiB value through SlateDB — impossible before this change — and checks that blob keys never leak into index scans.🤖 Generated with Claude Code