DatalevinDatalevin
Part VII - Appendices

Appendix F: Key-Value API

This appendix is a compact reference for public Datalevin KV functions exposed from datalevin.core. Chapter 10 is the tutorial introduction. Chapters 19 and 20 cover durability, snapshots, WAL, re-indexing, and ingestion strategy. Appendix A covers host-language binding conventions.

1. Store Lifecycle

Function Common forms Purpose
open-kv (d/open-kv dir), (d/open-kv dir opts) Open a local KV store or a remote dtlv:// KV database.
close-kv (d/close-kv db) Close a KV store.
closed-kv? (d/closed-kv? db) Return true if the KV store is closed.
dir (d/dir db) Return the path or URI string for the store.
datalog-kv (d/datalog-kv conn), (d/datalog-kv db) Return the KV handle backing a local or remote Datalog connection or DB.
copy (d/copy db dest), (d/copy db dest compact?) Copy a Datalog or KV database to another directory, optionally compacting.
sync (d/sync db) Force a synchronous flush to disk.
re-index (d/re-index db opts) Dump, clear, reload, and rebuild a KV store with new options.

This table lists common option groups, not every accepted key.

Common option group Keys
Capacity and environment :mapsize, :max-dbs, :max-readers, :flags, :temp?, :inmemory?
Store identity :db-name, :db-identity
Embedded write collection :write-batch-size, :write-batch-delay-us
WAL commit path :wal?, :wal-durability-profile, :wal-group-commit, :wal-group-commit-ms
LMDB checkpointing :lmdb-sync-interval-ms, :lmdb-sync-interval, :wal-checkpoint-stale-threshold-ms
WAL segments and retention :wal-segment-max-bytes, :wal-segment-max-ms, segment preallocation options, :wal-retention-bytes, :wal-retention-ms, retention backpressure options
Snapshots :snapshot-scheduler?, :snapshot-dir, :snapshot-interval-ms, :snapshot-max-lsn-delta, :snapshot-max-log-bytes-delta, :snapshot-max-age-ms, snapshot contention and defer options
Client :client-opts
Spill to disk :spill-opts
HA :ha-mode, :ha-control-plane, lease timing, fencing, promotion, and clock-skew options

Chapter 19 covers WAL, snapshot, checkpoint, and retention tuning in detail. WAL is disabled by default; for a new store, when it is enabled and no durability profile is specified, the profile defaults to :strict. Existing stores retain their persisted profile as Chapter 19 describes. Without WAL, the default LMDB environment flags are #{:nordahead :notls}. Persistent WAL environments add :writemap when no explicit :flags option is supplied; WAL also enables LMDB :nosync, with durability supplied by the WAL profile and checkpoints explicitly syncing LMDB. Chapter 19 explains these defaults, and Chapter 20, Section 1.4 covers embedded write collection options.

d/datalog-kv returns a borrowed handle that is safe to retain and reuse for as long as its owning Datalog connection or raw DB remains open. Unlike the Datalog DB object returned by d/db, it does not need to be obtained again after writes. Close the owner, not the borrowed KV handle. re-index is an offline maintenance operation; use it only when other threads or programs are not accessing the same store.

For a Datalog database that also stores application KV DBIs through datalog-kv, the dtlv dump/dtlv load auto mode can round-trip the mixed store: Datalog schema and datoms plus user-created KV DBIs in the same LMDB environment. Appendix E lists the datalevin.main functions and shows the command-line form. Use copy when you want a physical database copy; use dump/load when you want a portable text export/import workflow.

2. DBI Management

Function Common forms Purpose
open-dbi (d/open-dbi db name), (d/open-dbi db name opts) Open a regular named DBI.
open-list-dbi (d/open-list-dbi db name), (d/open-list-dbi db name opts) Open a DUPSORT/list DBI.
clear-dbi (d/clear-dbi db name) Remove data from a DBI but leave the DBI open.
drop-dbi (d/drop-dbi db name) Clear and delete a DBI.
list-dbis (d/list-dbis db) Return names of sub-databases in the store.
stat (d/stat db), (d/stat db name) Return LMDB statistics for the environment or a DBI.
entries (d/entries db name) Return the number of entries in a DBI.

