Files
Martin Vogel 6012f4626e fix(graph): IMPORTS edges keyed by local_name across buffer, store and dump
Distills PR #770 (graph-buffer dedup key) and completes the edge-uniqueness
contract it left partial: the contract lives in THREE places, and changing
only the buffer ships databases that violate their own UNIQUE constraint
(PRAGMA integrity_check: non-unique entry in sqlite_autoindex_edges_1).

A single 'import { A, B } from ./lib' produced ONE IMPORTS edge: the graph
buffer dedups edges on (source_id, target_id, type) and merge-replaces
properties on collision, so the second symbol silently overwrote the first.
pass_calls.c, pass_usages.c, pass_semantic.c and pass_lsp_cross.c parse one
local_name per IMPORTS edge for cross-file resolution, so the dropped
symbol's calls failed to resolve too — not just "who imports X" queries.

Changes, kept in sync across all three sites:

- graph_buffer.c (from #770): make_edge_key() folds local_name into the
  dedup key for IMPORTS edges only; all three key call-sites updated.
  Hardened beyond #770: EDGE_KEY_BUF bumped to 256 and oversized
  local_names are re-keyed with an FNV-1a hash of the full name instead
  of being silently truncated (two long names sharing a prefix must not
  collide back into one edge).
- store.c: edges gains local_name_gen, a VIRTUAL generated column
  (IMPORTS -> coalesce(json_extract(properties,'$.local_name'),''),
  else '' — NOT NULL because NULLs never conflict in a UNIQUE index);
  uniqueness widened to UNIQUE(source_id, target_id, type,
  local_name_gen) and the insert upsert's conflict target matches.
  init_schema probes pre-#768 DBs (no local_name_gen) and fails the
  open: SQLite cannot ALTER a table constraint in place, and an
  unopenable DB already takes the existing repair path — full index
  deletes + rebuilds, artifact import refuses and falls back to a
  reindex. Read-only query opens skip init_schema and keep working.
- sqlite_writer.c/.h: dump DDL matches the widened schema; the hand-built
  sqlite_autoindex_edges_1 comparator and entry builder include the
  local_name column; CBMDumpEdge carries local_name extracted via real
  JSON parsing (yyjson) so index entries match json_extract's unescaped
  values exactly, per the idx_edges_url_path precedent.
- artifact.h: CBM_ARTIFACT_SCHEMA_VERSION 1 -> 2 so old binaries refuse
  artifacts carrying the widened schema (their 3-column conflict target
  can no longer prepare against it).

Tests (reproduce-first, all red on the unfixed code):
- gbuf tier: multi-symbol import -> 2 IMPORTS edges; long-local_name
  truncation guard.
- store tier: distinct local_name coexists as 2 rows, same local_name
  still upserts, non-IMPORTS dedup unchanged.
- writer tier: dumped DB passes integrity_check with 2 sibling imports
  and exposes matching local_name_gen values.
- end-to-end: TS fixture through the real pipeline -> 2 queryable
  IMPORTS edges AND integrity_check ok; with only the buffer half of
  the fix this test still fails on integrity_check, proving the schema
  half is required.

Closes #768.

Co-authored-by: Alexandros Pappas <11921291+apappas1129@users.noreply.github.com>
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-07-03 20:16:39 +02:00

94 lines
4.2 KiB
C

#ifndef CBM_SQLITE_WRITER_H
#define CBM_SQLITE_WRITER_H
#include <stdint.h>
// --- Input structs (flat, borrowed strings) ---
typedef struct {
int64_t id; // sequential ID (1..N), assigned by Go
const char *project;
const char *label;
const char *name;
const char *qualified_name;
const char *file_path;
int start_line;
int end_line;
const char *properties; // JSON string
} CBMDumpNode;
typedef struct {
int64_t id; // sequential ID (1..M), assigned by Go
const char *project;
int64_t source_id; // final sequential ID (1..N)
int64_t target_id; // final sequential ID (1..N)
const char *type;
const char *properties; // JSON string
const char *url_path; // extracted from properties by Go (for idx_edges_url_path)
const char *local_name; // for IMPORTS edges: the UNESCAPED
// json_extract(properties,'$.local_name') value; ""/NULL
// otherwise. Feeds sqlite_autoindex_edges_1 — must match
// what SQLite computes for the local_name_gen column or
// integrity_check reports the row missing from the index.
} CBMDumpEdge;
typedef struct {
int64_t node_id; // final sequential ID (matches nodes.id)
const char *project;
const uint8_t *vector; // int8-quantized vector blob
int vector_len; // length in bytes (e.g. 256 for d=256)
} CBMDumpVector;
typedef struct {
int64_t id; // sequential ID (1..T)
const char *project;
const char *token; // the token string
const uint8_t *vector; // int8-quantized enriched RI vector blob
int vector_len; // length in bytes (e.g. 256 for d=256)
float idf; // inverse document frequency weight
} CBMDumpTokenVec;
// --- Public API ---
// Write a complete SQLite .db file from sorted in-memory data.
// Constructs B-tree pages directly — no SQL parser, no INSERTs.
// Returns 0 on success, non-zero on error.
// vectors/vector_count and token_vecs/token_vec_count may be NULL/0.
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);
// --- Streaming writer: incremental bulk node-table append ---
//
// Lets the indexer flush node rows (including heavy `properties`) to the DB in
// batches, mid-pipeline, freeing heavy memory — while preserving the direct-page
// bulk write (no per-row INSERTs). The nodes table is built across append calls
// via a persistent page builder; everything else (edges, vectors, metadata,
// indexes, sqlite_master) is written at finalize. cbm_write_db() above is a
// one-shot wrapper over this API (open -> append all nodes -> finalize) and
// produces byte-identical output.
//
// Usage: w = cbm_writer_open(path);
// cbm_writer_append_nodes(w, batch, n) x N (ascending, contiguous ids);
// cbm_writer_finalize(w, ...); // consumes + frees w, closes the file.
typedef struct cbm_db_writer cbm_db_writer_t;
cbm_db_writer_t *cbm_writer_open(const char *path);
// Append a batch of node records. Heavy `properties` are consumed here, so the
// caller may free them after this returns. Node ids must be ascending and
// contiguous across the whole sequence of append calls. Returns 0 on success.
int cbm_writer_append_nodes(cbm_db_writer_t *w, const CBMDumpNode *nodes, int count);
// Finalize: build the nodes-table interior, write edges/vectors/token_vectors,
// metadata, all indexes, and sqlite_master + header. The node/edge/vector arrays
// supply the (light) columns the index builders sort on; node `properties` are
// NOT read here (already written during append). Frees w and closes the file.
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);
#endif // CBM_SQLITE_WRITER_H