fa47b98d18
Self-contained paged store (lang/runtime/engram_store.{c,h}): 16KiB slotted pages,
u32 TLV self-describing records (forward-compatible), overflow chains, B+-tree
id-index + from/to adjacency, page free-list, tombstones, double superblock + crc
recovery. Not yet wired to activation (M3). 33/33 tests pass (ASan/UBSan clean);
5k nodes/20k edges round-trip bit-exact incl 768xf32 emb + hebb; store 25MB vs 64MB
JSON. Format is final — see design §2.4.
1085 lines
47 KiB
C
1085 lines
47 KiB
C
/* engram_store.c — M1 paged store: writer/reader for the FINAL on-disk format.
|
|
*
|
|
* Self-contained. Standard libs only. See engram_store.h and the design doc
|
|
* (engram-tiered-storage-engine.md §2/§4/§7). Write-through page I/O via
|
|
* pread/pwrite (no buffer pool yet — that is M4). Every mutation immediately
|
|
* lands on disk; store_sync/close rewrite both superblocks and fsync.
|
|
*
|
|
* On-disk layout (all integers little-endian):
|
|
* page 0,1 : superblock + mirror (recovery picks higher valid sb_seq)
|
|
* page N : generic 32-byte page header, then type-specific body
|
|
*
|
|
* Generic page header (32 bytes):
|
|
* +0 u64 page_id
|
|
* +8 u8 page_type (NODE|EDGE|INDEX|OVERFLOW|FREE)
|
|
* +9 u8 reserved
|
|
* +10 u16 slot_count (slotted pages) / nkeys (index pages)
|
|
* +12 u16 free_bytes (slotted pages: contiguous heap gap)
|
|
* +14 u16 reserved
|
|
* +16 u64 lsn (LSN of last write — ARIES redo idempotency, M2)
|
|
* +24 u32 crc32 (over the whole page with these 4 bytes zeroed)
|
|
* +28 u32 reserved
|
|
*
|
|
* Slotted page (NODE/EDGE): slot directory grows from +32 (6 bytes each:
|
|
* [u16 off][u16 len][u16 flags]); records grow down from PAGE_SIZE.
|
|
*
|
|
* On-page record: [u16 rec_len][u8 rec_ver][u8 rec_flags] then either the TLV
|
|
* field body (inline) or, if REC_OVERFLOW, a stub [u64 head_page][u64 total].
|
|
*
|
|
* TLV field: [u8 tag][u32 len][bytes]. NOTE: the design §2 sketch says u16 len;
|
|
* node content can exceed 64 KiB (tests use 100 KiB), so the field length MUST
|
|
* be u32. This is the one format decision beyond §2 — flagged in the report.
|
|
*
|
|
* Overflow page: header, then +32 [u64 next_page][u32 chunk_len][payload...].
|
|
*
|
|
* Index page (B+-tree, u64 keys, duplicates allowed, fixed payload per tree):
|
|
* +32 u8 is_leaf; +36 u64 next_leaf; +44 entries.
|
|
* leaf entry = [u64 key][payload]; payload = primary(10) or adjacency(11).
|
|
* internal = nkeys u64 keys, then (nkeys+1) u64 child page ids.
|
|
*/
|
|
#include "engram_store.h"
|
|
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <stdint.h>
|
|
#include <unistd.h>
|
|
#include <fcntl.h>
|
|
#include <errno.h>
|
|
#include <time.h>
|
|
#include <sys/stat.h>
|
|
|
|
/* STORE_BLL_K must track ENGRAM_BLL_K in el_runtime.c. */
|
|
typedef char store__bll_k_check[(STORE_BLL_K == 10) ? 1 : -1];
|
|
|
|
#define STORE_HDR 32u /* generic page header size */
|
|
#define SLOT_SIZE 6u /* [u16 off][u16 len][u16 flags] */
|
|
#define SLOT_LIVE 1u
|
|
#define SLOT_DEAD 2u
|
|
|
|
#define REC_HDR 4u /* [u16 rec_len][u8 ver][u8 flags] */
|
|
#define REC_VER 1u
|
|
#define REC_OVERFLOW 1u /* rec_flags bit0: body is in an overflow chain */
|
|
|
|
#define MAX_INLINE_BODY (STORE_PAGE_SIZE - STORE_HDR - SLOT_SIZE - REC_HDR)
|
|
|
|
/* Index page geometry. */
|
|
#define IDX_LEAF_OFF 32u
|
|
#define IDX_NEXT_OFF 36u
|
|
#define IDX_ENT_OFF 44u
|
|
#define IDX_BODY (STORE_PAGE_SIZE - IDX_ENT_OFF)
|
|
|
|
/* Overflow page geometry. */
|
|
#define OVF_NEXT_OFF 32u
|
|
#define OVF_LEN_OFF 40u
|
|
#define OVF_DATA_OFF 44u
|
|
#define OVF_CHUNK (STORE_PAGE_SIZE - OVF_DATA_OFF)
|
|
|
|
/* Superblock layout (page 0 and mirror page 1). */
|
|
#define SB_MAGIC_OFF 0u
|
|
#define SB_FMT_OFF 8u
|
|
#define SB_PGSZ_OFF 12u
|
|
#define SB_PGCNT_OFF 16u
|
|
#define SB_FREE_OFF 24u
|
|
#define SB_ROOT_OFF 32u
|
|
#define SB_ADJ_OFF 40u
|
|
#define SB_LAYER_OFF 48u
|
|
#define SB_CKPT_OFF 56u
|
|
#define SB_SEQ_OFF 64u
|
|
#define SB_UUID_OFF 72u /* 16 bytes */
|
|
#define SB_CRC_OFF 88u
|
|
|
|
/* Which index tree. */
|
|
enum { TREE_PRIMARY = 0, TREE_ADJ = 1 };
|
|
#define PRIMARY_PAYLOAD 10u /* [u64 page_id][u16 slot] */
|
|
#define ADJ_PAYLOAD 11u /* [u8 dir][u64 page_id][u16 slot] */
|
|
#define ADJ_DIR_FROM 0u
|
|
#define ADJ_DIR_TO 1u
|
|
#define MAX_PAYLOAD 16u
|
|
|
|
/* Node/Edge TLV tag namespaces (per-kind; page type also disambiguates). */
|
|
enum {
|
|
NT_ID = 1, NT_CONTENT, NT_NODE_TYPE, NT_LABEL, NT_TIER, NT_TAGS, NT_METADATA,
|
|
NT_SALIENCE, NT_IMPORTANCE, NT_CONFIDENCE, NT_DECAY,
|
|
NT_ACT_COUNT, NT_LAST_ACT, NT_CREATED, NT_UPDATED,
|
|
NT_BG_ACT, NT_WM_WEIGHT, NT_SUPPRESS, NT_LAYER,
|
|
NT_ACCESS_TS, NT_ACCESS_HEAD, NT_ACCESS_FILLED, NT_WM_ANCHOR,
|
|
NT_EMB, NT_EMB_DIM
|
|
};
|
|
enum {
|
|
ET_ID = 1, ET_FROM, ET_TO, ET_RELATION, ET_METADATA,
|
|
ET_WEIGHT, ET_HEBB, ET_CONFIDENCE, ET_CREATED, ET_UPDATED, ET_LAST_FIRED,
|
|
ET_INHIBITORY, ET_LAYER
|
|
};
|
|
|
|
struct EngramPagedStore {
|
|
int fd;
|
|
char path[1024];
|
|
uint32_t format_version;
|
|
uint32_t page_size;
|
|
uint64_t page_count;
|
|
uint64_t free_list_head; /* page id of first FREE page, 0 = none */
|
|
uint64_t root_index_page; /* primary id->loc B+-tree root */
|
|
uint64_t adj_index_page; /* from/to adjacency B+-tree root */
|
|
uint64_t layer_registry_page;
|
|
uint64_t last_checkpoint_lsn; /* 0 in M1 (no checkpoints yet) */
|
|
uint64_t sb_seq;
|
|
uint8_t uuid[16];
|
|
uint64_t next_lsn;
|
|
uint64_t cur_node_page; /* last NODE page with room, 0 = none */
|
|
uint64_t cur_edge_page; /* last EDGE page with room, 0 = none */
|
|
int leaf_max; /* test hook; 0 = natural */
|
|
int int_max;
|
|
};
|
|
|
|
/* ── little-endian scalar codecs ──────────────────────────────────────────── */
|
|
static void put_u16(uint8_t* p, uint16_t v){ p[0]=(uint8_t)v; p[1]=(uint8_t)(v>>8); }
|
|
static void put_u32(uint8_t* p, uint32_t v){ for(int i=0;i<4;i++) p[i]=(uint8_t)(v>>(8*i)); }
|
|
static void put_u64(uint8_t* p, uint64_t v){ for(int i=0;i<8;i++) p[i]=(uint8_t)(v>>(8*i)); }
|
|
static uint16_t get_u16(const uint8_t* p){ return (uint16_t)(p[0] | (p[1]<<8)); }
|
|
static uint32_t get_u32(const uint8_t* p){ uint32_t v=0; for(int i=0;i<4;i++) v|=(uint32_t)p[i]<<(8*i); return v; }
|
|
static uint64_t get_u64(const uint8_t* p){ uint64_t v=0; for(int i=0;i<8;i++) v|=(uint64_t)p[i]<<(8*i); return v; }
|
|
static void put_f64(uint8_t* p, double d){ uint64_t u; memcpy(&u,&d,8); put_u64(p,u); }
|
|
static double get_f64(const uint8_t* p){ uint64_t u=get_u64(p); double d; memcpy(&d,&u,8); return d; }
|
|
|
|
/* ── crc32 (IEEE 802.3, reflected, poly 0xEDB88320); matches eg_crc32 in
|
|
* el_runtime.c so integrity checks are consistent cross-module.
|
|
* crc32("") == 0, crc32("123456789") == 0xCBF43926. ─────────────────────── */
|
|
static uint32_t s_crc_table[256];
|
|
static int s_crc_ready = 0;
|
|
static void crc_init(void){
|
|
for (uint32_t i=0;i<256;i++){
|
|
uint32_t c=i;
|
|
for (int k=0;k<8;k++) c = (c&1) ? (0xEDB88320u ^ (c>>1)) : (c>>1);
|
|
s_crc_table[i]=c;
|
|
}
|
|
s_crc_ready=1;
|
|
}
|
|
static uint32_t crc32_buf(const void* data, size_t n){
|
|
if (!s_crc_ready) crc_init();
|
|
const uint8_t* p = (const uint8_t*)data;
|
|
uint32_t c = 0xFFFFFFFFu;
|
|
for (size_t i=0;i<n;i++) c = s_crc_table[(c ^ p[i]) & 0xFF] ^ (c >> 8);
|
|
return c ^ 0xFFFFFFFFu;
|
|
}
|
|
|
|
/* FNV-1a 64-bit over a NUL-terminated id; the B+-tree key. Exact id is verified
|
|
* against the record on read, so hash collisions are correctness-safe. */
|
|
static uint64_t id_hash(const char* s){
|
|
uint64_t h = 1469598103934665603ULL;
|
|
for (; *s; ++s){ h ^= (uint8_t)*s; h *= 1099511628211ULL; }
|
|
return h;
|
|
}
|
|
|
|
/* ── raw page I/O ─────────────────────────────────────────────────────────── */
|
|
static int page_read(EngramPagedStore* s, uint64_t id, uint8_t* buf){
|
|
off_t off = (off_t)id * STORE_PAGE_SIZE;
|
|
ssize_t r = pread(s->fd, buf, STORE_PAGE_SIZE, off);
|
|
if (r != (ssize_t)STORE_PAGE_SIZE) return -1;
|
|
return 0;
|
|
}
|
|
static int page_write_raw(EngramPagedStore* s, uint64_t id, const uint8_t* buf){
|
|
off_t off = (off_t)id * STORE_PAGE_SIZE;
|
|
ssize_t w = pwrite(s->fd, buf, STORE_PAGE_SIZE, off);
|
|
if (w != (ssize_t)STORE_PAGE_SIZE) return -1;
|
|
return 0;
|
|
}
|
|
/* Stamp lsn + crc into a generic page header, then write. */
|
|
static int page_write(EngramPagedStore* s, uint64_t id, uint8_t* buf){
|
|
put_u64(buf + 0, id);
|
|
put_u64(buf + 16, ++s->next_lsn);
|
|
put_u32(buf + 24, 0);
|
|
uint32_t crc = crc32_buf(buf, STORE_PAGE_SIZE);
|
|
put_u32(buf + 24, crc);
|
|
return page_write_raw(s, id, buf);
|
|
}
|
|
static int page_crc_ok(const uint8_t* buf){
|
|
uint8_t tmp[STORE_PAGE_SIZE];
|
|
memcpy(tmp, buf, STORE_PAGE_SIZE);
|
|
uint32_t stored = get_u32(tmp + 24);
|
|
put_u32(tmp + 24, 0);
|
|
return crc32_buf(tmp, STORE_PAGE_SIZE) == stored;
|
|
}
|
|
|
|
/* Allocate a page: reuse a FREE page if available, else extend the file. */
|
|
static uint64_t page_alloc(EngramPagedStore* s, uint8_t type){
|
|
uint8_t buf[STORE_PAGE_SIZE];
|
|
uint64_t id;
|
|
if (s->free_list_head){
|
|
id = s->free_list_head;
|
|
if (page_read(s, id, buf) != 0) return 0;
|
|
s->free_list_head = get_u64(buf + OVF_NEXT_OFF); /* next_free stashed here */
|
|
} else {
|
|
id = s->page_count++;
|
|
}
|
|
memset(buf, 0, STORE_PAGE_SIZE);
|
|
buf[8] = type;
|
|
if (type == STORE_PT_NODE || type == STORE_PT_EDGE){
|
|
put_u16(buf + 10, 0);
|
|
put_u16(buf + 12, (uint16_t)(STORE_PAGE_SIZE - STORE_HDR)); /* all heap free */
|
|
}
|
|
if (page_write(s, id, buf) != 0) return 0;
|
|
return id;
|
|
}
|
|
/* Return a now-empty page to the free list. */
|
|
static int page_free(EngramPagedStore* s, uint64_t id){
|
|
uint8_t buf[STORE_PAGE_SIZE];
|
|
memset(buf, 0, STORE_PAGE_SIZE);
|
|
buf[8] = STORE_PT_FREE;
|
|
put_u64(buf + OVF_NEXT_OFF, s->free_list_head);
|
|
if (page_write(s, id, buf) != 0) return -1;
|
|
s->free_list_head = id;
|
|
return 0;
|
|
}
|
|
|
|
/* ── slotted page helpers (operate on a page already in `buf`) ────────────── */
|
|
static int slp_count(const uint8_t* buf){ return get_u16(buf + 10); }
|
|
static int slp_free(const uint8_t* buf){ return get_u16(buf + 12); }
|
|
static void slp_slot(const uint8_t* buf, int i, uint16_t* off, uint16_t* len, uint16_t* fl){
|
|
const uint8_t* p = buf + STORE_HDR + (size_t)i*SLOT_SIZE;
|
|
*off = get_u16(p); *len = get_u16(p+2); *fl = get_u16(p+4);
|
|
}
|
|
static void slp_set_slot(uint8_t* buf, int i, uint16_t off, uint16_t len, uint16_t fl){
|
|
uint8_t* p = buf + STORE_HDR + (size_t)i*SLOT_SIZE;
|
|
put_u16(p, off); put_u16(p+2, len); put_u16(p+4, fl);
|
|
}
|
|
static int slp_live_count(const uint8_t* buf){
|
|
int n = slp_count(buf), live = 0;
|
|
for (int i=0;i<n;i++){ uint16_t o,l,f; slp_slot(buf,i,&o,&l,&f); if (f==SLOT_LIVE) live++; }
|
|
return live;
|
|
}
|
|
/* Place `rec` (reclen bytes) into the page. Returns slot index, or -1 if no room.
|
|
* Prefers reusing a DEAD slot whose span fits (reclaims tombstoned space). */
|
|
static int slp_put(uint8_t* buf, const uint8_t* rec, uint16_t reclen){
|
|
int n = slp_count(buf);
|
|
for (int i=0;i<n;i++){
|
|
uint16_t o,l,f; slp_slot(buf,i,&o,&l,&f);
|
|
if (f==SLOT_DEAD && l>=reclen){
|
|
memcpy(buf + o, rec, reclen);
|
|
slp_set_slot(buf, i, o, reclen, SLOT_LIVE);
|
|
return i;
|
|
}
|
|
}
|
|
int freeb = slp_free(buf);
|
|
if ((int)reclen + (int)SLOT_SIZE > freeb) return -1;
|
|
uint16_t heap_top = (uint16_t)(STORE_HDR + n*SLOT_SIZE + freeb);
|
|
uint16_t off = (uint16_t)(heap_top - reclen);
|
|
memcpy(buf + off, rec, reclen);
|
|
slp_set_slot(buf, n, off, reclen, SLOT_LIVE);
|
|
put_u16(buf + 10, (uint16_t)(n+1));
|
|
put_u16(buf + 12, (uint16_t)(freeb - reclen - SLOT_SIZE));
|
|
return n;
|
|
}
|
|
|
|
/* ── overflow chains ──────────────────────────────────────────────────────── */
|
|
static uint64_t ovf_write_chain(EngramPagedStore* s, const uint8_t* blob, size_t len){
|
|
uint64_t head = 0, prev = 0;
|
|
size_t off = 0;
|
|
do {
|
|
uint64_t id = page_alloc(s, STORE_PT_OVERFLOW);
|
|
if (!id) return 0;
|
|
if (!head) head = id;
|
|
if (prev){
|
|
uint8_t pbuf[STORE_PAGE_SIZE];
|
|
if (page_read(s, prev, pbuf)!=0) return 0;
|
|
put_u64(pbuf + OVF_NEXT_OFF, id);
|
|
if (page_write(s, prev, pbuf)!=0) return 0;
|
|
}
|
|
uint8_t buf[STORE_PAGE_SIZE];
|
|
memset(buf, 0, STORE_PAGE_SIZE);
|
|
buf[8] = STORE_PT_OVERFLOW;
|
|
size_t chunk = len - off; if (chunk > OVF_CHUNK) chunk = OVF_CHUNK;
|
|
put_u64(buf + OVF_NEXT_OFF, 0);
|
|
put_u32(buf + OVF_LEN_OFF, (uint32_t)chunk);
|
|
memcpy(buf + OVF_DATA_OFF, blob + off, chunk);
|
|
if (page_write(s, id, buf)!=0) return 0;
|
|
prev = id; off += chunk;
|
|
} while (off < len);
|
|
return head;
|
|
}
|
|
static uint8_t* ovf_read_chain(EngramPagedStore* s, uint64_t head, size_t total){
|
|
uint8_t* out = (uint8_t*)malloc(total ? total : 1);
|
|
if (!out) return NULL;
|
|
size_t off = 0; uint64_t id = head;
|
|
while (id){
|
|
uint8_t buf[STORE_PAGE_SIZE];
|
|
if (page_read(s, id, buf)!=0){ free(out); return NULL; }
|
|
uint32_t chunk = get_u32(buf + OVF_LEN_OFF);
|
|
if (off + chunk > total){ free(out); return NULL; }
|
|
memcpy(out + off, buf + OVF_DATA_OFF, chunk);
|
|
off += chunk;
|
|
id = get_u64(buf + OVF_NEXT_OFF);
|
|
}
|
|
if (off != total){ free(out); return NULL; }
|
|
return out;
|
|
}
|
|
|
|
/* ── growable byte buffer for TLV assembly ────────────────────────────────── */
|
|
typedef struct { uint8_t* p; size_t len, cap; } Buf;
|
|
static int buf_reserve(Buf* b, size_t extra){
|
|
if (b->len + extra <= b->cap) return 0;
|
|
size_t nc = b->cap ? b->cap*2 : 256;
|
|
while (nc < b->len + extra) nc *= 2;
|
|
uint8_t* np = (uint8_t*)realloc(b->p, nc);
|
|
if (!np) return -1;
|
|
b->p = np; b->cap = nc; return 0;
|
|
}
|
|
static int buf_raw(Buf* b, const void* data, size_t n){
|
|
if (buf_reserve(b, n)) return -1;
|
|
memcpy(b->p + b->len, data, n); b->len += n; return 0;
|
|
}
|
|
static int tlv_field(Buf* b, uint8_t tag, const void* data, uint32_t len){
|
|
if (buf_reserve(b, 5 + len)) return -1;
|
|
b->p[b->len++] = tag;
|
|
put_u32(b->p + b->len, len); b->len += 4;
|
|
if (len){ memcpy(b->p + b->len, data, len); b->len += len; }
|
|
return 0;
|
|
}
|
|
static int tlv_str(Buf* b, uint8_t tag, const char* s){
|
|
if (!s) return 0; /* NULL string omitted → reader defaults NULL */
|
|
return tlv_field(b, tag, s, (uint32_t)strlen(s));
|
|
}
|
|
static int tlv_f64(Buf* b, uint8_t tag, double v){ uint8_t t[8]; put_f64(t,v); return tlv_field(b,tag,t,8); }
|
|
static int tlv_i64(Buf* b, uint8_t tag, int64_t v){ uint8_t t[8]; put_u64(t,(uint64_t)v); return tlv_field(b,tag,t,8); }
|
|
static int tlv_i32(Buf* b, uint8_t tag, int32_t v){ uint8_t t[4]; put_u32(t,(uint32_t)v); return tlv_field(b,tag,t,4); }
|
|
static int tlv_u32(Buf* b, uint8_t tag, uint32_t v){ uint8_t t[4]; put_u32(t,v); return tlv_field(b,tag,t,4); }
|
|
|
|
/* ── node/edge serialization to a TLV body ────────────────────────────────── */
|
|
static uint8_t* node_serialize(const StoreNode* n, size_t* out_len){
|
|
Buf b = {0,0,0};
|
|
tlv_str(&b, NT_ID, n->id);
|
|
tlv_str(&b, NT_CONTENT, n->content);
|
|
tlv_str(&b, NT_NODE_TYPE, n->node_type);
|
|
tlv_str(&b, NT_LABEL, n->label);
|
|
tlv_str(&b, NT_TIER, n->tier);
|
|
tlv_str(&b, NT_TAGS, n->tags);
|
|
tlv_str(&b, NT_METADATA, n->metadata);
|
|
tlv_f64(&b, NT_SALIENCE, n->salience);
|
|
tlv_f64(&b, NT_IMPORTANCE, n->importance);
|
|
tlv_f64(&b, NT_CONFIDENCE, n->confidence);
|
|
tlv_f64(&b, NT_DECAY, n->temporal_decay_rate);
|
|
tlv_i64(&b, NT_ACT_COUNT, n->activation_count);
|
|
tlv_i64(&b, NT_LAST_ACT, n->last_activated);
|
|
tlv_i64(&b, NT_CREATED, n->created_at);
|
|
tlv_i64(&b, NT_UPDATED, n->updated_at);
|
|
tlv_f64(&b, NT_BG_ACT, n->background_activation);
|
|
tlv_f64(&b, NT_WM_WEIGHT, n->working_memory_weight);
|
|
tlv_i32(&b, NT_SUPPRESS, n->suppression_count);
|
|
tlv_u32(&b, NT_LAYER, n->layer_id);
|
|
{ uint8_t at[STORE_BLL_K*8];
|
|
for (int i=0;i<STORE_BLL_K;i++) put_u64(at + i*8, (uint64_t)n->access_ts[i]);
|
|
tlv_field(&b, NT_ACCESS_TS, at, sizeof at); }
|
|
tlv_i32(&b, NT_ACCESS_HEAD, n->access_head);
|
|
tlv_i32(&b, NT_ACCESS_FILLED, n->access_filled);
|
|
tlv_f64(&b, NT_WM_ANCHOR, n->wm_anchor);
|
|
if (n->emb && n->emb_dim > 0){
|
|
uint8_t* eb = (uint8_t*)malloc((size_t)n->emb_dim*4);
|
|
for (int i=0;i<n->emb_dim;i++){ uint32_t u; memcpy(&u,&n->emb[i],4); put_u32(eb+i*4,u); }
|
|
tlv_field(&b, NT_EMB, eb, (uint32_t)n->emb_dim*4);
|
|
free(eb);
|
|
tlv_i32(&b, NT_EMB_DIM, n->emb_dim);
|
|
}
|
|
if (n->unknown && n->unknown_len) buf_raw(&b, n->unknown, n->unknown_len);
|
|
*out_len = b.len;
|
|
return b.p;
|
|
}
|
|
static uint8_t* edge_serialize(const StoreEdge* e, size_t* out_len){
|
|
Buf b = {0,0,0};
|
|
tlv_str(&b, ET_ID, e->id);
|
|
tlv_str(&b, ET_FROM, e->from_id);
|
|
tlv_str(&b, ET_TO, e->to_id);
|
|
tlv_str(&b, ET_RELATION, e->relation);
|
|
tlv_str(&b, ET_METADATA, e->metadata);
|
|
tlv_f64(&b, ET_WEIGHT, e->weight);
|
|
tlv_f64(&b, ET_HEBB, e->hebb);
|
|
tlv_f64(&b, ET_CONFIDENCE, e->confidence);
|
|
tlv_i64(&b, ET_CREATED, e->created_at);
|
|
tlv_i64(&b, ET_UPDATED, e->updated_at);
|
|
tlv_i64(&b, ET_LAST_FIRED, e->last_fired);
|
|
tlv_i32(&b, ET_INHIBITORY, e->inhibitory);
|
|
tlv_u32(&b, ET_LAYER, e->layer_id);
|
|
if (e->unknown && e->unknown_len) buf_raw(&b, e->unknown, e->unknown_len);
|
|
*out_len = b.len;
|
|
return b.p;
|
|
}
|
|
|
|
/* ── TLV parse helpers ────────────────────────────────────────────────────── */
|
|
static char* dup_str(const uint8_t* p, uint32_t len){
|
|
char* s = (char*)malloc(len + 1);
|
|
if (!s) return NULL;
|
|
memcpy(s, p, len); s[len] = 0; return s;
|
|
}
|
|
static void node_parse(const uint8_t* body, size_t len, StoreNode* n){
|
|
memset(n, 0, sizeof *n);
|
|
Buf unk = {0,0,0};
|
|
size_t i = 0;
|
|
while (i + 5 <= len){
|
|
uint8_t tag = body[i];
|
|
uint32_t flen = get_u32(body + i + 1);
|
|
if (i + 5 + flen > len) break;
|
|
const uint8_t* v = body + i + 5;
|
|
switch (tag){
|
|
case NT_ID: n->id = dup_str(v, flen); break;
|
|
case NT_CONTENT: n->content = dup_str(v, flen); break;
|
|
case NT_NODE_TYPE: n->node_type = dup_str(v, flen); break;
|
|
case NT_LABEL: n->label = dup_str(v, flen); break;
|
|
case NT_TIER: n->tier = dup_str(v, flen); break;
|
|
case NT_TAGS: n->tags = dup_str(v, flen); break;
|
|
case NT_METADATA: n->metadata = dup_str(v, flen); break;
|
|
case NT_SALIENCE: n->salience = get_f64(v); break;
|
|
case NT_IMPORTANCE: n->importance = get_f64(v); break;
|
|
case NT_CONFIDENCE: n->confidence = get_f64(v); break;
|
|
case NT_DECAY: n->temporal_decay_rate = get_f64(v); break;
|
|
case NT_ACT_COUNT: n->activation_count = (int64_t)get_u64(v); break;
|
|
case NT_LAST_ACT: n->last_activated = (int64_t)get_u64(v); break;
|
|
case NT_CREATED: n->created_at = (int64_t)get_u64(v); break;
|
|
case NT_UPDATED: n->updated_at = (int64_t)get_u64(v); break;
|
|
case NT_BG_ACT: n->background_activation = get_f64(v); break;
|
|
case NT_WM_WEIGHT: n->working_memory_weight = get_f64(v); break;
|
|
case NT_SUPPRESS: n->suppression_count = (int32_t)get_u32(v); break;
|
|
case NT_LAYER: n->layer_id = get_u32(v); break;
|
|
case NT_ACCESS_TS:
|
|
for (int k=0;k<STORE_BLL_K && (uint32_t)(k*8+8)<=flen;k++)
|
|
n->access_ts[k] = (int64_t)get_u64(v + k*8);
|
|
break;
|
|
case NT_ACCESS_HEAD: n->access_head = (int32_t)get_u32(v); break;
|
|
case NT_ACCESS_FILLED: n->access_filled = (int32_t)get_u32(v); break;
|
|
case NT_WM_ANCHOR: n->wm_anchor = get_f64(v); break;
|
|
case NT_EMB: {
|
|
int32_t dim = (int32_t)(flen/4);
|
|
n->emb = (float*)malloc(flen ? flen : 4);
|
|
for (int32_t k=0;k<dim;k++){ uint32_t u=get_u32(v+k*4); memcpy(&n->emb[k],&u,4); }
|
|
if (n->emb_dim == 0) n->emb_dim = dim; /* honour explicit dim if seen */
|
|
break; }
|
|
case NT_EMB_DIM: n->emb_dim = (int32_t)get_u32(v); break;
|
|
default:
|
|
/* unknown tag: preserve verbatim */
|
|
buf_raw(&unk, body + i, 5 + flen);
|
|
break;
|
|
}
|
|
i += 5 + flen;
|
|
}
|
|
n->unknown = unk.p; n->unknown_len = unk.len;
|
|
}
|
|
static void edge_parse(const uint8_t* body, size_t len, StoreEdge* e){
|
|
memset(e, 0, sizeof *e);
|
|
Buf unk = {0,0,0};
|
|
size_t i = 0;
|
|
while (i + 5 <= len){
|
|
uint8_t tag = body[i];
|
|
uint32_t flen = get_u32(body + i + 1);
|
|
if (i + 5 + flen > len) break;
|
|
const uint8_t* v = body + i + 5;
|
|
switch (tag){
|
|
case ET_ID: e->id = dup_str(v, flen); break;
|
|
case ET_FROM: e->from_id = dup_str(v, flen); break;
|
|
case ET_TO: e->to_id = dup_str(v, flen); break;
|
|
case ET_RELATION: e->relation = dup_str(v, flen); break;
|
|
case ET_METADATA: e->metadata = dup_str(v, flen); break;
|
|
case ET_WEIGHT: e->weight = get_f64(v); break;
|
|
case ET_HEBB: e->hebb = get_f64(v); break;
|
|
case ET_CONFIDENCE: e->confidence = get_f64(v); break;
|
|
case ET_CREATED: e->created_at = (int64_t)get_u64(v); break;
|
|
case ET_UPDATED: e->updated_at = (int64_t)get_u64(v); break;
|
|
case ET_LAST_FIRED: e->last_fired = (int64_t)get_u64(v); break;
|
|
case ET_INHIBITORY: e->inhibitory = (int32_t)get_u32(v); break;
|
|
case ET_LAYER: e->layer_id = get_u32(v); break;
|
|
default:
|
|
buf_raw(&unk, body + i, 5 + flen);
|
|
break;
|
|
}
|
|
i += 5 + flen;
|
|
}
|
|
e->unknown = unk.p; e->unknown_len = unk.len;
|
|
}
|
|
|
|
/* ── B+-tree ──────────────────────────────────────────────────────────────── */
|
|
static uint32_t tree_payload(int tree){ return tree==TREE_PRIMARY ? PRIMARY_PAYLOAD : ADJ_PAYLOAD; }
|
|
static uint64_t tree_root(EngramPagedStore* s, int tree){
|
|
return tree==TREE_PRIMARY ? s->root_index_page : s->adj_index_page;
|
|
}
|
|
static void tree_set_root(EngramPagedStore* s, int tree, uint64_t id){
|
|
if (tree==TREE_PRIMARY) s->root_index_page = id; else s->adj_index_page = id;
|
|
}
|
|
static int leaf_max_entries(EngramPagedStore* s, uint32_t payload){
|
|
int nat = (int)(IDX_BODY / (8 + payload));
|
|
if (s->leaf_max > 0 && s->leaf_max < nat) return s->leaf_max;
|
|
return nat;
|
|
}
|
|
static int int_max_keys(EngramPagedStore* s){
|
|
/* keys*8 + (keys+1)*8 <= IDX_BODY → keys <= IDX_BODY/8 - 1 */
|
|
int nat = (int)(IDX_BODY / 8) - 1;
|
|
if (s->int_max > 0 && s->int_max < nat) return s->int_max;
|
|
return nat;
|
|
}
|
|
|
|
/* Recursive insert. Returns 0 (no split) or 1 (split; sep_key and new_page set). */
|
|
static int btree_insert(EngramPagedStore* s, int tree, uint64_t page_id,
|
|
uint64_t key, const uint8_t* payload,
|
|
uint64_t* sep_key, uint64_t* new_page){
|
|
uint32_t pl = tree_payload(tree);
|
|
uint32_t esz = 8 + pl;
|
|
uint8_t buf[STORE_PAGE_SIZE];
|
|
if (page_read(s, page_id, buf)!=0) return -1;
|
|
int nkeys = get_u16(buf + 10);
|
|
int is_leaf = buf[IDX_LEAF_OFF];
|
|
|
|
if (is_leaf){
|
|
/* find insert position (after equal keys → stable duplicates) */
|
|
int pos = 0;
|
|
while (pos < nkeys){
|
|
uint64_t k = get_u64(buf + IDX_ENT_OFF + (size_t)pos*esz);
|
|
if (k > key) break;
|
|
pos++;
|
|
}
|
|
int lmax = leaf_max_entries(s, pl);
|
|
if (nkeys + 1 <= lmax){
|
|
uint8_t* base = buf + IDX_ENT_OFF;
|
|
memmove(base + (size_t)(pos+1)*esz, base + (size_t)pos*esz,
|
|
(size_t)(nkeys - pos)*esz);
|
|
put_u64(base + (size_t)pos*esz, key);
|
|
memcpy(base + (size_t)pos*esz + 8, payload, pl);
|
|
put_u16(buf + 10, (uint16_t)(nkeys+1));
|
|
return page_write(s, page_id, buf)==0 ? 0 : -1;
|
|
}
|
|
/* split: build the full sorted list in a temp, halve it */
|
|
int total = nkeys + 1;
|
|
uint8_t* all = (uint8_t*)malloc((size_t)total*esz);
|
|
if (!all) return -1;
|
|
memcpy(all, buf + IDX_ENT_OFF, (size_t)pos*esz);
|
|
put_u64(all + (size_t)pos*esz, key);
|
|
memcpy(all + (size_t)pos*esz + 8, payload, pl);
|
|
memcpy(all + (size_t)(pos+1)*esz, buf + IDX_ENT_OFF + (size_t)pos*esz,
|
|
(size_t)(nkeys - pos)*esz);
|
|
int left = total/2, right = total - left;
|
|
uint64_t old_next = get_u64(buf + IDX_NEXT_OFF);
|
|
uint64_t rid = page_alloc(s, STORE_PT_INDEX);
|
|
if (!rid){ free(all); return -1; }
|
|
uint8_t rbuf[STORE_PAGE_SIZE];
|
|
if (page_read(s, rid, rbuf)!=0){ free(all); return -1; }
|
|
rbuf[IDX_LEAF_OFF] = 1;
|
|
put_u16(rbuf + 10, (uint16_t)right);
|
|
put_u64(rbuf + IDX_NEXT_OFF, old_next);
|
|
memcpy(rbuf + IDX_ENT_OFF, all + (size_t)left*esz, (size_t)right*esz);
|
|
/* left page keeps first `left` entries, points to right */
|
|
buf[IDX_LEAF_OFF] = 1;
|
|
put_u16(buf + 10, (uint16_t)left);
|
|
put_u64(buf + IDX_NEXT_OFF, rid);
|
|
memcpy(buf + IDX_ENT_OFF, all, (size_t)left*esz);
|
|
*sep_key = get_u64(all + (size_t)left*esz);
|
|
free(all);
|
|
if (page_write(s, rid, rbuf)!=0) return -1;
|
|
if (page_write(s, page_id, buf)!=0) return -1;
|
|
*new_page = rid;
|
|
return 1;
|
|
}
|
|
|
|
/* internal: keys at IDX_ENT_OFF (nkeys u64), children after (nkeys+1 u64) */
|
|
uint8_t* K = buf + IDX_ENT_OFF;
|
|
uint8_t* C = buf + IDX_ENT_OFF + (size_t)nkeys*8;
|
|
int ci = 0;
|
|
while (ci < nkeys && key >= get_u64(K + (size_t)ci*8)) ci++;
|
|
uint64_t child = get_u64(C + (size_t)ci*8);
|
|
uint64_t csep, cnew;
|
|
int sp = btree_insert(s, tree, child, key, payload, &csep, &cnew);
|
|
if (sp < 0) return -1;
|
|
if (sp == 0) return 0;
|
|
|
|
/* child split → insert (csep, cnew) at position ci / ci+1 */
|
|
int nk = nkeys + 1;
|
|
uint64_t* keys = (uint64_t*)malloc((size_t)nk*8);
|
|
uint64_t* kids = (uint64_t*)malloc((size_t)(nk+1)*8);
|
|
if (!keys || !kids){ free(keys); free(kids); return -1; }
|
|
for (int i=0;i<nkeys;i++) keys[i] = get_u64(K + (size_t)i*8);
|
|
for (int i=0;i<=nkeys;i++) kids[i] = get_u64(C + (size_t)i*8);
|
|
for (int i=nkeys;i>ci;i--) keys[i] = keys[i-1];
|
|
keys[ci] = csep;
|
|
for (int i=nkeys+1;i>ci+1;i--) kids[i] = kids[i-1];
|
|
kids[ci+1] = cnew;
|
|
|
|
int imax = int_max_keys(s);
|
|
if (nk <= imax){
|
|
put_u16(buf + 10, (uint16_t)nk);
|
|
uint8_t* K2 = buf + IDX_ENT_OFF;
|
|
uint8_t* C2 = buf + IDX_ENT_OFF + (size_t)nk*8;
|
|
for (int i=0;i<nk;i++) put_u64(K2 + (size_t)i*8, keys[i]);
|
|
for (int i=0;i<=nk;i++) put_u64(C2 + (size_t)i*8, kids[i]);
|
|
free(keys); free(kids);
|
|
return page_write(s, page_id, buf)==0 ? 0 : -1;
|
|
}
|
|
/* split internal: middle key moves up */
|
|
int mid = nk/2;
|
|
uint64_t up = keys[mid];
|
|
int lk = mid, rk = nk - mid - 1;
|
|
uint64_t rid = page_alloc(s, STORE_PT_INDEX);
|
|
if (!rid){ free(keys); free(kids); return -1; }
|
|
uint8_t rbuf[STORE_PAGE_SIZE];
|
|
if (page_read(s, rid, rbuf)!=0){ free(keys); free(kids); return -1; }
|
|
rbuf[IDX_LEAF_OFF] = 0;
|
|
put_u16(rbuf + 10, (uint16_t)rk);
|
|
{ uint8_t* RK = rbuf + IDX_ENT_OFF; uint8_t* RC = rbuf + IDX_ENT_OFF + (size_t)rk*8;
|
|
for (int i=0;i<rk;i++) put_u64(RK + (size_t)i*8, keys[mid+1+i]);
|
|
for (int i=0;i<=rk;i++) put_u64(RC + (size_t)i*8, kids[mid+1+i]); }
|
|
put_u16(buf + 10, (uint16_t)lk);
|
|
{ uint8_t* LK = buf + IDX_ENT_OFF; uint8_t* LC = buf + IDX_ENT_OFF + (size_t)lk*8;
|
|
for (int i=0;i<lk;i++) put_u64(LK + (size_t)i*8, keys[i]);
|
|
for (int i=0;i<=lk;i++) put_u64(LC + (size_t)i*8, kids[i]); }
|
|
free(keys); free(kids);
|
|
if (page_write(s, rid, rbuf)!=0) return -1;
|
|
if (page_write(s, page_id, buf)!=0) return -1;
|
|
*sep_key = up; *new_page = rid;
|
|
return 1;
|
|
}
|
|
|
|
static int btree_put(EngramPagedStore* s, int tree, uint64_t key, const uint8_t* payload){
|
|
uint64_t root = tree_root(s, tree);
|
|
uint64_t sep, np;
|
|
int sp = btree_insert(s, tree, root, key, payload, &sep, &np);
|
|
if (sp < 0) return -1;
|
|
if (sp == 0) return 0;
|
|
/* root split → new internal root */
|
|
uint64_t nr = page_alloc(s, STORE_PT_INDEX);
|
|
if (!nr) return -1;
|
|
uint8_t buf[STORE_PAGE_SIZE];
|
|
if (page_read(s, nr, buf)!=0) return -1;
|
|
buf[IDX_LEAF_OFF] = 0;
|
|
put_u16(buf + 10, 1);
|
|
put_u64(buf + IDX_ENT_OFF, sep);
|
|
put_u64(buf + IDX_ENT_OFF + 8, root);
|
|
put_u64(buf + IDX_ENT_OFF + 16, np);
|
|
if (page_write(s, nr, buf)!=0) return -1;
|
|
tree_set_root(s, tree, nr);
|
|
return 0;
|
|
}
|
|
|
|
/* Collect every payload whose key == target. Payloads appended to *out (esz-pl
|
|
* bytes each, i.e. just the payload, pl bytes). Caller frees *out. */
|
|
static int btree_lookup(EngramPagedStore* s, int tree, uint64_t target,
|
|
uint8_t** out, size_t* count){
|
|
uint32_t pl = tree_payload(tree);
|
|
uint32_t esz = 8 + pl;
|
|
*out = NULL; *count = 0;
|
|
uint64_t id = tree_root(s, tree);
|
|
uint8_t buf[STORE_PAGE_SIZE];
|
|
/* descend to the leftmost leaf that could hold target */
|
|
for (;;){
|
|
if (page_read(s, id, buf)!=0) return -1;
|
|
if (buf[IDX_LEAF_OFF]) break;
|
|
int nkeys = get_u16(buf + 10);
|
|
uint8_t* K = buf + IDX_ENT_OFF;
|
|
uint8_t* C = buf + IDX_ENT_OFF + (size_t)nkeys*8;
|
|
/* Descend to the LEFTMOST child that may hold `target`. Duplicate keys
|
|
* span a contiguous run of leaves (a leaf split can leave copies of the
|
|
* separator value on both sides), so we must use strict '>' here — not
|
|
* the '>=' of insert routing — then scan forward across next_leaf. */
|
|
int ci = 0;
|
|
while (ci < nkeys && target > get_u64(K + (size_t)ci*8)) ci++;
|
|
id = get_u64(C + (size_t)ci*8);
|
|
}
|
|
Buf acc = {0,0,0};
|
|
for (;;){
|
|
int nkeys = get_u16(buf + 10);
|
|
int past = 0;
|
|
for (int i=0;i<nkeys;i++){
|
|
uint64_t k = get_u64(buf + IDX_ENT_OFF + (size_t)i*esz);
|
|
if (k < target) continue;
|
|
if (k > target){ past = 1; break; }
|
|
if (buf_raw(&acc, buf + IDX_ENT_OFF + (size_t)i*esz + 8, pl)){ free(acc.p); return -1; }
|
|
}
|
|
if (past) break;
|
|
uint64_t nxt = get_u64(buf + IDX_NEXT_OFF);
|
|
if (!nxt) break;
|
|
if (page_read(s, nxt, buf)!=0){ free(acc.p); return -1; }
|
|
}
|
|
*out = acc.p; *count = acc.len / pl;
|
|
return 0;
|
|
}
|
|
|
|
/* ── superblock ───────────────────────────────────────────────────────────── */
|
|
static int sb_write_one(EngramPagedStore* s, uint64_t page){
|
|
uint8_t buf[STORE_PAGE_SIZE];
|
|
memset(buf, 0, STORE_PAGE_SIZE);
|
|
memcpy(buf + SB_MAGIC_OFF, STORE_MAGIC, 8); /* 7 chars + NUL */
|
|
put_u32(buf + SB_FMT_OFF, s->format_version);
|
|
put_u32(buf + SB_PGSZ_OFF, s->page_size);
|
|
put_u64(buf + SB_PGCNT_OFF, s->page_count);
|
|
put_u64(buf + SB_FREE_OFF, s->free_list_head);
|
|
put_u64(buf + SB_ROOT_OFF, s->root_index_page);
|
|
put_u64(buf + SB_ADJ_OFF, s->adj_index_page);
|
|
put_u64(buf + SB_LAYER_OFF, s->layer_registry_page);
|
|
put_u64(buf + SB_CKPT_OFF, s->last_checkpoint_lsn);
|
|
put_u64(buf + SB_SEQ_OFF, s->sb_seq);
|
|
memcpy(buf + SB_UUID_OFF, s->uuid, 16);
|
|
put_u32(buf + SB_CRC_OFF, 0);
|
|
uint32_t crc = crc32_buf(buf, STORE_PAGE_SIZE);
|
|
put_u32(buf + SB_CRC_OFF, crc);
|
|
return page_write_raw(s, page, buf);
|
|
}
|
|
static int sb_load_one(EngramPagedStore* s, uint64_t page, uint8_t* buf, uint64_t* seq_out){
|
|
if (page_read(s, page, buf)!=0) return -1;
|
|
if (memcmp(buf + SB_MAGIC_OFF, STORE_MAGIC, 8)!=0) return -1;
|
|
uint32_t stored = get_u32(buf + SB_CRC_OFF);
|
|
uint8_t tmp[STORE_PAGE_SIZE];
|
|
memcpy(tmp, buf, STORE_PAGE_SIZE);
|
|
put_u32(tmp + SB_CRC_OFF, 0);
|
|
if (crc32_buf(tmp, STORE_PAGE_SIZE) != stored) return -1;
|
|
*seq_out = get_u64(buf + SB_SEQ_OFF);
|
|
return 0;
|
|
}
|
|
static void sb_apply(EngramPagedStore* s, const uint8_t* buf){
|
|
s->format_version = get_u32(buf + SB_FMT_OFF);
|
|
s->page_size = get_u32(buf + SB_PGSZ_OFF);
|
|
s->page_count = get_u64(buf + SB_PGCNT_OFF);
|
|
s->free_list_head = get_u64(buf + SB_FREE_OFF);
|
|
s->root_index_page = get_u64(buf + SB_ROOT_OFF);
|
|
s->adj_index_page = get_u64(buf + SB_ADJ_OFF);
|
|
s->layer_registry_page= get_u64(buf + SB_LAYER_OFF);
|
|
s->last_checkpoint_lsn= get_u64(buf + SB_CKPT_OFF);
|
|
s->sb_seq = get_u64(buf + SB_SEQ_OFF);
|
|
memcpy(s->uuid, buf + SB_UUID_OFF, 16);
|
|
}
|
|
|
|
int store_sync(EngramPagedStore* s){
|
|
if (!s) return -1;
|
|
s->sb_seq++;
|
|
/* write primary, fsync, then mirror, fsync — so a torn write of one leaves
|
|
* the other valid; recovery prefers the higher valid sb_seq. */
|
|
if (sb_write_one(s, 0)!=0) return -1;
|
|
if (fsync(s->fd)!=0) return -1;
|
|
if (sb_write_one(s, 1)!=0) return -1;
|
|
if (fsync(s->fd)!=0) return -1;
|
|
return 0;
|
|
}
|
|
|
|
/* ── lifecycle ────────────────────────────────────────────────────────────── */
|
|
static void gen_uuid(uint8_t out[16]){
|
|
int fd = open("/dev/urandom", O_RDONLY);
|
|
if (fd >= 0){ ssize_t r = read(fd, out, 16); close(fd); if (r == 16) return; }
|
|
uint64_t t = (uint64_t)time(NULL) ^ ((uint64_t)getpid() << 32);
|
|
for (int i=0;i<16;i++){ t = t*6364136223846793005ULL + 1442695040888963407ULL; out[i] = (uint8_t)(t>>56); }
|
|
}
|
|
static uint64_t idx_alloc_leaf(EngramPagedStore* s){
|
|
uint64_t id = page_alloc(s, STORE_PT_INDEX);
|
|
if (!id) return 0;
|
|
uint8_t buf[STORE_PAGE_SIZE];
|
|
if (page_read(s, id, buf)!=0) return 0;
|
|
buf[IDX_LEAF_OFF] = 1;
|
|
put_u16(buf + 10, 0);
|
|
put_u64(buf + IDX_NEXT_OFF, 0);
|
|
if (page_write(s, id, buf)!=0) return 0;
|
|
return id;
|
|
}
|
|
|
|
EngramPagedStore* store_create(const char* path){
|
|
if (!s_crc_ready) crc_init();
|
|
struct stat st;
|
|
if (stat(path, &st) == 0){ errno = EEXIST; return NULL; }
|
|
EngramPagedStore* s = (EngramPagedStore*)calloc(1, sizeof *s);
|
|
if (!s) return NULL;
|
|
s->fd = open(path, O_RDWR | O_CREAT | O_EXCL, 0600);
|
|
if (s->fd < 0){ free(s); return NULL; }
|
|
snprintf(s->path, sizeof s->path, "%s", path);
|
|
s->format_version = STORE_FORMAT_VERSION;
|
|
s->page_size = STORE_PAGE_SIZE;
|
|
s->page_count = 2; /* pages 0,1 are the superblock + mirror */
|
|
s->free_list_head = 0;
|
|
s->next_lsn = 0;
|
|
s->sb_seq = 0;
|
|
gen_uuid(s->uuid);
|
|
/* reserve pages 0,1 on disk */
|
|
uint8_t zero[STORE_PAGE_SIZE]; memset(zero, 0, sizeof zero);
|
|
if (page_write_raw(s, 0, zero)!=0 || page_write_raw(s, 1, zero)!=0){ close(s->fd); free(s); return NULL; }
|
|
s->root_index_page = idx_alloc_leaf(s);
|
|
s->adj_index_page = idx_alloc_leaf(s);
|
|
s->layer_registry_page = page_alloc(s, STORE_PT_NODE); /* reserved; unused in M1 */
|
|
if (!s->root_index_page || !s->adj_index_page || !s->layer_registry_page){
|
|
close(s->fd); free(s); return NULL;
|
|
}
|
|
if (store_sync(s)!=0){ close(s->fd); free(s); return NULL; }
|
|
return s;
|
|
}
|
|
|
|
EngramPagedStore* store_open(const char* path){
|
|
if (!s_crc_ready) crc_init();
|
|
EngramPagedStore* s = (EngramPagedStore*)calloc(1, sizeof *s);
|
|
if (!s) return NULL;
|
|
s->fd = open(path, O_RDWR);
|
|
if (s->fd < 0){ free(s); return NULL; }
|
|
snprintf(s->path, sizeof s->path, "%s", path);
|
|
uint8_t b0[STORE_PAGE_SIZE], b1[STORE_PAGE_SIZE];
|
|
uint64_t s0 = 0, s1 = 0;
|
|
int ok0 = sb_load_one(s, 0, b0, &s0) == 0;
|
|
int ok1 = sb_load_one(s, 1, b1, &s1) == 0;
|
|
if (!ok0 && !ok1){ close(s->fd); free(s); return NULL; }
|
|
const uint8_t* pick;
|
|
if (ok0 && ok1) pick = (s0 >= s1) ? b0 : b1;
|
|
else pick = ok0 ? b0 : b1;
|
|
sb_apply(s, pick);
|
|
if (s->page_size != STORE_PAGE_SIZE){ close(s->fd); free(s); return NULL; }
|
|
s->next_lsn = s->sb_seq; /* resume LSNs above the last synced value */
|
|
s->cur_node_page = 0;
|
|
s->cur_edge_page = 0;
|
|
return s;
|
|
}
|
|
|
|
int store_close(EngramPagedStore* s){
|
|
if (!s) return -1;
|
|
int rc = store_sync(s);
|
|
if (s->fd >= 0) close(s->fd);
|
|
free(s);
|
|
return rc;
|
|
}
|
|
|
|
/* ── put node/edge ────────────────────────────────────────────────────────── */
|
|
/* Build the on-page record (inline or overflow stub) from a TLV body. */
|
|
static int build_record(EngramPagedStore* s, const uint8_t* body, size_t blen,
|
|
uint8_t** rec_out, uint16_t* reclen_out){
|
|
if (blen <= MAX_INLINE_BODY){
|
|
uint16_t reclen = (uint16_t)(REC_HDR + blen);
|
|
uint8_t* rec = (uint8_t*)malloc(reclen);
|
|
if (!rec) return -1;
|
|
put_u16(rec, reclen); rec[2] = REC_VER; rec[3] = 0;
|
|
memcpy(rec + REC_HDR, body, blen);
|
|
*rec_out = rec; *reclen_out = reclen;
|
|
return 0;
|
|
}
|
|
uint64_t head = ovf_write_chain(s, body, blen);
|
|
if (!head) return -1;
|
|
uint16_t reclen = REC_HDR + 16;
|
|
uint8_t* rec = (uint8_t*)malloc(reclen);
|
|
if (!rec) return -1;
|
|
put_u16(rec, reclen); rec[2] = REC_VER; rec[3] = REC_OVERFLOW;
|
|
put_u64(rec + REC_HDR, head);
|
|
put_u64(rec + REC_HDR + 8, (uint64_t)blen);
|
|
*rec_out = rec; *reclen_out = reclen;
|
|
return 0;
|
|
}
|
|
/* Place a record into a NODE or EDGE page; returns (page_id,slot). Uses/creates
|
|
* the current page of that type; on-page dead-slot reuse handled by slp_put. */
|
|
static int place_record(EngramPagedStore* s, uint8_t ptype, const uint8_t* rec,
|
|
uint16_t reclen, uint64_t* page_id_out, uint16_t* slot_out){
|
|
uint64_t* cur = (ptype==STORE_PT_NODE) ? &s->cur_node_page : &s->cur_edge_page;
|
|
uint8_t buf[STORE_PAGE_SIZE];
|
|
if (*cur){
|
|
if (page_read(s, *cur, buf)==0){
|
|
int slot = slp_put(buf, rec, reclen);
|
|
if (slot >= 0){
|
|
if (page_write(s, *cur, buf)!=0) return -1;
|
|
*page_id_out = *cur; *slot_out = (uint16_t)slot; return 0;
|
|
}
|
|
}
|
|
}
|
|
uint64_t pid = page_alloc(s, ptype);
|
|
if (!pid) return -1;
|
|
if (page_read(s, pid, buf)!=0) return -1;
|
|
int slot = slp_put(buf, rec, reclen);
|
|
if (slot < 0) return -1; /* record too big for an empty page */
|
|
if (page_write(s, pid, buf)!=0) return -1;
|
|
*cur = pid;
|
|
*page_id_out = pid; *slot_out = (uint16_t)slot;
|
|
return 0;
|
|
}
|
|
|
|
int store_put_node(EngramPagedStore* s, const StoreNode* n){
|
|
if (!s || !n || !n->id) return -1;
|
|
size_t blen; uint8_t* body = node_serialize(n, &blen);
|
|
if (!body) return -1;
|
|
uint8_t* rec; uint16_t reclen;
|
|
if (build_record(s, body, blen, &rec, &reclen)){ free(body); return -1; }
|
|
free(body);
|
|
uint64_t pid; uint16_t slot;
|
|
if (place_record(s, STORE_PT_NODE, rec, reclen, &pid, &slot)){ free(rec); return -1; }
|
|
free(rec);
|
|
uint8_t payload[PRIMARY_PAYLOAD];
|
|
put_u64(payload, pid); put_u16(payload + 8, slot);
|
|
return btree_put(s, TREE_PRIMARY, id_hash(n->id), payload);
|
|
}
|
|
|
|
int store_put_edge(EngramPagedStore* s, const StoreEdge* e){
|
|
if (!s || !e || !e->id || !e->from_id || !e->to_id) return -1;
|
|
size_t blen; uint8_t* body = edge_serialize(e, &blen);
|
|
if (!body) return -1;
|
|
uint8_t* rec; uint16_t reclen;
|
|
if (build_record(s, body, blen, &rec, &reclen)){ free(body); return -1; }
|
|
free(body);
|
|
uint64_t pid; uint16_t slot;
|
|
if (place_record(s, STORE_PT_EDGE, rec, reclen, &pid, &slot)){ free(rec); return -1; }
|
|
free(rec);
|
|
/* primary index: edge id → loc */
|
|
uint8_t pp[PRIMARY_PAYLOAD];
|
|
put_u64(pp, pid); put_u16(pp + 8, slot);
|
|
if (btree_put(s, TREE_PRIMARY, id_hash(e->id), pp)!=0) return -1;
|
|
/* adjacency: from_id and to_id → edge loc */
|
|
uint8_t af[ADJ_PAYLOAD]; af[0]=ADJ_DIR_FROM; put_u64(af+1,pid); put_u16(af+9,slot);
|
|
if (btree_put(s, TREE_ADJ, id_hash(e->from_id), af)!=0) return -1;
|
|
uint8_t at[ADJ_PAYLOAD]; at[0]=ADJ_DIR_TO; put_u64(at+1,pid); put_u16(at+9,slot);
|
|
if (btree_put(s, TREE_ADJ, id_hash(e->to_id), at)!=0) return -1;
|
|
return 0;
|
|
}
|
|
|
|
/* ── read a record body (resolving overflow) at (page,slot). Returns malloc'd
|
|
* body + length, and whether the slot was live. ─────────────────────────── */
|
|
static int read_body(EngramPagedStore* s, uint64_t page, uint16_t slot,
|
|
uint8_t** body_out, size_t* blen_out, int* live_out){
|
|
uint8_t buf[STORE_PAGE_SIZE];
|
|
if (page_read(s, page, buf)!=0) return -1;
|
|
if (slot >= slp_count(buf)) return -1;
|
|
uint16_t off,len,fl; slp_slot(buf, slot, &off, &len, &fl);
|
|
*live_out = (fl == SLOT_LIVE);
|
|
if (len < REC_HDR) return -1;
|
|
uint8_t rec_flags = buf[off + 3];
|
|
if (rec_flags & REC_OVERFLOW){
|
|
uint64_t head = get_u64(buf + off + REC_HDR);
|
|
uint64_t total = get_u64(buf + off + REC_HDR + 8);
|
|
uint8_t* body = ovf_read_chain(s, head, (size_t)total);
|
|
if (!body) return -1;
|
|
*body_out = body; *blen_out = (size_t)total;
|
|
} else {
|
|
uint16_t reclen = get_u16(buf + off);
|
|
size_t blen = reclen - REC_HDR;
|
|
uint8_t* body = (uint8_t*)malloc(blen ? blen : 1);
|
|
if (!body) return -1;
|
|
memcpy(body, buf + off + REC_HDR, blen);
|
|
*body_out = body; *blen_out = blen;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
int store_get_node(EngramPagedStore* s, const char* id, StoreNode* out){
|
|
if (!s || !id || !out) return -1;
|
|
uint8_t* locs; size_t n;
|
|
if (btree_lookup(s, TREE_PRIMARY, id_hash(id), &locs, &n)!=0) return -1;
|
|
int found = 0;
|
|
for (size_t i=0;i<n;i++){
|
|
uint64_t page = get_u64(locs + i*PRIMARY_PAYLOAD);
|
|
uint16_t slot = get_u16(locs + i*PRIMARY_PAYLOAD + 8);
|
|
uint8_t* body; size_t blen; int live;
|
|
if (read_body(s, page, slot, &body, &blen, &live)!=0) continue;
|
|
StoreNode cand; node_parse(body, blen, &cand); free(body);
|
|
if (cand.id && strcmp(cand.id, id)==0){
|
|
if (live){ /* latest live wins (later entries) */
|
|
if (found) store_node_free(out);
|
|
*out = cand; out->tombstoned = 0; found = 1;
|
|
} else store_node_free(&cand);
|
|
} else store_node_free(&cand);
|
|
}
|
|
free(locs);
|
|
return found ? 1 : 0;
|
|
}
|
|
|
|
int store_tombstone(EngramPagedStore* s, const char* id){
|
|
if (!s || !id) return -1;
|
|
uint8_t* locs; size_t n;
|
|
if (btree_lookup(s, TREE_PRIMARY, id_hash(id), &locs, &n)!=0) return -1;
|
|
int hit = 0;
|
|
for (size_t i=0;i<n;i++){
|
|
uint64_t page = get_u64(locs + i*PRIMARY_PAYLOAD);
|
|
uint16_t slot = get_u16(locs + i*PRIMARY_PAYLOAD + 8);
|
|
uint8_t buf[STORE_PAGE_SIZE];
|
|
if (page_read(s, page, buf)!=0) continue;
|
|
if (slot >= slp_count(buf)) continue;
|
|
uint16_t off,len,fl; slp_slot(buf, slot, &off, &len, &fl);
|
|
if (fl != SLOT_LIVE) continue;
|
|
/* verify id matches this record before killing it */
|
|
uint8_t* body; size_t blen; int live;
|
|
if (read_body(s, page, slot, &body, &blen, &live)!=0) continue;
|
|
StoreNode cand; node_parse(body, blen, &cand); free(body);
|
|
int match = cand.id && strcmp(cand.id, id)==0;
|
|
store_node_free(&cand);
|
|
if (!match) continue;
|
|
slp_set_slot(buf, slot, off, len, SLOT_DEAD);
|
|
if (page_write(s, page, buf)!=0){ free(locs); return -1; }
|
|
hit = 1;
|
|
/* if the page is now empty, reclaim it to the free list */
|
|
if (page_read(s, page, buf)==0 && slp_live_count(buf)==0){
|
|
if (s->cur_node_page == page) s->cur_node_page = 0;
|
|
if (s->cur_edge_page == page) s->cur_edge_page = 0;
|
|
page_free(s, page);
|
|
}
|
|
}
|
|
free(locs);
|
|
return hit ? 0 : 0; /* absent id is a no-op success */
|
|
}
|
|
|
|
static int get_edges_dir(EngramPagedStore* s, const char* id, uint8_t want_dir,
|
|
StoreEdge** out, size_t* n){
|
|
*out = NULL; *n = 0;
|
|
uint8_t* locs; size_t cnt;
|
|
if (btree_lookup(s, TREE_ADJ, id_hash(id), &locs, &cnt)!=0) return -1;
|
|
StoreEdge* arr = NULL; size_t used = 0, cap = 0;
|
|
for (size_t i=0;i<cnt;i++){
|
|
uint8_t dir = locs[i*ADJ_PAYLOAD];
|
|
if (dir != want_dir) continue;
|
|
uint64_t page = get_u64(locs + i*ADJ_PAYLOAD + 1);
|
|
uint16_t slot = get_u16(locs + i*ADJ_PAYLOAD + 9);
|
|
uint8_t* body; size_t blen; int live;
|
|
if (read_body(s, page, slot, &body, &blen, &live)!=0) continue;
|
|
if (!live){ free(body); continue; }
|
|
StoreEdge e; edge_parse(body, blen, &e); free(body);
|
|
const char* side = (want_dir==ADJ_DIR_FROM) ? e.from_id : e.to_id;
|
|
if (!side || strcmp(side, id)!=0){ store_edge_free(&e); continue; }
|
|
if (used == cap){
|
|
cap = cap ? cap*2 : 8;
|
|
StoreEdge* na = (StoreEdge*)realloc(arr, cap*sizeof *na);
|
|
if (!na){ store_edge_free(&e); break; }
|
|
arr = na;
|
|
}
|
|
arr[used++] = e;
|
|
}
|
|
free(locs);
|
|
*out = arr; *n = used;
|
|
return 0;
|
|
}
|
|
int store_get_edges_from(EngramPagedStore* s, const char* from_id, StoreEdge** out, size_t* n){
|
|
if (!s || !from_id) return -1;
|
|
return get_edges_dir(s, from_id, ADJ_DIR_FROM, out, n);
|
|
}
|
|
int store_get_edges_to(EngramPagedStore* s, const char* to_id, StoreEdge** out, size_t* n){
|
|
if (!s || !to_id) return -1;
|
|
return get_edges_dir(s, to_id, ADJ_DIR_TO, out, n);
|
|
}
|
|
|
|
/* ── integrity ────────────────────────────────────────────────────────────── */
|
|
int store_check(EngramPagedStore* s, unsigned flags){
|
|
if (!s) return -1;
|
|
int bad = 0;
|
|
uint8_t buf[STORE_PAGE_SIZE];
|
|
for (uint64_t id = 0; id < s->page_count; id++){
|
|
if (page_read(s, id, buf)!=0) return -1;
|
|
if (id == 0 || id == 1){
|
|
uint32_t stored = get_u32(buf + SB_CRC_OFF);
|
|
uint8_t tmp[STORE_PAGE_SIZE]; memcpy(tmp, buf, STORE_PAGE_SIZE);
|
|
put_u32(tmp + SB_CRC_OFF, 0);
|
|
int okmagic = memcmp(buf + SB_MAGIC_OFF, STORE_MAGIC, 8)==0;
|
|
if (!okmagic || crc32_buf(tmp, STORE_PAGE_SIZE) != stored) bad++;
|
|
} else {
|
|
if (flags & STORE_CHECK_CRC){ if (!page_crc_ok(buf)) bad++; }
|
|
}
|
|
}
|
|
return bad;
|
|
}
|
|
|
|
/* ── ownership helpers ────────────────────────────────────────────────────── */
|
|
void store_node_free(StoreNode* n){
|
|
if (!n) return;
|
|
free(n->id); free(n->content); free(n->node_type); free(n->label);
|
|
free(n->tier); free(n->tags); free(n->metadata); free(n->emb); free(n->unknown);
|
|
memset(n, 0, sizeof *n);
|
|
}
|
|
void store_edge_free(StoreEdge* e){
|
|
if (!e) return;
|
|
free(e->id); free(e->from_id); free(e->to_id); free(e->relation);
|
|
free(e->metadata); free(e->unknown);
|
|
memset(e, 0, sizeof *e);
|
|
}
|
|
void store_edges_free(StoreEdge* arr, size_t n){
|
|
if (!arr) return;
|
|
for (size_t i=0;i<n;i++) store_edge_free(&arr[i]);
|
|
free(arr);
|
|
}
|
|
void store__set_btree_order(EngramPagedStore* s, int leaf_max, int internal_max){
|
|
if (!s) return;
|
|
s->leaf_max = leaf_max; s->int_max = internal_max;
|
|
}
|
|
uint64_t store_page_count(const EngramPagedStore* s){ return s ? s->page_count : 0; }
|