Types & Handles

TypeDescription
KVStoreOpaque handle to an open database.
KVColumnFamilyOpaque handle to a column family.
KVIteratorOpaque handle to a key-value iterator.
KVStoreStatsStatistics counters (see Diagnostics).
KVStoreConfigConfiguration struct for kvstore_open_v2.

Error Codes

CodeValueDescription
KVSTORE_OK0Operation succeeded.
KVSTORE_ERROR1Generic error.
KVSTORE_BUSY5Database locked by another connection.
KVSTORE_LOCKED6Database locked within the same connection.
KVSTORE_NOMEM7Memory allocation failed.
KVSTORE_READONLY8Database is read-only.
KVSTORE_CORRUPT11Database file is corrupted.
KVSTORE_NOTFOUND12Key or column family not found.
KVSTORE_AUTH_FAILED13Wrong password or encrypted store opened without a key.
KVSTORE_PROTOCOL15Database lock protocol error.

All codes are aliases for the corresponding SQLITE_* values.

Constants

Journal Modes

ConstantValueDescription
KVSTORE_JOURNAL_DELETE0Rollback journal mode.
KVSTORE_JOURNAL_WAL1Write-Ahead Logging — recommended.

Sync Levels

ConstantValueDescription
KVSTORE_SYNC_OFF0No fsync — fastest, not crash-safe.
KVSTORE_SYNC_NORMAL1WAL-safe — survives process crash (default).
KVSTORE_SYNC_FULL2Power-safe — fsync on every commit.

Checkpoint Modes

ConstantValueDescription
KVSTORE_CHECKPOINT_PASSIVE0Non-blocking copy; may not flush all frames.
KVSTORE_CHECKPOINT_FULL1Wait for writers, then copy all frames.
KVSTORE_CHECKPOINT_RESTART2Like FULL, then reset WAL write position.
KVSTORE_CHECKPOINT_TRUNCATE3Like RESTART, then truncate WAL file to zero.

Other

ConstantValueDescription
KVSTORE_MAX_COLUMN_FAMILIES64Maximum column families per database.
KVSTORE_NO_TTL-1Sentinel: key exists but has no expiry.

Configuration — KVStoreConfig

typedef struct KVStoreConfig KVStoreConfig;
struct KVStoreConfig {
  int journalMode;   /* KVSTORE_JOURNAL_WAL (default) or KVSTORE_JOURNAL_DELETE */
  int syncLevel;     /* KVSTORE_SYNC_NORMAL (default), _OFF, or _FULL */
  int cacheSize;     /* page cache in pages (0 = default = 2000 pages ≈ 8 MB); < 0 = budget in KiB */
  int pageSize;      /* page size in bytes (0 = 4096; new databases only) */
  int readOnly;      /* 1 = open read-only; default 0 */
  int busyTimeout;   /* ms to retry on SQLITE_BUSY (0 = fail immediately) */
  int allowEncrypted; /* internal — leave 0 (use kvstore_open_encrypted) */
  int walAutoCheckpoint; /* checkpoint when WAL ≥ N pages (0 = default 1000, < 0 = off) */
};
FieldDefaultNotes
journalModeKVSTORE_JOURNAL_WALWAL strongly recommended.
syncLevelKVSTORE_SYNC_NORMALSafe against process crash.
cacheSize2000 (~8 MB)Larger cache improves read-heavy loads. A negative value is a memory budget in KiB (e.g. -256 ≈ 256 KB of cache, whatever the page size) — the usual choice on small devices. The default can also be changed at build time with -DKVSTORE_DEFAULT_CACHE_SIZE=N.
pageSize4096 bytesIgnored for existing databases.
readOnly0
busyTimeout0 (fail immediately)Set > 0 for multi-process workloads.
allowEncrypted0Internal flag used by kvstore_open_encrypted. Leave it 0; zero-initialising the struct does this.
walAutoCheckpoint0 → 1000 pagesAfter a commit, run a PASSIVE (non-blocking) checkpoint once the WAL holds at least this many pages — the same rule SQLite uses. Keeps the WAL around 4 MB, so page lookups in it stay cheap (an unbounded WAL slowed reads by 8–16% on 1M rows). N > 0 = custom threshold; N < 0 = off (WAL grows until kvstore_checkpoint or close). Build-time default: -DKVSTORE_DEFAULT_WAL_AUTOCHECKPOINT=N.

Database Lifecycle

kvstore_open_v2

int kvstore_open_v2( const char *zFilename, /* path to file; NULL = in-memory */ KVStore **ppKV, /* out: handle */ const KVStoreConfig *pConfig /* NULL = all defaults */ );

Open or create a database with full configuration control. Pass NULL for pConfig to use all defaults (WAL mode, NORMAL sync, 8 MB cache). Returns KVSTORE_AUTH_FAILED if the file is an encrypted store — use kvstore_open_encrypted instead.

/* default open */
KVStore *kv;
kvstore_open_v2("mydb.db", &kv, NULL);

/* fully configured */
KVStoreConfig cfg = {0};
cfg.journalMode = KVSTORE_JOURNAL_WAL;
cfg.syncLevel   = KVSTORE_SYNC_FULL;
cfg.cacheSize   = 4000;
cfg.busyTimeout = 5000;
kvstore_open_v2("mydb.db", &kv, &cfg);

/* read-only */
KVStoreConfig ro = {0};
ro.readOnly = 1;
kvstore_open_v2("mydb.db", &kv, &ro);
Returns

KVSTORE_OK on success, error code otherwise.

kvstore_open

int kvstore_open( const char *zFilename, KVStore **ppKV, int journalMode /* KVSTORE_JOURNAL_WAL or _DELETE */ );

