Basic CRUD

Open a store, write and read key-value pairs, check existence, and delete entries.

Hello World examples/basic.c
#define SNKV_IMPLEMENTATION
#include "snkv.h"

KVStore *pKV = NULL;
kvstore_open("hello.db", &pKV, KVSTORE_JOURNAL_WAL);

kvstore_put(pKV, "greeting", 8, "Hello, World!", 13);

void *pValue = NULL; int nValue = 0;
kvstore_get(pKV, "greeting", 8, &pValue, &nValue);
printf("Retrieved: %.*s\n", nValue, (char*)pValue);
snkv_free(pValue);

kvstore_close(pKV);
CRUD + Existence Check examples/basic.c
/* Create */
kvstore_put(pKV, "user:1", 6, "Alice Smith", 11);

/* Update */
kvstore_put(pKV, "user:1", 6, "Alice Johnson", 13);

/* Delete */
kvstore_delete(pKV, "user:1", 6);

/* Existence check */
int exists = 0;
kvstore_exists(pKV, "item:laptop", 11, &exists);
printf("%s\n", exists ? "EXISTS" : "NOT FOUND");

Transactions

Batch multiple writes into a single atomic commit, or roll back on error.

Atomic Batch + Rollback examples/transactions.c
int rc = kvstore_begin(pKV, 1);   /* 1 = write */

kvstore_put(pKV, "account:alice", 13, "800", 3);
kvstore_put(pKV, "account:bob",   11, "700", 3);

if (rc == KVSTORE_OK) {
    kvstore_commit(pKV);
    printf("Transfer committed\n");
} else {
    kvstore_rollback(pKV);   /* both writes discarded */
}

Column Families

Logical namespaces within a single file. Each CF is a separate B-tree — keys never bleed across CFs.

Create, Use, and List examples/column_families.c
KVColumnFamily *pUsersCF = NULL, *pProductsCF = NULL;

kvstore_cf_create(pKV, "users",    &pUsersCF);
kvstore_cf_create(pKV, "products", &pProductsCF);

kvstore_cf_put(pUsersCF,    "user:1",  6, "alice@example.com", 17);
kvstore_cf_put(pProductsCF, "prod:100",8, "Laptop:$999",       11);

/* List all CFs */
char **azNames; int nCount;
kvstore_cf_list(pKV, &azNames, &nCount);
for (int i = 0; i < nCount; i++) {
    printf("  CF: %s\n", azNames[i]);
    snkv_free(azNames[i]);
}
snkv_free(azNames);

kvstore_cf_close(pUsersCF);
kvstore_cf_close(pProductsCF);

Iterators

Walk all keys in ascending order or scan only keys matching a prefix using the B-tree directly.

Full Scan examples/iterators.c
KVIterator *pIter = NULL;
kvstore_iterator_create(pKV, &pIter);

for (kvstore_iterator_first(pIter);
     !kvstore_iterator_eof(pIter);
     kvstore_iterator_next(pIter)) {

    void *pKey, *pValue; int nKey, nValue;
    kvstore_iterator_key(pIter, &pKey, &nKey);
    kvstore_iterator_value(pIter, &pValue, &nValue);
    printf("%.*s -> %.*s\n", nKey, (char*)pKey, nValue, (char*)pValue);
}

kvstore_iterator_close(pIter);
Prefix Scan examples/iterators.c
KVIterator *pIter = NULL;
kvstore_prefix_iterator_create(pKV, "user:", 5, &pIter);

for (kvstore_iterator_first(pIter);
     !kvstore_iterator_eof(pIter);
     kvstore_iterator_next(pIter)) {

    void *pKey; int nKey;
    kvstore_iterator_key(pIter, &pKey, &nKey);
    printf("  %.*s\n", nKey, (char*)pKey);
}

kvstore_iterator_close(pIter);

Reverse Iterators

Walk keys in descending order — pure B-tree traversal, no sort, no full scan.

Reverse Full + Reverse Prefix Scan examples/iterator_reverse.c
/* Reverse full scan — largest key first */
KVIterator *pIter = NULL;
kvstore_reverse_iterator_create(pKV, &pIter);

