Key-value store
A fast key-value store in one file: values with an optional lifetime, atomic counters, and keys listed by prefix, kept across restarts.
Version 1.0.0 · 5 functions · 1 crate
Functions
What use kv.qwer gives a program, with the types Quillo sees. To take only some, or under other names: use kv_set, kv_get from kv (Quillo 0.6).
kv_set(db_path: str, key: str, value: any, ttl_seconds: int) → nullor an errorKeeps value (any value: text, number, list, map) at key in the store file db_path, made if missing. ttl_seconds above 0 makes it expire; 0 keeps it for good.
kv_get(db_path: str, key: str) → anyor an errorThe value at key, or null when it was never set, was deleted, or has expired.
kv_delete(db_path: str, key: str) → boolor an errorRemoves key, and says whether it was there.
kv_incr(db_path: str, key: str, by: int) → intor an errorAdds by to the number at key (missing counts as 0) in one transaction, and gives the new number: safe with many requests at once.
kv_list(db_path: str, prefix: str, limit: int) → listor an errorThe keys starting with prefix ("" for all), in order, with their values: [{ key, value }], at most limit (1 to 10,000). Expired ones are left out.
Get it
$ quillo install kv # kv.qwer into this folder$ quillo install kv --example # and its example, kv.qweOr with curl:
$ curl -O https://quillolang.org/plugins/files/kv.qwer$ curl -O https://quillolang.org/plugins/files/kv.qweThen, in your program, all of it or only what you call:
use kv.qweruse kv_set, kv_get from kv // Quillo 0.6: only theseIt uses this crate, which cargo fetches the first time the plugin is built:
redb = "2.6"Example
// Feature flags and a visit counter kept in a key-value file that survives restarts.//// quillo run kv.qwe then open http://localhost:8080/flagsuse "kv.qwer"let KV = "data/app.kv"route GET /flags { kv_set(KV, "flag:dark_mode", true, 0)? kv_set(KV, "flag:new_checkout", { percent: 25 }, 0)? kv_set(KV, "flag:banner", "Sale ends Friday", 3600)? kv_set(KV, "other", 1, 0)? kv_list(KV, "flag:", 100)? | map((f) => { name: replace(f.key, "flag:", ""), value: f.value })}route GET /visit { let n = kv_incr(KV, `visits:${request.ip}`, 1)? { visits: n, banner: kv_get(KV, "flag:banner")?, missing: kv_get(KV, "nope")? }}route GET /reset { { removed: kv_delete(KV, "flag:banner")?, banner: kv_get(KV, "flag:banner")? }}$ quillo run kv.qwe$ curl 'localhost:8080/flags'$ curl 'localhost:8080/visit'$ curl 'localhost:8080/reset'Source
//! A fast embedded key-value store in one file: settings, sessions, counters and caches that survive restarts, with keys listed by prefix.//! version: 1.0.0//!//! [dependencies]//! redb = "2.6"use redb::{Database, ReadableTable, TableDefinition};use serde_json::{json, Value};use std::collections::HashMap;use std::sync::{Arc, Mutex, OnceLock};const TABLE: TableDefinition<&str, &str> = TableDefinition::new("kv");/// One open database per file, kept for the life of the server (redb locks its file).fn db(path: &str) -> Result<Arc<Database>, String> { static DBS: OnceLock<Mutex<HashMap<String, Arc<Database>>>> = OnceLock::new(); let mut dbs = DBS.get_or_init(|| Mutex::new(HashMap::new())).lock().unwrap_or_else(|e| e.into_inner()); if let Some(d) = dbs.get(path) { return Ok(d.clone()); } if let Some(dir) = std::path::Path::new(path).parent() { if !dir.as_os_str().is_empty() { std::fs::create_dir_all(dir).map_err(|e| e.to_string())?; } } let d = Arc::new(Database::create(path).map_err(|e| format!("{}: {}", path, e))?); dbs.insert(path.to_string(), d.clone()); Ok(d)}fn now() -> i64 { std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).map(|d| d.as_secs() as i64).unwrap_or(0)}/// The stored value, or None when it is missing or its time is up.fn live(raw: &str) -> Option<Value> { let v: Value = serde_json::from_str(raw).ok()?; let exp = v.get("e").and_then(Value::as_i64).unwrap_or(0); if exp > 0 && exp <= now() { return None; } v.get("v").cloned()}fn put(path: &str, key: &str, value: &Value, ttl: i64) -> Result<(), String> { if key.is_empty() || key.len() > 1024 { return Err("a key is 1 to 1024 bytes".into()); } let rec = json!({ "v": value, "e": if ttl > 0 { now() + ttl } else { 0 } }).to_string(); let d = db(path)?; let w = d.begin_write().map_err(|e| e.to_string())?; { let mut t = w.open_table(TABLE).map_err(|e| e.to_string())?; t.insert(key, rec.as_str()).map_err(|e| e.to_string())?; } w.commit().map_err(|e| e.to_string())}/// Keeps `value` (any value: text, number, list, map) at `key` in the store file `db_path`, made if missing./// `ttl_seconds` above 0 makes it expire; 0 keeps it for good.pub fn kv_set(db_path: &str, key: &str, value: serde_json::Value, ttl_seconds: i64) -> Result<(), String> { put(db_path, key, &value, ttl_seconds)}/// The value at `key`, or null when it was never set, was deleted, or has expired.pub fn kv_get(db_path: &str, key: &str) -> Result<Value, String> { let d = db(db_path)?; let r = d.begin_read().map_err(|e| e.to_string())?; let t = match r.open_table(TABLE) { Ok(t) => t, Err(redb::TableError::TableDoesNotExist(_)) => return Ok(Value::Null), Err(e) => return Err(e.to_string()), }; let got = t.get(key).map_err(|e| e.to_string())?; Ok(got.and_then(|g| live(g.value())).unwrap_or(Value::Null))}/// Removes `key`, and says whether it was there.pub fn kv_delete(db_path: &str, key: &str) -> Result<bool, String> { let d = db(db_path)?; let w = d.begin_write().map_err(|e| e.to_string())?; let had = { let mut t = w.open_table(TABLE).map_err(|e| e.to_string())?; let old = t.remove(key).map_err(|e| e.to_string())?; old.is_some() }; w.commit().map_err(|e| e.to_string())?; Ok(had)}/// Adds `by` to the number at `key` (missing counts as 0) in one transaction, and gives the new number: safe with many requests at once.pub fn kv_incr(db_path: &str, key: &str, by: i64) -> Result<i64, String> { let d = db(db_path)?; let w = d.begin_write().map_err(|e| e.to_string())?; let n = { let mut t = w.open_table(TABLE).map_err(|e| e.to_string())?; let old = t.get(key).map_err(|e| e.to_string())?.and_then(|g| live(g.value())).and_then(|v| v.as_i64()).unwrap_or(0); let n = old.saturating_add(by); let rec = json!({ "v": n, "e": 0 }).to_string(); t.insert(key, rec.as_str()).map_err(|e| e.to_string())?; n }; w.commit().map_err(|e| e.to_string())?; Ok(n)}/// The keys starting with `prefix` ("" for all), in order, with their values: `[{ key, value }]`, at most `limit` (1 to 10,000). Expired ones are left out.pub fn kv_list(db_path: &str, prefix: &str, limit: i64) -> Result<Vec<Value>, String> { let d = db(db_path)?; let r = d.begin_read().map_err(|e| e.to_string())?; let t = match r.open_table(TABLE) { Ok(t) => t, Err(redb::TableError::TableDoesNotExist(_)) => return Ok(vec![]), Err(e) => return Err(e.to_string()), }; let mut out = Vec::new(); for item in t.range(prefix..).map_err(|e| e.to_string())? { let (k, v) = item.map_err(|e| e.to_string())?; if !k.value().starts_with(prefix) || out.len() >= limit.clamp(1, 10_000) as usize { break; } if let Some(v) = live(v.value()) { out.push(json!({ "key": k.value(), "value": v })); } } Ok(out)}Related
cache_disk.qwer · 1.0.0A disk cache for slow answers: text under a key with a lifetime, stored by content hash and checked when read.8 views · 8 downloadsMemcached memcached.qwer · 1.0.0Memcached's text protocol over pooled connections: get, multi-get, set, add, replace, gets and cas, incr, decr, touch and delete, long lifetimes turned into the absolute times the protocol wants, values kept as text or JSON, and several servers shared by ketama consistent hashing.4 views · 10 downloadsPoloDB polodb.qwer · 1.0.0An embedded document database in one file with a MongoDB-like API: insert maps, find them with $gt and $in filters, update with $set and $inc.7 views · 8 downloadsRedis redis.qwer · 1.0.0Redis as a cache and counter store: get, set with a lifetime, delete and count, over one connection kept open.7 views · 7 downloadsLZ4 lz4.qwer · 1.0.0LZ4 compression of files and text in the standard .lz4 frame format: the fastest to pack and unpack, for caches, logs and data passed between services.6 views · 7 downloadsHashes hashes.qwer · 1.0.0MD5, SHA-1, SHA-2, SHA-3, BLAKE2, xxHash and CRC-32 of text and files, and Subresource Integrity values for script tags.7 views · 6 downloadsThread 0 posts
On Key-value store: questions, tips, corrections, and code that uses it.
No posts yet. The first one opens the thread.