Simplified open. Equivalent to kvstore_open_v2 with only journalMode set; all other fields use defaults. Returns KVSTORE_AUTH_FAILED if the file is an encrypted store.

KVStore *kv = NULL;
kvstore_open("mydata.db", &kv, KVSTORE_JOURNAL_WAL);

kvstore_close

int kvstore_close(KVStore *pKV);

Close the database and free all resources. Any uncommitted transaction is rolled back. Runs a WAL checkpoint before closing.

Key-Value Operations — Default Column Family

kvstore_put

int kvstore_put( KVStore *pKV, const void *pKey, int nKey, const void *pValue, int nValue );

Insert or update a key-value pair. If the key already exists, its value is replaced. Keys and values are binary-safe. Without an explicit transaction, each call auto-commits.

kvstore_get

int kvstore_get( KVStore *pKV, const void *pKey, int nKey, void **ppValue, int *pnValue );

Retrieve a value by key. The returned buffer is heap-allocated by SNKV. Caller must free with snkv_free().

void *value = NULL; int vlen = 0;
int rc = kvstore_get(kv, "user:1", 6, &value, &vlen);
if (rc == KVSTORE_OK) {
    printf("%.*s\n", vlen, (char*)value);
    snkv_free(value);
}
Returns

KVSTORE_OK if found · KVSTORE_NOTFOUND if key does not exist.

kvstore_delete

int kvstore_delete(KVStore *pKV, const void *pKey, int nKey);

Delete a key. Returns KVSTORE_NOTFOUND if the key did not exist.

kvstore_exists

int kvstore_exists(KVStore *pKV, const void *pKey, int nKey, int *pExists);

Check if a key exists without reading its value. More efficient than kvstore_get for existence checks. Sets *pExists to 1 if found, 0 otherwise.

Column Family Management

Column families provide multiple logical key-value namespaces within a single file. Each is stored in its own B-tree. Maximum KVSTORE_MAX_COLUMN_FAMILIES (64) per database, counting the default CF — i.e. up to 63 named CFs. Internal CFs (TTL indexes, encryption metadata, vector store) do not count towards the limit. Names starting with __ or _snkv_ are reserved for internal use.

kvstore_cf_create

int kvstore_cf_create(KVStore *pKV, const char *zName, KVColumnFamily **ppCF);

Create a new column family. zName max 255 characters, must be unique and must not start with __ or _snkv_. *ppCF receives the handle. Passing "default" returns the default CF.

Returns

KVSTORE_OK · KVSTORE_ERROR if the name is invalid or reserved, the CF already exists, or 63 named CFs already exist · KVSTORE_NOMEM if the handle cannot be allocated (the CF may already be created — open it with kvstore_cf_open).

kvstore_cf_open

int kvstore_cf_open(KVStore *pKV, const char *zName, KVColumnFamily **ppCF);

Open an existing column family. Returns KVSTORE_NOTFOUND if it does not exist, KVSTORE_ERROR for a reserved name, KVSTORE_NOMEM if the handle cannot be allocated. Each call returns a new, independent handle; close every handle with kvstore_cf_close.

kvstore_cf_get_default

int kvstore_cf_get_default(KVStore *pKV, KVColumnFamily **ppCF);

Get a handle to the default column family. Always exists; created automatically on open.

kvstore_cf_drop

int kvstore_cf_drop(KVStore *pKV, const char *zName);

