Files
wehub-resource-sync 41cb1c0170
OpenSSF Scorecard / scorecard (push) Failing after 0s
DCO / dco (push) Failing after 0s
CodeQL SAST / analyze (push) Failing after 1s
Deploy Pages / deploy (push) Failing after 1s
chore: import upstream snapshot with attribution
2026-07-13 12:28:05 +08:00

2311 lines
83 KiB
C
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// sqlite_writer.c — Direct SQLite page writer.
// Constructs a valid .db file from sorted in-memory data without using
// the SQL parser, INSERT statements, or B-tree rebalancing.
//
// SQLite file format reference: https://www.sqlite.org/fileformat2.html
//
// Key invariants:
// - Page size: 4096 bytes
// - Page 1 has a 100-byte database header before the B-tree header
// - Leaf table B-tree pages: flag 0x0D
// - Interior table B-tree pages: flag 0x05
// - Leaf index B-tree pages: flag 0x0A
// - Interior index B-tree pages: flag 0x02
// - Records: header (varint count + serial types) + body (column values)
// - Varints: 1-9 bytes, big-endian, MSB continuation
#include "sqlite_writer.h"
#include "foundation/constants.h"
#include "foundation/compat_fs.h"
#include "foundation/compat_thread.h"
#include "foundation/profile.h"
#include <stddef.h> // NULL
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <stdbool.h>
#define CBM_PAGE_SIZE 65536
/* SQLite reserves the page containing the 1 GiB file offset (the "pending byte"
* used for file locking on Windows). This page MUST be skipped during allocation
* otherwise integrity_check reports "2nd reference to page N" because it marks
* this page as referenced before walking any tree.
*
* PENDING_BYTE = 0x40000000 = 1073741824 (1 GiB)
* PENDING_BYTE_PAGE = (PENDING_BYTE / page_size) + 1
* 64KB pages → page 16385
* 32KB pages → page 32769
* 16KB pages → page 65537
*/
#define SQLITE_MAX_PAGE_SIZE 65536
#define CBM_PENDING_BYTE (0x40000000u)
#define CBM_PENDING_BYTE_PAGE ((CBM_PENDING_BYTE / CBM_PAGE_SIZE) + 1)
/* Skip the pending byte page if allocation lands on it. */
static inline uint32_t cbm_skip_pending_byte(uint32_t pgno) {
return pgno == CBM_PENDING_BYTE_PAGE ? pgno + SKIP_ONE : pgno;
}
#define SCHEMA_FORMAT 4
#define FILE_FORMAT 1
#define SQLITE_VERSION 3046000 // 3.46.0
// Varint encoding constants.
#define VARINT_MASK 0x7f
#define VARINT_CONTINUE 0x80
#define BYTE_MASK 0xff
enum {
VARINT_SHIFT = 7,
VARINT_BUF_SIZE = 10,
VARINT_MIN_LEN = 1,
SERIAL_INT8 = 1,
SERIAL_INT16 = 2,
SERIAL_INT24 = 3,
SERIAL_INT32 = 4,
SERIAL_INT48 = 5,
SERIAL_INT64 = 6,
SERIAL_FLOAT64 = 7,
SERIAL_CONST_ZERO = 8,
SERIAL_CONST_ONE = 9,
SERIAL_SIZE_INT8 = 1,
SERIAL_SIZE_INT16 = 2,
SERIAL_SIZE_INT24 = 3,
SERIAL_SIZE_INT32 = 4,
SERIAL_SIZE_INT48 = 6,
SERIAL_SIZE_INT64 = 8,
BTREE_HEADER_SIZE = 8,
BTREE_INTERIOR_HDR = 12,
BTREE_PTR_SIZE = 4,
CELL_PTR_SIZE = 2,
INITIAL_PAGE_CAP = 4096,
INITIAL_LEAF_CAP = 256,
INITIAL_PARENT_CAP = 64,
GROWTH_FACTOR = 2,
VARINT_MAX_BYTES = 9,
INT64_BYTES = 8,
SORT_THRESHOLD = 20,
MAX_NAME_LEN = 64,
HASH_INIT = 5381,
HASH_MULT = 33,
HDR_FREEBLOCK_OFF = 1,
HDR_CELLCOUNT_OFF = 3,
HDR_CONTENT_OFF = 5,
HDR_FRAGBYTES_OFF = 7,
HDR_RIGHTCHILD_OFF = 8,
INTERIOR_TABLE_FLAG = 0x05,
INTERIOR_INDEX_FLAG = 0x02,
NEWLINE_BYTE = 0x0A,
NODE_SORT_THREADS = 4,
EDGE_SORT_THREADS = 7,
TOTAL_SORT_THREADS = 11,
ERR_SORT_FAILED = -4,
ERR_WRITE_FAILED = -3,
ERR_MASTER_OVERFLOW = -2,
MAX_EMBED_FRACTION = 64,
MIN_EMBED_FRACTION = 32,
LEAF_PAYLOAD_FRACTION = 32,
INTERIOR_CELL_BUF = 20,
FIRST_ROWID = 1,
FIRST_DATA_PAGE = 2,
NSORT_NAME = 1,
NSORT_FILE = 2,
NSORT_QN = 3,
ESORT_TARGET = 1,
ESORT_TYPE = 2,
ESORT_PROJ_TGT_TYPE = 3,
ESORT_PROJ_SRC_TYPE = 4,
ESORT_URL_PATH = 5,
ESORT_SRC_TGT_TYPE = 6,
SQLITE_HEADER_SIZE = 100,
SHIFT_8 = 8,
SHIFT_16 = 16,
SHIFT_24 = 24,
};
#define TEXT_SERIAL_BASE 13
// SQLite text serial type offset: serial_type = len*2 + TEXT_SERIAL_BASE.
#define TEXT_SERIAL_BASE 13
// SQLite blob serial type offset: serial_type = len*2 + BLOB_SERIAL_BASE.
#define BLOB_SERIAL_BASE 12
#define BLOB_SERIAL_MUL 2 /* serial_type = len * BLOB_SERIAL_MUL + BLOB_SERIAL_BASE */
// SQLite integer storage range limits.
#define INT8_MAX_VAL 127
#define INT16_MAX_VAL 32767
#define INT24_MIN_VAL (-8388608)
#define INT24_MAX_VAL 8388607
#define INT32_MIN_VAL (-2147483648LL)
#define INT32_MAX_VAL 2147483647LL
#define INT48_MIN_VAL (-140737488355328LL)
#define INT48_MAX_VAL 140737488355327LL
// SQLite B-tree page type flags.
#define BTREE_LEAF_TABLE 0x0D
#define BTREE_INTERIOR_TABLE 0x05
#define BTREE_LEAF_INDEX 0x0A
#define BTREE_INTERIOR_INDEX 0x02
// SQLite 100-byte database header field offsets.
#define HDR_OFF_CBM_PAGE_SIZE 16
#define HDR_OFF_WRITE_VERSION 18
#define HDR_OFF_READ_VERSION 19
#define HDR_OFF_RESERVED 20
#define HDR_OFF_MAX_EMBED_FRAC 21
#define HDR_OFF_MIN_EMBED_FRAC 22
#define HDR_OFF_LEAF_FRAC 23
#define HDR_OFF_FILE_CHANGE 24
#define HDR_OFF_DB_SIZE 28
#define HDR_OFF_FREELIST_TRUNK 32
#define HDR_OFF_FREELIST_COUNT 36
#define HDR_OFF_SCHEMA_COOKIE 40
#define HDR_OFF_SCHEMA_FORMAT 44
#define HDR_OFF_DEFAULT_CACHE 48
#define HDR_OFF_AUTOVAC_TOP 52
#define HDR_OFF_TEXT_ENCODING 56
#define HDR_OFF_USER_VERSION 60
#define HDR_OFF_INCR_VACUUM 64
#define HDR_OFF_APP_ID 68
#define HDR_OFF_VERSION_VALID 92
#define HDR_OFF_SQLITE_VERSION 96
// --- Varint encoding ---
static int put_varint(uint8_t *buf, int64_t value) {
uint64_t v = (uint64_t)value;
if (v <= VARINT_MASK) {
buf[0] = (uint8_t)v;
return SERIAL_SIZE_INT8;
}
// Encode in big-endian with MSB continuation bits
uint8_t tmp[VARINT_BUF_SIZE];
int n = 0;
while (v > VARINT_MASK) {
tmp[n++] = (uint8_t)(v & VARINT_MASK);
v >>= VARINT_SHIFT;
}
tmp[n++] = (uint8_t)v;
// Reverse into output with continuation bits
for (int i = 0; i < n; i++) {
buf[i] = tmp[n - SKIP_ONE - i];
if (i < n - SKIP_ONE) {
buf[i] |= VARINT_CONTINUE;
}
}
return n;
}
static int varint_len(int64_t value) {
uint64_t v = (uint64_t)value;
int n = VARINT_MIN_LEN;
while (v > VARINT_MASK) {
v >>= VARINT_SHIFT;
n++;
}
return n;
}
// SQLite serial type for a TEXT value
static int64_t text_serial_type(int len) {
return (len * PAIR_LEN) + TEXT_SERIAL_BASE;
}
// SQLite serial type for an integer value
static int64_t int_serial_type(int64_t val) {
if (val == 0) {
return SERIAL_CONST_ZERO;
}
if (val == SERIAL_INT8) {
return SERIAL_CONST_ONE;
}
if (val >= -INT8_MAX_VAL - SKIP_ONE && val <= INT8_MAX_VAL) {
return SERIAL_SIZE_INT8;
}
if (val >= -INT16_MAX_VAL - SKIP_ONE && val <= INT16_MAX_VAL) {
return SERIAL_SIZE_INT16;
}
if (val >= INT24_MIN_VAL && val <= INT24_MAX_VAL) {
return SERIAL_SIZE_INT24;
}
if (val >= INT32_MIN_VAL && val <= INT32_MAX_VAL) {
return SERIAL_SIZE_INT32;
}
if (val >= INT48_MIN_VAL && val <= INT48_MAX_VAL) {
return SERIAL_SIZE_INT48;
}
return SERIAL_SIZE_INT64;
}
// Bytes needed to store an integer of given serial type
static int int_storage_bytes(int serial_type) {
switch (serial_type) {
case 0:
return 0; // NULL
case SERIAL_INT8:
return SERIAL_SIZE_INT8;
case SERIAL_INT16:
return SERIAL_SIZE_INT16;
case SERIAL_INT24:
return SERIAL_SIZE_INT24;
case SERIAL_INT32:
return SERIAL_SIZE_INT32;
case SERIAL_INT48:
return SERIAL_SIZE_INT48;
case SERIAL_INT64:
return SERIAL_SIZE_INT64;
case SERIAL_CONST_ZERO: // integer 0
case SERIAL_CONST_ONE: // integer 1
default:
return 0;
}
}
// Write integer in big-endian for given byte count
static void put_int_be(uint8_t *buf, int64_t val, int nbytes) {
for (int i = nbytes - SKIP_ONE; i >= 0; i--) {
buf[i] = (uint8_t)(val & BYTE_MASK);
val >>= SHIFT_8;
}
}
// Write a 2-byte big-endian value
static void put_u16(uint8_t *buf, uint16_t val) {
buf[0] = (uint8_t)(val >> SHIFT_8);
buf[SKIP_ONE] = (uint8_t)(val & BYTE_MASK);
}
// Write a 4-byte big-endian value
static void put_u32(uint8_t *buf, uint32_t val) {
buf[0] = (uint8_t)(val >> SHIFT_24);
buf[SKIP_ONE] = (uint8_t)(val >> SHIFT_16);
buf[PAIR_LEN] = (uint8_t)(val >> SHIFT_8);
buf[SERIAL_SIZE_INT24] = (uint8_t)(val & BYTE_MASK);
}
// --- Dynamic buffer ---
typedef struct {
uint8_t *data;
int len;
int cap;
} DynBuf;
static void dynbuf_init(DynBuf *b) {
b->data = NULL;
b->len = 0;
b->cap = 0;
}
static bool dynbuf_ensure(DynBuf *b, int needed) {
if (b->len + needed <= b->cap) {
return true;
}
int newcap = b->cap == 0 ? INITIAL_PAGE_CAP : b->cap;
while (newcap < b->len + needed) {
newcap *= GROWTH_FACTOR;
}
uint8_t *p = (uint8_t *)realloc(b->data, newcap);
if (!p) {
(void)fprintf(stderr, "cbm_write_db: dynbuf realloc failed size=%d\n", newcap);
return false;
}
b->data = p;
b->cap = newcap;
return true;
}
static bool dynbuf_append(DynBuf *b, const void *data, int len) {
if (len <= 0) {
return true;
}
if (!data) {
return false;
}
if (!dynbuf_ensure(b, len)) {
return false;
}
memcpy(b->data + b->len, data, len);
b->len += len;
return true;
}
static void dynbuf_free(DynBuf *b) {
free(b->data);
b->data = NULL;
b->len = b->cap = 0;
}
// --- Record builder ---
// Builds a SQLite record: header (header_len varint + serial types) + body (values)
typedef struct {
DynBuf header; // serial type varints
DynBuf body; // column values
} RecordBuilder;
static void rec_init(RecordBuilder *r) {
dynbuf_init(&r->header);
dynbuf_init(&r->body);
}
static void rec_free(RecordBuilder *r) {
dynbuf_free(&r->header);
dynbuf_free(&r->body);
}
static void rec_add_null(RecordBuilder *r) {
uint8_t v[SKIP_ONE] = {0};
dynbuf_append(&r->header, v, SKIP_ONE);
}
static void rec_add_int(RecordBuilder *r, int64_t val) {
int64_t st = int_serial_type(val);
uint8_t vbuf[VARINT_MAX_BYTES];
int vlen = put_varint(vbuf, st);
dynbuf_append(&r->header, vbuf, vlen);
int nbytes = int_storage_bytes((int)st);
if (nbytes > 0) {
uint8_t ibuf[INT64_BYTES];
put_int_be(ibuf, val, nbytes);
dynbuf_append(&r->body, ibuf, nbytes);
}
}
static void rec_add_text(RecordBuilder *r, const char *s) {
int slen = s ? (int)strlen(s) : 0;
int64_t st = text_serial_type(slen);
uint8_t vbuf[VARINT_MAX_BYTES];
int vlen = put_varint(vbuf, st);
dynbuf_append(&r->header, vbuf, vlen);
if (slen > 0) {
dynbuf_append(&r->body, s, slen);
}
}
static void rec_add_blob(RecordBuilder *r, const uint8_t *data, int len) {
int64_t st = len > 0 ? ((int64_t)len * BLOB_SERIAL_MUL) + BLOB_SERIAL_BASE : 0;
uint8_t vbuf[VARINT_MAX_BYTES];
int vlen = put_varint(vbuf, st);
dynbuf_append(&r->header, vbuf, vlen);
if (len > 0 && data) {
dynbuf_append(&r->body, data, len);
}
}
// Finalize: returns the complete record bytes (header_len + header + body).
// Caller must free the returned buffer.
static uint8_t *rec_finalize(RecordBuilder *r, int *out_len) {
*out_len = 0;
int header_content_len = r->header.len;
int header_len_varint_len = varint_len(header_content_len + varint_len(header_content_len));
// The header size varint includes itself, so we may need to iterate
int total_header = header_len_varint_len + header_content_len;
// Check if the header_len varint changes size when it includes itself
int recalc = varint_len(total_header);
if (recalc != header_len_varint_len) {
header_len_varint_len = recalc;
total_header = header_len_varint_len + header_content_len;
}
int total = total_header + r->body.len;
uint8_t *buf = (uint8_t *)malloc(total);
if (!buf) {
return NULL;
}
int pos = put_varint(buf, total_header);
memcpy(buf + pos, r->header.data, header_content_len);
pos += header_content_len;
memcpy(buf + pos, r->body.data, r->body.len);
*out_len = total;
return buf;
}
// --- Page builder ---
// Accumulates cells (records) into B-tree leaf pages.
typedef struct {
uint32_t page_num; // page number of this page (1-based)
int64_t max_key; // max rowid on this page (table B-trees)
uint8_t *sep_cell; // separator cell content for index interior pages (owned, NULL for table)
int sep_cell_len;
} PageRef;
typedef struct {
FILE *fp;
uint32_t next_page; // next page number to allocate
int page1_offset; // 100 for page 1, 0 for others
bool is_index; // true for index B-trees
// Current leaf page being built
uint8_t page[CBM_PAGE_SIZE];
int cell_count;
int content_offset; // where cell content starts (grows down from page end)
int ptr_offset; // where cell pointers are written (grows up from header)
// Completed leaf pages for building interior nodes
PageRef *leaves;
int leaf_count;
int leaf_cap;
} PageBuilder;
static void pb_init(PageBuilder *pb, FILE *fp, uint32_t start_page, bool is_index) {
pb->fp = fp;
pb->next_page = start_page;
pb->is_index = is_index;
pb->cell_count = 0;
pb->content_offset = CBM_PAGE_SIZE;
pb->page1_offset = (start_page == SKIP_ONE) ? SQLITE_HEADER_SIZE : 0;
// Header: flag(1) + freeblock(2) + cell_count(2) + content_start(2) + fragmented(1) = 8
pb->ptr_offset = pb->page1_offset + BTREE_HEADER_SIZE;
memset(pb->page, 0, CBM_PAGE_SIZE);
pb->leaves = NULL;
pb->leaf_count = 0;
pb->leaf_cap = 0;
}
static void pb_free(PageBuilder *pb) {
if (pb->leaves) {
for (int i = 0; i < pb->leaf_count; i++) {
free(pb->leaves[i].sep_cell);
}
free(pb->leaves);
}
}
// Flush current leaf page to file
static void pb_flush_leaf(PageBuilder *pb) {
if (pb->cell_count == 0) {
return;
}
int hdr = pb->page1_offset;
// Write leaf page header
pb->page[hdr + 0] = pb->is_index ? BTREE_LEAF_INDEX : BTREE_LEAF_TABLE; // leaf flag
put_u16(pb->page + hdr + HDR_FREEBLOCK_OFF, 0); // first freeblock
put_u16(pb->page + hdr + HDR_CELLCOUNT_OFF, (uint16_t)pb->cell_count);
put_u16(pb->page + hdr + HDR_CONTENT_OFF, (uint16_t)pb->content_offset);
pb->page[hdr + HDR_FRAGBYTES_OFF] = 0; // fragmented free bytes
// Write page to file. Skip the pending byte page (SQLite reserved).
pb->next_page = cbm_skip_pending_byte(pb->next_page);
uint32_t page_num = pb->next_page;
long offset = (long)(page_num - SKIP_ONE) * CBM_PAGE_SIZE;
(void)fseek(pb->fp, offset, SEEK_SET);
(void)fwrite(pb->page, SKIP_ONE, CBM_PAGE_SIZE, pb->fp);
// Record this leaf for interior page building
if (pb->leaf_count >= pb->leaf_cap) {
int old_cap = pb->leaf_cap;
pb->leaf_cap = old_cap == 0 ? INITIAL_LEAF_CAP : old_cap * GROWTH_FACTOR;
void *tmp = realloc(pb->leaves, (size_t)pb->leaf_cap * sizeof(PageRef));
if (!tmp) {
free(pb->leaves);
pb->leaves = NULL;
return;
}
pb->leaves = (PageRef *)tmp;
/* Zero-init new slots */
memset(&pb->leaves[old_cap], 0, ((size_t)pb->leaf_cap - (size_t)old_cap) * sizeof(PageRef));
}
pb->leaves[pb->leaf_count].page_num = page_num;
// max_key is set by caller before flush
pb->leaf_count++;
// Reset for next page
pb->next_page++;
pb->cell_count = 0;
pb->content_offset = CBM_PAGE_SIZE;
pb->page1_offset = 0; // only page 1 has the 100-byte header
pb->ptr_offset = BTREE_HEADER_SIZE; // standard B-tree header size for non-page-1
memset(pb->page, 0, CBM_PAGE_SIZE);
}
// Check if a cell of given size fits in the current page
static bool pb_cell_fits(PageBuilder *pb, int cell_len) {
// Cell pointer (2 bytes) + cell content
int available = pb->content_offset - pb->ptr_offset - CELL_PTR_SIZE;
return cell_len <= available;
}
// Add a cell to the current leaf page.
// For table leaves: varint(payload_len) + varint(rowid) + payload
// For index leaves: varint(payload_len) + payload
static void pb_add_cell(PageBuilder *pb, const uint8_t *cell, int cell_len) {
// Write cell content (grows down)
pb->content_offset -= cell_len;
memcpy(pb->page + pb->content_offset, cell, cell_len);
// Write cell pointer (grows up)
put_u16(pb->page + pb->ptr_offset, (uint16_t)pb->content_offset);
pb->ptr_offset += CELL_PTR_SIZE;
pb->cell_count++;
}
// Build interior pages from child page references.
// Returns the root page number.
//
// SQLite interior page structure:
// - Header has right-child pointer (the last child page)
// - Each cell contains: child_page(4) + key
// - For N children, there are N-1 cells (children[0..N-2] get cells,
// children[N-1] becomes the right-child in the header)
// - Cell[j] = {left_child: children[j].page, key: children[j].max_key/sep_cell}
// - Lookup: X ≤ K0 → cell[0].left_child, K0 < X ≤ K1 → cell[1].left_child, etc.
// - Table keys: varint(rowid)
// - Index keys: varint(payload_len) + payload (full index record)
// Build an interior cell for a child PageRef. Returns cell length.
// For table B-trees: child_page(4) + varint(rowid).
// For index B-trees: child_page(4) + separator_cell.
// cell_buf must be at least 20 bytes for table cells.
// For index cells, returns malloc'd data via *out_heap (caller frees).
static int build_interior_cell(const PageRef *child, bool is_index, uint8_t *cell_buf,
uint8_t **out_heap) {
*out_heap = NULL;
if (!is_index) {
put_u32(cell_buf, child->page_num);
return BTREE_PTR_SIZE + put_varint(cell_buf + BTREE_PTR_SIZE, child->max_key);
}
int clen = BTREE_PTR_SIZE + child->sep_cell_len;
uint8_t *data = (uint8_t *)malloc(clen);
put_u32(data, child->page_num);
memcpy(data + 4, child->sep_cell, child->sep_cell_len);
*out_heap = data;
return clen;
}
// Write a completed interior page to disk and record it as a parent.
// Returns updated parent_count, or -1 on allocation failure.
static int write_interior_page(PageBuilder *pb, uint8_t *page, int cell_count, int content_offset,
uint32_t right_child_page, const PageRef *children,
int right_child_idx, bool is_index, PageRef **parents,
int parent_count, int *parent_cap) {
pb->next_page = cbm_skip_pending_byte(pb->next_page);
uint32_t pnum = pb->next_page++;
page[0] = is_index ? INTERIOR_INDEX_FLAG : INTERIOR_TABLE_FLAG;
put_u16(page + HDR_FREEBLOCK_OFF, 0);
put_u16(page + HDR_CELLCOUNT_OFF, (uint16_t)cell_count);
put_u16(page + HDR_CONTENT_OFF, (uint16_t)content_offset);
page[HDR_FRAGBYTES_OFF] = 0;
put_u32(page + HDR_RIGHTCHILD_OFF, right_child_page);
(void)fseek(pb->fp, (long)(pnum - SKIP_ONE) * CBM_PAGE_SIZE, SEEK_SET);
(void)fwrite(page, SKIP_ONE, CBM_PAGE_SIZE, pb->fp);
if (parent_count >= *parent_cap) {
int old_pcap = *parent_cap;
*parent_cap = old_pcap == 0 ? INITIAL_PARENT_CAP : old_pcap * GROWTH_FACTOR;
PageRef *tmp = (PageRef *)realloc(*parents, *parent_cap * sizeof(PageRef));
if (!tmp) {
free(*parents);
*parents = NULL;
return CBM_NOT_FOUND;
}
*parents = tmp;
memset(&(*parents)[old_pcap], 0,
((size_t)*parent_cap - (size_t)old_pcap) * sizeof(PageRef));
}
(*parents)[parent_count].page_num = pnum;
(*parents)[parent_count].max_key = children[right_child_idx].max_key;
if (is_index && children[right_child_idx].sep_cell) {
int slen = children[right_child_idx].sep_cell_len;
(*parents)[parent_count].sep_cell = (uint8_t *)malloc(slen);
memcpy((*parents)[parent_count].sep_cell, children[right_child_idx].sep_cell, slen);
(*parents)[parent_count].sep_cell_len = slen;
} else {
(*parents)[parent_count].sep_cell = NULL;
(*parents)[parent_count].sep_cell_len = 0;
}
return parent_count + SKIP_ONE;
}
// Free a PageRef array (sep_cell allocations), unless it's the original leaves.
static void free_children(PageRef *children, int child_count, const PageRef *leaves) {
if (children != leaves) {
for (int j = 0; j < child_count; j++) {
free(children[j].sep_cell);
}
free(children);
}
}
// Fill an interior page with cells from children[*idx..child_count-2].
// Updates cell_count, content_offset, ptr_offset, and *idx.
static void fill_interior_page(uint8_t *page, const PageRef *children, int child_count,
bool is_index, int *idx, int *cell_count, int *content_offset,
int *ptr_offset) {
while (*idx < child_count - SKIP_ONE) {
uint8_t tbuf[INTERIOR_CELL_BUF];
uint8_t *heap_cell = NULL;
int clen = build_interior_cell(&children[*idx], is_index, tbuf, &heap_cell);
uint8_t *cell_data = heap_cell ? heap_cell : tbuf;
int available = *content_offset - *ptr_offset - CELL_PTR_SIZE;
if (clen > available && *cell_count > 0) {
free(heap_cell);
break;
}
*content_offset -= clen;
memcpy(page + *content_offset, cell_data, clen);
put_u16(page + *ptr_offset, (uint16_t)*content_offset);
*ptr_offset += CELL_PTR_SIZE;
(*cell_count)++;
free(heap_cell);
(*idx)++;
}
}
static uint32_t pb_build_interior(PageBuilder *pb, bool is_index) {
if (!pb->leaves) {
return 0;
}
if (pb->leaf_count <= SKIP_ONE) {
return pb->leaves[0].page_num;
}
PageRef *children = pb->leaves;
int child_count = pb->leaf_count;
while (child_count > SKIP_ONE && children) {
PageRef *parents = NULL;
int parent_count = 0;
int parent_cap = 0;
int i = 0;
while (i < child_count) {
uint8_t page[CBM_PAGE_SIZE];
memset(page, 0, CBM_PAGE_SIZE);
int cell_count = 0;
int content_offset = CBM_PAGE_SIZE;
int ptr_offset = BTREE_INTERIOR_HDR;
fill_interior_page(page, children, child_count, is_index, &i, &cell_count,
&content_offset, &ptr_offset);
int right_child_idx = (i < child_count - SKIP_ONE) ? i : child_count - SKIP_ONE;
uint32_t right_child_page = 0;
if (right_child_idx >= 0 && right_child_idx < child_count) {
right_child_page = children[right_child_idx].page_num;
}
if (i < child_count - SKIP_ONE) {
i++;
} else {
i = child_count;
}
parent_count = write_interior_page(pb, page, cell_count, content_offset,
right_child_page, children, right_child_idx,
is_index, &parents, parent_count, &parent_cap);
if (parent_count < 0) {
break;
}
}
free_children(children, child_count, pb->leaves);
children = parents;
child_count = parent_count;
}
uint32_t root = children ? children[0].page_num : 0;
free_children(children, child_count, pb->leaves);
return root;
}
// --- Table record builders ---
// Build a nodes table record: (id, project, label, name, qualified_name, file_path, start_line,
// end_line, properties)
static uint8_t *build_node_record(const CBMDumpNode *n, int *out_len) {
RecordBuilder r;
rec_init(&r);
rec_add_int(&r, n->id);
rec_add_text(&r, n->project);
rec_add_text(&r, n->label);
rec_add_text(&r, n->name);
rec_add_text(&r, n->qualified_name);
rec_add_text(&r, n->file_path ? n->file_path : "");
rec_add_int(&r, n->start_line);
rec_add_int(&r, n->end_line);
rec_add_text(&r, n->properties ? n->properties : "{}");
uint8_t *data = rec_finalize(&r, out_len);
rec_free(&r);
return data;
}
// Build an edges table record: (id, project, source_id, target_id, type, properties)
// url_path_gen and local_name_gen are VIRTUAL generated columns — NOT stored in the record.
static uint8_t *build_edge_record(const CBMDumpEdge *e, int *out_len) {
RecordBuilder r;
rec_init(&r);
rec_add_int(&r, e->id);
rec_add_text(&r, e->project);
rec_add_int(&r, e->source_id);
rec_add_int(&r, e->target_id);
rec_add_text(&r, e->type);
rec_add_text(&r, e->properties ? e->properties : "{}");
uint8_t *data = rec_finalize(&r, out_len);
rec_free(&r);
return data;
}
// Build a node_vectors table record: (node_id, project, vector)
// Includes node_id in the record body (same pattern as build_node_record).
static uint8_t *build_vector_record(const CBMDumpVector *v, int *out_len) {
RecordBuilder r;
rec_init(&r);
rec_add_int(&r, v->node_id);
rec_add_text(&r, v->project);
rec_add_blob(&r, v->vector, v->vector_len);
uint8_t *data = rec_finalize(&r, out_len);
rec_free(&r);
return data;
}
// Build a token_vectors table record: (id, project, token, vector, idf)
static uint8_t *build_token_vec_record(const CBMDumpTokenVec *tv, int *out_len) {
RecordBuilder r;
rec_init(&r);
rec_add_int(&r, tv->id);
rec_add_text(&r, tv->project);
rec_add_text(&r, tv->token);
rec_add_blob(&r, tv->vector, tv->vector_len);
/* Store IDF as integer × 1000 for fixed-point (avoid float in record) */
enum { IDF_FIXED_POINT_SCALE = 1000 };
rec_add_int(&r, (int64_t)(tv->idf * IDF_FIXED_POINT_SCALE));
uint8_t *data = rec_finalize(&r, out_len);
rec_free(&r);
return data;
}
// Build a projects table record: (name, indexed_at, root_path)
static uint8_t *build_project_record(const char *name, const char *indexed_at,
const char *root_path, int *out_len) {
RecordBuilder r;
rec_init(&r);
rec_add_text(&r, name);
rec_add_text(&r, indexed_at);
rec_add_text(&r, root_path);
uint8_t *data = rec_finalize(&r, out_len);
rec_free(&r);
return data;
}
// --- Table cell builder ---
// Table leaf cell: varint(payload_len) + varint(rowid) + payload
static uint8_t *build_table_cell(int64_t rowid, const uint8_t *payload, int payload_len,
int *out_cell_len) {
int rl = varint_len(payload_len);
int kl = varint_len(rowid);
int total = rl + kl + payload_len;
uint8_t *cell = (uint8_t *)malloc(total);
if (!cell) {
return NULL;
}
int pos = 0;
pos += put_varint(cell + pos, payload_len);
pos += put_varint(cell + pos, rowid);
memcpy(cell + pos, payload, payload_len);
*out_cell_len = pos + payload_len;
return cell;
}
// Build a table leaf cell with overflow: stores only the first local_len bytes of
// payload inline, followed by a 4-byte overflow page number.
// total_payload_len is the FULL original payload length (written as the payload-size
// varint so SQLite knows the real record size).
static uint8_t *build_table_cell_overflow(int64_t rowid, const uint8_t *payload,
int total_payload_len, int local_len,
uint32_t overflow_page, int *out_cell_len) {
int rl = varint_len(total_payload_len);
int kl = varint_len(rowid);
// cell = varint(total_payload_len) + varint(rowid) + payload[0..local_len) + uint32(overflow)
int total = rl + kl + local_len + BTREE_PTR_SIZE;
uint8_t *cell = (uint8_t *)malloc(total);
if (!cell) {
return NULL;
}
int pos = 0;
pos += put_varint(cell + pos, total_payload_len);
pos += put_varint(cell + pos, rowid);
memcpy(cell + pos, payload, local_len);
pos += local_len;
put_u32(cell + pos, overflow_page);
pos += BTREE_PTR_SIZE;
*out_cell_len = pos;
return cell;
}
// --- Overflow page writer ---
// Writes overflow pages for payload bytes that exceed local storage.
// Returns the first overflow page number (embedded in the leaf cell).
// Each overflow page: 4-byte next-page pointer + up to (CBM_PAGE_SIZE-4) bytes of data.
static uint32_t write_overflow_pages(FILE *fp, uint32_t *next_page, const uint8_t *data,
int data_len) {
int per_page = CBM_PAGE_SIZE - BTREE_PTR_SIZE;
uint32_t first_page = 0;
long prev_next_ptr_offset = -SKIP_ONE;
int offset = 0;
while (offset < data_len) {
uint32_t pnum = (*next_page)++;
if (first_page == 0) {
first_page = pnum;
}
// Backpatch previous overflow page's next-page pointer
if (prev_next_ptr_offset >= 0) {
uint8_t ptr[BTREE_PTR_SIZE];
put_u32(ptr, pnum);
(void)fseek(fp, prev_next_ptr_offset, SEEK_SET);
(void)fwrite(ptr, SKIP_ONE, BTREE_PTR_SIZE, fp);
}
int chunk = data_len - offset;
if (chunk > per_page) {
chunk = per_page;
}
uint8_t page[CBM_PAGE_SIZE];
memset(page, 0, CBM_PAGE_SIZE);
put_u32(page, 0); // next-page pointer — 0 for now, backpatched on next iteration
memcpy(page + BTREE_PTR_SIZE, data + offset, chunk);
long page_offset = (long)(pnum - SKIP_ONE) * CBM_PAGE_SIZE;
prev_next_ptr_offset = page_offset;
(void)fseek(fp, page_offset, SEEK_SET);
(void)fwrite(page, SKIP_ONE, CBM_PAGE_SIZE, fp);
offset += chunk;
}
return first_page;
}
// --- Index record builders ---
// Build an index entry for a 2-column TEXT index (project, col) + rowid.
// Index records: varint(payload_len) + payload(record of indexed cols + rowid)
static uint8_t *build_index_entry_2text_rowid(const char *col1, const char *col2, int64_t rowid,
int *out_len) {
// Build the record portion: (col1, col2, rowid)
RecordBuilder r;
rec_init(&r);
rec_add_text(&r, col1);
rec_add_text(&r, col2);
rec_add_int(&r, rowid);
int payload_len = 0;
uint8_t *payload = rec_finalize(&r, &payload_len);
rec_free(&r);
if (!payload) {
*out_len = 0;
return NULL;
}
// Index cell: varint(payload_len) + payload
int vl = varint_len(payload_len);
int total = vl + payload_len;
uint8_t *cell = (uint8_t *)malloc(total);
if (!cell) {
free(payload);
*out_len = 0;
return NULL;
}
int pos = put_varint(cell, payload_len);
memcpy(cell + pos, payload, payload_len);
free(payload);
*out_len = total;
return cell;
}
// Build index entry for (int64, text) + rowid (e.g., idx_edges_source)
static uint8_t *build_index_entry_int_text_rowid(int64_t val, const char *text, int64_t rowid,
int *out_len) {
RecordBuilder r;
rec_init(&r);
rec_add_int(&r, val);
rec_add_text(&r, text);
rec_add_int(&r, rowid);
int payload_len = 0;
uint8_t *payload = rec_finalize(&r, &payload_len);
rec_free(&r);
if (!payload) {
*out_len = 0;
return NULL;
}
int vl = varint_len(payload_len);
int total = vl + payload_len;
uint8_t *cell = (uint8_t *)malloc(total);
if (!cell) {
free(payload);
*out_len = 0;
return NULL;
}
int pos = put_varint(cell, payload_len);
memcpy(cell + pos, payload, payload_len);
free(payload);
*out_len = total;
return cell;
}
// Build index entry for (text, int64, text) + rowid (e.g., idx_edges_target_type)
static uint8_t *build_index_entry_text_int_text_rowid(const char *t1, int64_t val, const char *t2,
int64_t rowid, int *out_len) {
RecordBuilder r;
rec_init(&r);
rec_add_text(&r, t1);
rec_add_int(&r, val);
rec_add_text(&r, t2);
rec_add_int(&r, rowid);
int payload_len = 0;
uint8_t *payload = rec_finalize(&r, &payload_len);
rec_free(&r);
if (!payload) {
*out_len = 0;
return NULL;
}
int vl = varint_len(payload_len);
int total = vl + payload_len;
uint8_t *cell = (uint8_t *)malloc(total);
if (!cell) {
free(payload);
*out_len = 0;
return NULL;
}
int pos = put_varint(cell, payload_len);
memcpy(cell + pos, payload, payload_len);
free(payload);
*out_len = total;
return cell;
}
// Build UNIQUE index entry for (int64, int64, text, text) + rowid — edges
// unique(source_id, target_id, type, local_name_gen) (#768).
static uint8_t *build_index_entry_unique_2int_2text_rowid(int64_t v1, int64_t v2, const char *text,
const char *text2, int64_t rowid,
int *out_len) {
RecordBuilder r;
rec_init(&r);
rec_add_int(&r, v1);
rec_add_int(&r, v2);
rec_add_text(&r, text);
rec_add_text(&r, text2);
rec_add_int(&r, rowid);
int payload_len = 0;
uint8_t *payload = rec_finalize(&r, &payload_len);
rec_free(&r);
if (!payload) {
*out_len = 0;
return NULL;
}
int vlen = varint_len(payload_len);
int total = vlen + payload_len;
uint8_t *cell = (uint8_t *)malloc(total);
if (!cell) {
free(payload);
*out_len = 0;
return NULL;
}
int pos = put_varint(cell, payload_len);
memcpy(cell + pos, payload, payload_len);
free(payload);
*out_len = total;
return cell;
}
// --- Write a table B-tree from records ---
// Ensure leaves array has capacity for one more entry.
// Returns false on allocation failure.
static bool pb_ensure_leaf_cap(PageBuilder *pb) {
if (pb->leaf_count < pb->leaf_cap) {
return true;
}
pb->leaf_cap = pb->leaf_cap == 0 ? INITIAL_LEAF_CAP : pb->leaf_cap * GROWTH_FACTOR;
void *tmp = realloc(pb->leaves, (size_t)pb->leaf_cap * sizeof(PageRef));
if (!tmp) {
free(pb->leaves);
pb->leaves = NULL;
return false;
}
pb->leaves = (PageRef *)tmp;
return true;
}
// SQLite overflow thresholds for leaf table B-tree pages (PAGE_SIZE=65536, reserved=0):
// usable = PAGE_SIZE = 65536
// max_local = usable - 35 = 65501
// min_local = (usable - 12) * 32 / 255 - 23 = 8199 (C integer arithmetic, same as SQLite)
#define TABLE_OVERFLOW_MAX_LOCAL 65501
// SQLite index B-tree local-payload thresholds for PAGE_SIZE=65536, reserved=0:
// X (max local) = ((U-12)*64/255) - 23 = 16422
// M (min local) = ((U-12)*32/255) - 23 = 8199
// An index cell whose payload exceeds X MUST spill to overflow pages; storing
// it fully inline makes SQLite read key bytes as an overflow page number
// (integrity_check: "invalid page number", name lookups silently miss — seen
// on elasticsearch's very long Section names in idx_nodes_name).
#define INDEX_OVERFLOW_MAX_LOCAL 16422
#define INDEX_OVERFLOW_MIN_LOCAL 8199
// Read a SQLite varint (1-9 bytes). Returns bytes consumed.
static int get_varint(const uint8_t *buf, uint64_t *out) {
uint64_t v = 0;
for (int i = 0; i < 8; i++) {
v = (v << 7) | (uint64_t)(buf[i] & 0x7f);
if ((buf[i] & 0x80) == 0) {
*out = v;
return i + 1;
}
}
v = (v << 8) | (uint64_t)buf[8];
*out = v;
return 9;
}
// If an index cell's payload exceeds X, rewrite it to spill the tail to
// overflow pages: varint(payload_len) + payload[0..local) + u32(first_ovfl).
// Returns the (possibly new, malloc'd) cell; frees the original when replaced.
static uint8_t *overflowize_index_cell(FILE *fp, uint32_t *next_page, uint8_t *cell,
int *cell_len) {
uint64_t plen = 0;
int vlen = get_varint(cell, &plen);
if ((int64_t)plen <= INDEX_OVERFLOW_MAX_LOCAL) {
return cell;
}
int64_t per_ovfl = (int64_t)CBM_PAGE_SIZE - BTREE_PTR_SIZE;
int64_t k = INDEX_OVERFLOW_MIN_LOCAL + (((int64_t)plen - INDEX_OVERFLOW_MIN_LOCAL) % per_ovfl);
int local = (k <= INDEX_OVERFLOW_MAX_LOCAL) ? (int)k : INDEX_OVERFLOW_MIN_LOCAL;
uint32_t first_ovfl =
write_overflow_pages(fp, next_page, cell + vlen + local, (int)plen - local);
int nlen = vlen + local + BTREE_PTR_SIZE;
uint8_t *data = (uint8_t *)malloc((size_t)nlen);
if (!data) {
return cell; /* fall back to the (broken) inline form on OOM */
}
memcpy(data, cell, (size_t)(vlen + local));
put_u32(data + vlen + local, first_ovfl);
free(cell);
*cell_len = nlen;
return data;
}
#define TABLE_OVERFLOW_MIN_LOCAL 8199
// Add a table cell to the PageBuilder, flushing leaf pages as needed.
// If the payload exceeds max_local, overflow pages are written and only the
// local portion plus a 4-byte overflow page pointer is stored in the leaf cell.
static void pb_add_table_cell_with_flush(PageBuilder *pb, int64_t rowid, const uint8_t *payload,
int payload_len, int64_t prev_rowid) {
int cell_len = 0;
uint8_t *cell = NULL;
if (payload_len > TABLE_OVERFLOW_MAX_LOCAL) {
// Compute local_len per SQLite spec for leaf table cells.
int ovfl_page_data = CBM_PAGE_SIZE - BTREE_PTR_SIZE;
int remainder = (payload_len - TABLE_OVERFLOW_MIN_LOCAL) % ovfl_page_data;
int local_len = TABLE_OVERFLOW_MIN_LOCAL + remainder;
if (local_len > TABLE_OVERFLOW_MAX_LOCAL) {
local_len = TABLE_OVERFLOW_MIN_LOCAL;
}
// Write overflow pages for the bytes that don't fit locally.
uint32_t overflow_page = write_overflow_pages(pb->fp, &pb->next_page, payload + local_len,
payload_len - local_len);
if (overflow_page == 0) {
return; // overflow write failed
}
cell = build_table_cell_overflow(rowid, payload, payload_len, local_len, overflow_page,
&cell_len);
} else {
cell = build_table_cell(rowid, payload, payload_len, &cell_len);
}
if (!cell) {
return;
}
if (!pb_cell_fits(pb, cell_len) && pb->cell_count > 0) {
if (!pb_ensure_leaf_cap(pb)) {
free(cell);
return;
}
pb->leaves[pb->leaf_count].max_key = prev_rowid;
pb->leaves[pb->leaf_count].sep_cell = NULL;
pb->leaves[pb->leaf_count].sep_cell_len = 0;
pb_flush_leaf(pb);
}
pb_add_cell(pb, cell, cell_len);
free(cell);
}
// Finalize a table PageBuilder: flush last leaf and build interior pages.
static uint32_t pb_finalize_table(PageBuilder *pb, uint32_t *next_page, int64_t last_rowid) {
if (pb->cell_count > 0) {
pb_ensure_leaf_cap(pb);
if (!pb->leaves) {
pb_free(pb);
return 0;
}
pb->leaves[pb->leaf_count].max_key = last_rowid;
pb->leaves[pb->leaf_count].sep_cell = NULL;
pb->leaves[pb->leaf_count].sep_cell_len = 0;
pb_flush_leaf(pb);
}
*next_page = pb->next_page;
uint32_t root;
if (pb->leaf_count == SKIP_ONE) {
root = pb->leaves[0].page_num;
} else if (pb->leaf_count > SKIP_ONE) {
root = pb_build_interior(pb, false);
*next_page = pb->next_page;
} else {
root = 0; // shouldn't happen when count > 0
}
pb_free(pb);
return root;
}
// Write leaf pages for a table, returns root page.
// rowids must be sequential starting from 1 (or single-row PK text).
static uint32_t write_table_btree(FILE *fp, uint32_t *next_page, const uint8_t **records,
const int *record_lens, const int64_t *rowids, int count,
bool first_is_page1) {
if (count == 0) {
// Empty table: write a single empty leaf page
*next_page = cbm_skip_pending_byte(*next_page);
uint32_t pnum = (*next_page)++;
uint8_t page[CBM_PAGE_SIZE];
memset(page, 0, CBM_PAGE_SIZE);
int hdr = first_is_page1 ? SQLITE_HEADER_SIZE : 0;
page[hdr] = BTREE_LEAF_TABLE; // leaf table
put_u16(page + hdr + HDR_FREEBLOCK_OFF, 0); // no freeblocks
put_u16(page + hdr + HDR_CELLCOUNT_OFF, 0); // 0 cells
put_u16(page + hdr + HDR_CONTENT_OFF, (uint16_t)CBM_PAGE_SIZE); // content at end of page
page[hdr + HDR_FRAGBYTES_OFF] = 0; // 0 fragmented bytes
(void)fseek(fp, (long)(pnum - SKIP_ONE) * CBM_PAGE_SIZE, SEEK_SET);
(void)fwrite(page, SKIP_ONE, CBM_PAGE_SIZE, fp);
return pnum;
}
PageBuilder pb;
pb_init(&pb, fp, *next_page, false);
pb.page1_offset = first_is_page1 ? SQLITE_HEADER_SIZE : 0;
pb.ptr_offset = pb.page1_offset + BTREE_HEADER_SIZE;
for (int i = 0; i < count; i++) {
pb_add_table_cell_with_flush(&pb, rowids[i], records[i], record_lens[i],
i > 0 ? rowids[i - SKIP_ONE] : 0);
}
return pb_finalize_table(&pb, next_page, rowids[count - SKIP_ONE]);
}
// Promote the last cell from current page to separator, un-add it, and flush.
static bool pb_promote_and_flush(PageBuilder *pb, uint8_t **cells, int *cell_lens, int prev_idx) {
if (!pb_ensure_leaf_cap(pb)) {
return false;
}
pb->leaves[pb->leaf_count].max_key = 0;
pb->leaves[pb->leaf_count].sep_cell = (uint8_t *)malloc(cell_lens[prev_idx]);
memcpy(pb->leaves[pb->leaf_count].sep_cell, cells[prev_idx], cell_lens[prev_idx]);
pb->leaves[pb->leaf_count].sep_cell_len = cell_lens[prev_idx];
// Un-add the last cell — it's promoted to the interior separator.
// SQLite index B-tree interior cells are counted by integrity_check,
// so this cell exists in the interior page instead of the leaf.
pb->cell_count--;
pb->content_offset += cell_lens[prev_idx];
pb->ptr_offset -= CELL_PTR_SIZE;
pb_flush_leaf(pb);
return true;
}
// Write an empty index leaf page.
static uint32_t write_empty_index_leaf(FILE *fp, uint32_t *next_page) {
*next_page = cbm_skip_pending_byte(*next_page);
uint32_t pnum = (*next_page)++;
uint8_t page[CBM_PAGE_SIZE];
memset(page, 0, CBM_PAGE_SIZE);
page[0] = NEWLINE_BYTE;
put_u16(page + HDR_FREEBLOCK_OFF, 0);
put_u16(page + HDR_CELLCOUNT_OFF, 0);
put_u16(page + HDR_CONTENT_OFF, (uint16_t)CBM_PAGE_SIZE);
page[HDR_FRAGBYTES_OFF] = 0;
(void)fseek(fp, (long)(pnum - SKIP_ONE) * CBM_PAGE_SIZE, SEEK_SET);
(void)fwrite(page, SKIP_ONE, CBM_PAGE_SIZE, fp);
return pnum;
}
// Write leaf pages for an index, returns root page.
static uint32_t write_index_btree(FILE *fp, uint32_t *next_page, uint8_t **cells, int *cell_lens,
int count) {
if (count == 0) {
return write_empty_index_leaf(fp, next_page);
}
/* Spill oversized index payloads to overflow pages BEFORE page building so
* every cell added below is within the local-payload limit (see
* INDEX_OVERFLOW_MAX_LOCAL). Overflow pages are allocated from *next_page
* ahead of the leaf pages, which is fine — page order is arbitrary. */
for (int i = 0; i < count; i++) {
cells[i] = overflowize_index_cell(fp, next_page, cells[i], &cell_lens[i]);
}
PageBuilder pb;
pb_init(&pb, fp, *next_page, true);
for (int i = 0; i < count; i++) {
if (!pb_cell_fits(&pb, cell_lens[i])) {
if (pb.cell_count > 0) {
if (!pb_promote_and_flush(&pb, cells, cell_lens, i - SKIP_ONE)) {
return 0;
}
}
// After flush, check if the cell still doesn't fit on an empty page.
// Index cells larger than a full page can never be stored; skip them.
if (!pb_cell_fits(&pb, cell_lens[i])) {
(void)fprintf(stderr, "cbm_write_db: index cell oversized, skipped len=%d idx=%d\n",
cell_lens[i], i);
continue;
}
}
pb_add_cell(&pb, cells[i], cell_lens[i]);
}
if (pb.cell_count > 0) {
if (!pb_ensure_leaf_cap(&pb)) {
return 0;
}
pb.leaves[pb.leaf_count].max_key = 0;
int last = count - SKIP_ONE;
pb.leaves[pb.leaf_count].sep_cell = (uint8_t *)malloc(cell_lens[last]);
memcpy(pb.leaves[pb.leaf_count].sep_cell, cells[last], cell_lens[last]);
pb.leaves[pb.leaf_count].sep_cell_len = cell_lens[last];
pb_flush_leaf(&pb);
}
*next_page = pb.next_page;
uint32_t root;
if (!pb.leaves) {
root = 0;
} else if (pb.leaf_count == SKIP_ONE) {
root = pb.leaves[0].page_num;
} else {
root = pb_build_interior(&pb, true);
*next_page = pb.next_page;
}
pb_free(&pb);
return root;
}
// --- sqlite_master entries ---
typedef struct {
const char *type; // "table" or "index"
const char *name; // table/index name
const char *tbl_name; // table name
uint32_t rootpage; // root page number
const char *sql; // CREATE statement
} MasterEntry;
static uint8_t *build_master_record(const MasterEntry *e, int *out_len) {
RecordBuilder r;
rec_init(&r);
rec_add_text(&r, e->type);
rec_add_text(&r, e->name);
rec_add_text(&r, e->tbl_name);
rec_add_int(&r, (int64_t)e->rootpage);
if (e->sql) {
rec_add_text(&r, e->sql);
} else {
rec_add_null(&r);
}
uint8_t *data = rec_finalize(&r, out_len);
rec_free(&r);
return data;
}
// --- qsort comparators for index sorting ---
// Single-threaded writer: static context is safe.
static const CBMDumpNode *g_sort_nodes;
static const CBMDumpEdge *g_sort_edges;
static inline int cmp_i64(int64_t a, int64_t b) {
return (a > b) - (a < b);
}
static inline const char *safe_str(const char *s) {
return s ? s : "";
}
// Allocate permutation array [0, 1, ..., n-1], sort with comparator.
// Returns NULL on allocation failure.
static int *make_sorted_perm(int n, int (*cmp)(const void *, const void *)) {
int *perm = (int *)malloc(n * sizeof(int));
if (!perm) {
(void)fprintf(stderr, "cbm_write_db: perm malloc failed n=%d size=%zu\n", n,
(size_t)n * sizeof(int));
return NULL;
}
for (int i = 0; i < n; i++) {
perm[i] = i;
}
qsort(perm, n, sizeof(int), cmp);
return perm;
}
// --- Node index comparators (project is same for all, skip it) ---
static int cmp_node_by_label(const void *a, const void *b) {
int ia = *(const int *)a;
int ib = *(const int *)b;
int c = strcmp(safe_str(g_sort_nodes[ia].label), safe_str(g_sort_nodes[ib].label));
if (c) {
return c;
}
return cmp_i64(g_sort_nodes[ia].id, g_sort_nodes[ib].id);
}
static int cmp_node_by_name(const void *a, const void *b) {
int ia = *(const int *)a;
int ib = *(const int *)b;
int c = strcmp(safe_str(g_sort_nodes[ia].name), safe_str(g_sort_nodes[ib].name));
if (c) {
return c;
}
return cmp_i64(g_sort_nodes[ia].id, g_sort_nodes[ib].id);
}
static int cmp_node_by_file(const void *a, const void *b) {
int ia = *(const int *)a;
int ib = *(const int *)b;
int c = strcmp(safe_str(g_sort_nodes[ia].file_path), safe_str(g_sort_nodes[ib].file_path));
if (c) {
return c;
}
return cmp_i64(g_sort_nodes[ia].id, g_sort_nodes[ib].id);
}
static int cmp_node_by_qn(const void *a, const void *b) {
int ia = *(const int *)a;
int ib = *(const int *)b;
int c = strcmp(safe_str(g_sort_nodes[ia].qualified_name),
safe_str(g_sort_nodes[ib].qualified_name));
if (c) {
return c;
}
return cmp_i64(g_sort_nodes[ia].id, g_sort_nodes[ib].id);
}
// --- Edge index comparators ---
// idx_edges_source: (source_id, type) + rowid
static int cmp_edge_by_source_type(const void *a, const void *b) {
int ia = *(const int *)a;
int ib = *(const int *)b;
int c = cmp_i64(g_sort_edges[ia].source_id, g_sort_edges[ib].source_id);
if (c) {
return c;
}
c = strcmp(safe_str(g_sort_edges[ia].type), safe_str(g_sort_edges[ib].type));
if (c) {
return c;
}
return cmp_i64(g_sort_edges[ia].id, g_sort_edges[ib].id);
}
// idx_edges_target: (target_id, type) + rowid
static int cmp_edge_by_target_type(const void *a, const void *b) {
int ia = *(const int *)a;
int ib = *(const int *)b;
int c = cmp_i64(g_sort_edges[ia].target_id, g_sort_edges[ib].target_id);
if (c) {
return c;
}
c = strcmp(safe_str(g_sort_edges[ia].type), safe_str(g_sort_edges[ib].type));
if (c) {
return c;
}
return cmp_i64(g_sort_edges[ia].id, g_sort_edges[ib].id);
}
// idx_edges_type: (project, type) + rowid
static int cmp_edge_by_type(const void *a, const void *b) {
int ia = *(const int *)a;
int ib = *(const int *)b;
int c = strcmp(safe_str(g_sort_edges[ia].type), safe_str(g_sort_edges[ib].type));
if (c) {
return c;
}
return cmp_i64(g_sort_edges[ia].id, g_sort_edges[ib].id);
}
// idx_edges_target_type: (project, target_id, type) + rowid
static int cmp_edge_by_proj_target_type(const void *a, const void *b) {
int ia = *(const int *)a;
int ib = *(const int *)b;
int c = cmp_i64(g_sort_edges[ia].target_id, g_sort_edges[ib].target_id);
if (c) {
return c;
}
c = strcmp(safe_str(g_sort_edges[ia].type), safe_str(g_sort_edges[ib].type));
if (c) {
return c;
}
return cmp_i64(g_sort_edges[ia].id, g_sort_edges[ib].id);
}
// idx_edges_source_type: (project, source_id, type) + rowid
static int cmp_edge_by_proj_source_type(const void *a, const void *b) {
int ia = *(const int *)a;
int ib = *(const int *)b;
int c = cmp_i64(g_sort_edges[ia].source_id, g_sort_edges[ib].source_id);
if (c) {
return c;
}
c = strcmp(safe_str(g_sort_edges[ia].type), safe_str(g_sort_edges[ib].type));
if (c) {
return c;
}
return cmp_i64(g_sort_edges[ia].id, g_sort_edges[ib].id);
}
// idx_edges_url_path: (project, url_path_gen) + rowid — NULL sorts first
static int cmp_edge_by_url_path(const void *a, const void *b) {
int ia = *(const int *)a;
int ib = *(const int *)b;
const char *ua = g_sort_edges[ia].url_path;
const char *ub = g_sort_edges[ib].url_path;
bool na = (!ua || ua[0] == '\0');
bool nb = (!ub || ub[0] == '\0');
if (na && nb) {
return cmp_i64(g_sort_edges[ia].id, g_sort_edges[ib].id);
}
if (na) {
return CBM_NOT_FOUND;
}
if (nb) {
return SERIAL_SIZE_INT8;
}
int c = strcmp(ua, ub);
if (c) {
return c;
}
return cmp_i64(g_sort_edges[ia].id, g_sort_edges[ib].id);
}
// autoindex_edges_1: UNIQUE(source_id, target_id, type, local_name_gen) + rowid (#768)
static int cmp_edge_by_src_tgt_type(const void *a, const void *b) {
int ia = *(const int *)a;
int ib = *(const int *)b;
int c = cmp_i64(g_sort_edges[ia].source_id, g_sort_edges[ib].source_id);
if (c) {
return c;
}
c = cmp_i64(g_sort_edges[ia].target_id, g_sort_edges[ib].target_id);
if (c) {
return c;
}
c = strcmp(safe_str(g_sort_edges[ia].type), safe_str(g_sort_edges[ib].type));
if (c) {
return c;
}
c = strcmp(safe_str(g_sort_edges[ia].local_name), safe_str(g_sort_edges[ib].local_name));
if (c) {
return c;
}
return cmp_i64(g_sort_edges[ia].id, g_sort_edges[ib].id);
}
// --- Parallel sort support ---
typedef struct {
int count;
int (*cmp)(const void *, const void *);
int *perm; // output: sorted permutation array, caller frees
} SortJob;
static void *sort_worker(void *arg) {
SortJob *j = (SortJob *)arg;
j->perm = make_sorted_perm(j->count, j->cmp);
return NULL;
}
/* Edge index cell builder callback: builds one index cell from an edge. */
typedef uint8_t *(*edge_cell_fn)(const CBMDumpEdge *e, int *out_len);
static uint8_t *ecell_source(const CBMDumpEdge *e, int *out_len) {
return build_index_entry_int_text_rowid(e->source_id, e->type, e->id, out_len);
}
static uint8_t *ecell_target(const CBMDumpEdge *e, int *out_len) {
return build_index_entry_int_text_rowid(e->target_id, e->type, e->id, out_len);
}
static uint8_t *ecell_type(const CBMDumpEdge *e, int *out_len) {
return build_index_entry_2text_rowid(e->project, e->type, e->id, out_len);
}
static uint8_t *ecell_proj_target_type(const CBMDumpEdge *e, int *out_len) {
return build_index_entry_text_int_text_rowid(e->project, e->target_id, e->type, e->id, out_len);
}
static uint8_t *ecell_proj_source_type(const CBMDumpEdge *e, int *out_len) {
return build_index_entry_text_int_text_rowid(e->project, e->source_id, e->type, e->id, out_len);
}
static uint8_t *ecell_src_tgt_type(const CBMDumpEdge *e, int *out_len) {
return build_index_entry_unique_2int_2text_rowid(e->source_id, e->target_id, e->type,
safe_str(e->local_name), e->id, out_len);
}
static uint8_t *ecell_url_path(const CBMDumpEdge *e, int *out_len) {
const char *url = (e->url_path && e->url_path[0] != '\0') ? e->url_path : NULL;
RecordBuilder r;
rec_init(&r);
rec_add_text(&r, e->project);
if (url) {
rec_add_text(&r, url);
} else {
rec_add_null(&r);
}
rec_add_int(&r, e->id);
int payload_len = 0;
uint8_t *payload = rec_finalize(&r, &payload_len);
rec_free(&r);
int vlen = varint_len(payload_len);
int total = vlen + payload_len;
uint8_t *cell = (uint8_t *)malloc(total);
if (!cell) {
free(payload);
*out_len = 0;
return NULL;
}
int pos = put_varint(cell, payload_len);
memcpy(cell + pos, payload, payload_len);
free(payload);
*out_len = total;
return cell;
}
/* Build an edge index from a pre-sorted permutation using a cell builder callback. */
static uint32_t build_edge_index_sorted(FILE *fp, uint32_t *next_page, CBMDumpEdge *edges,
int edge_count, int *perm, edge_cell_fn cell_fn) {
if (edge_count <= 0) {
return write_index_btree(fp, next_page, NULL, NULL, 0);
}
if (!perm) {
return 0;
}
uint8_t **idx_cells = (uint8_t **)malloc(edge_count * sizeof(uint8_t *));
int *idx_lens = (int *)malloc(edge_count * sizeof(int));
if (!idx_cells || !idx_lens) {
free(perm);
free(idx_cells);
free(idx_lens);
return 0;
}
for (int i = 0; i < edge_count; i++) {
int si = perm[i];
idx_cells[i] = cell_fn(&edges[si], &idx_lens[i]);
if (!idx_cells[i]) {
for (int j = 0; j < i; j++) {
free(idx_cells[j]);
}
free(idx_cells);
free(idx_lens);
free(perm);
return 0;
}
}
free(perm);
uint32_t root = write_index_btree(fp, next_page, idx_cells, idx_lens, edge_count);
for (int i = 0; i < edge_count; i++) {
free(idx_cells[i]);
}
free(idx_cells);
free(idx_lens);
return root;
}
/* Node column getter for index building. */
typedef const char *(*node_col_fn)(const CBMDumpNode *n);
static const char *ncol_label(const CBMDumpNode *n) {
return n->label;
}
static const char *ncol_name(const CBMDumpNode *n) {
return n->name;
}
static const char *ncol_file(const CBMDumpNode *n) {
return n->file_path ? n->file_path : "";
}
static const char *ncol_qn(const CBMDumpNode *n) {
return n->qualified_name;
}
/* Build a 2-text node index from a pre-sorted permutation. Returns root page or 0. */
static uint32_t build_node_index_sorted(FILE *fp, uint32_t *next_page, CBMDumpNode *nodes,
int node_count, int *perm, node_col_fn col_fn) {
if (node_count <= 0) {
return write_index_btree(fp, next_page, NULL, NULL, 0);
}
if (!perm) {
return 0;
}
uint8_t **idx_cells = (uint8_t **)malloc(node_count * sizeof(uint8_t *));
int *idx_lens = (int *)malloc(node_count * sizeof(int));
if (!idx_cells || !idx_lens) {
free(perm);
free(idx_cells);
free(idx_lens);
return 0;
}
for (int i = 0; i < node_count; i++) {
int si = perm[i];
idx_cells[i] = build_index_entry_2text_rowid(nodes[si].project, col_fn(&nodes[si]),
nodes[si].id, &idx_lens[i]);
if (!idx_cells[i]) {
for (int j = 0; j < i; j++) {
free(idx_cells[j]);
}
free(idx_cells);
free(idx_lens);
free(perm);
return 0;
}
}
free(perm);
uint32_t root = write_index_btree(fp, next_page, idx_cells, idx_lens, node_count);
for (int i = 0; i < node_count; i++) {
free(idx_cells[i]);
}
free(idx_cells);
free(idx_lens);
return root;
}
// --- Main entry point ---
/* Write context passed to sub-phases of cbm_write_db. */
typedef struct {
FILE *fp;
uint32_t next_page;
const char *project;
const char *root_path;
const char *indexed_at;
CBMDumpNode *nodes;
int node_count;
CBMDumpEdge *edges;
int edge_count;
CBMDumpVector *vectors;
int vector_count;
CBMDumpTokenVec *token_vecs;
int token_vec_count;
} write_db_ctx_t;
/* Callback type for building a record from an item at index i. */
typedef uint8_t *(*build_record_fn)(const void *items, int i, int *out_len);
typedef int64_t (*get_rowid_fn)(const void *items, int i);
/* Write a streaming B-tree table from count items, or an empty table if count == 0. */
static int write_one_table(write_db_ctx_t *w, uint32_t *root, const void *items, int count,
build_record_fn build_rec, get_rowid_fn get_id) {
if (count <= 0 || !items) {
*root = write_table_btree(w->fp, &w->next_page, NULL, NULL, NULL, 0, false);
return 0;
}
PageBuilder pb;
pb_init(&pb, w->fp, w->next_page, false);
for (int i = 0; i < count; i++) {
int rec_len;
uint8_t *rec = build_rec(items, i, &rec_len);
if (!rec) {
return ERR_WRITE_FAILED;
}
int64_t rowid = get_id(items, i);
int64_t prev_id = i > 0 ? get_id(items, i - SKIP_ONE) : 0;
pb_add_table_cell_with_flush(&pb, rowid, rec, rec_len, prev_id);
free(rec);
}
*root = pb_finalize_table(&pb, &w->next_page, get_id(items, count - SKIP_ONE));
return 0;
}
/* Adapter functions for write_one_table (nodes are written via the streaming
* PageBuilder in cbm_writer_append_nodes, so no node adapter is needed here). */
static uint8_t *adapt_build_edge(const void *items, int i, int *out_len) {
return build_edge_record(&((const CBMDumpEdge *)items)[i], out_len);
}
static int64_t adapt_edge_id(const void *items, int i) {
return ((const CBMDumpEdge *)items)[i].id;
}
static uint8_t *adapt_build_vector(const void *items, int i, int *out_len) {
return build_vector_record(&((const CBMDumpVector *)items)[i], out_len);
}
static int64_t adapt_vector_id(const void *items, int i) {
return ((const CBMDumpVector *)items)[i].node_id;
}
static uint8_t *adapt_build_token_vec(const void *items, int i, int *out_len) {
return build_token_vec_record(&((const CBMDumpTokenVec *)items)[i], out_len);
}
static int64_t adapt_token_vec_id(const void *items, int i) {
return ((const CBMDumpTokenVec *)items)[i].id;
}
/* Phase 2: Write metadata tables (projects, file_hashes, summaries, sqlite_sequence). */
static void write_metadata_tables(write_db_ctx_t *w, uint32_t *projects_root,
uint32_t *file_hashes_root, uint32_t *summaries_root,
uint32_t *sqlite_seq_root) {
int proj_rec_len;
uint8_t *proj_rec =
build_project_record(w->project, w->indexed_at, w->root_path, &proj_rec_len);
const uint8_t *proj_recs[] = {proj_rec};
int proj_lens[] = {proj_rec_len};
int64_t proj_rowids[] = {FIRST_ROWID};
*projects_root =
write_table_btree(w->fp, &w->next_page, proj_recs, proj_lens, proj_rowids, SKIP_ONE, false);
free(proj_rec);
*file_hashes_root = write_table_btree(w->fp, &w->next_page, NULL, NULL, NULL, 0, false);
*summaries_root = write_table_btree(w->fp, &w->next_page, NULL, NULL, NULL, 0, false);
RecordBuilder r1;
RecordBuilder r2;
rec_init(&r1);
rec_add_text(&r1, "nodes");
rec_add_int(&r1, w->node_count > 0 ? w->nodes[w->node_count - SKIP_ONE].id : 0);
int seq1_len;
uint8_t *seq1 = rec_finalize(&r1, &seq1_len);
rec_free(&r1);
rec_init(&r2);
rec_add_text(&r2, "edges");
rec_add_int(&r2, w->edge_count > 0 ? w->edges[w->edge_count - SKIP_ONE].id : 0);
int seq2_len;
uint8_t *seq2 = rec_finalize(&r2, &seq2_len);
rec_free(&r2);
const uint8_t *seq_recs[] = {seq1, seq2};
int seq_lens[] = {seq1_len, seq2_len};
int64_t seq_rowids[] = {FIRST_ROWID, FIRST_DATA_PAGE};
*sqlite_seq_root =
write_table_btree(w->fp, &w->next_page, seq_recs, seq_lens, seq_rowids, PAIR_LEN, false);
free(seq1);
free(seq2);
}
/* Write the SQLite file header on page 1 with master entries. */
static void write_sqlite_file_header(uint8_t *page1, uint32_t total_pages) {
memcpy(page1, "SQLite format 3\000", 16);
put_u16(page1 + HDR_OFF_CBM_PAGE_SIZE,
CBM_PAGE_SIZE == SQLITE_MAX_PAGE_SIZE ? (uint16_t)SKIP_ONE : (uint16_t)CBM_PAGE_SIZE);
page1[HDR_OFF_WRITE_VERSION] = FILE_FORMAT;
page1[HDR_OFF_READ_VERSION] = FILE_FORMAT;
page1[HDR_OFF_RESERVED] = 0;
page1[HDR_OFF_MAX_EMBED_FRAC] = MAX_EMBED_FRACTION;
page1[HDR_OFF_MIN_EMBED_FRAC] = MIN_EMBED_FRACTION;
page1[HDR_OFF_LEAF_FRAC] = LEAF_PAYLOAD_FRACTION;
put_u32(page1 + HDR_OFF_FILE_CHANGE, SKIP_ONE);
put_u32(page1 + HDR_OFF_DB_SIZE, total_pages);
put_u32(page1 + HDR_OFF_FREELIST_TRUNK, 0);
put_u32(page1 + HDR_OFF_FREELIST_COUNT, 0);
put_u32(page1 + HDR_OFF_SCHEMA_COOKIE, SKIP_ONE);
put_u32(page1 + HDR_OFF_SCHEMA_FORMAT, SCHEMA_FORMAT);
put_u32(page1 + HDR_OFF_DEFAULT_CACHE, 0);
put_u32(page1 + HDR_OFF_AUTOVAC_TOP, 0);
put_u32(page1 + HDR_OFF_TEXT_ENCODING, SKIP_ONE);
put_u32(page1 + HDR_OFF_USER_VERSION, 0);
put_u32(page1 + HDR_OFF_INCR_VACUUM, 0);
put_u32(page1 + HDR_OFF_APP_ID, 0);
put_u32(page1 + HDR_OFF_VERSION_VALID, SKIP_ONE);
put_u32(page1 + HDR_OFF_SQLITE_VERSION, SQLITE_VERSION);
}
/* Build master records, write page 1 B-tree + file header. */
static int write_master_page1(FILE *fp, MasterEntry *master, int master_count, uint32_t next_page) {
const uint8_t **master_records = (const uint8_t **)malloc(master_count * sizeof(uint8_t *));
int *master_lens = (int *)malloc(master_count * sizeof(int));
int64_t *master_rowids = (int64_t *)malloc(master_count * sizeof(int64_t));
for (int i = 0; i < master_count; i++) {
master_rowids[i] = i + SKIP_ONE;
master_records[i] = build_master_record(&master[i], &master_lens[i]);
}
uint8_t page1[CBM_PAGE_SIZE];
memset(page1, 0, CBM_PAGE_SIZE);
int hdr = SQLITE_HEADER_SIZE;
page1[hdr] = BTREE_LEAF_TABLE;
int content_off = CBM_PAGE_SIZE;
int ptr_off = hdr + BTREE_HEADER_SIZE;
int mcell_count = 0;
for (int i = 0; i < master_count; i++) {
int cell_len = 0;
uint8_t *cell =
build_table_cell(master_rowids[i], master_records[i], master_lens[i], &cell_len);
int available = content_off - ptr_off - CELL_PTR_SIZE;
if (!cell || cell_len > available) {
free(cell);
for (int j = 0; j < master_count; j++) {
free((void *)master_records[j]);
}
free(master_records);
free(master_lens);
free(master_rowids);
return ERR_MASTER_OVERFLOW;
}
content_off -= cell_len;
memcpy(page1 + content_off, cell, cell_len);
put_u16(page1 + ptr_off, (uint16_t)content_off);
ptr_off += CELL_PTR_SIZE;
mcell_count++;
free(cell);
}
put_u16(page1 + hdr + HDR_FREEBLOCK_OFF, 0);
put_u16(page1 + hdr + HDR_CELLCOUNT_OFF, (uint16_t)mcell_count);
put_u16(page1 + hdr + HDR_CONTENT_OFF, (uint16_t)content_off);
page1[hdr + HDR_FRAGBYTES_OFF] = 0;
write_sqlite_file_header(page1, next_page - SKIP_ONE);
(void)fseek(fp, 0, SEEK_SET);
(void)fwrite(page1, SKIP_ONE, CBM_PAGE_SIZE, fp);
for (int i = 0; i < master_count; i++) {
free((void *)master_records[i]);
}
free(master_records);
free(master_lens);
free(master_rowids);
return 0;
}
/* Pad file to exact page boundary. */
static void pad_file_to_page_boundary(FILE *fp, uint32_t next_page) {
(void)fseek(fp, 0, SEEK_END);
long file_size = ftell(fp);
long expected_size = (long)(next_page - SKIP_ONE) * CBM_PAGE_SIZE;
if (file_size < expected_size) {
uint8_t zero = 0;
(void)fseek(fp, expected_size - SKIP_ONE, SEEK_SET);
(void)fwrite(&zero, SKIP_ONE, SKIP_ONE, fp);
}
}
/* Build all 4 node index B-trees. Returns 0 on success, ERR_SORT_FAILED on failure. */
static int build_node_indexes(FILE *fp, uint32_t *next_page, CBMDumpNode *nodes, int node_count,
SortJob *nsorts, uint32_t *label_root, uint32_t *name_root,
uint32_t *file_root, uint32_t *qn_root) {
*label_root =
build_node_index_sorted(fp, next_page, nodes, node_count, nsorts[0].perm, ncol_label);
*name_root = build_node_index_sorted(fp, next_page, nodes, node_count, nsorts[NSORT_NAME].perm,
ncol_name);
*file_root = build_node_index_sorted(fp, next_page, nodes, node_count, nsorts[NSORT_FILE].perm,
ncol_file);
*qn_root =
build_node_index_sorted(fp, next_page, nodes, node_count, nsorts[NSORT_QN].perm, ncol_qn);
if (node_count > 0 && (!*label_root || !*name_root || !*file_root || !*qn_root)) {
return ERR_SORT_FAILED;
}
return 0;
}
/* Build all 7 edge index B-trees. Returns 0 on success, ERR_SORT_FAILED on failure. */
static int build_edge_indexes(FILE *fp, uint32_t *next_page, CBMDumpEdge *edges, int edge_count,
SortJob *esorts, uint32_t *source_root, uint32_t *target_root,
uint32_t *type_root, uint32_t *tgt_type_root, uint32_t *src_type_root,
uint32_t *url_path_root, uint32_t *auto_root) {
*source_root =
build_edge_index_sorted(fp, next_page, edges, edge_count, esorts[0].perm, ecell_source);
*target_root = build_edge_index_sorted(fp, next_page, edges, edge_count,
esorts[ESORT_TARGET].perm, ecell_target);
*type_root = build_edge_index_sorted(fp, next_page, edges, edge_count, esorts[ESORT_TYPE].perm,
ecell_type);
*tgt_type_root = build_edge_index_sorted(
fp, next_page, edges, edge_count, esorts[ESORT_PROJ_TGT_TYPE].perm, ecell_proj_target_type);
*src_type_root = build_edge_index_sorted(
fp, next_page, edges, edge_count, esorts[ESORT_PROJ_SRC_TYPE].perm, ecell_proj_source_type);
*url_path_root = build_edge_index_sorted(fp, next_page, edges, edge_count,
esorts[ESORT_URL_PATH].perm, ecell_url_path);
*auto_root = build_edge_index_sorted(fp, next_page, edges, edge_count,
esorts[ESORT_SRC_TGT_TYPE].perm, ecell_src_tgt_type);
if (edge_count > 0 && (!*source_root || !*target_root || !*type_root || !*tgt_type_root ||
!*src_type_root || !*url_path_root || !*auto_root)) {
return ERR_SORT_FAILED;
}
return 0;
}
/* Launch parallel sort threads for all index permutations. */
static void parallel_sort_indexes(SortJob *nsorts, int n_node, SortJob *esorts, int n_edge) {
cbm_thread_t st[TOTAL_SORT_THREADS];
int nt = 0;
for (int i = 0; i < n_node; i++) {
if (nsorts[i].count > 0) {
cbm_thread_create(&st[nt++], 0, sort_worker, &nsorts[i]);
}
}
for (int i = 0; i < n_edge; i++) {
if (esorts[i].count > 0) {
cbm_thread_create(&st[nt++], 0, sort_worker, &esorts[i]);
}
}
for (int i = 0; i < nt; i++) {
cbm_thread_join(&st[i]);
}
}
/* Write everything after the nodes table: the edges/vectors/token_vectors data
* tables, metadata tables, all indexes, and the sqlite_master page-1 + file
* header. `nodes_root` is the root of the already-written nodes table. Closes
* w->fp before returning (success or error). */
static int write_db_after_nodes(write_db_ctx_t *w, uint32_t nodes_root) {
FILE *fp = w->fp;
CBMDumpNode *nodes = w->nodes;
int node_count = w->node_count;
CBMDumpEdge *edges = w->edges;
int edge_count = w->edge_count;
// Phase 1 (cont.): remaining data tables (edge + vector + token_vector records)
CBM_PROF_START(t_data);
uint32_t edges_root;
uint32_t vectors_root;
uint32_t token_vecs_root;
int rc =
write_one_table(w, &edges_root, w->edges, w->edge_count, adapt_build_edge, adapt_edge_id);
if (rc != 0) {
(void)fclose(fp);
return rc;
}
rc = write_one_table(w, &vectors_root, w->vectors, w->vector_count, adapt_build_vector,
adapt_vector_id);
if (rc != 0) {
(void)fclose(fp);
return rc;
}
rc = write_one_table(w, &token_vecs_root, w->token_vecs, w->token_vec_count,
adapt_build_token_vec, adapt_token_vec_id);
if (rc != 0) {
(void)fclose(fp);
return rc;
}
CBM_PROF_END_N("write_db", "1_data_tables", t_data, node_count + edge_count);
// Phase 2: Metadata tables (projects, file_hashes, summaries, sqlite_sequence)
CBM_PROF_START(t_meta);
uint32_t projects_root;
uint32_t file_hashes_root;
uint32_t summaries_root;
uint32_t sqlite_seq_root;
write_metadata_tables(w, &projects_root, &file_hashes_root, &summaries_root, &sqlite_seq_root);
uint32_t next_page = w->next_page;
CBM_PROF_END("write_db", "2_metadata_tables", t_meta);
// --- Build indexes (all sorted by key columns before writing) ---
// Set sort contexts for qsort comparators.
g_sort_nodes = nodes;
g_sort_edges = edges;
// Parallel sort: all 11 index permutations sorted simultaneously.
// Sorting is O(N log N) per index — the dominant CPU cost in index building.
// Cell building + B-tree writing remains serial (sequential page allocation).
SortJob nsorts[] = {
{node_count, cmp_node_by_label, NULL},
{node_count, cmp_node_by_name, NULL},
{node_count, cmp_node_by_file, NULL},
{node_count, cmp_node_by_qn, NULL},
};
SortJob esorts[] = {
{edge_count, cmp_edge_by_source_type, NULL},
{edge_count, cmp_edge_by_target_type, NULL},
{edge_count, cmp_edge_by_type, NULL},
{edge_count, cmp_edge_by_proj_target_type, NULL},
{edge_count, cmp_edge_by_proj_source_type, NULL},
{edge_count, cmp_edge_by_url_path, NULL},
{edge_count, cmp_edge_by_src_tgt_type, NULL},
};
CBM_PROF_START(t_sort);
parallel_sort_indexes(nsorts, NODE_SORT_THREADS, esorts, EDGE_SORT_THREADS);
CBM_PROF_END_N("write_db", "3_parallel_sort_indexes", t_sort, node_count + edge_count);
/* Phase 4-5: Build node + edge index B-trees */
CBM_PROF_START(t_node_idx);
uint32_t idx_nodes_label_root;
uint32_t idx_nodes_name_root;
uint32_t idx_nodes_file_root;
uint32_t autoindex_nodes_root;
int nrc = build_node_indexes(fp, &next_page, nodes, node_count, nsorts, &idx_nodes_label_root,
&idx_nodes_name_root, &idx_nodes_file_root, &autoindex_nodes_root);
CBM_PROF_END_N("write_db", "4_node_indexes_seq", t_node_idx, node_count * NODE_SORT_THREADS);
if (nrc != 0) {
(void)fclose(fp);
return nrc;
}
CBM_PROF_START(t_edge_idx);
uint32_t idx_edges_source_root;
uint32_t idx_edges_target_root;
uint32_t idx_edges_type_root;
uint32_t idx_edges_target_type_root;
uint32_t idx_edges_source_type_root;
uint32_t idx_edges_url_path_root;
uint32_t autoindex_edges_root;
int erc = build_edge_indexes(fp, &next_page, edges, edge_count, esorts, &idx_edges_source_root,
&idx_edges_target_root, &idx_edges_type_root,
&idx_edges_target_type_root, &idx_edges_source_type_root,
&idx_edges_url_path_root, &autoindex_edges_root);
CBM_PROF_END_N("write_db", "5_edge_indexes_seq", t_edge_idx, edge_count * EDGE_SORT_THREADS);
if (erc != 0) {
(void)fclose(fp);
return erc;
}
// Autoindex for projects(name TEXT PK) — single text column
uint32_t autoindex_projects_root;
{
// 1 row: project name
RecordBuilder r;
rec_init(&r);
rec_add_text(&r, w->project);
rec_add_int(&r, FIRST_ROWID); /* rowid */
int plen = 0;
uint8_t *payload = rec_finalize(&r, &plen);
rec_free(&r);
int vl = varint_len(plen);
int total = vl + plen;
uint8_t *cell = (uint8_t *)malloc(total);
int pos = put_varint(cell, plen);
memcpy(cell + pos, payload, plen);
free(payload);
uint8_t *cells_arr[] = {cell};
int lens_arr[] = {total};
autoindex_projects_root = write_index_btree(fp, &next_page, cells_arr, lens_arr, SKIP_ONE);
free(cell);
}
// Autoindex for file_hashes(project, rel_path PK) — empty (0 rows)
uint32_t autoindex_file_hashes_root = write_index_btree(fp, &next_page, NULL, NULL, 0);
// Autoindex for project_summaries(project TEXT PK) — empty (0 rows)
uint32_t autoindex_summaries_root = write_index_btree(fp, &next_page, NULL, NULL, 0);
// --- sqlite_master table (page 1) ---
// This must be written last because it references root pages of all other tables/indexes.
// CRITICAL: sqlite_master entries must follow standard SQLite ordering:
// table → autoindex → user indexes → next table → autoindex → user indexes → ...
// SQLite's schema loader expects autoindexes immediately after their table.
// Mis-ordering causes rootpage mapping corruption in the schema cache.
MasterEntry master[] = {
{"table", "projects", "projects", projects_root,
"CREATE TABLE projects (\n\t\tname TEXT PRIMARY KEY,\n\t\tindexed_at TEXT NOT "
"NULL,\n\t\troot_path TEXT NOT NULL\n\t)"},
{"index", "sqlite_autoindex_projects_1", "projects", autoindex_projects_root, NULL},
{"table", "file_hashes", "file_hashes", file_hashes_root,
"CREATE TABLE file_hashes (\n\t\tproject TEXT NOT NULL REFERENCES projects(name) ON "
"DELETE CASCADE,\n\t\trel_path TEXT NOT NULL,\n\t\tsha256 TEXT NOT NULL,\n\t\tmtime_ns "
"INTEGER NOT NULL DEFAULT 0,\n\t\tsize INTEGER NOT NULL DEFAULT 0,\n\t\tPRIMARY KEY "
"(project, rel_path)\n\t)"},
{"index", "sqlite_autoindex_file_hashes_1", "file_hashes", autoindex_file_hashes_root,
NULL},
{"table", "nodes", "nodes", nodes_root,
"CREATE TABLE nodes (\n\t\tid INTEGER PRIMARY KEY AUTOINCREMENT,\n\t\tproject TEXT NOT "
"NULL REFERENCES projects(name) ON DELETE CASCADE,\n\t\tlabel TEXT NOT NULL,\n\t\tname "
"TEXT NOT NULL,\n\t\tqualified_name TEXT NOT NULL,\n\t\tfile_path TEXT DEFAULT "
"'',\n\t\tstart_line INTEGER DEFAULT 0,\n\t\tend_line INTEGER DEFAULT 0,\n\t\tproperties "
"TEXT DEFAULT '{}',\n\t\tUNIQUE(project, qualified_name)\n\t)"},
{"index", "sqlite_autoindex_nodes_1", "nodes", autoindex_nodes_root, NULL},
{"index", "idx_nodes_label", "nodes", idx_nodes_label_root,
"CREATE INDEX idx_nodes_label ON nodes(project, label)"},
{"index", "idx_nodes_name", "nodes", idx_nodes_name_root,
"CREATE INDEX idx_nodes_name ON nodes(project, name)"},
{"index", "idx_nodes_file", "nodes", idx_nodes_file_root,
"CREATE INDEX idx_nodes_file ON nodes(project, file_path)"},
// local_name_gen + widened UNIQUE (#768): must stay semantically
// identical to init_schema in src/store/store.c, and the hand-built
// sqlite_autoindex_edges_1 (cmp_edge_by_src_tgt_type +
// ecell_src_tgt_type) must produce exactly the values SQLite computes
// for local_name_gen, or integrity_check fails on the dumped DB.
{"table", "edges", "edges", edges_root,
"CREATE TABLE edges (\n\t\tid INTEGER PRIMARY KEY AUTOINCREMENT,\n\t\tproject TEXT NOT "
"NULL REFERENCES projects(name) ON DELETE CASCADE,\n\t\tsource_id INTEGER NOT NULL "
"REFERENCES nodes(id) ON DELETE CASCADE,\n\t\ttarget_id INTEGER NOT NULL REFERENCES "
"nodes(id) ON DELETE CASCADE,\n\t\ttype TEXT NOT NULL,\n\t\tproperties TEXT DEFAULT "
"'{}',\n\t\turl_path_gen TEXT GENERATED ALWAYS AS "
"(json_extract(properties,'$.url_path')),\n\t\tlocal_name_gen TEXT GENERATED ALWAYS AS "
"(CASE WHEN type='IMPORTS' THEN coalesce(json_extract(properties,'$.local_name'),'') "
"ELSE '' END),\n\t\tUNIQUE(source_id, target_id, type, local_name_gen)\n\t)"},
{"index", "sqlite_autoindex_edges_1", "edges", autoindex_edges_root, NULL},
{"index", "idx_edges_source", "edges", idx_edges_source_root,
"CREATE INDEX idx_edges_source ON edges(source_id, type)"},
{"index", "idx_edges_target", "edges", idx_edges_target_root,
"CREATE INDEX idx_edges_target ON edges(target_id, type)"},
{"index", "idx_edges_type", "edges", idx_edges_type_root,
"CREATE INDEX idx_edges_type ON edges(project, type)"},
{"index", "idx_edges_target_type", "edges", idx_edges_target_type_root,
"CREATE INDEX idx_edges_target_type ON edges(project, target_id, type)"},
{"index", "idx_edges_source_type", "edges", idx_edges_source_type_root,
"CREATE INDEX idx_edges_source_type ON edges(project, source_id, type)"},
{"index", "idx_edges_url_path", "edges", idx_edges_url_path_root,
"CREATE INDEX idx_edges_url_path ON edges(project, url_path_gen)"},
{"table", "project_summaries", "project_summaries", summaries_root,
"CREATE TABLE project_summaries (\n\t\t\tproject TEXT PRIMARY KEY,\n\t\t\tsummary TEXT "
"NOT NULL,\n\t\t\tsource_hash TEXT NOT NULL,\n\t\t\tcreated_at TEXT NOT "
"NULL,\n\t\t\tupdated_at TEXT NOT NULL\n\t\t)"},
{"index", "sqlite_autoindex_project_summaries_1", "project_summaries",
autoindex_summaries_root, NULL},
{"table", "node_vectors", "node_vectors", vectors_root,
"CREATE TABLE node_vectors (\n\t\tnode_id INTEGER PRIMARY KEY,\n\t\tproject TEXT NOT "
"NULL,\n\t\tvector BLOB NOT NULL\n\t)"},
{"table", "token_vectors", "token_vectors", token_vecs_root,
"CREATE TABLE token_vectors (\n\t\tid INTEGER PRIMARY KEY,\n\t\tproject "
"TEXT NOT NULL,\n\t\ttoken TEXT NOT NULL,\n\t\tvector BLOB NOT NULL,\n\t\tidf INTEGER "
"NOT NULL\n\t)"},
{"table", "sqlite_sequence", "sqlite_sequence", sqlite_seq_root,
"CREATE TABLE sqlite_sequence(name,seq)"},
};
int master_count = sizeof(master) / sizeof(master[0]);
int rc2 = write_master_page1(fp, master, master_count, next_page);
if (rc2 != 0) {
(void)fclose(fp);
return rc2;
}
pad_file_to_page_boundary(fp, next_page);
(void)fclose(fp);
return 0;
}
// --- Streaming writer (incremental bulk node-table append) ---
struct cbm_db_writer {
write_db_ctx_t wc; // fp + next_page carried across calls; arrays filled at finalize
PageBuilder nodes_pb; // persistent nodes-table builder (leaves flush as they fill)
int64_t last_node_rowid; // last appended node id (prev_rowid for the next cell)
int64_t node_rows_written;
int err; // sticky error
};
cbm_db_writer_t *cbm_writer_open(const char *path) {
/* Installing a fresh DB generation: drop the destination's leftover
* -wal/-shm or a crashed session's WAL gets replayed on top of the
* new file at the next open (#897). */
cbm_remove_db_sidecars(path);
/* cbm_fopen, not raw fopen: the cache dir lives under the user profile,
* and an ANSI-CP fopen fails to create the DB on non-ASCII Windows
* profiles — the reported phase=dump failure (#996). Everything around
* this call (cbm_mkdir_p, sqlite3_open_v2 reopen) is already wide-safe. */
FILE *fp = cbm_fopen(path, "wb");
if (!fp) {
return NULL;
}
cbm_db_writer_t *w = (cbm_db_writer_t *)calloc(CBM_ALLOC_ONE, sizeof(*w));
if (!w) {
(void)fclose(fp);
return NULL;
}
w->wc.fp = fp;
w->wc.next_page = FIRST_DATA_PAGE;
/* Nodes are never page 1 (page 1 is sqlite_master, written at finalize). */
pb_init(&w->nodes_pb, fp, FIRST_DATA_PAGE, false);
return w;
}
int cbm_writer_append_nodes(cbm_db_writer_t *w, const CBMDumpNode *nodes, int count) {
if (!w) {
return CBM_NOT_FOUND;
}
if (w->err) {
return w->err;
}
for (int i = 0; i < count; i++) {
int rec_len;
uint8_t *rec = build_node_record(&nodes[i], &rec_len);
if (!rec) {
w->err = ERR_WRITE_FAILED;
return w->err;
}
/* prev_rowid is the previous node's id (0 for the very first), matching
* the one-shot write_one_table loop — so output is byte-identical. */
pb_add_table_cell_with_flush(&w->nodes_pb, nodes[i].id, rec, rec_len, w->last_node_rowid);
free(rec);
w->last_node_rowid = nodes[i].id;
w->node_rows_written++;
}
return 0;
}
int cbm_writer_finalize(cbm_db_writer_t *w, const char *project, const char *root_path,
const char *indexed_at, CBMDumpNode *nodes, int node_count,
CBMDumpEdge *edges, int edge_count, CBMDumpVector *vectors,
int vector_count, CBMDumpTokenVec *token_vecs, int token_vec_count) {
if (!w) {
return CBM_NOT_FOUND;
}
int err = w->err;
uint32_t nodes_root = 0;
if (err == 0) {
if (w->node_rows_written == 0) {
pb_free(&w->nodes_pb);
nodes_root = write_table_btree(w->wc.fp, &w->wc.next_page, NULL, NULL, NULL, 0, false);
} else {
nodes_root = pb_finalize_table(&w->nodes_pb, &w->wc.next_page, w->last_node_rowid);
}
}
w->wc.project = project;
w->wc.root_path = root_path;
w->wc.indexed_at = indexed_at;
w->wc.nodes = nodes;
w->wc.node_count = node_count;
w->wc.edges = edges;
w->wc.edge_count = edge_count;
w->wc.vectors = vectors;
w->wc.vector_count = vector_count;
w->wc.token_vecs = token_vecs;
w->wc.token_vec_count = token_vec_count;
write_db_ctx_t wc = w->wc; /* value copy survives free(w) */
free(w);
if (err != 0) {
(void)fclose(wc.fp); /* wc is a value copy, valid after free(w) */
return err;
}
return write_db_after_nodes(&wc, nodes_root);
}
int cbm_write_db(const char *path, const char *project, const char *root_path,
const char *indexed_at, CBMDumpNode *nodes, int node_count, CBMDumpEdge *edges,
int edge_count, CBMDumpVector *vectors, int vector_count,
CBMDumpTokenVec *token_vecs, int token_vec_count) {
/* One-shot = open + append all nodes in a single batch + finalize.
* Produces byte-identical output to the former monolithic writer. */
cbm_db_writer_t *w = cbm_writer_open(path);
if (!w) {
return CBM_NOT_FOUND;
}
(void)cbm_writer_append_nodes(w, nodes,
node_count); /* error recorded in w, handled by finalize */
return cbm_writer_finalize(w, project, root_path, indexed_at, nodes, node_count, edges,
edge_count, vectors, vector_count, token_vecs, token_vec_count);
}