Basic CRUD
Open a store, write and read key-value pairs, check existence, and delete entries.
#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);
/* 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.
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.
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.
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);
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 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 — 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);
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.
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);
>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);
>/* 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.
>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 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).
#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);
/* 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 */
/* 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);
/* 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.
/* 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; }
/* 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);
/* 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. */