Use regular DBIs for one value per key and list DBIs for sorted multi-values per key. :validate-data? checks keys and values against KV type descriptors; it is independent of Datalog schema validation. :key-type and :value-type name registered custom types for ordered keys and list items; Section 12 covers them.

Option or flag group Values
DBI options :key-type, :value-type, :key-size, :val-size, :validate-data?, :closed-schema?, :flags
Default DBI flags #{:create :counted :prefix-compression}
Common LMDB flags :create, :reversekey, :dupsort, :integerkey, :dupfixed, :integerdup, :reversedup, :counted, :prefix-compression

3. Writes and Transactions

Transaction functions:

Function Common forms Purpose
transact-kv (d/transact-kv db txs), (d/transact-kv db name txs k-type v-type) Synchronously apply KV write operations.
update-kv (d/update-kv db name key f), (d/update-kv db name key f k-type), (d/update-kv db name key f k-type v-type & args) Atomically replace one value with (apply f old-value args); returns :transacted. Types default to :data.
prepare-update-kv (d/prepare-update-kv db name f), (d/prepare-update-kv db name f k-type), (d/prepare-update-kv db name f k-type v-type & args) Prepare a reusable atomic update; invoke with a key or use execute-prepared.
transact-kv-async (d/transact-kv-async db txs), (d/transact-kv-async db name txs k-type v-type callback) Batch asynchronous KV writes; returns a future.
with-transaction-kv (d/with-transaction-kv [tx db] body...), (d/with-transaction-kv [tx db {:timeout-ms n}] body...) Run reads and writes in one explicit KV transaction, optionally with a timeout.
abort-transact-kv (d/abort-transact-kv tx) Roll back writes from inside with-transaction-kv.

Transaction data forms:

Transaction item Shape
Self-contained put [:put dbi-name key value], [:put dbi-name key value key-type value-type], or [:put dbi-name key value key-type value-type flags]
Self-contained delete [:del dbi-name key] or [:del dbi-name key key-type]
Single-DBI put [:put key value] or [:put key value flags]
Single-DBI delete [:del key]
List operators :put-list and :del-list use the same transaction shapes as puts, with a sequential value collection.

key-type, value-type, and flags are optional. Common write flags include :nooverwrite, :nodupdata, :current, :reserve, :append, :appenddup, and :multiple. Use :append only with input already sorted in the target DBI's key order.

KV keys and put values cannot be nil. For list DBI writes, prefer put-list-items and del-list-items; they wrap the lower-level :put-list and :del-list transaction operators.

update-kv supports single-value DBIs only. Missing keys pass nil to f; exceptions or invalid/nil results abort the update. Use an inter-fn for remote stores; local calls also accept ordinary Clojure functions. The function may run more than once, so use it only to compute the new value. Passing an explicit transaction's KV handle makes the update part of that transaction. Chapter 10, Section 5.1 shows counter updates in all four languages.

prepare-update-kv fixes the function, DBI, types, and extra arguments. Each invocation atomically updates the supplied key and returns :transacted. For remote stores, use inter-fn; the preparation reuses the serialized and validated function per connection and registers again after reconnect or handle eviction. Keep the owning store open while reusing the preparation.

4. Encoding Helpers

For raw key and value encode and decode:

Function Common forms Purpose
put-buffer (d/put-buffer bf x), (d/put-buffer bf x x-type) Encode a value into a ByteBuffer. Mostly useful in raw visitors.
read-buffer (d/read-buffer bf), (d/read-buffer bf v-type) Decode a value from a ByteBuffer. Mostly useful in raw visitors.
k (d/k kv) Return the raw key buffer from an IKV visitor object.
v (d/v kv) Return the raw value buffer from an IKV visitor object.

Data types:

Descriptor form Meaning
:data General serialized data.
:string, :long, :float, :double, :bigint, :bigdec Scalar numeric or string encodings.
:bytes, :keyword, :symbol, :boolean, :instant, :uuid Other scalar encodings.
[:string :instant], [:long], [:int :string], [:int] Tuple key or value encodings. :int components use signed 32-bit ordering; :id, :data, and :bytes are not tuple component types.

