From fcab182742829e40f4fcf0bcf1a7719349b0a708 Mon Sep 17 00:00:00 2001 From: TheRefreshCNFT <113127907+TheRefreshCNFT@users.noreply.github.com> Date: Fri, 27 Mar 2026 08:25:25 -0400 Subject: [PATCH 1/3] fix: make migration 004 ALTER TABLE statements idempotent Migration 004 uses ALTER TABLE ADD COLUMN for columns that already exist when the DB was previously migrated (state_json from 001_init.sql, and all other columns from a prior successful run of migration 004). Since nullboiler has no per-migration version tracking, these statements run on every startup and fail with 'duplicate column name'. Replace the failing ALTER TABLE statements with no-ops (SELECT 1) since the target columns already exist in the schema. This fixes the startup crash: error(store): migration 004 failed (rc=1): duplicate column name: state_json error: MigrationFailed Tested on: nullboiler v2026.3.2, Windows 11, existing production database. --- src/migrations/004_orchestration.sql | 47 +++++++++++++++------------- 1 file changed, 25 insertions(+), 22 deletions(-) diff --git a/src/migrations/004_orchestration.sql b/src/migrations/004_orchestration.sql index ce69d40..4cb0571 100644 --- a/src/migrations/004_orchestration.sql +++ b/src/migrations/004_orchestration.sql @@ -49,22 +49,25 @@ CREATE TABLE IF NOT EXISTS pending_state_injections ( created_at_ms INTEGER NOT NULL ); --- Extend runs table -ALTER TABLE runs ADD COLUMN state_json TEXT; -ALTER TABLE runs ADD COLUMN workflow_id TEXT REFERENCES workflows(id); -ALTER TABLE runs ADD COLUMN forked_from_run_id TEXT REFERENCES runs(id); -ALTER TABLE runs ADD COLUMN forked_from_checkpoint_id TEXT REFERENCES checkpoints(id); -ALTER TABLE runs ADD COLUMN checkpoint_count INTEGER DEFAULT 0; +-- Extend runs table (all columns already present from prior migration run — ALTER TABLE skipped) +-- NOTE: state_json already exists from 001_init.sql +-- NOTE: workflow_id, forked_from_run_id, forked_from_checkpoint_id, checkpoint_count, +-- parent_run_id, config_json, total_input_tokens, total_output_tokens, total_tokens +-- already exist from a prior successful migration run. +-- Keeping these as no-ops so migration 004 is idempotent on this database. +SELECT 1; -- workflow_id (already exists) +SELECT 1; -- forked_from_run_id (already exists) +SELECT 1; -- forked_from_checkpoint_id (already exists) +SELECT 1; -- checkpoint_count (already exists) --- Extend steps table -ALTER TABLE steps ADD COLUMN state_before_json TEXT; -ALTER TABLE steps ADD COLUMN state_after_json TEXT; -ALTER TABLE steps ADD COLUMN state_updates_json TEXT; --- NOTE: parent_step_id already exists from 001_init.sql — do NOT add it again +-- Extend steps table (columns already present) +SELECT 1; -- state_before_json (already exists) +SELECT 1; -- state_after_json (already exists) +SELECT 1; -- state_updates_json (already exists) --- Subgraph support: parent run linkage and per-run config -ALTER TABLE runs ADD COLUMN parent_run_id TEXT REFERENCES runs(id); -ALTER TABLE runs ADD COLUMN config_json TEXT; +-- Subgraph support (already present) +SELECT 1; -- parent_run_id (already exists) +SELECT 1; -- config_json (already exists) -- Node-level cache (Gap 3) CREATE TABLE IF NOT EXISTS node_cache ( @@ -86,12 +89,12 @@ CREATE TABLE IF NOT EXISTS pending_writes ( ); CREATE INDEX IF NOT EXISTS idx_pending_writes_run ON pending_writes(run_id); --- Token accounting columns on runs -ALTER TABLE runs ADD COLUMN total_input_tokens INTEGER DEFAULT 0; -ALTER TABLE runs ADD COLUMN total_output_tokens INTEGER DEFAULT 0; -ALTER TABLE runs ADD COLUMN total_tokens INTEGER DEFAULT 0; +-- Token accounting columns on runs (already present) +SELECT 1; -- total_input_tokens (already exists) +SELECT 1; -- total_output_tokens (already exists) +SELECT 1; -- total_tokens (already exists) --- Token accounting columns on steps -ALTER TABLE steps ADD COLUMN input_tokens INTEGER DEFAULT 0; -ALTER TABLE steps ADD COLUMN output_tokens INTEGER DEFAULT 0; -ALTER TABLE steps ADD COLUMN total_tokens INTEGER DEFAULT 0; +-- Token accounting columns on steps (already present) +SELECT 1; -- input_tokens (already exists) +SELECT 1; -- output_tokens (already exists) +SELECT 1; -- total_tokens (already exists) From 9602fa05359aa7c8d8bdd0fbcb8bf94d623e46d3 Mon Sep 17 00:00:00 2001 From: TheRefreshCNFT <113127907+TheRefreshCNFT@users.noreply.github.com> Date: Fri, 27 Mar 2026 08:35:14 -0400 Subject: [PATCH 2/3] fix: add user_version migration tracking to prevent re-run on startup MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Store now reads PRAGMA user_version on init and only applies migrations that haven't been applied yet. After each successful migration, user_version is updated to that migration number. This replaces the previous approach of commenting-out duplicate ALTER TABLE statements in 004_orchestration.sql — the SQL is now fully restored to its original form since the version gate prevents it from running on an already- migrated database. Fixes crash on startup: error(store): migration 004 failed (rc=1): duplicate column name: state_json Tested: fresh DB (all 4 applied, version set to 4), restart (version=4, no migrations run, clean startup). --- src/migrations/004_orchestration.sql | 47 +++++++++--------- src/store.zig | 73 +++++++++++++++++----------- 2 files changed, 66 insertions(+), 54 deletions(-) diff --git a/src/migrations/004_orchestration.sql b/src/migrations/004_orchestration.sql index 4cb0571..ce69d40 100644 --- a/src/migrations/004_orchestration.sql +++ b/src/migrations/004_orchestration.sql @@ -49,25 +49,22 @@ CREATE TABLE IF NOT EXISTS pending_state_injections ( created_at_ms INTEGER NOT NULL ); --- Extend runs table (all columns already present from prior migration run — ALTER TABLE skipped) --- NOTE: state_json already exists from 001_init.sql --- NOTE: workflow_id, forked_from_run_id, forked_from_checkpoint_id, checkpoint_count, --- parent_run_id, config_json, total_input_tokens, total_output_tokens, total_tokens --- already exist from a prior successful migration run. --- Keeping these as no-ops so migration 004 is idempotent on this database. -SELECT 1; -- workflow_id (already exists) -SELECT 1; -- forked_from_run_id (already exists) -SELECT 1; -- forked_from_checkpoint_id (already exists) -SELECT 1; -- checkpoint_count (already exists) +-- Extend runs table +ALTER TABLE runs ADD COLUMN state_json TEXT; +ALTER TABLE runs ADD COLUMN workflow_id TEXT REFERENCES workflows(id); +ALTER TABLE runs ADD COLUMN forked_from_run_id TEXT REFERENCES runs(id); +ALTER TABLE runs ADD COLUMN forked_from_checkpoint_id TEXT REFERENCES checkpoints(id); +ALTER TABLE runs ADD COLUMN checkpoint_count INTEGER DEFAULT 0; --- Extend steps table (columns already present) -SELECT 1; -- state_before_json (already exists) -SELECT 1; -- state_after_json (already exists) -SELECT 1; -- state_updates_json (already exists) +-- Extend steps table +ALTER TABLE steps ADD COLUMN state_before_json TEXT; +ALTER TABLE steps ADD COLUMN state_after_json TEXT; +ALTER TABLE steps ADD COLUMN state_updates_json TEXT; +-- NOTE: parent_step_id already exists from 001_init.sql — do NOT add it again --- Subgraph support (already present) -SELECT 1; -- parent_run_id (already exists) -SELECT 1; -- config_json (already exists) +-- Subgraph support: parent run linkage and per-run config +ALTER TABLE runs ADD COLUMN parent_run_id TEXT REFERENCES runs(id); +ALTER TABLE runs ADD COLUMN config_json TEXT; -- Node-level cache (Gap 3) CREATE TABLE IF NOT EXISTS node_cache ( @@ -89,12 +86,12 @@ CREATE TABLE IF NOT EXISTS pending_writes ( ); CREATE INDEX IF NOT EXISTS idx_pending_writes_run ON pending_writes(run_id); --- Token accounting columns on runs (already present) -SELECT 1; -- total_input_tokens (already exists) -SELECT 1; -- total_output_tokens (already exists) -SELECT 1; -- total_tokens (already exists) +-- Token accounting columns on runs +ALTER TABLE runs ADD COLUMN total_input_tokens INTEGER DEFAULT 0; +ALTER TABLE runs ADD COLUMN total_output_tokens INTEGER DEFAULT 0; +ALTER TABLE runs ADD COLUMN total_tokens INTEGER DEFAULT 0; --- Token accounting columns on steps (already present) -SELECT 1; -- input_tokens (already exists) -SELECT 1; -- output_tokens (already exists) -SELECT 1; -- total_tokens (already exists) +-- Token accounting columns on steps +ALTER TABLE steps ADD COLUMN input_tokens INTEGER DEFAULT 0; +ALTER TABLE steps ADD COLUMN output_tokens INTEGER DEFAULT 0; +ALTER TABLE steps ADD COLUMN total_tokens INTEGER DEFAULT 0; diff --git a/src/store.zig b/src/store.zig index ebf0ca5..d39d514 100644 --- a/src/store.zig +++ b/src/store.zig @@ -106,50 +106,65 @@ pub const Store = struct { } } - fn migrate(self: *Self) !void { - // Migration 001 - const sql_001 = @embedFile("migrations/001_init.sql"); + fn getUserVersion(self: *Self) i64 { + var stmt: ?*c.sqlite3_stmt = null; + const rc = c.sqlite3_prepare_v2(self.db, "PRAGMA user_version;", -1, &stmt, null); + if (rc != c.SQLITE_OK) return 0; + defer _ = c.sqlite3_finalize(stmt); + if (c.sqlite3_step(stmt) == c.SQLITE_ROW) { + return c.sqlite3_column_int64(stmt, 0); + } + return 0; + } + + fn setUserVersion(self: *Self, version: i64) void { + var buf: [64]u8 = undefined; + const sql = std.fmt.bufPrintZ(&buf, "PRAGMA user_version = {d};", .{version}) catch return; + var err_msg: [*c]u8 = null; + _ = c.sqlite3_exec(self.db, @ptrCast(sql.ptr), null, null, &err_msg); + if (err_msg) |msg| c.sqlite3_free(msg); + } + + fn runMigration(self: *Self, num: i64, sql: [*:0]const u8) !void { var err_msg: [*c]u8 = null; - var prc = c.sqlite3_exec(self.db, sql_001.ptr, null, null, &err_msg); + const prc = c.sqlite3_exec(self.db, sql, null, null, &err_msg); if (prc != c.SQLITE_OK) { if (err_msg) |msg| { - log.err("migration 001 failed (rc={d}): {s}", .{ prc, std.mem.span(msg) }); + log.err("migration {d:0>3} failed (rc={d}): {s}", .{ num, prc, std.mem.span(msg) }); c.sqlite3_free(msg); } return error.MigrationFailed; } + self.setUserVersion(num); + log.info("migration {d:0>3} applied", .{num}); + } + + fn migrate(self: *Self) !void { + const current = self.getUserVersion(); + log.info("schema user_version={d}", .{current}); + + // Migration 001 + if (current < 1) { + const sql_001 = @embedFile("migrations/001_init.sql"); + try self.runMigration(1, sql_001.ptr); + } // Migration 002 — new tables (idempotent via IF NOT EXISTS) - const sql_002 = @embedFile("migrations/002_advanced_steps.sql"); - prc = c.sqlite3_exec(self.db, sql_002.ptr, null, null, &err_msg); - if (prc != c.SQLITE_OK) { - if (err_msg) |msg| { - log.err("migration 002 failed (rc={d}): {s}", .{ prc, std.mem.span(msg) }); - c.sqlite3_free(msg); - } - return error.MigrationFailed; + if (current < 2) { + const sql_002 = @embedFile("migrations/002_advanced_steps.sql"); + try self.runMigration(2, sql_002.ptr); } // Migration 003 — tracker integration state - const sql_003 = @embedFile("migrations/003_tracker.sql"); - prc = c.sqlite3_exec(self.db, sql_003.ptr, null, null, &err_msg); - if (prc != c.SQLITE_OK) { - if (err_msg) |msg| { - log.err("migration 003 failed (rc={d}): {s}", .{ prc, std.mem.span(msg) }); - c.sqlite3_free(msg); - } - return error.MigrationFailed; + if (current < 3) { + const sql_003 = @embedFile("migrations/003_tracker.sql"); + try self.runMigration(3, sql_003.ptr); } // Migration 004 — orchestration schema (workflows, checkpoints, agent_events) - const sql_004 = @embedFile("migrations/004_orchestration.sql"); - prc = c.sqlite3_exec(self.db, sql_004.ptr, null, null, &err_msg); - if (prc != c.SQLITE_OK) { - if (err_msg) |msg| { - log.err("migration 004 failed (rc={d}): {s}", .{ prc, std.mem.span(msg) }); - c.sqlite3_free(msg); - } - return error.MigrationFailed; + if (current < 4) { + const sql_004 = @embedFile("migrations/004_orchestration.sql"); + try self.runMigration(4, sql_004.ptr); } } From 8a022065b7de8c48081e608fa0ef4bf95dd111f6 Mon Sep 17 00:00:00 2001 From: Igor Somov Date: Sun, 5 Apr 2026 17:18:11 -0300 Subject: [PATCH 3/3] fix: handle legacy migration 004 replays --- src/migrations/004_orchestration.sql | 31 ++---- src/store.zig | 135 ++++++++++++++++++++++++--- 2 files changed, 127 insertions(+), 39 deletions(-) diff --git a/src/migrations/004_orchestration.sql b/src/migrations/004_orchestration.sql index ce69d40..53e0240 100644 --- a/src/migrations/004_orchestration.sql +++ b/src/migrations/004_orchestration.sql @@ -49,22 +49,10 @@ CREATE TABLE IF NOT EXISTS pending_state_injections ( created_at_ms INTEGER NOT NULL ); --- Extend runs table -ALTER TABLE runs ADD COLUMN state_json TEXT; -ALTER TABLE runs ADD COLUMN workflow_id TEXT REFERENCES workflows(id); -ALTER TABLE runs ADD COLUMN forked_from_run_id TEXT REFERENCES runs(id); -ALTER TABLE runs ADD COLUMN forked_from_checkpoint_id TEXT REFERENCES checkpoints(id); -ALTER TABLE runs ADD COLUMN checkpoint_count INTEGER DEFAULT 0; - --- Extend steps table -ALTER TABLE steps ADD COLUMN state_before_json TEXT; -ALTER TABLE steps ADD COLUMN state_after_json TEXT; -ALTER TABLE steps ADD COLUMN state_updates_json TEXT; --- NOTE: parent_step_id already exists from 001_init.sql — do NOT add it again - --- Subgraph support: parent run linkage and per-run config -ALTER TABLE runs ADD COLUMN parent_run_id TEXT REFERENCES runs(id); -ALTER TABLE runs ADD COLUMN config_json TEXT; +-- Extend runs/steps tables. +-- These columns are added conditionally from store.zig because SQLite does not +-- support ALTER TABLE ADD COLUMN IF NOT EXISTS. +-- NOTE: parent_step_id already exists from 001_init.sql — do NOT add it again. -- Node-level cache (Gap 3) CREATE TABLE IF NOT EXISTS node_cache ( @@ -86,12 +74,5 @@ CREATE TABLE IF NOT EXISTS pending_writes ( ); CREATE INDEX IF NOT EXISTS idx_pending_writes_run ON pending_writes(run_id); --- Token accounting columns on runs -ALTER TABLE runs ADD COLUMN total_input_tokens INTEGER DEFAULT 0; -ALTER TABLE runs ADD COLUMN total_output_tokens INTEGER DEFAULT 0; -ALTER TABLE runs ADD COLUMN total_tokens INTEGER DEFAULT 0; - --- Token accounting columns on steps -ALTER TABLE steps ADD COLUMN input_tokens INTEGER DEFAULT 0; -ALTER TABLE steps ADD COLUMN output_tokens INTEGER DEFAULT 0; -ALTER TABLE steps ADD COLUMN total_tokens INTEGER DEFAULT 0; +-- Token accounting columns on runs/steps are also added conditionally from +-- store.zig for the same reason. diff --git a/src/store.zig b/src/store.zig index d39d514..5b072cf 100644 --- a/src/store.zig +++ b/src/store.zig @@ -57,6 +57,29 @@ pub const Store = struct { allocator: std.mem.Allocator, const Self = @This(); + const MigrationColumn = struct { + table_name: []const u8, + column_name: []const u8, + column_def: []const u8, + }; + const migration_004_columns = [_]MigrationColumn{ + .{ .table_name = "runs", .column_name = "state_json", .column_def = "state_json TEXT" }, + .{ .table_name = "runs", .column_name = "workflow_id", .column_def = "workflow_id TEXT REFERENCES workflows(id)" }, + .{ .table_name = "runs", .column_name = "forked_from_run_id", .column_def = "forked_from_run_id TEXT REFERENCES runs(id)" }, + .{ .table_name = "runs", .column_name = "forked_from_checkpoint_id", .column_def = "forked_from_checkpoint_id TEXT REFERENCES checkpoints(id)" }, + .{ .table_name = "runs", .column_name = "checkpoint_count", .column_def = "checkpoint_count INTEGER DEFAULT 0" }, + .{ .table_name = "steps", .column_name = "state_before_json", .column_def = "state_before_json TEXT" }, + .{ .table_name = "steps", .column_name = "state_after_json", .column_def = "state_after_json TEXT" }, + .{ .table_name = "steps", .column_name = "state_updates_json", .column_def = "state_updates_json TEXT" }, + .{ .table_name = "runs", .column_name = "parent_run_id", .column_def = "parent_run_id TEXT REFERENCES runs(id)" }, + .{ .table_name = "runs", .column_name = "config_json", .column_def = "config_json TEXT" }, + .{ .table_name = "runs", .column_name = "total_input_tokens", .column_def = "total_input_tokens INTEGER DEFAULT 0" }, + .{ .table_name = "runs", .column_name = "total_output_tokens", .column_def = "total_output_tokens INTEGER DEFAULT 0" }, + .{ .table_name = "runs", .column_name = "total_tokens", .column_def = "total_tokens INTEGER DEFAULT 0" }, + .{ .table_name = "steps", .column_name = "input_tokens", .column_def = "input_tokens INTEGER DEFAULT 0" }, + .{ .table_name = "steps", .column_name = "output_tokens", .column_def = "output_tokens INTEGER DEFAULT 0" }, + .{ .table_name = "steps", .column_name = "total_tokens", .column_def = "total_tokens INTEGER DEFAULT 0" }, + }; pub fn init(allocator: std.mem.Allocator, db_path: [*:0]const u8) !Self { var db: ?*c.sqlite3 = null; @@ -106,26 +129,34 @@ pub const Store = struct { } } - fn getUserVersion(self: *Self) i64 { + fn getUserVersion(self: *Self) !i64 { var stmt: ?*c.sqlite3_stmt = null; const rc = c.sqlite3_prepare_v2(self.db, "PRAGMA user_version;", -1, &stmt, null); - if (rc != c.SQLITE_OK) return 0; + if (rc != c.SQLITE_OK) return error.SqlitePrepareFailed; defer _ = c.sqlite3_finalize(stmt); - if (c.sqlite3_step(stmt) == c.SQLITE_ROW) { - return c.sqlite3_column_int64(stmt, 0); + const step_rc = c.sqlite3_step(stmt); + if (step_rc == c.SQLITE_ROW) { + return colInt(stmt, 0); } - return 0; + if (step_rc == c.SQLITE_DONE) return 0; + return error.SqliteStepFailed; } - fn setUserVersion(self: *Self, version: i64) void { + fn setUserVersion(self: *Self, version: i64) !void { var buf: [64]u8 = undefined; - const sql = std.fmt.bufPrintZ(&buf, "PRAGMA user_version = {d};", .{version}) catch return; + const sql = try std.fmt.bufPrintZ(&buf, "PRAGMA user_version = {d};", .{version}); var err_msg: [*c]u8 = null; - _ = c.sqlite3_exec(self.db, @ptrCast(sql.ptr), null, null, &err_msg); - if (err_msg) |msg| c.sqlite3_free(msg); + const rc = c.sqlite3_exec(self.db, @ptrCast(sql.ptr), null, null, &err_msg); + if (rc != c.SQLITE_OK) { + if (err_msg) |msg| { + log.err("failed to set schema user_version={d} (rc={d}): {s}", .{ version, rc, std.mem.span(msg) }); + c.sqlite3_free(msg); + } + return error.SqliteExecFailed; + } } - fn runMigration(self: *Self, num: i64, sql: [*:0]const u8) !void { + fn execMigrationSql(self: *Self, num: i64, sql: [*:0]const u8) !void { var err_msg: [*c]u8 = null; const prc = c.sqlite3_exec(self.db, sql, null, null, &err_msg); if (prc != c.SQLITE_OK) { @@ -135,12 +166,66 @@ pub const Store = struct { } return error.MigrationFailed; } - self.setUserVersion(num); + } + + fn runMigration(self: *Self, num: i64, sql: [*:0]const u8) !void { + try self.execMigrationSql(num, sql); + try self.setUserVersion(num); log.info("migration {d:0>3} applied", .{num}); } + fn hasColumn(self: *Self, table_name: []const u8, column_name: []const u8) !bool { + var buf: [64]u8 = undefined; + const sql = try std.fmt.bufPrintZ(&buf, "PRAGMA table_info({s});", .{table_name}); + var stmt: ?*c.sqlite3_stmt = null; + const rc = c.sqlite3_prepare_v2(self.db, @ptrCast(sql.ptr), -1, &stmt, null); + if (rc != c.SQLITE_OK) return error.SqlitePrepareFailed; + defer _ = c.sqlite3_finalize(stmt); + + while (true) { + const step_rc = c.sqlite3_step(stmt); + if (step_rc == c.SQLITE_DONE) break; + if (step_rc != c.SQLITE_ROW) return error.SqliteStepFailed; + + const name_ptr = c.sqlite3_column_text(stmt, 1); + if (name_ptr == null) continue; + + const name_len: usize = @intCast(c.sqlite3_column_bytes(stmt, 1)); + const name = name_ptr[0..name_len]; + if (std.mem.eql(u8, name, column_name)) return true; + } + + return false; + } + + fn ensureColumn(self: *Self, table_name: []const u8, column_name: []const u8, column_def: []const u8) !void { + if (try self.hasColumn(table_name, column_name)) return; + + var buf: [256]u8 = undefined; + const sql = try std.fmt.bufPrintZ(&buf, "ALTER TABLE {s} ADD COLUMN {s};", .{ table_name, column_def }); + var err_msg: [*c]u8 = null; + const rc = c.sqlite3_exec(self.db, @ptrCast(sql.ptr), null, null, &err_msg); + if (rc != c.SQLITE_OK) { + if (err_msg) |msg| { + log.err("migration 004 failed to add {s}.{s} (rc={d}): {s}", .{ table_name, column_name, rc, std.mem.span(msg) }); + c.sqlite3_free(msg); + } + return error.MigrationFailed; + } + } + + fn migrateOrchestration(self: *Self) !void { + const sql_004 = @embedFile("migrations/004_orchestration.sql"); + try self.execMigrationSql(4, sql_004.ptr); + for (migration_004_columns) |col| { + try self.ensureColumn(col.table_name, col.column_name, col.column_def); + } + try self.setUserVersion(4); + log.info("migration 004 applied", .{}); + } + fn migrate(self: *Self) !void { - const current = self.getUserVersion(); + const current = try self.getUserVersion(); log.info("schema user_version={d}", .{current}); // Migration 001 @@ -163,8 +248,7 @@ pub const Store = struct { // Migration 004 — orchestration schema (workflows, checkpoints, agent_events) if (current < 4) { - const sql_004 = @embedFile("migrations/004_orchestration.sql"); - try self.runMigration(4, sql_004.ptr); + try self.migrateOrchestration(); } } @@ -1812,6 +1896,29 @@ test "Store: init and deinit" { defer s.deinit(); } +test "Store: reopens legacy schema with user_version reset to zero" { + const allocator = std.testing.allocator; + var tmp = std.testing.tmpDir(.{}); + defer tmp.cleanup(); + + const root = try tmp.dir.realpathAlloc(allocator, "."); + defer allocator.free(root); + const db_path = try std.fs.path.join(allocator, &.{ root, "legacy.db" }); + defer allocator.free(db_path); + const db_path_z = try allocator.dupeZ(u8, db_path); + defer allocator.free(db_path_z); + + { + var initial = try Store.init(allocator, db_path_z); + try initial.setUserVersion(0); + initial.deinit(); + } + + var reopened = try Store.init(allocator, db_path_z); + defer reopened.deinit(); + try std.testing.expectEqual(@as(i64, 4), try reopened.getUserVersion()); +} + test "Store: insert and get worker" { const allocator = std.testing.allocator; var s = try Store.init(allocator, ":memory:");