Delete a column family and all its data permanently. The default column family cannot be dropped. Also removes any hidden TTL index CFs. Other open CF handles stay valid (the file may move another table's root page; open handles follow it). The drop is all-or-nothing: on any error nothing is changed (inside your own transaction, roll it back).

Returns

KVSTORE_OK · KVSTORE_NOTFOUND if it does not exist · KVSTORE_ERROR for "default" or a reserved name · KVSTORE_BUSY if a handle to this CF is still open, or if any iterator is open on the store — close them first.

kvstore_cf_list

int kvstore_cf_list(KVStore *pKV, char ***pazNames, int *pnCount);

List the named column families (the default CF is not included). Internal CFs (names starting with __ or _snkv_) are not listed. Caller must free each name and the array with snkv_free().

char **names = NULL; int count = 0;
kvstore_cf_list(kv, &names, &count);
for (int i = 0; i < count; i++) {
    printf("%s\n", names[i]);
    snkv_free(names[i]);
}
snkv_free(names);

kvstore_cf_close

void kvstore_cf_close(KVColumnFamily *pCF);

Release the column family handle. Does not delete data.

CF Key-Value Operations

Identical to the default-CF variants but take a KVColumnFamily* handle.

int kvstore_cf_put(KVColumnFamily *pCF, const void *pKey, int nKey, const void *pValue, int nValue); int kvstore_cf_get(KVColumnFamily *pCF, const void *pKey, int nKey, void **ppValue, int *pnValue); int kvstore_cf_delete(KVColumnFamily *pCF, const void *pKey, int nKey); int kvstore_cf_exists(KVColumnFamily *pCF, const void *pKey, int nKey, int *pExists);
Note: Caller must free the value from kvstore_cf_get with snkv_free().

Iterators

Iterate all key-value pairs in ascending lexicographic key order.

kvstore_iterator_create / kvstore_cf_iterator_create

int kvstore_iterator_create(KVStore *pKV, KVIterator **ppIter); int kvstore_cf_iterator_create(KVColumnFamily *pCF, KVIterator **ppIter);

Create a forward iterator for the default or a specific column family.

kvstore_iterator_first / next / eof

int kvstore_iterator_first(KVIterator *pIter); int kvstore_iterator_next(KVIterator *pIter); int kvstore_iterator_eof(KVIterator *pIter); /* returns 1 at end */

Position at first entry, advance to next, or test for end-of-sequence.

kvstore_iterator_key / value

int kvstore_iterator_key(KVIterator *pIter, void **ppKey, int *pnKey); int kvstore_iterator_value(KVIterator *pIter, void **ppValue, int *pnValue);

Read the current key or value. Pointers are owned by the iterator — do not free them. Valid until the next next() or close() call.

KVIterator *it = NULL;
kvstore_iterator_create(kv, &it);
kvstore_iterator_first(it);
while (!kvstore_iterator_eof(it)) {
    void *key, *val; int klen, vlen;
    kvstore_iterator_key(it, &key, &klen);
    kvstore_iterator_value(it, &val, &vlen);
    printf("%.*s\n", klen, (char*)key);
    kvstore_iterator_next(it);
}
kvstore_iterator_close(it);

kvstore_iterator_close

void kvstore_iterator_close(KVIterator *pIter);

Release the iterator and its cursor. Always call this when done.

Prefix Iterators

Pre-positioned at the first key matching the prefix. Stop automatically when keys no longer match. Do not call kvstore_iterator_first() — read directly, then call next().

int kvstore_prefix_iterator_create( KVStore *pKV, const void *pPrefix, int nPrefix, KVIterator **ppIter ); int kvstore_cf_prefix_iterator_create( KVColumnFamily *pCF, const void *pPrefix, int nPrefix, KVIterator **ppIter );
KVIterator *it = NULL;
kvstore_prefix_iterator_create(kv, "user:", 5, &it);
/* already at first match — do NOT call first() */
while (!kvstore_iterator_eof(it)) {
    void *key; int klen;
    kvstore_iterator_key(it, &key, &klen);
    kvstore_iterator_next(it);
}
kvstore_iterator_close(it);

Reverse Iterators

Traverse keys in descending order. Use kvstore_iterator_last() and kvstore_iterator_prev() instead of first() and next(). All key/value/eof/close accessors work identically.

int kvstore_reverse_iterator_create(KVStore *pKV, KVIterator **ppIter); int kvstore_cf_reverse_iterator_create(KVColumnFamily *pCF, KVIterator **ppIter); /* Reverse prefix — pre-positioned at last matching key */ int kvstore_reverse_prefix_iterator_create( KVStore *pKV, const void *pPrefix, int nPrefix, KVIterator **ppIter ); int kvstore_cf_reverse_prefix_iterator_create( KVColumnFamily *pCF, const void *pPrefix, int nPrefix, KVIterator **ppIter ); int kvstore_iterator_last(KVIterator *pIter); /* seek to last key */ int kvstore_iterator_prev(KVIterator *pIter); /* move to previous key */
/* Full reverse scan */
KVIterator *it = NULL;
kvstore_reverse_iterator_create(kv, &it);
kvstore_iterator_last(it);
while (!kvstore_iterator_eof(it)) {
    void *key; int klen;
    kvstore_iterator_key(it, &key, &klen);
    kvstore_iterator_prev(it);
}
kvstore_iterator_close(it);

/* Reverse prefix — do NOT call last(), already positioned */
kvstore_reverse_prefix_iterator_create(kv, "user:", 5, &it);
while (!kvstore_iterator_eof(it)) {
    kvstore_iterator_prev(it);
}
kvstore_iterator_close(it);

Iterator Seek

Jump an open iterator to an arbitrary key in O(log N) time. Works on forward and reverse iterators, including prefix iterators.

kvstore_iterator_seek

int kvstore_iterator_seek(KVIterator *pIter, const void *pKey, int nKey);

Reposition the iterator without closing and reopening it.

  • Forward iterator — seek to the first key >= (pKey, nKey).
  • Reverse iterator — seek to the last key <= (pKey, nKey).

If the seek lands outside a prefix iterator’s prefix, the iterator immediately enters the eof state. Call kvstore_iterator_key / kvstore_iterator_next as usual after a successful seek.

ReturnMeaning
KVSTORE_OKSeek succeeded (check eof to know if a key was found).
KVSTORE_ERRORInvalid arguments.
/* forward: scan from "m" onwards */
KVIterator *it = NULL;
kvstore_iterator_create(kv, &it);
kvstore_iterator_seek(it, "m", 1);
while (!kvstore_iterator_eof(it)) {
    void *key; int klen;
    kvstore_iterator_key(it, &key, &klen);
    kvstore_iterator_next(it);
}
kvstore_iterator_close(it);

/* reverse: scan from "m" backwards */
kvstore_reverse_iterator_create(kv, &it);
kvstore_iterator_seek(it, "m", 1);
while (!kvstore_iterator_eof(it)) {
    kvstore_iterator_prev(it);
}
kvstore_iterator_close(it);

Transactions

int kvstore_begin(KVStore *pKV, int wrflag); /* 1=write, 0=read */ int kvstore_commit(KVStore *pKV); int kvstore_rollback(KVStore *pKV);

Explicit ACID transactions for batching operations atomically. Without an explicit transaction, each put/delete auto-commits. Wrapping bulk writes in a single transaction is significantly faster.

kvstore_begin(kv, 1);
kvstore_put(kv, "key1", 4, "val1", 4);
kvstore_put(kv, "key2", 4, "val2", 4);
kvstore_commit(kv);   /* both writes are atomic */
KVSTORE_BUSY: Set cfg.busyTimeout > 0 to auto-retry on lock contention, or handle the return code and retry manually.
If kvstore_commit fails (e.g. KVSTORE_BUSY or an I/O error) the transaction stays open: retry the commit, or call kvstore_rollback. Auto-commits inside other calls roll back automatically when they fail, so a failed call never leaves half its changes behind.
Inside an explicit write transaction, kvstore_sync, kvstore_reencrypt and kvstore_remove_encryption return KVSTORE_ERROR and kvstore_checkpoint returns KVSTORE_BUSY — commit or roll back first. kvstore_purge_expired and kvstore_cf_drop work inside it and become part of your transaction.

TTL / Key Expiry

Per-key expiry with zero overhead for stores that never use TTL. Hidden index CFs are created lazily on first use and are invisible in kvstore_cf_list.

kvstore_put_ttl / kvstore_get_ttl

int64_t kvstore_now_ms(void); /* current time in ms since Unix epoch */ int kvstore_put_ttl( KVStore *pKV, const void *pKey, int nKey, const void *pValue, int nValue, int64_t expire_ms /* absolute ms since epoch; 0 = no TTL */ ); int kvstore_get_ttl( KVStore *pKV, const void *pKey, int nKey, void **ppValue, int *pnValue, int64_t *pnRemaining /* may be NULL */ );
/* write key that expires in 30 minutes */
int64_t exp = kvstore_now_ms() + 30 * 60 * 1000;
kvstore_put_ttl(kv, "sess:abc", 8, token, tlen, exp);

/* read with lazy expiry */
void *val = NULL; int nVal = 0; int64_t rem = 0;
int rc = kvstore_get_ttl(kv, "sess:abc", 8, &val, &nVal, &rem);
if (rc == KVSTORE_OK) { /* rem = ms remaining */ snkv_free(val); }

kvstore_ttl_remaining

int kvstore_ttl_remaining(KVStore *pKV, const void *pKey, int nKey, int64_t *pnRemaining);
Return*pnRemainingMeaning
KVSTORE_OKKVSTORE_NO_TTL (-1)Key exists, no expiry.
KVSTORE_OK0Key just expired (lazy delete performed).
KVSTORE_OKN > 0N ms remain.
KVSTORE_NOTFOUND—Key does not exist.

kvstore_purge_expired

int kvstore_purge_expired(KVStore *pKV, int *pnDeleted); /* pnDeleted may be NULL */

Delete all expired keys, oldest expiry first. O(expired keys) — the expiry index is sorted by time, so it stops at the first key that has not expired. Works in batches of up to 256 keys per write transaction (build-time setting KVSTORE_PURGE_BATCH), releasing the lock between batches; memory use is one key at a time. If you have a write transaction open, it works inside it instead: the deletes commit or roll back with your transaction.

CF-level TTL: All four functions have CF equivalents operating on a KVColumnFamily* handle. Each user CF maintains independent TTL indexes; purging CF A never touches CF B.

kvstore_cf_put_ttl / kvstore_cf_get_ttl

int kvstore_cf_put_ttl( KVColumnFamily *pCF, const void *pKey, int nKey, const void *pValue, int nValue, int64_t expire_ms /* absolute ms since epoch; 0 = no TTL */ ); int kvstore_cf_get_ttl( KVColumnFamily *pCF, const void *pKey, int nKey, void **ppValue, int *pnValue, int64_t *pnRemaining /* may be NULL */ );

CF-level equivalents of kvstore_put_ttl / kvstore_get_ttl. Semantics are identical; the TTL indexes are created lazily per CF and are fully independent.

kvstore_cf_ttl_remaining

int kvstore_cf_ttl_remaining( KVColumnFamily *pCF, const void *pKey, int nKey, int64_t *pnRemaining /* must not be NULL */ );

Return remaining TTL for a key in this column family without fetching the value. Return codes and *pnRemaining semantics are identical to kvstore_ttl_remaining.

kvstore_cf_purge_expired

int kvstore_cf_purge_expired(KVColumnFamily *pCF, int *pnDeleted); /* pnDeleted may be NULL */

Delete all expired keys in this column family only. O(expired keys). Same batching and transaction behaviour as kvstore_purge_expired. Purging CF A never affects other column families.

Conditional Insert (Put-If-Absent)

Atomically insert a key-value pair only when the key does not already exist. Expired keys (lazy TTL) are treated as absent. Supports an optional TTL on the new entry.

kvstore_put_if_absent / kvstore_cf_put_if_absent

int kvstore_put_if_absent( KVStore *pKV, const void *pKey, int nKey, const void *pValue, int nValue, int64_t expire_ms, /* 0 = no TTL */ int *pInserted /* 1=inserted, 0=already existed; may be NULL */ ); int kvstore_cf_put_if_absent(KVColumnFamily *pCF, /* ... same params ... */);
expire_msBehaviour
0No TTL — new entry is permanent.
> 0Absolute expiry in ms since Unix epoch (kvstore_now_ms() + delta_ms).

The existence check and the write are performed inside a single write transaction, so no other writer can insert the same key concurrently.

int inserted = 0;
kvstore_put_if_absent(kv, "lock", 4, "owner", 5, 0, &inserted);
if (inserted) {
    puts("acquired lock");
} else {
    puts("lock already held");
}

/* with TTL: session token that expires in 1 hour */
int64_t exp = kvstore_now_ms() + 3600 * 1000;
kvstore_put_if_absent(kv, session_id, sid_len, token, tok_len, exp, NULL);

Bulk Clear

Remove every key-value pair from a column family in a single atomic write transaction. TTL index entries are also cleared. Much faster than iterating and deleting individually.

kvstore_clear / kvstore_cf_clear

int kvstore_clear(KVStore *pKV); int kvstore_cf_clear(KVColumnFamily *pCF);

Truncates the column family’s B-tree in O(pages) time using SQLite’s BtreeClearTable — the B-tree structure (root page) is preserved so future inserts work immediately without re-opening.

Precondition: No open iterators pointing at the column family may exist when clear is called. BtreeClearTable invalidates all cursors on the cleared table; accessing a stale cursor is undefined behaviour.
/* flush a rate-limit namespace at midnight */
kvstore_cf_clear(rate_limit_cf);

/* wipe everything in the default CF */
kvstore_clear(kv);

Key Count

Count keys in a column family without iterating over them. Uses SQLite’s sqlite3BtreeCount which reads only page headers — O(pages) ≈ O(N/100).

kvstore_count / kvstore_cf_count

int kvstore_count(KVStore *pKV, int64_t *pnCount); int kvstore_cf_count(KVColumnFamily *pCF, int64_t *pnCount);

Counts only the main data B-tree of the column family. TTL index CFs are separate B-trees and are not included. Expired-but-not-yet-purged keys are included in the count — call kvstore_purge_expired first for an exact live count.

Uncommitted writes: Inside your own write transaction the count includes that transaction's pending (uncommitted) writes. Other connections see committed data only.
int64_t n;
kvstore_count(kv, &n);
printf("default CF has %lld entries\n", (long long)n);

kvstore_purge_expired(kv, NULL);
kvstore_count(kv, &n);
printf("live entries after purge: %lld\n", (long long)n);

Diagnostics & Maintenance

kvstore_errmsg

const char *kvstore_errmsg(KVStore *pKV);

Return the last error message string. Do not free. Valid until the next operation on the same handle. When several threads share one handle, use kvstore_errmsg_copy instead.

kvstore_libversion / kvstore_libversion_number

const char *kvstore_libversion(void); /* "0.9.0" */ int kvstore_libversion_number(void); /* 9000 */

Version of the library actually linked. The header defines the same values as macros for compile-time checks: KVSTORE_VERSION ("0.9.0"), KVSTORE_VERSION_MAJOR / _MINOR / _PATCH, and KVSTORE_VERSION_NUMBER = MAJOR×1000000 + MINOR×1000 + PATCH. Comparing kvstore_libversion_number() with KVSTORE_VERSION_NUMBER detects a header/library mismatch when SNKV is used as a shared library.

kvstore_errmsg_copy

int kvstore_errmsg_copy(KVStore *pKV, char *zBuf, int nBuf);

Copy the last error message into zBuf (always NUL-terminated, truncated to fit). The copy is taken under the handle mutex, so it is safe when threads share a handle. Returns KVSTORE_ERROR if zBuf is NULL or nBuf <= 0.

kvstore_stats / kvstore_stats_reset

int kvstore_stats(KVStore *pKV, KVStoreStats *pStats); int kvstore_stats_reset(KVStore *pKV); /* zero all cumulative counters */

kvstore_stats fills the KVStoreStats struct. All counters are cumulative from open (or last reset). kvstore_stats_reset zeros them; nDbPages is a live gauge and is never zeroed.

FieldTypeDescription
nPutsuint64_tTotal put operations (includes put_ttl, put_if_absent).
nGetsuint64_tTotal get operations (includes get_ttl).
nDeletesuint64_tTotal delete operations.
nIterationsuint64_tTotal iterators created.
nErrorsuint64_tTotal errors encountered.
nBytesReaduint64_tTotal value bytes returned by get operations.
nBytesWrittenuint64_tTotal (key + value) bytes written by put operations.
nWalCommitsuint64_tWrite transactions successfully committed.
nCheckpointsuint64_tWAL checkpoints performed (auto + explicit).
nTtlExpireduint64_tKeys lazily expired on get / exists calls.
nTtlPurgeduint64_tKeys removed by kvstore_purge_expired.
nDbPagesuint64_tCurrent total pages in the database file (gauge, not zeroed by reset).

kvstore_integrity_check

int kvstore_integrity_check(KVStore *pKV, char **pzErrMsg);

Walk the entire B-tree and verify structural integrity. Returns KVSTORE_OK or KVSTORE_CORRUPT with details in *pzErrMsg. Caller must free *pzErrMsg with snkv_free().

kvstore_sync

int kvstore_sync(KVStore *pKV);

Make every committed transaction durable: fsyncs the WAL file (or rollback journal) and the database file (fsync on Linux/macOS, FlushFileBuffers on Windows). Useful with KVSTORE_SYNC_OFF or NORMAL, where commits do not fsync: commit cheaply, then call kvstore_sync at points that must survive a power loss. It does not checkpoint — use kvstore_checkpoint for that.

Returns

KVSTORE_OK · KVSTORE_ERROR while an explicit write transaction is open (commit or roll back first) · an I/O error code if the disk sync fails.

kvstore_incremental_vacuum

int kvstore_incremental_vacuum(KVStore *pKV, int nPage);

Reclaim unused pages and shrink the database file. Pass 0 to free all unused pages, or a positive nPage to limit work per call (good for latency-sensitive apps). Safe while iterators are open: they continue from their current key. Runs in its own write transaction, or inside yours if one is open.

kvstore_incremental_vacuum(kv, 0);    /* free all */
kvstore_incremental_vacuum(kv, 50);   /* incremental: 50 pages */

kvstore_checkpoint

int kvstore_checkpoint(KVStore *pKV, int mode, int *pnLog, int *pnCkpt);

Copy WAL frames back into the main database file. pnLog and pnCkpt may be NULL. Requires no active write transaction. Non-WAL databases: no-op returning KVSTORE_OK.

int nLog = 0, nCkpt = 0;
kvstore_checkpoint(kv, KVSTORE_CHECKPOINT_PASSIVE, &nLog, &nCkpt);
printf("%d frames total, %d checkpointed\n", nLog, nCkpt);

/* reclaim WAL disk space */
kvstore_checkpoint(kv, KVSTORE_CHECKPOINT_TRUNCATE, NULL, NULL);

Memory Management

MacroDescription
sqliteMalloc(n)Legacy alias of snkv_malloc, kept for compatibility.
sqliteFree(p)Legacy alias of snkv_free, kept for compatibility.
sqliteRealloc(p, n)Legacy: resize an SNKV allocation. Not needed by the public API.
sqliteStrDup(s)Legacy: duplicate a C string into SNKV memory. Not needed by the public API.
snkv_malloc(n)Use this to allocate n zero-initialised bytes. Call it only after the first kvstore_open.
snkv_free(p)Use this to free any memory returned by SNKV.
Rule: Any pointer returned by kvstore_get, kvstore_cf_get, kvstore_cf_list, or kvstore_integrity_check must be freed with snkv_free(). Iterator key/value pointers are iterator-owned — do NOT free them.

Thread Safety

  • One lock per handle. Every KVStore handle has one lock, so it may be shared across threads: calls on it run one at a time. Column families have no lock of their own.
  • Transactions belong to one thread. kvstore_begin(kv, 1) keeps the handle locked until kvstore_commit / kvstore_rollback, which must be called by the same thread. Other threads' calls on that handle wait until then — they never join or roll back another thread's transaction.
  • Different handles run in parallel. For maximum throughput, give each thread its own KVStore on the same file: in WAL mode readers run concurrently with one writer; a second writer gets KVSTORE_BUSY (or waits up to busyTimeout).
  • Use kvstore_errmsg_copy when threads share a handle — the pointer from kvstore_errmsg can be overwritten by another thread's error.
  • KVIterator handles are not thread-safe — use each iterator from one thread.
  • Do not call kvstore_close while other threads may still use the handle. (Close waits for another thread's open transaction to finish.)
  • A KVVecStore is safe to share between threads: every vector call takes the store's own mutex.

Encryption

Per-value AES-grade encryption using XChaCha20-Poly1305 (via Monocypher). Passwords are stretched with Argon2id. All existing APIs — put, get, delete, iterate, TTL, column families — work transparently on encrypted stores. Keys are stored in plaintext; only values are encrypted.

kvstore_open_encrypted

int kvstore_open_encrypted( const char *zFilename, const void *pPassword, int nPassword, KVStore **ppKV, const KVStoreConfig *pConfig /* may be NULL for defaults */ );

Open or create an encrypted store. On a fresh file a new Argon2id-derived key is generated from the password. On an existing encrypted file the password is verified before access is granted. If the file is a plain (non-encrypted) store with existing data, all values are encrypted in-place using the supplied password — the store becomes fully encrypted transparently. Returns KVSTORE_AUTH_FAILED if the password is wrong on an already-encrypted file, KVSTORE_CORRUPT if the key-derivation parameters stored in the file are out of range (corrupt or crafted file — rejected before anything is allocated), and KVSTORE_NOMEM if the key-derivation memory cannot be allocated. *ppKV is set to NULL on any error — no resource leak.

/* Create / open encrypted */
KVStore *kv = NULL;
int rc = kvstore_open_encrypted("mydb.db", "hunter2", 7, &kv, NULL);
if (rc == KVSTORE_AUTH_FAILED) { puts("wrong password"); }

/* All standard operations work transparently */
kvstore_put(kv, "key", 3, "secret", 6);
kvstore_close(kv);

kvstore_is_encrypted

int kvstore_is_encrypted(KVStore *pKV);

Returns 1 if the store was opened with kvstore_open_encrypted, 0 otherwise.

kvstore_reencrypt

int kvstore_reencrypt(KVStore *pKV, const void *pNewPassword, int nNewPassword);

Change the encryption password in-place. Derives a new key from the new password, re-encrypts every value in every column family, and updates the authentication metadata — all in a single atomic transaction. On success the old password is invalid immediately. On any failure (including a failed commit) nothing is changed and the old password stays valid. Returns KVSTORE_ERROR if the store is not encrypted or an explicit transaction is open (commit or roll back first).

kvstore_reencrypt(kv, "new-pass", 8);
/* kv is still open and usable with the new password */

kvstore_remove_encryption

int kvstore_remove_encryption(KVStore *pKV);

Decrypt all values in-place and remove the authentication metadata. After this call the store is a plain (non-encrypted) SNKV database and can be opened with kvstore_open or kvstore_open_v2. Runs in one transaction: on any failure nothing is changed and the store stays encrypted. Returns KVSTORE_ERROR if the store is not encrypted or an explicit transaction is open.

kvstore_remove_encryption(kv);
kvstore_close(kv);

/* Now opens without a password */
KVStore *plain = NULL;
kvstore_open("mydb.db", &plain, KVSTORE_JOURNAL_WAL);

kvstore_now_ms

int64_t kvstore_now_ms(void);

Current time in milliseconds since the Unix epoch. Useful for computing absolute TTL expiry timestamps: kvstore_now_ms() + delta_ms.

Cryptographic details:
  • KDF: Argon2id (default m=64 MB, t=3 iterations) — password + 16-byte random salt → 256-bit key. The cost is stored in each file; on small devices build with e.g. -DSNKV_ENC_M_COST=256u (256 KB) for new keys
  • Cipher: XChaCha20-Poly1305 — 24-byte random nonce + 16-byte MAC per value
  • Overhead: 40 bytes per stored value (nonce + MAC)
  • Key scope: wiped from memory on kvstore_close
  • Column families: all CFs share one key; re-encryption covers all CFs atomically

Vector Store

An HNSW approximate nearest-neighbour index built on top of KVStore, using the usearch C SDK. Vectors and KV data share a single .db file. The index is rebuilt from the database on open and saved to a .usearch sidecar on close for fast reload. All writes are atomic KVStore transactions; the usearch index is updated after each successful commit.

Include kvstore_vec.h and link against libsnkv_vec.a (built with make vector). The core store (libsnkv.a / snkv.h) does not include the vector layer — it requires g++ to compile the usearch C++ core.

Types & Constants

Opaque handle

KVVecStoreOpaque handle returned by kvstore_vec_open.

Result structs

KVVecSearchResultpKey/nKey, pValue/nValue, distance, pMetadata/nMetadata. Free with kvstore_vec_free_results.
KVVecKeyResultpKey/nKey, distance. Free with kvstore_vec_free_key_results.
KVVecStatsFilled by kvstore_vec_stats. See Stats section.
KVVecItemOne item for kvstore_vec_put_batch: pKey/nKey, pVal/nVal, pVec, pMeta/nMeta.

Distance spaces

KVVEC_SPACE_L20Squared Euclidean (‖a−b‖²) — distances are comparable but not sqrt L2; default
KVVEC_SPACE_COSINE1Cosine distance (1 − dot(a,b) / (‖a‖·‖b‖))
KVVEC_SPACE_IP2Inner product (negative dot product)
Note on L2: KVVEC_SPACE_L2 returns squared Euclidean distance — squaring is omitted for performance. Relative ordering is identical to true L2, so nearest-neighbour results are unaffected. To recover metric distance, take sqrtf(result.distance).

Index precision (dtype)

KVVEC_DTYPE_F320float32 — full precision (default)
KVVEC_DTYPE_F161float16 — half RAM, negligible recall loss
KVVEC_DTYPE_I82int8 — quarter RAM, cosine-like metrics only

Extra error codes

KVVEC_DIM_MISMATCH20Opened with wrong dim for existing store
KVVEC_DTYPE_MISMATCH21Opened with wrong dtype for existing store
KVVEC_SPACE_MISMATCH22Opened with wrong space for existing store
KVVEC_INDEX_DROPPED23Index has been dropped; search unavailable
KVVEC_INDEX_EMPTY24Index is empty; put a vector first
KVVEC_BAD_VECTOR25Vector shape or value is invalid

Open / Close

kvstore_vec_open

int kvstore_vec_open( const char *zPath, int dim, int space, /* KVVEC_SPACE_* */ int connectivity, /* HNSW M; 0 → default 16 */ int expansion_add, /* ef_construction; 0 → default 128 */ int expansion_search, /* ef at query time; 0 → default 64 */ int dtype, /* KVVEC_DTYPE_* */ const uint8_t *pPassword, /* NULL for plain store */ int nPassword, KVVecStore **ppVS );

Open or create a vector store. dim, space, and dtype are immutable after the first open — passing different values on a subsequent open returns the corresponding mismatch error. Pass pPassword/nPassword for an encrypted store; sidecar persistence is disabled for encrypted stores. *ppVS is NULL on any error.

KVVecStore *vs = NULL;
int rc = kvstore_vec_open(
    "store.db", 128, KVVEC_SPACE_COSINE,
    0, 0, 0,            /* defaults */
    KVVEC_DTYPE_F32,
    NULL, 0,              /* plain (unencrypted) */
    &vs
);
if (rc == KVVEC_DIM_MISMATCH) puts("wrong dim for existing store");

kvstore_vec_close

void kvstore_vec_close(KVVecStore *pVS);

Close the store and free all resources. For unencrypted file-backed stores, saves the HNSW graph to {path}.usearch and the next-id stamp to {path}.usearch.nid so the next open can skip the O(n·dim) rebuild. Safe to call with NULL.

Writes

kvstore_vec_put

int kvstore_vec_put( KVVecStore *pVS, const void *pKey, int nKey, const void *pVal, int nVal, const float *pVec, /* dim float32 values */ int64_t expire_ms, /* 0 = no TTL */ const void *pMeta, int nMeta /* NULL/0 = preserve; "" nMeta=0 = clear */ );

Insert or update a key/value pair and add its vector to the HNSW index. The KVStore write (all 5 internal CFs) is one atomic transaction; the usearch index update happens after commit. If the key already exists, the old usearch label is removed before the new one is added (count stays constant on overwrite).

float vec[128] = { /* ... */ };
kvstore_vec_put(vs, "doc:1", 5, "hello", 5,
                vec,
                0,                     /* no TTL */
                "{\"tag\":\"ai\"}", 12);  /* metadata */

kvstore_vec_put_batch

int kvstore_vec_put_batch( KVVecStore *pVS, const KVVecItem *pItems, int nItems, int64_t expire_ms /* applied uniformly; 0 = no TTL */ );

Insert or update nItems records in one atomic transaction. Last-write-wins for duplicate keys within the batch — only the last vector for a repeated key enters the index. All usearch insertions happen after the commit.

KVVecItem items[3] = {
    { "k1", 2, "val1", 4, vec1, NULL, 0 },
    { "k2", 2, "val2", 4, vec2, NULL, 0 },
    { "k3", 2, "val3", 4, vec3, meta, nMeta },
};
kvstore_vec_put_batch(vs, items, 3, 0);

kvstore_vec_kv_put

int kvstore_vec_kv_put(KVVecStore *pVS, const void *pKey, int nKey, const void *pVal, int nVal);

Plain KV write — does not update the vector index. Useful for storing non-vector data (config, counters) alongside vector keys in the same file.

Reads

kvstore_vec_get

int kvstore_vec_get(KVVecStore *pVS, const void *pKey, int nKey, void **ppVal, int *pnVal);

Fetch value bytes. TTL-aware: returns KVSTORE_NOTFOUND and lazily deletes the key if it has expired. Caller must snkv_free(*ppVal).

kvstore_vec_get_vector

int kvstore_vec_get_vector(KVVecStore *pVS, const void *pKey, int nKey, float **ppVec, int *pnFloats);

Fetch the stored float32 vector. *ppVec is a flat array of dim floats. Caller must snkv_free(*ppVec). TTL-aware: returns KVSTORE_NOTFOUND if the key is missing or expired. Returns KVVEC_INDEX_DROPPED after kvstore_vec_drop_index.

kvstore_vec_get_metadata

int kvstore_vec_get_metadata(KVVecStore *pVS, const void *pKey, int nKey, void **ppMeta, int *pnMeta);

Fetch JSON metadata bytes. TTL-aware: returns KVSTORE_NOTFOUND with *ppMeta=NULL if the key is missing or expired. For a live key: returns KVSTORE_OK with *ppMeta=NULL when no metadata has ever been written to this store (tags CF does not yet exist), and KVSTORE_NOTFOUND with *ppMeta=NULL when this key has no metadata. Neither is an error. Metadata stays readable after the store is reopened. Caller must snkv_free(*ppMeta) on KVSTORE_OK with non-NULL result.

kvstore_vec_contains

int kvstore_vec_contains(KVVecStore *pVS, const void *pKey, int nKey);

Returns 1 if the key exists and is not expired, 0 otherwise.

kvstore_vec_count

int64_t kvstore_vec_count(KVVecStore *pVS);

Returns the number of active (non-deleted) vectors in the HNSW index (usearch_size).

Delete

kvstore_vec_delete

int kvstore_vec_delete(KVVecStore *pVS, const void *pKey, int nKey);

Delete the key's value and its vector, label and metadata rows in one atomic transaction, then remove it from the HNSW index after the commit. Returns KVSTORE_NOTFOUND if the key does not exist.

TTL

Pass a non-zero expire_ms to kvstore_vec_put (absolute milliseconds since the Unix epoch — use kvstore_now_ms() + delta). Expired vectors are filtered out transparently during search and lazily deleted on get. Use kvstore_vec_purge_expired for bulk cleanup.

/* Expire in 60 seconds */
int64_t exp = kvstore_now_ms() + 60000;
kvstore_vec_put(vs, "sess:abc", 8, token, nToken, vec, exp, NULL, 0);

/* Bulk-delete all expired vectors */
int nPurged = 0;
kvstore_vec_purge_expired(vs, &nPurged);

Stats

kvstore_vec_stats

int kvstore_vec_stats(KVVecStore *pVS, KVVecStats *pStats);

Fill *pStats with current index configuration and runtime state. Always returns KVSTORE_OK.

dimVector dimension
spaceKVVEC_SPACE_*
dtypeKVVEC_DTYPE_*
connectivityHNSW M parameter
expansion_addef_construction
expansion_searchef at query time
countActive vectors in HNSW index (usearch_size)
capacityAllocated HNSW capacity
fill_ratiocount / capacity — 0–1 ratio (multiply by 100 for %). Auto-doubles at 0.9.
vec_cf_countEntries in the __snkv_vec__ CF (may include expired)
has_metadata1 if the __snkv_vec_tags__ CF exists
sidecar_enabled1 for unencrypted file-backed stores

Maintenance

kvstore_vec_purge_expired

int kvstore_vec_purge_expired(KVVecStore *pVS, int *pnDeleted);

Scan the __snkv_vec__ CF and delete all expired (or orphaned) vectors from every vector CF and the HNSW index, in batches of up to 64 per transaction (build-time setting KVVEC_PURGE_BATCH), so memory stays bounded however many keys expired. If a later batch fails, earlier batches stay deleted and the error is returned. *pnDeleted (may be NULL) is set to the number removed. Use this in preference to kvstore_purge_expired to keep vector CFs in sync. Returns KVSTORE_OK (0 deleted) when there are no TTL entries.

kvstore_vec_drop_index

int kvstore_vec_drop_index(KVVecStore *pVS);

Drop all five __snkv_vec*__ column families in one transaction and free the in-memory HNSW index. KV data in the default CF is preserved and readable via kvstore_vec_get. After this call, kvstore_vec_search returns KVVEC_INDEX_DROPPED; the drop persists across reopen. Sidecar files are removed. Returns KVSTORE_BUSY if an iterator is open on the store; on any error the index is kept and the error is returned.

kvstore_vec_drop_index(vs);    /* index gone, KV intact */

void *v = NULL; int n = 0;
kvstore_vec_get(vs, "doc:1", 5, &v, &n);  /* still works */
snkv_free(v);

Build

The vector layer requires g++ (C++17) to compile the usearch HNSW core. The default make / make test targets use pure gcc and are unaffected.

# Build libsnkv_vec.a (core + usearch objects)
make vector

# Build and run the comprehensive example
make vector-examples
make run-vector-examples

# Build and run the test suite
make test-vector

Internal storage (5 Column Families)

__snkv_vec__key → float32[dim] — stored vector
__snkv_vec_idk__key → uint64 label (8B big-endian) — maps key to HNSW label
__snkv_vec_idi__uint64 label → key — reverse lookup used during search
__snkv_vec_meta__config keys: ndim, metric, connectivity, expansion_add, expansion_search, dtype, next_id
__snkv_vec_tags__key → JSON bytes — created lazily on first metadata write

Stores created by older versions used single-underscore names (_snkv_vec_ …); they are renamed automatically on open.

Sidecar files

{path}.usearchusearch HNSW binary graph — saved on close, loaded on open
{path}.usearch.nid8-byte big-endian next-id stamp — used to detect stale sidecars
Sidecar persistence is disabled for encrypted stores and in-memory stores (zPath=NULL). The sidecar is used only if the stamp in .usearch.nid matches the stored next_id and it holds exactly as many vectors as the database; otherwise it is discarded and the index is rebuilt from the CFs. Both files are deleted once loaded, so they exist only after a clean kvstore_vec_close — after a crash the next open rebuilds instead of loading a stale index. Opening a store with a password deletes any sidecar left by an earlier unencrypted session.