Skip to content

Commit 4be5d6f

Browse files
committed
Always run schema migration before DB operations
Existing databases created before schema v3 (missing symbol_kind, symbol_visibility, symbol_scope, symbol_arity columns) would crash with SqlPrepareFailed on search/update/watch. Now initSchema runs unconditionally after opening any DB, ensuring migrations apply.
1 parent cdc5214 commit 4be5d6f

4 files changed

Lines changed: 80 additions & 15 deletions

File tree

src/main.zig

Lines changed: 20 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -320,6 +320,7 @@ pub fn main() !void {
320320
try ensureParentDir(settings.db_path);
321321
const db = try storage.openFileWithVec(allocator, settings.db_path);
322322
defer storage.close(db);
323+
_ = try storage.initSchema(allocator, db, .{ .embedding_dim = settings.embedding_dim });
323324

324325
var http_client = ollama.StdHttpTransport.init(allocator);
325326
defer http_client.deinit();
@@ -387,6 +388,9 @@ pub fn main() !void {
387388
const db = try storage.openFileWithVec(allocator, settings.db_path);
388389
defer storage.close(db);
389390

391+
// Always run schema init/migration so older DBs get new columns
392+
_ = try storage.initSchema(allocator, db, .{ .embedding_dim = settings.embedding_dim });
393+
390394
var stderr_buf: [4096]u8 = undefined;
391395
var stderr_writer = std.fs.File.stderr().writer(&stderr_buf);
392396
const stderr = &stderr_writer.interface;
@@ -415,9 +419,6 @@ pub fn main() !void {
415419
.model = settings.ollama_model,
416420
};
417421

418-
// Need to init schema before indexing into a fresh DB
419-
_ = try storage.initSchema(allocator, db, .{ .embedding_dim = settings.embedding_dim });
420-
421422
_ = try performFullIndex(
422423
allocator,
423424
db,
@@ -561,7 +562,7 @@ pub fn main() !void {
561562
const input_text = try readStdin(allocator);
562563
defer allocator.free(input_text);
563564
try runReplaceSymbol(allocator, file_path, pattern, input_text, stdout);
564-
tryReindexFile(allocator, settings.db_path, settings.root_path, file_path, registry);
565+
tryReindexFile(allocator, settings.db_path, settings.root_path, file_path, registry, settings.embedding_dim);
565566
try stdout.flush();
566567
},
567568
.insert_after => {
@@ -572,7 +573,7 @@ pub fn main() !void {
572573
const input_text = try readStdin(allocator);
573574
defer allocator.free(input_text);
574575
try runInsertAfter(allocator, file_path, pattern, input_text, stdout);
575-
tryReindexFile(allocator, settings.db_path, settings.root_path, file_path, registry);
576+
tryReindexFile(allocator, settings.db_path, settings.root_path, file_path, registry, settings.embedding_dim);
576577
try stdout.flush();
577578
},
578579
.insert_before => {
@@ -583,7 +584,7 @@ pub fn main() !void {
583584
const input_text = try readStdin(allocator);
584585
defer allocator.free(input_text);
585586
try runInsertBefore(allocator, file_path, pattern, input_text, stdout);
586-
tryReindexFile(allocator, settings.db_path, settings.root_path, file_path, registry);
587+
tryReindexFile(allocator, settings.db_path, settings.root_path, file_path, registry, settings.embedding_dim);
587588
try stdout.flush();
588589
},
589590
.replace_lines => {
@@ -596,7 +597,7 @@ pub fn main() !void {
596597
const input_text = try readStdin(allocator);
597598
defer allocator.free(input_text);
598599
try runReplaceLines(allocator, file_path, from_ref, to_ref, input_text, stdout);
599-
tryReindexFile(allocator, settings.db_path, settings.root_path, file_path, registry);
600+
tryReindexFile(allocator, settings.db_path, settings.root_path, file_path, registry, settings.embedding_dim);
600601
try stdout.flush();
601602
},
602603
.insert_at => {
@@ -607,7 +608,7 @@ pub fn main() !void {
607608
const input_text = try readStdin(allocator);
608609
defer allocator.free(input_text);
609610
try runInsertAt(allocator, file_path, ref, input_text, stdout);
610-
tryReindexFile(allocator, settings.db_path, settings.root_path, file_path, registry);
611+
tryReindexFile(allocator, settings.db_path, settings.root_path, file_path, registry, settings.embedding_dim);
611612
try stdout.flush();
612613
},
613614
.replace_content => {
@@ -619,7 +620,7 @@ pub fn main() !void {
619620
const input_text = try readStdin(allocator);
620621
defer allocator.free(input_text);
621622
try runReplaceContent(allocator, file_path, needle, parsed.regex_mode, parsed.replace_all, input_text, stdout);
622-
tryReindexFile(allocator, settings.db_path, settings.root_path, file_path, registry);
623+
tryReindexFile(allocator, settings.db_path, settings.root_path, file_path, registry, settings.embedding_dim);
623624
try stdout.flush();
624625
},
625626
.references => {
@@ -637,7 +638,7 @@ pub fn main() !void {
637638
exitWithError("error: rename requires --file <path>\n");
638639
const new_name = parsed.rename_to orelse
639640
exitWithError("error: rename requires --to <new_name>\n");
640-
try runRename(allocator, file_path, pattern, new_name, parsed.output, parsed.dry_run, settings.db_path, settings.root_path, registry, settings.lsp_overrides, stdout);
641+
try runRename(allocator, file_path, pattern, new_name, parsed.output, parsed.dry_run, settings.db_path, settings.root_path, registry, settings.lsp_overrides, settings.embedding_dim, stdout);
641642
try stdout.flush();
642643
},
643644
.mcp_serve => {
@@ -739,6 +740,7 @@ pub fn main() !void {
739740
// Open existing DB or create new one (don't destroy existing index)
740741
const db = try storage.openFileWithVec(allocator, settings.db_path);
741742
defer storage.close(db);
743+
_ = try storage.initSchema(allocator, db, .{ .embedding_dim = settings.embedding_dim });
742744

743745
var http_client = ollama.StdHttpTransport.init(allocator);
744746
defer http_client.deinit();
@@ -1361,7 +1363,7 @@ fn ensureWeightsWithDefaults(path: []const u8) !void {
13611363
/// Opens the DB, calls indexer.reindexFile, and closes the DB.
13621364
/// If no index exists or any step fails, the edit is still successful —
13631365
/// the background watcher will eventually catch up.
1364-
fn tryReindexFile(allocator: std.mem.Allocator, db_path: []const u8, root_path: []const u8, file_path: []const u8, registry: plugin.Registry) void {
1366+
fn tryReindexFile(allocator: std.mem.Allocator, db_path: []const u8, root_path: []const u8, file_path: []const u8, registry: plugin.Registry, embedding_dim: usize) void {
13651367
var stderr_buf: [4096]u8 = undefined;
13661368
var stderr_writer = std.fs.File.stderr().writer(&stderr_buf);
13671369
const stderr = &stderr_writer.interface;
@@ -1372,6 +1374,11 @@ fn tryReindexFile(allocator: std.mem.Allocator, db_path: []const u8, root_path:
13721374
return;
13731375
};
13741376
defer storage.close(db);
1377+
_ = storage.initSchema(allocator, db, .{ .embedding_dim = embedding_dim }) catch |err| {
1378+
_ = stderr.print("warning: reindex skipped (schema migration failed): {}\n", .{err}) catch {};
1379+
_ = stderr.flush() catch {};
1380+
return;
1381+
};
13751382
const abs_root = std.fs.cwd().realpathAlloc(allocator, root_path) catch |err| {
13761383
_ = stderr.print("warning: reindex skipped (could not resolve root): {}\n", .{err}) catch {};
13771384
_ = stderr.flush() catch {};
@@ -2418,6 +2425,7 @@ pub fn runRename(
24182425
root_path: []const u8,
24192426
registry: plugin.Registry,
24202427
lsp_overrides: []const config.LspOverride,
2428+
embedding_dim: usize,
24212429
writer: *std.Io.Writer,
24222430
) !void {
24232431
const loc = try locateSymbol(allocator, file_path, pattern) orelse {
@@ -2562,7 +2570,7 @@ pub fn runRename(
25622570
try writer.print("warning: failed to apply edits to {s}: {}\n", .{ path, err });
25632571
continue;
25642572
};
2565-
tryReindexFile(allocator, db_path, root_path, path, registry);
2573+
tryReindexFile(allocator, db_path, root_path, path, registry, embedding_dim);
25662574
}
25672575
}
25682576
}

src/mcp.zig

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -260,12 +260,13 @@ fn callTool(allocator: std.mem.Allocator, name: []const u8, args: ?std.json.Obje
260260
const pattern = getArg(args, "pattern") orelse return error.MissingArgument;
261261
const to = getArg(args, "to") orelse return error.MissingArgument;
262262
const dry_run = getArgBool(args, "dry_run");
263-
main.runRename(allocator, file, pattern, to, .json, dry_run, settings.db_path, settings.root_path, plugin.defaultRegistry(), settings.lsp_overrides, &out.writer) catch return error.ToolFailed;
263+
main.runRename(allocator, file, pattern, to, .json, dry_run, settings.db_path, settings.root_path, plugin.defaultRegistry(), settings.lsp_overrides, settings.embedding_dim, &out.writer) catch return error.ToolFailed;
264264
} else if (std.mem.eql(u8, name, "search") or std.mem.eql(u8, name, "query")) {
265265
const query = getArg(args, "query") orelse return error.MissingArgument;
266266
try ensureParentDir(settings.db_path);
267267
const db = storage.openFileWithVec(allocator, settings.db_path) catch return error.ToolFailed;
268268
defer storage.close(db);
269+
_ = storage.initSchema(allocator, db, .{ .embedding_dim = settings.embedding_dim }) catch return error.ToolFailed;
269270

270271
var http_client = ollama.StdHttpTransport.init(allocator);
271272
defer http_client.deinit();
@@ -281,7 +282,6 @@ fn callTool(allocator: std.mem.Allocator, name: []const u8, args: ?std.json.Obje
281282
.base_url = settings.ollama_url,
282283
.model = settings.ollama_model,
283284
};
284-
_ = storage.initSchema(allocator, db, .{ .embedding_dim = settings.embedding_dim }) catch return error.ToolFailed;
285285
_ = indexer.indexAll(allocator, db, settings.root_path, plugin.defaultRegistry(), embedder_for_index.embedder(), .{
286286
.embedding_dim = settings.embedding_dim,
287287
.batch_size = settings.batch_size,

src/search.zig

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2886,6 +2886,62 @@ test "bm25 column weights rank name match above doc_comment match" {
28862886
try std.testing.expectEqualStrings("hash", sr.results[0].symbol.name);
28872887
}
28882888

2889+
test "search works against v2 schema DB after initSchema migration" {
2890+
// Simulates opening an existing DB created before schema v3 (no symbol_kind etc.)
2891+
// initSchema must add the missing columns so search SQL can prepare.
2892+
const allocator = std.testing.allocator;
2893+
const db = try storage.openMemoryWithVec(allocator);
2894+
defer storage.close(db);
2895+
2896+
// Create v2-style schema WITHOUT symbol_kind/visibility/scope/arity columns
2897+
const v2_meta: [:0]const u8 = "CREATE TABLE meta (key TEXT PRIMARY KEY, value TEXT NOT NULL);\x00";
2898+
const v2_symbols: [:0]const u8 =
2899+
"CREATE TABLE symbols (" ++
2900+
"id INTEGER PRIMARY KEY, " ++
2901+
"lang TEXT NOT NULL, " ++
2902+
"file_path TEXT NOT NULL, " ++
2903+
"start_line INTEGER NOT NULL, " ++
2904+
"start_hash TEXT, " ++
2905+
"end_line INTEGER NOT NULL, " ++
2906+
"end_hash TEXT, " ++
2907+
"symbol_name TEXT NOT NULL, " ++
2908+
"signature TEXT, " ++
2909+
"doc_comment TEXT" ++
2910+
");\x00";
2911+
const v2_unique: [:0]const u8 = "CREATE UNIQUE INDEX IF NOT EXISTS idx_symbols_unique ON symbols (file_path, start_line, end_line, symbol_name);\x00";
2912+
const v2_vec: [:0]const u8 = "CREATE VIRTUAL TABLE embeddings USING vec0(embedding float[2]);\x00";
2913+
2914+
_ = sqlite.sqlite3_exec(db, v2_meta, null, null, null);
2915+
_ = sqlite.sqlite3_exec(db, v2_symbols, null, null, null);
2916+
_ = sqlite.sqlite3_exec(db, v2_unique, null, null, null);
2917+
_ = sqlite.sqlite3_exec(db, v2_vec, null, null, null);
2918+
2919+
// Insert a symbol directly via SQL (v2 schema — no metadata columns)
2920+
const insert_sym: [:0]const u8 =
2921+
"INSERT INTO symbols (lang, file_path, start_line, end_line, symbol_name, signature) " ++
2922+
"VALUES ('zig', 'src/test.zig', 1, 10, 'compress', 'pub fn compress(data: []const u8) []u8');\x00";
2923+
_ = sqlite.sqlite3_exec(db, insert_sym, null, null, null);
2924+
const insert_emb: [:0]const u8 = "INSERT INTO embeddings (rowid, embedding) VALUES (1, X'0000000000000000');\x00";
2925+
_ = sqlite.sqlite3_exec(db, insert_emb, null, null, null);
2926+
2927+
// Run initSchema to migrate v2 → v3 (adds metadata columns).
2928+
// This is what main.zig now does unconditionally before search.
2929+
_ = try storage.initSchema(allocator, db, .{ .embedding_dim = 2 });
2930+
2931+
// Search should succeed after migration
2932+
var fake = FakeEmbedder{ .vector = &[_]f32{ 0.0, 0.0 } };
2933+
const sr = try search(allocator, db, fake.embedder(), "compress", .{
2934+
.top_n = 5,
2935+
.mode = .vector,
2936+
.score_dropoff = 0,
2937+
.min_score = 0,
2938+
});
2939+
defer freeResults(allocator, sr.results);
2940+
2941+
try std.testing.expect(sr.results.len >= 1);
2942+
try std.testing.expectEqualStrings("compress", sr.results[0].symbol.name);
2943+
}
2944+
28892945
test "likeCandidates orders exact name > prefix > substring > signature-only" {
28902946
const allocator = std.testing.allocator;
28912947
const db = try storage.openMemoryWithVec(allocator);

src/server.zig

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ pub fn serve(allocator: std.mem.Allocator, settings: Settings) !void {
5050
try ensureParentDir(settings.db_path);
5151
const db = try storage.openFileWithVec(allocator, settings.db_path);
5252
defer storage.close(db);
53+
_ = try storage.initSchema(allocator, db, .{ .embedding_dim = settings.embedding_dim });
5354

5455
var http_client = ollama.StdHttpTransport.init(allocator);
5556
defer http_client.deinit();
@@ -653,7 +654,7 @@ fn handleRequest(
653654

654655
var out: std.io.Writer.Allocating = .init(allocator);
655656
defer out.deinit();
656-
main.runRename(allocator, file_path, pattern, new_name, .json, dry_run, settings.db_path, settings.root_path, plugin.defaultRegistry(), settings.lsp_overrides, &out.writer) catch {
657+
main.runRename(allocator, file_path, pattern, new_name, .json, dry_run, settings.db_path, settings.root_path, plugin.defaultRegistry(), settings.lsp_overrides, settings.embedding_dim, &out.writer) catch {
657658
try req.respond("{\"error\":\"rename failed\"}\n", .{ .status = .internal_server_error });
658659
return;
659660
};

0 commit comments

Comments
 (0)