KV type descriptors use unqualified keywords, not Datalog value-type keywords such as :db.type/string. Prefer one consistent key type, or one tuple key type, per application DBI.

For DBI operations, a registered custom type name can select a declared custom key or list item type (Section 12). Custom values need the store's type registration and payload storage; raw put-buffer and read-buffer calls do not encode or decode complete custom values on their own.

:ignore is not a put-buffer or read-buffer descriptor. It is a range-scan sentinel accepted by scan APIs when the caller wants to skip value decoding.

5. Point Reads and Order Statistics

Function Common forms Purpose
get-value (d/get-value db name k), (d/get-value db name k k-type v-type ignore-key?) Get one key's value, or [k v] when ignore-key? is false.
get-rank (d/get-rank db name k), (d/get-rank db name k k-type) Return a key's zero-based rank in sorted key order.
get-by-rank (d/get-by-rank db name rank), (d/get-by-rank db name rank k-type v-type ignore-key?) Return the value or pair at a zero-based key rank.
sample-kv (d/sample-kv db name n), (d/sample-kv db name n k-type v-type ignore-key?) Return n random samples from a DBI, or nil if n exceeds the entry count.

Rank and sampling functions use DLMDB order-statistics support.

5.1 Prepared Point Reads

Prepared KV reads are available in Datalevin 1.2.0. Open the DBI before preparing the read. The Clojure forms are:

Form Fixed configuration
(d/prepare-get-value kv dbi) :data keys, :data values, value only.
(d/prepare-get-value kv dbi key-type) Chosen key type, :data values, value only.
(d/prepare-get-value kv dbi key-type value-type) Chosen types, value only.
(d/prepare-get-value kv dbi key-type value-type ignore-key?) Chosen types; false returns [key value].

Execute with (d/execute-prepared prepared key) or (prepared key). Missing keys return nil. Defaults and custom-type resolution match get-value: :data selects a declared custom key or list-item type when applicable.

Language Prepare Execute
Java kv.prepareGetValue(dbi, kType, vType, ignoreKey) prepared.execute(key)
Python kv.prepare_get_value(dbi, k_type, v_type, ignore_key=True) prepared.execute(key) or prepared(key)
JavaScript await kv.prepareGetValue(dbi, { kType, vType, ignoreKey: true }) await prepared.execute(key)

Types may be omitted to use :data. Returning only the value is the default; set ignoreKey / ignore_key to false to return the key and value together. Missing keys return null in Java and JavaScript, or None in Python.

Prepare once and reuse the read while the store is open; no separate close is needed. Each call reads the current value. To read uncommitted changes inside with-transaction-kv, prepare using its transaction handle. Chapter 7, Section 5 includes a complete example and remote behavior.

6. Key Ranges

Function Common forms Purpose
get-first (d/get-first db name k-range), (d/get-first db name k-range k-type v-type ignore-key?) Return the first value or pair in a key range.
get-first-n (d/get-first-n db name n k-range), (d/get-first-n db name n k-range k-type v-type ignore-key?) Return the first n values or pairs in a key range.
get-range (d/get-range db name k-range), (d/get-range db name k-range k-type v-type ignore-key?) Eagerly return values or pairs in a key range.
range-seq (d/range-seq db name k-range), (d/range-seq db name k-range k-type v-type ignore-key? opts) Lazily stream a key range in batches; close it with with-open.
key-range (d/key-range db name k-range), (d/key-range db name k-range k-type) Return only keys in a range.
range-count (d/range-count db name k-range), (d/range-count db name k-range k-type) Count KV pairs in a key range without materializing them.
key-range-count (d/key-range-count db name k-range k-type), (d/key-range-count db name k-range k-type cap) Count keys in a range, optionally stopping at cap.
key-range-list-count (d/key-range-list-count db name k-range k-type), (d/key-range-list-count db name k-range k-type cap) Count list items across keys in a list DBI key range.

Range specs use the keywords in Section 11. Use get-range for bounded results and range-seq for large scans.

7. Range Predicates and Visitors

