search for standard sites pub-search.waow.tech
search zig blog atproto

fix: keep local DB serving during full re-sync, add cover_image migration

The previous deploy caused a full re-sync which set local DB to
not-ready, forcing all searches through Turso (10-60s response times).

- Only set not-ready on first-ever sync (empty DB)
- Skip DELETE when re-syncing — INSERT OR REPLACE updates in place
- Add ALTER TABLE migration for cover_image on existing local DBs

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

+25 -7
+1
backend/src/db/LocalDb.zig
··· 243 243 // migrations for existing local DBs 244 244 c.exec("ALTER TABLE documents ADD COLUMN indexed_at TEXT", .{}) catch {}; 245 245 c.exec("ALTER TABLE documents ADD COLUMN embedded_at TEXT", .{}) catch {}; 246 + c.exec("ALTER TABLE documents ADD COLUMN cover_image TEXT DEFAULT ''", .{}) catch {}; 246 247 } 247 248 248 249 /// Row adapter matching result.Row interface (column-indexed access)
+24 -7
backend/src/db/sync.zig
··· 9 9 10 10 const BATCH_SIZE = 500; 11 11 12 - /// Full sync: fetch all data from Turso and populate local SQLite 13 - /// Local stays not-ready during sync — search goes to Turso (no mutex there). 14 - /// When sync completes, local becomes ready and search uses the fast local path. 12 + /// Full sync: fetch all data from Turso and populate local SQLite. 13 + /// Uses INSERT OR REPLACE so existing data stays queryable during sync. 14 + /// Only marks not-ready on first-ever sync (empty DB). 15 15 pub fn fullSync(turso: *Client, local: *LocalDb) !void { 16 16 std.debug.print("sync: starting full sync...\n", .{}); 17 17 18 - local.setReady(false); 18 + const conn = local.getConn() orelse return error.LocalNotOpen; 19 + 20 + // check if we have existing data — if so, keep serving while syncing 21 + const has_data = blk: { 22 + local.lock(); 23 + defer local.unlock(); 24 + const row = conn.row("SELECT COUNT(*) FROM documents", .{}) catch break :blk false; 25 + if (row) |r| { 26 + defer r.deinit(); 27 + break :blk r.int(0) > 0; 28 + } 29 + break :blk false; 30 + }; 19 31 20 - const conn = local.getConn() orelse return error.LocalNotOpen; 32 + if (!has_data) { 33 + // first-ever sync: nothing to serve, mark not-ready 34 + local.setReady(false); 21 35 22 - // clear existing data 23 - { 36 + // clear tables for clean initial sync 24 37 local.lock(); 25 38 defer local.unlock(); 26 39 conn.exec("DELETE FROM documents_fts", .{}) catch {}; ··· 28 41 conn.exec("DELETE FROM publications_fts", .{}) catch {}; 29 42 conn.exec("DELETE FROM publications", .{}) catch {}; 30 43 conn.exec("DELETE FROM document_tags", .{}) catch {}; 44 + } else { 45 + // re-sync: keep serving existing data while we refresh in-place 46 + // INSERT OR REPLACE will update rows; stale data is acceptable 47 + std.debug.print("sync: local has data, keeping ready during re-sync\n", .{}); 31 48 } 32 49 33 50 // sync documents in batches — fetch from Turso unlocked, write to local with brief lock