for (kvstore_iterator_last(pIter);
     !kvstore_iterator_eof(pIter);
     kvstore_iterator_prev(pIter)) {

    void *pKey; int nKey;
    kvstore_iterator_key(pIter, &pKey, &nKey);
    printf("%.*s\n", nKey, (char*)pKey);
}
kvstore_iterator_close(pIter);

/* Reverse prefix scan — largest matching key first */
kvstore_reverse_prefix_iterator_create(pKV, "score:", 6, &pIter);
for (kvstore_iterator_last(pIter);
     !kvstore_iterator_eof(pIter);
     kvstore_iterator_prev(pIter)) {
    /* ... */
}
kvstore_iterator_close(pIter);

TTL — Time-To-Live

Per-key expiry stored in a dedicated index. Expired keys are lazily deleted on access; bulk cleanup via kvstore_purge_expired.

Put with TTL, Lazy Expiry, Purge examples/ttl.c
/* Put with TTL — expire_ms is an absolute Unix timestamp in ms */
int64_t expire_ms = kvstore_now_ms() + 5000;   /* 5 seconds */
kvstore_put_ttl(pKV, "session", 7, "tok123", 6, expire_ms);

/* Inspect remaining lifetime */
int64_t remaining = 0;
kvstore_ttl_remaining(pKV, "session", 7, &remaining);
printf("TTL: %lld ms remaining\n", (long long)remaining);

/* Lazy expiry — expired key returns KVSTORE_NOTFOUND */
void *pVal = NULL; int nVal = 0; int64_t rem = 0;
int rc = kvstore_get_ttl(pKV, "session", 7, &pVal, &nVal, &rem);

/* Bulk purge all expired keys */
int n = 0;
kvstore_purge_expired(pKV, &n);
printf("Removed %d expired keys\n", n);
CF-level TTL examples/ttl.c
KVColumnFamily *pCF = NULL;
kvstore_cf_create(pKV, "rate_limits", &pCF);

int64_t expire_ms = kvstore_now_ms() + 1000;   /* 1 second */
kvstore_cf_put_ttl(pCF, "user:42", 7, "5", 1, expire_ms);

int64_t rem = 0;
kvstore_cf_ttl_remaining(pCF, "user:42", 7, &rem);
printf("Remaining: %lld ms\n", (long long)rem);

int n = 0;
kvstore_cf_purge_expired(pCF, &n);
kvstore_cf_close(pCF);

Seek · put_if_absent · clear · count · stats

O(log N) positional seek, atomic conditional insert, bulk truncation, page-level entry count, and extended diagnostics.

Iterator Seek — forward and reverse examples/new_apis.c
KVIterator *pIter = NULL;

/* Forward seek — positions at first key >= target */
kvstore_iterator_create(pKV, &pIter);
kvstore_iterator_seek(pIter, "cherry", 6);
while (!kvstore_iterator_eof(pIter)) {
    void *pKey; int nKey;
    kvstore_iterator_key(pIter, &pKey, &nKey);
    printf("%.*s\n", nKey, (char*)pKey);
    kvstore_iterator_next(pIter);
}
kvstore_iterator_close(pIter);

/* Reverse seek — positions at last key <= target */
kvstore_reverse_iterator_create(pKV, &pIter);
kvstore_iterator_last(pIter);
kvstore_iterator_seek(pIter, "bbc", 3);   /* lands on "bbb" */
kvstore_iterator_close(pIter);
put_if_absent — atomic conditional insert examples/new_apis.c
>int inserted = 0;

/* Key absent → inserted=1 */
kvstore_put_if_absent(pKV, "lock", 4, "owner:alice", 11, 0, &inserted);
printf("inserted=%d\n", inserted);   /* 1 */

/* Key present → inserted=0, original value unchanged */
kvstore_put_if_absent(pKV, "lock", 4, "owner:bob",   9,  0, &inserted);
printf("inserted=%d\n", inserted);   /* 0 */

/* With TTL — auto-releasing distributed lock */
int64_t exp = kvstore_now_ms() + 30000;
kvstore_put_if_absent(pKV, "job:1", 5, "worker-A", 8, exp, NULL);
clear · count · stats examples/new_apis.c
>/* count — O(pages) via sqlite3BtreeCount */
int64_t n = 0;
kvstore_count(pKV, &n);
printf("entries: %lld\n", (long long)n);