Function Common forms Purpose
get-some (d/get-some db name pred k-range ...) Return the first KV pair whose predicate is true.
range-filter (d/range-filter db name pred k-range ...) Eagerly return KV pairs whose predicate is true.
range-keep (d/range-keep db name pred k-range ...) Return non-nil predicate results over a key range.
range-some (d/range-some db name pred k-range ...) Return the first logical true predicate result.
range-filter-count (d/range-filter-count db name pred k-range ...) Count KV pairs whose predicate is true.
visit-key-range (d/visit-key-range db name visitor k-range ...) Visit only keys in a key range for side effects.
visit (d/visit db name visitor k-range ...) Visit KV pairs in a key range for side effects.

raw-pred? controls whether callbacks receive raw storage objects or decoded values, and the raw shape depends on the helper. visit-key-range receives a raw key ByteBuffer; visit and range/list-range predicates over key/value pairs receive an IKV object; visit-list for one key receives a raw value ByteBuffer. Raw callbacks expose short-lived buffers; decode or copy data inside the callback if it must outlive that call.

8. List DBI Operations

Write functions:

Function Common forms Purpose
put-list-items (d/put-list-items db name k vs k-type v-type) Add multiple values under one key.
del-list-items (d/del-list-items db name k k-type), (d/del-list-items db name k vs k-type v-type) Delete a whole list or selected list items.

Read functions:

Function Common forms Purpose
get-list (d/get-list db name k k-type v-type) Return all values for one key.
visit-list (d/visit-list db name visitor k k-type v-type raw-pred?) Visit values for one key.
list-count (d/list-count db name k k-type) Count values associated with one key.
in-list? (d/in-list? db name k v k-type v-type) Test whether one value is present under a key.
list-range (d/list-range db name k-range k-type v-range v-type) Return key/value pairs matching both ranges.
list-range-count (d/list-range-count db name k-range k-type) Approximate count across a key range; ignores value range boundaries.
list-range-first (d/list-range-first db name k-range k-type v-range v-type) Return the first matching list item.
list-range-first-n (d/list-range-first-n db name n k-range k-type v-range v-type) Return the first n matching list items.
list-range-filter (d/list-range-filter db name pred k-range k-type v-range v-type raw-pred?) Filter list range results with a predicate.
list-range-keep (d/list-range-keep db name pred k-range k-type v-range v-type raw-pred?) Return non-nil predicate results over list range matches.
list-range-filter-count (d/list-range-filter-count db name pred k-range k-type v-range v-type raw-pred?) Count list range matches whose predicate is true.
list-range-some (d/list-range-some db name pred k-range k-type v-range v-type raw-pred?) Return the first logical true predicate result.
visit-list-range (d/visit-list-range db name visitor k-range k-type v-range v-type raw-pred?) Visit list range matches for side effects.

List DBIs are opened with open-list-dbi. Each key maps to a sorted set of values, and list items must fit within the LMDB key-size limit.

9. WAL and Snapshots

Function Common forms Purpose
txlog-watermarks (d/txlog-watermarks db) Return WAL watermarks and runtime sync state, or {:wal? false} when WAL is disabled.
open-tx-log (d/open-tx-log db from-lsn), (d/open-tx-log db from-lsn upto-lsn) Read committed WAL records from an LSN range.
force-lmdb-sync! (d/force-lmdb-sync! db) Force an LMDB environment sync for the store.
create-snapshot! (d/create-snapshot! db) Create or rotate LMDB snapshots and update WAL snapshot bookkeeping.
list-snapshots (d/list-snapshots db) Return available snapshot metadata.
gc-txlog-segments! (d/gc-txlog-segments! db), (d/gc-txlog-segments! db retain-floor-lsn) Garbage-collect WAL segments, optionally retaining records from a floor LSN.

10. Environment Flags

Functions:

Function Common forms Purpose
set-env-flags (d/set-env-flags db flags on?) Set or clear LMDB environment flags.
get-env-flags (d/get-env-flags db) Return currently active LMDB environment flags.

Flags:

