Layer 01: the KV abstraction
The ordered key-value store and transaction primitives at the bottom of Rad's engine layer stack.
Source: rad/01_kv (package kv), with subpackages keyenc, kvslate, and
kvtest. This is the bottom of the layer stack described in rad/doc.go and
enforced by scripts/check_layers.sh: a package under rad/NN_* may import
only same-or-lower-numbered layers, so kv imports nothing from the rest of
Rad. Everything above it — catalog, IR, planner, executor, frontend — reaches
storage exclusively through the interfaces defined here.
What the layer is, and is not
Package kv defines a minimal ordered key-value store with optimistic
transactions. Its entire vocabulary is []byte keys, []byte values, and
half-open key ranges. It knows nothing about the catalog, tables, rows,
columns, indexes, or the IR's value model; the words "table" and "row" do not
appear in its API. The one semantic commitment it makes is ordering: keys
iterate in ascending lexicographic byte order, which is what lets the layers
above encode tuples such that a range scan returns rows in semantic order.
The companion subpackage keyenc is the other half of that commitment. It is
still storage vocabulary — bytes and Go primitives, no rows or types — but it
provides the order-preserving encodings (EncodeInt64, EncodeFloat64,
EncodeString, EncodeBool, EncodeNull) that layer 05 composes into
row-key tuples. Splitting them this way keeps the contract clean: kv
promises byte order, keyenc makes byte order coincide with value order.
The KV interface
kv.KV is the plain read/write surface:
type KV interface {
Get(ctx context.Context, key []byte) ([]byte, bool, error)
Put(ctx context.Context, key []byte, value []byte) error
Delete(ctx context.Context, key []byte) error
Scan(ctx context.Context, start, end []byte) (Iterator, error)
}
Get distinguishes "missing" from "error" with its boolean: a missing key is
(nil, false, nil), never an error. Put overwrites unconditionally.
Delete of a missing key is not an error. There is no compare-and-swap and
no versioning at this level; conditional logic belongs in transactions.
Scan iterates keys in [start, end) — start inclusive, end exclusive — in
ascending lexicographic order. A nil start means "from the beginning" and a
nil end means "to the end"; Scan(ctx, nil, nil) walks the whole store.
There is no descending scan and no prefix parameter: a prefix scan is spelled
Scan(ctx, prefix, keyenc.PrefixEnd(prefix)). PrefixEnd returns the
smallest key greater than every key with the given prefix (increment the last
non-0xFF byte, truncate after it), and returns nil — meaning "unbounded"
— when no such key exists (empty or all-0xFF prefix). That nil return
composes correctly with Scan's nil-end convention, so the prefix idiom is
total.
Iterators
type Iterator interface {
Next() bool
Key() []byte
Value() []byte
Err() error
Close() error
}
The usage protocol is the standard Go iterator dance: loop on Next, then
check Err, then Close. Key and Value are documented valid only until
the next Next; callers that hold data across steps must copy
(05_exec/scanrange.go does exactly this before its interleaved Get).
Interleaving other operations with an open iterator is part of the de facto
contract even though kv.go does not spell it out: the executor's
indexRangeIterator performs a point Get against the same view between
every Next while walking an index range, and the whole engine test suite
exercises this against SlateDB. What is not defined is the visibility of
writes made to the same transaction after Scan returned — a Txn scan
reflects the transaction's buffered writes as of the Scan call (the
conformance suite's ScanSeesOwnWrites pins this), but nothing tests or
promises what an already-open iterator shows if you Put mid-iteration.
Upper layers do not do that; new code should not either.
One lifecycle rule is explicit: iterators obtained from a Txn must be
closed before Commit.
Errors
The package defines exactly one sentinel, kv.ErrConflict, checked with
errors.Is (layer 05 re-exports the check as exec.IsConflict). Everything
else is an opaque implementation error. Absence is signalled by boolean
returns, never by error values, so there is no ErrNotFound to forget to
check.
Transactions
kv.TransactionalKV is what backends implement: the plain KV surface plus
Begin(ctx, level) (Txn, error). Plain operations on the store are
implicitly single-op transactional. A Txn is itself a KV — this embedding
is the load-bearing design decision of the layer, because it means every
function above that takes a kv.KV "view" works identically inside or
outside a transaction. The catalog's DDL helpers and the executor's
constraint checks are all written against kv.KV and get transactionality
for free (see catalog.Catalog.ddl, exec.Engine.Txn).
A Txn reads from a stable snapshot taken at Begin, overlaid with its own
buffered writes: its own Puts appear in its Gets and Scans, its own
Deletes hide keys, and concurrent commits by others are invisible until it
finishes. Writes are buffered in memory and applied atomically and durably at
Commit. A Txn is not safe for concurrent use by multiple goroutines.
Concurrency control is optimistic, with the isolation level chosen at
Begin:
kv.Snapshot— detects write-write conflicts only: two transactions that both write the same key cannot both commit; the second getsErrConflict. Susceptible to write skew (the conformance suite deliberately proves the skew is allowed at this level).kv.SerializableSnapshot— additionally detects read-write conflicts. Both point reads and the requested bounds of scans are validated at commit against keys committed by others afterBegin. The bounds rule is what makes phantom detection work: scanning an empty range records the range itself, so a concurrent insert anywhere in it conflicts even though the scan returned nothing. This is precisely what makes optimistic unique-index existence checks sound (kvtest.testPhantomRangeConflictmodels one).
Conflicts surface only at Commit, as an error wrapping ErrConflict; there
is no locking and no early abort. The transaction is unusable after Commit
returns, success or failure. Rollback discards the transaction and is a
no-op after Commit or a prior Rollback, so defer txn.Rollback() is
always safe — every caller in the tree uses that pattern.
Writes always run under SerializableSnapshot: catalog.Catalog.ddl and
exec.Engine.Begin both hard-code it. Snapshot is the read-side level —
05_exec opens one per autocommit statement (Engine.Execute,
GetByPrimaryKey, ScanTable, ScanIndex) so schema resolution and data
reads share a single storage moment, then discards it with Rollback.
The SlateDB adapter
kvslate.Store adapts SlateDB, via the slatedb.io/slatedb-go uniffi
bindings, to kv.TransactionalKV. The mapping is nearly one-to-one because
SlateDB already speaks the same language: raw byte keys in lexicographic
order, range scans with inclusive-start/exclusive-end bounds
(kvslate.keyRange), and native transactions. kv.Txn maps onto
slatedb.DbTransaction; kv.Snapshot and kv.SerializableSnapshot map onto
SlateDB's identically-named isolation levels; commit-time
slatedb.ErrErrorTransaction is wrapped as kv.ErrConflict. Commit uses
SlateDB's default write options, which await durability.
kvslate.Open(name, storeURL) opens a database on an object store resolved
from a URL — "file:///path" for a real deployment, "memory:///" for
tests. Context handling in the adapter is entry-only: every method checks
ctx.Err() before calling into SlateDB, but a running scan or commit is not
interruptible mid-flight, and iterator Next does not consult the context at
all. The adapter also carries uniffi lifecycle chores (Destroy on
transactions, iterators, and commit handles) that the kv interfaces
deliberately hide.
There is no pure-Go in-memory implementation of kv.TransactionalKV. The
"in-memory test double" that rad/doc.go mentions is SlateDB itself on a
memory:/// object store — which means every test in the repository that
touches storage, from 02_catalog up through e2e, needs the native SlateDB
library (CGO_LDFLAGS="-L<repo>/lib -Wl,-rpath,<repo>/lib", per the
kvslate package comment).
Key encoding
keyenc implements CockroachDB-style order-preserving encodings. Every
encoded value starts with a type tag (TagNull 0x01 < TagBool 0x02 <
TagInt64 0x03 < TagFloat64 0x04 < TagText 0x05), so mixed-type
sequences order deterministically — NULL before everything — and decode
unambiguously via Peek. Within a type, byte order matches value order:
integers flip the sign bit and store big-endian; floats flip the sign bit
when non-negative and all bits when negative (callers must reject NaN — its
ordering is undefined and nothing here checks); strings escape interior NUL
as {0x00, 0xFF} and terminate with {0x00, 0x01}, which both preserves
order ("app" < "apple") and prevents prefix collisions between distinct
tuples.
Encodings are self-delimiting, so values concatenate into tuple keys with no
separator, and the decoders (DecodeInt64, DecodeString, …) each return
the byte count consumed so tuples split back apart. Note the tag ordering's
corollary: int64 and float64 are different types to the encoding, so a
key position that mixes them does not sort numerically across the two — every
encoded float sorts after every encoded int. A column must encode with one
consistent tag; that discipline belongs to layer 05, not here.
Invariants downstream layers rely on
Three properties of this layer are load-bearing for correctness above it.
First, ascending lexicographic scan order plus keyenc's order preservation
is why 05_exec can serve an ordered or range-bounded query as a single
Scan over /rad/data/<table>/... keys with no sort. Second,
SerializableSnapshot's validation of scan bounds (not just returned keys)
is why the executor's read-then-write patterns — unique-index existence
checks, foreign-key checks — are sound without locks; a phantom insert into a
checked range forces ErrConflict and the caller retries the whole
exec.Engine.Txn callback. Third, Txn being a kv.KV is why the entire
catalog and executor codebase is written once against views rather than twice
against store-vs-transaction.
Testing
kvtest.Run is the executable definition of the transaction semantics: a
conformance suite over any kv.TransactionalKV, covering commit visibility,
rollback idempotence (including rollback-after-commit being a no-op), snapshot
read stability, scans seeing own writes and hiding own deletes, write-write
conflicts under both levels, read-key conflicts, phantom range conflicts, and
the write-skew pair (allowed under Snapshot, blocked under
SerializableSnapshot). kvslate_test.go runs the suite against SlateDB on
memory:/// (TestTransactionConformance) and adds adapter-specific checks:
lexicographic ordering with binary keys, [start, end) bound semantics, and
the PrefixEnd prefix-scan idiom against realistically shaped
/rad/data/... keys.
keyenc is tested three ways: ordered fixtures plus randomized
order-agreement checks (bytes.Compare of encodings must agree with value
comparison over a thousand random pairs), round-trip decoding including
random binary strings with NUL and 0xFF edge cases, and testable examples
(example_test.go) that double as usage documentation for order
preservation, self-delimiting tuples, and PrefixEnd. Cross-type tag
ordering and the embedded-NUL prefix-safety property each have a dedicated
test, since index correctness depends on both.