/* clear — O(pages) bulk truncation; TTL index cleared atomically */
kvstore_clear(pKV);
kvstore_count(pKV, &n);
printf("after clear: %lld\n", (long long)n);   /* 0 */

/* Extended stats — 12 counters */
KVStoreStats st = {0};
kvstore_stats(pKV, &st);
printf("puts=%llu gets=%llu bytes_written=%llu db_pages=%llu\n",
       st.nPuts, st.nGets, st.nBytesWritten, st.nDbPages);

kvstore_stats_reset(pKV);   /* zero cumulative counters */

Session Store

A practical in-process session store using a dedicated column family with TTL.

Session store with TTL + column family examples/session_store.c
>KVColumnFamily *pSessions = NULL;
kvstore_cf_create(pKV, "sessions", &pSessions);

/* Create session — expires in 30 minutes */
int64_t exp = kvstore_now_ms() + 1800000;
kvstore_cf_put_ttl(pSessions, "sess:abc123", 11,
                   "user_id=42", 10, exp);

/* Validate — KVSTORE_NOTFOUND means expired or never existed */
void *pVal = NULL; int nVal = 0; int64_t rem = 0;
int rc = kvstore_cf_get_ttl(pSessions, "sess:abc123", 11,
                             &pVal, &nVal, &rem);
if (rc == KVSTORE_OK) {
    printf("Valid session: %.*s (%lld ms left)\n",
           nVal, (char*)pVal, (long long)rem);
    snkv_free(pVal);
} else {
    printf("Session expired or not found\n");
}

kvstore_cf_close(pSessions);

Checkpoint

Flush WAL frames back to the main database file to control file size.

Manual and auto-checkpoint examples/checkpoint_demo.c
>/* Manual checkpoint — TRUNCATE mode resets WAL to zero bytes */
int nLog = 0, nCkpt = 0;
kvstore_checkpoint(pKV, KVSTORE_CHECKPOINT_TRUNCATE, &nLog, &nCkpt);
printf("WAL frames: %d total, %d checkpointed\n", nLog, nCkpt);

/* Auto-checkpoint is ON by default (PASSIVE, every 1000 WAL pages).
   Tune or disable it: KVStoreConfig.walAutoCheckpoint = N pages (-1 = off), open with kvstore_open_v2 */

Encryption

XChaCha20-Poly1305 AEAD encryption with Argon2id key derivation. Keys are stored plaintext (B-tree order preserved); values are encrypted. Overhead is 40 bytes per value (SNKV_ENC_OVERHEAD).

Open encrypted store + read/write examples/enc.c
#define SNKV_IMPLEMENTATION
#include "snkv.h"

KVStore *pKV = NULL;
const char *zPass = "my-secret-password";

/* Open (or create) an encrypted store */
int rc = kvstore_open_encrypted("secure.db", &pKV,
                                 KVSTORE_JOURNAL_WAL,
                                 zPass, (int)strlen(zPass));
if (rc == KVSTORE_AUTH_FAILED) {
    fprintf(stderr, "Wrong password\n");
    return 1;
}

/* Normal put/get — encryption/decryption is transparent */
kvstore_put(pKV, "secret", 6, "classified", 10);

void *pVal = NULL; int nVal = 0;
kvstore_get(pKV, "secret", 6, &pVal, &nVal);
printf("%.*s\n", nVal, (char*)pVal);   /* classified */
snkv_free(pVal);

kvstore_close(pKV);
is_encrypted · remove_encryption · reencrypt examples/enc.c
/* Check whether the open store is encrypted */
int bEnc = 0;
kvstore_is_encrypted(pKV, &bEnc);
printf("encrypted: %d\n", bEnc);   /* 1 */

/* Password rotation — re-encrypt in place with new password */
kvstore_reencrypt(pKV, "new-password", 12);

/* Strip encryption — convert to plain store */
kvstore_remove_encryption(pKV);