Common flag group Keywords
Read and locking :rdonly-env, :notls, :nolock
Durability and write behavior :nosync, :nometasync, :writemap, :mapasync
Filesystem and memory :nordahead, :nomeminit, :nosubdir, :fixedmap

For a Datalog connection, call datalog-kv first and apply set-env-flags, get-env-flags, or sync to the backing KV handle.

11. Range Keywords

Range type Meaning
:all / :all-back Whole keyspace, forward or backward.
:at-least / :at-most-back Inclusive lower bound, forward or backward.
:at-most / :at-least-back Inclusive upper bound, forward or backward.
:closed / :closed-back Inclusive lower and upper bounds.
:closed-open / :closed-open-back Inclusive lower bound, exclusive upper bound.
:open-closed / :open-closed-back Exclusive lower bound, inclusive upper bound.
:open / :open-back Exclusive lower and upper bounds.
:greater-than / :less-than-back Exclusive lower bound.
:less-than / :greater-than-back Exclusive upper bound.

The same range shapes are used for key ranges and list value ranges.

12. Custom Data Types

register-type defines a custom type for ordered KV keys, ordered list items, and Datalog attribute values. All DBIs and Datalog attributes in the same database share the registration, which persists when the database is closed and reopened. Registration through a remote handle requires database alter permission. Chapter 10, Section 3.4 introduces custom types with examples.

12.1 Type Definitions

Function Common forms Purpose
register-type (d/register-type handle type-name definition) Register a custom type; returns type-name.

handle is a local or remote KV handle or Datalog connection. type-name is a namespaced keyword distinct from the built-in type names. A definition has these fields:

Definition field Meaning Default
:index :type Built-in scalar or tuple type used to encode the order key Required
:index :order-fn Function from the complete value to its order key Required
:payload Optional map containing both :serialize and :deserialize Built-in serialization
:version Positive integer identifying the definition's version 1

Supported scalar types for order keys are :keyword, :symbol, :string, :boolean, :long, :double, :float, :instant, :uuid, :bytes, :bigint, :bigdec, and :id. Use KV type names such as :long. For a tuple, declare a nonempty vector of these scalar types, excluding :bytes and :id. The order function must return a value of the declared type; for a tuple, it must return a vector with exactly the declared number of components, even for a one-component tuple.

Supply functions as datalevin.interpret/inter-fn values or UDF descriptors. An ordinary Clojure fn cannot be stored in a type definition. A UDF descriptor identifies a runtime implementation by language, role, id, and optional version:

{:udf/lang :python :udf/kind :order-fn
 :udf/id :task/order :udf/version 1}

Use UDF kind :order-fn for the order function, :serializer for the payload encoder, and :deserializer for the payload decoder. The serializer accepts a complete value and returns bytes; the deserializer reconstructs the value from those bytes.

inter-fn source and captured values persist with the registration. UDF implementations must be supplied at runtime: pass {:runtime-opts {:udf-registry registry}} when opening a local KV store or Datalog connection. Recreate the registry before reopening the database in a new process. Appendix E, Section 11.2 lists the registry helpers.

Registering the same definition again is safe; a conflicting definition is rejected. Neither increasing :version nor replacing a UDF implementation rebuilds indexes or converts stored payloads. For an incompatible change, register a new type name and migrate values into a new DBI or attribute while the old type's functions remain available.

12.2 DBI Declarations

DBI declarations select a registered type for a field:

Use Declaration
Custom keys (d/open-dbi kv "tasks" {:key-type :app/task})
Ordered custom list items (d/open-list-dbi kv "tasks-by-owner" {:value-type :app/task})
Both custom keys and items (d/open-list-dbi kv "related" {:key-type :app/task :value-type :app/task})

A custom :value-type requires a list DBI. Declarations persist and cannot be changed by reopening the DBI with different options. Omitting an operation's type argument, or passing the default :data, selects the declared custom type. Other explicit types must match the declaration. A custom type cannot be used for an undeclared key or list item.

12.3 Language Bindings

Host-language registration and descriptor factories:

