ProtoCore v0.0.2
Deterministic, zero-heap network stack for embedded targets
Loading...
Searching...
No Matches
dbm.cpp
Go to the documentation of this file.
1// Copyright (C) 2026 Douglas Quigg (dstroy0) <dquigg123@gmail.com>
2// SPDX-License-Identifier: AGPL-3.0-or-later
3
4/**
5 * @file dbm.cpp
6 * @brief Log-structured hash key-value store on the WAL (see dbm.h).
7 */
8
10
11#if PC_ENABLE_DBM
12
13#include <string.h>
14
15namespace
16{
17// dbm record payload header: op u8 | key_len u16 | val_len u32.
18const size_t DBM_HDR = 7;
19
20void put_u16(uint8_t *p, uint16_t v)
21{
22 p[0] = (uint8_t)v;
23 p[1] = (uint8_t)(v >> 8);
24}
25void put_u32(uint8_t *p, uint32_t v)
26{
27 p[0] = (uint8_t)v;
28 p[1] = (uint8_t)(v >> 8);
29 p[2] = (uint8_t)(v >> 16);
30 p[3] = (uint8_t)(v >> 24);
31}
32uint16_t get_u16(const uint8_t *p)
33{
34 return (uint16_t)((uint16_t)p[0] | ((uint16_t)p[1] << 8));
35}
36uint32_t get_u32(const uint8_t *p)
37{
38 return (uint32_t)p[0] | ((uint32_t)p[1] << 8) | ((uint32_t)p[2] << 16) | ((uint32_t)p[3] << 24);
39}
40
41// FNV-1a 64-bit over the key.
42uint64_t key_hash(const char *key, uint16_t len)
43{
44 uint64_t h = 0xcbf29ce484222325ULL;
45 for (uint16_t i = 0; i < len; i++)
46 {
47 h ^= (uint8_t)key[i];
48 h *= 0x100000001b3ULL;
49 }
50 return h;
51}
52
53// Find a live slot for (hash,key). Linear probe, stopping at the first empty. -1 if absent.
54int find_live(const pc_dbm *db, uint64_t hash, const char *key, uint16_t key_len)
55{
56 const size_t n = PC_DBM_SLOTS;
57 size_t start = (size_t)(hash % n);
58 for (size_t i = 0; i < n; i++)
59 {
60 size_t j = (start + i) % n;
61 const pc_dbm_slot *s = &db->slots[j];
62 if (s->state == 0)
63 {
64 return -1; // empty -> the probe chain ends, key not present
65 }
66 if (s->state == 1 && s->hash == hash && s->key_len == key_len && memcmp(s->key, key, key_len) == 0)
67 {
68 return (int)j;
69 }
70 }
71 return -1;
72}
73
74// Find the slot to write (hash,key): an existing live match, or the first reusable slot (tombstone/empty).
75// Sets *is_new when the returned slot is not already this key. -1 if the table has no room for a new key.
76int reserve(const pc_dbm *db, uint64_t hash, const char *key, uint16_t key_len, bool *is_new)
77{
78 const size_t n = PC_DBM_SLOTS;
79 size_t start = (size_t)(hash % n);
80 int first_free = -1;
81 for (size_t i = 0; i < n; i++)
82 {
83 size_t j = (start + i) % n;
84 const pc_dbm_slot *s = &db->slots[j];
85 if (s->state == 1)
86 {
87 if (s->hash == hash && s->key_len == key_len && memcmp(s->key, key, key_len) == 0)
88 {
89 *is_new = false;
90 return (int)j;
91 }
92 continue;
93 }
94 if (s->state == 2)
95 {
96 if (first_free < 0)
97 {
98 first_free = (int)j; // reusable tombstone; keep probing for a live match
99 }
100 continue;
101 }
102 // empty: the key is not present -> insert at the earliest reusable slot (tombstone or here)
103 *is_new = true;
104 return first_free >= 0 ? first_free : (int)j;
105 }
106 // No empty slot seen (table saturated with live/tombstones); only a tombstone can hold a new key.
107 *is_new = true;
108 return first_free;
109}
110
111struct ReplayCtx
112{
113 pc_dbm *db;
114 bool overflow;
115};
116
117void replay_cb(uint64_t seq, uint64_t data_off, const uint8_t *payload, uint32_t len, void *ctx)
118{
119 (void)seq;
120 ReplayCtx *rc = (ReplayCtx *)ctx;
121 pc_dbm *db = rc->db;
122 if (len < DBM_HDR)
123 {
124 return;
125 }
126 uint8_t op = payload[0];
127 uint16_t klen = get_u16(payload + 1);
128 uint32_t vlen = get_u32(payload + 3);
129 if (klen == 0 || klen > PC_DBM_KEY_MAX)
130 {
131 return;
132 }
133 if (DBM_HDR + (size_t)klen + vlen > len)
134 {
135 return; // truncated / malformed payload
136 }
137 const char *key = (const char *)(payload + DBM_HDR);
138 uint64_t h = key_hash(key, klen);
139 if (op == 0) // put
140 {
141 bool is_new = false;
142 int slot = reserve(db, h, key, klen, &is_new);
143 if (slot < 0)
144 {
145 rc->overflow = true;
146 return;
147 }
148 pc_dbm_slot *s = &db->slots[slot];
149 if (s->state != 1)
150 {
151 db->count++;
152 }
153 s->state = 1;
154 s->hash = h;
155 s->key_len = klen;
156 memcpy(s->key, key, klen);
157 s->val_off = data_off + WAL_RECORD_HEADER + DBM_HDR + klen;
158 s->val_len = vlen;
159 }
160 else if (op == 1) // delete
161 {
162 int slot = find_live(db, h, key, klen);
163 if (slot >= 0)
164 {
165 db->slots[slot].state = 2;
166 db->count--;
167 }
168 }
169}
170} // namespace
171
172bool pc_dbm_open(pc_dbm *db, WalStore *wal)
173{
174 memset(db, 0, sizeof(*db));
175 db->wal = wal;
176 ReplayCtx rc = {db, false};
177 uint8_t scratch[WAL_RECORD_HEADER + DBM_HDR + PC_DBM_KEY_MAX + PC_DBM_VAL_MAX];
178 pc_wal_store_scan(wal, replay_cb, &rc, scratch, sizeof(scratch));
179 return !rc.overflow;
180}
181
182bool pc_dbm_put(pc_dbm *db, const char *key, uint16_t key_len, const uint8_t *val, uint32_t val_len)
183{
184 if (key_len == 0 || key_len > PC_DBM_KEY_MAX || val_len > PC_DBM_VAL_MAX)
185 {
186 return false;
187 }
188 uint64_t h = key_hash(key, key_len);
189 bool is_new = false;
190 int slot = reserve(db, h, key, key_len, &is_new);
191 if (slot < 0)
192 {
193 return false; // index full: do not append an orphan record
194 }
195
196 uint8_t rec[DBM_HDR + PC_DBM_KEY_MAX + PC_DBM_VAL_MAX];
197 rec[0] = 0;
198 put_u16(rec + 1, key_len);
199 put_u32(rec + 3, val_len);
200 memcpy(rec + DBM_HDR, key, key_len);
201 if (val_len)
202 {
203 memcpy(rec + DBM_HDR + key_len, val, val_len);
204 }
205 uint64_t old_head = pc_wal_store_used(db->wal);
206 if (!pc_wal_store_append(db->wal, rec, (uint32_t)(DBM_HDR + key_len + val_len)))
207 {
208 return false; // WAL full: index unchanged
209 }
210
211 pc_dbm_slot *s = &db->slots[slot];
212 if (s->state != 1)
213 {
214 db->count++;
215 }
216 s->state = 1;
217 s->hash = h;
218 s->key_len = key_len;
219 memcpy(s->key, key, key_len);
220 s->val_off = old_head + WAL_RECORD_HEADER + DBM_HDR + key_len;
221 s->val_len = val_len;
222 return true;
223}
224
225long pc_dbm_get(pc_dbm *db, const char *key, uint16_t key_len, uint8_t *buf, size_t cap)
226{
227 uint64_t h = key_hash(key, key_len);
228 int slot = find_live(db, h, key, key_len);
229 if (slot < 0)
230 {
231 return -1;
232 }
233 const pc_dbm_slot *s = &db->slots[slot];
234 if (s->val_len > cap)
235 {
236 return -1;
237 }
238 if (s->val_len && !pc_wal_store_pread(db->wal, s->val_off, buf, s->val_len))
239 {
240 return -1;
241 }
242 return (long)s->val_len;
243}
244
245bool pc_dbm_del(pc_dbm *db, const char *key, uint16_t key_len)
246{
247 uint64_t h = key_hash(key, key_len);
248 int slot = find_live(db, h, key, key_len);
249 if (slot < 0)
250 {
251 return false;
252 }
253 uint8_t rec[DBM_HDR + PC_DBM_KEY_MAX];
254 rec[0] = 1;
255 put_u16(rec + 1, key_len);
256 put_u32(rec + 3, 0);
257 memcpy(rec + DBM_HDR, key, key_len);
258 if (!pc_wal_store_append(db->wal, rec, (uint32_t)(DBM_HDR + key_len)))
259 {
260 return false; // WAL full: key stays live
261 }
262 db->slots[slot].state = 2;
263 db->count--;
264 return true;
265}
266
267bool pc_dbm_contains(const pc_dbm *db, const char *key, uint16_t key_len)
268{
269 return find_live(db, key_hash(key, key_len), key, key_len) >= 0;
270}
271
272uint32_t pc_dbm_count(const pc_dbm *db)
273{
274 return db->count;
275}
276
277bool pc_dbm_sync(pc_dbm *db)
278{
279 return pc_wal_store_checkpoint(db->wal);
280}
281
282uint32_t pc_dbm_iterate(const pc_dbm *db, pc_dbm_iter_cb cb, void *ctx)
283{
284 uint32_t visited = 0;
285 for (uint32_t i = 0; i < PC_DBM_SLOTS; i++)
286 {
287 const pc_dbm_slot *s = &db->slots[i];
288 if (s->state != 1)
289 {
290 continue;
291 }
292 visited++;
293 if (cb && !cb(s->key, s->key_len, ctx))
294 {
295 break;
296 }
297 }
298 return visited;
299}
300
301uint64_t pc_dbm_live_bytes(const pc_dbm *db)
302{
303 uint64_t bytes = 0;
304 for (uint32_t i = 0; i < PC_DBM_SLOTS; i++)
305 {
306 const pc_dbm_slot *s = &db->slots[i];
307 if (s->state == 1)
308 {
309 bytes += WAL_RECORD_HEADER + DBM_HDR + s->key_len + s->val_len; // one framed record per live key
310 }
311 }
312 return bytes;
313}
314
315bool pc_dbm_compact(pc_dbm *db, WalStore *dst)
316{
317 // Copy each live key (latest value, no tombstones) into the fresh destination. Read the value straight
318 // from the old log so this needs no per-key RAM beyond one record buffer, the same the put path uses.
319 uint8_t rec[DBM_HDR + PC_DBM_KEY_MAX + PC_DBM_VAL_MAX];
320 for (uint32_t i = 0; i < PC_DBM_SLOTS; i++)
321 {
322 const pc_dbm_slot *s = &db->slots[i];
323 if (s->state != 1)
324 {
325 continue;
326 }
327 rec[0] = 0; // put
328 put_u16(rec + 1, s->key_len);
329 put_u32(rec + 3, s->val_len);
330 memcpy(rec + DBM_HDR, s->key, s->key_len);
331 // On any failure, return before rebinding so db keeps using its intact original log (no data loss).
332 if (s->val_len && !pc_wal_store_pread(db->wal, s->val_off, rec + DBM_HDR + s->key_len, s->val_len))
333 {
334 return false;
335 }
336 if (!pc_wal_store_append(dst, rec, (uint32_t)(DBM_HDR + s->key_len + s->val_len)))
337 {
338 return false; // destination too small
339 }
340 }
341 if (!pc_wal_store_checkpoint(dst))
342 {
343 return false;
344 }
345 return pc_dbm_open(db, dst); // rebind to the compacted log + rebuild the index with fresh offsets
346}
347
348#endif // PC_ENABLE_DBM
Log-structured hash key-value store on the WAL (PC_ENABLE_DBM, requires PC_ENABLE_WAL).
#define PC_DBM_KEY_MAX
#define PC_DBM_SLOTS
#define PC_DBM_VAL_MAX