项目文件夹

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

2311 行
83 KiB
C

此文件含有模棱两可的 Unicode 字符
此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。
// 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);
}