Language Register Descriptor factories
Java handle.registerType(name, definition) UdfDescriptor.orderFn, .serializer, .deserializer
Python handle.register_type(name, definition) UdfDescriptor.order_fn, .serializer, .deserializer
JavaScript await handle.registerType(name, definition) UdfDescriptor.orderFn, .serializer, .deserializer

Definition maps use the field names above. String keys may include a leading colon. Java also accepts KVType scalar and tuple declarations. Python and JavaScript descriptor factories default to their respective host languages.

Python and JavaScript can also store and return instances of application classes. Define the order function and payload encoder and decoder, then bind the class to the type definition in the runtime registry before opening the database:

from dataclasses import dataclass
import json
from datalevin import UdfDescriptor, create_udf_registry, open_kv

@dataclass
class Task:
    priority: int
    name: str

registry = create_udf_registry()
registry.order_udf("task/order")(lambda task: task.priority)
registry.serializer_udf("task/encode")(
    lambda task: json.dumps([task.priority, task.name]).encode("utf-8"))
registry.deserializer_udf("task/decode")(
    lambda payload: Task(*json.loads(payload)))

definition = {
    "index": {"type": ":long",
              "order-fn": UdfDescriptor.order_fn("task/order")},
    "payload": {"serialize": UdfDescriptor.serializer("task/encode"),
                "deserialize": UdfDescriptor.deserializer("task/decode")},
}
registry.bind_native_type("app/task", Task, definition)

with open_kv("/tmp/datalevin-custom-python",
             opts={":runtime-opts": {":udf-registry": registry}}) as kv:
    kv.register_type("app/task", definition)
    kv.open_dbi("tasks", {":key-type": ":app/task"})
    kv.transact([(":put", Task(3, "Compile"), "queued")], dbi_name="tasks")
    assert kv.get_value("tasks", Task(3, "Compile")) == "queued"
import { UdfDescriptor, createUdfRegistry, openKv } from "datalevin-node";

class Task {
  constructor(priority, name) {
    this.priority = priority;
    this.name = name;
  }
}

const registry = await createUdfRegistry();
await registry.orderUdf("task/order", task => task.priority);
await registry.serializerUdf("task/encode", task =>
  Buffer.from(JSON.stringify([task.priority.toString(), task.name])));
await registry.deserializerUdf("task/decode", payload => {
  const [priority, name] = JSON.parse(payload.toString("utf8"));
  return new Task(BigInt(priority), name);
});
const definition = {
  index: { type: ":long", "order-fn": UdfDescriptor.orderFn("task/order") },
  payload: {
    serialize: UdfDescriptor.serializer("task/encode"),
    deserialize: UdfDescriptor.deserializer("task/decode")
  }
};
await registry.bindNativeType("app/task", Task, definition);

const kv = await openKv("/tmp/datalevin-custom-javascript", {
  ":runtime-opts": { ":udf-registry": registry }
});
try {
  await kv.registerType("app/task", definition);
  await kv.openDbi("tasks", { ":key-type": ":app/task" });
  await kv.transact([[":put", new Task(3n, "Compile"), "queued"]],
                    { dbiName: "tasks" });
  console.log(await kv.getValue("tasks", new Task(3n, "Compile"))); // queued
} finally {
  await kv.close();
}

Exact matching compares decoded values using Python == or JavaScript's node:util.isDeepStrictEqual. JavaScript also accepts an equals argument to bindNativeType for custom equality. Native class bindings exist only at runtime and must be supplied again when reopening the database in a new process.

Native instances can be custom keys, list items, Datalog attribute values, range endpoints, lookup references, and query inputs. They also work with remote operations and queries that spill to disk. An entity returns a custom attribute with cardinality many as a Python list or a JavaScript Set.

Native instances require a custom type declaration. They cannot be stored in untyped :data payloads or passed to helpers that convert values before a database handle is available. Within the JVM, native values currently share a single hash code, which can make large hash joins, maps, sets, and deduplication expensive.

12.4 Remote Databases

For remote databases, order and payload functions execute on the server. Install UDF bindings there through datalevin.server/*server-runtime-opts-fn*; client registries are not sent to the server. To reconstruct native class instances on the client, also supply the caller's registry through :runtime-opts when opening the remote handle.

No examples for this chapter yet.