kvstore_is_encrypted(pKV, &bEnc);
printf("encrypted: %d\n", bEnc);   /* 0 */
Encrypted column families examples/enc.c
/* CFs inherit encryption from the store — no extra config needed */
KVColumnFamily *pCF = NULL;
kvstore_cf_create(pKV, "users", &pCF);

kvstore_cf_put(pCF, "alice", 5, "secret_data", 11);

void *pOut = NULL; int nOut = 0;
kvstore_cf_get(pCF, "alice", 5, &pOut, &nOut);
printf("%.*s\n", nOut, (char*)pOut);   /* secret_data — decrypted */
snkv_free(pOut);

kvstore_cf_close(pCF);
Plaintext confirmed after remove_encryption examples/enc.c
/* Write sentinel, encrypt, then strip — verify raw bytes */
kvstore_open_encrypted("data.db", &pKV, KVSTORE_JOURNAL_WAL,
                        "pass", 4);
kvstore_put(pKV, "k", 1, "PLAINTEXT_SENTINEL", 18);
kvstore_remove_encryption(pKV);
kvstore_close(pKV);

/* "PLAINTEXT_SENTINEL" is now readable in raw file bytes */

/* Re-open as plain store — no password required */
kvstore_open("data.db", &pKV, KVSTORE_JOURNAL_WAL);
void *pVal = NULL; int nVal = 0;
kvstore_get(pKV, "k", 1, &pVal, &nVal);
printf("%.*s\n", nVal, (char*)pVal);   /* PLAINTEXT_SENTINEL */
snkv_free(pVal);
kvstore_close(pKV);

Multi-reader / Single-writer

Each thread opens its own KVStore handle. WAL byte-range locks in the -shm file enforce single-writer / multi-reader isolation at the OS level — no application mutex is needed. A second writer attempting to begin while one is active receives KVSTORE_BUSY immediately.

1 writer + N readers — per-thread handles examples/concurrent_rw.c
/* Each thread opens its OWN handle — no shared pointer, no mutex needed */
static KVStore *open_store(int busy_timeout_ms) {
    KVStoreConfig cfg = {0};
    cfg.journalMode = KVSTORE_JOURNAL_WAL;
    cfg.busyTimeout = busy_timeout_ms;
    /* separate handles never wait for each other's lock — WAL coordinates them */
    KVStore *kv = NULL;
    kvstore_open_v2(DB_FILE, &kv, &cfg);
    return kv;
}

static void *writer_thread(void *arg) {
    KVStore *kv = open_store(5000);
    kvstore_begin(kv, 1);          /* acquires WAL WRITE_LOCK */
    for (int i = 0; i < N; i++) kvstore_put(kv, key[i], ...);
    kvstore_commit(kv);            /* releases WRITE_LOCK */
    kvstore_close(kv);
    return NULL;
}

static void *reader_thread(void *arg) {
    KVStore *kv = open_store(5000);
    for (int i = 0; i < N; i++) {
        kvstore_get(kv, key[i], ...); /* concurrent with writer — safe */
        /* KVSTORE_NOTFOUND is fine: writer may not have committed yet */
    }
    kvstore_close(kv);
    return NULL;
}
Second writer rejected — KVSTORE_BUSY examples/concurrent_rw.c
/* Writer 1 holds the write lock */
KVStore *kv1 = open_store(0);
kvstore_begin(kv1, 1);   /* WRITE_LOCK acquired */

/* Writer 2 tries with busyTimeout=0 — fails immediately */
KVStore *kv2 = open_store(0);
int rc = kvstore_begin(kv2, 1);
/* rc == KVSTORE_BUSY — WAL refuses the second writer */
kvstore_close(kv2);

/* Release writer 1; writer 2 can now succeed */
kvstore_rollback(kv1);
kvstore_close(kv1);
Sharing one handle between threads examples/concurrent_rw.c
/* Every handle has one lock: a SINGLE KVStore* may be shared */
KVStore *shared = NULL;
kvstore_open("mydb.db", &shared, KVSTORE_JOURNAL_WAL);

/* Thread A and Thread B can safely call kvstore_put/get on 'shared';  */
/* a kvstore_begin/commit stays with the thread that called begin.     */
/* For parallel reads, prefer one handle per thread.                   */