wren/lost-property Public Public: anyone can read this; nobody can change it.
workbenchthis machine
Migrate the old note table instead of crashing on it
Production (and now the preview built from it) still had the note table from the starter template: id, body, created_at, no item_id. CREATE TABLE IF NOT EXISTS left that shape in place, so every query touching note.item_id threw and / answered 500. A table missing item_id is now renamed to note_legacy once, keeping its rows, before the current schema is created. Co-Authored-By: Claude Sonnet 5 <[email protected]>
2 files
changed +40
−0
src/db.test.ts added +30
| @@ -0,0 +1,30 @@ | ||
| 1 | import { expect, test } from "bun:test"; | |
| 2 | import { Database } from "bun:sqlite"; | |
| 3 | import { mkdtempSync } from "node:fs"; | |
| 4 | import { tmpdir } from "node:os"; | |
| 5 | import { join } from "node:path"; | |
| 6 | ||
| 7 | // Seed a data directory with the note table an earlier version of this app | |
| 8 | // left behind: no item_id, so it does not fit the current schema. | |
| 9 | const dir = mkdtempSync(join(tmpdir(), "app-")); | |
| 10 | process.env.FORGE_APP_DATA = dir; | |
| 11 | const seed = new Database(join(dir, "app.db")); | |
| 12 | seed.exec( | |
| 13 | "CREATE TABLE note (id INTEGER PRIMARY KEY, body TEXT NOT NULL, created_at TEXT NOT NULL DEFAULT (datetime('now')))", | |
| 14 | ); | |
| 15 | seed.exec("INSERT INTO note (body) VALUES ('an old note from before items existed')"); | |
| 16 | seed.close(); | |
| 17 | ||
| 18 | const { db, createItem, addNote, listNotes, listItems } = await import("./db.ts"); | |
| 19 | ||
| 20 | test("moves an old, incompatible note table aside instead of crashing", () => { | |
| 21 | const row = db.query<{ body: string }, []>("SELECT body FROM note_legacy").get(); | |
| 22 | expect(row?.body).toBe("an old note from before items existed"); | |
| 23 | }); | |
| 24 | ||
| 25 | test("the collection works normally after migrating", () => { | |
| 26 | const id = createItem("a test item"); | |
| 27 | addNote(id, "sighting", "hello"); | |
| 28 | expect(listNotes(id)).toHaveLength(1); | |
| 29 | expect(listItems()[0]?.note_count).toBe(1); | |
| 30 | }); |
src/db.ts +10
| @@ -13,6 +13,16 @@ const dataDir = process.env.FORGE_APP_DATA ?? "./data"; | ||
| 13 | 13 | mkdirSync(dataDir, { recursive: true }); |
| 14 | 14 | export const db = new Database(join(dataDir, "app.db")); |
| 15 | 15 | db.exec("PRAGMA journal_mode = WAL"); |
| 16 | ||
| 17 | // An earlier version of this app kept a flat note table with no item to | |
| 18 | // belong to. `CREATE TABLE IF NOT EXISTS` below would leave that shape in | |
| 19 | // place forever, so a table missing item_id is moved aside once, keeping | |
| 20 | // its rows rather than losing them. | |
| 21 | const existingNoteColumns = db.query<{ name: string }, []>("PRAGMA table_info(note)").all(); | |
| 22 | if (existingNoteColumns.length > 0 && !existingNoteColumns.some((c) => c.name === "item_id")) { | |
| 23 | db.exec("ALTER TABLE note RENAME TO note_legacy"); | |
| 24 | } | |
| 25 | ||
| 16 | 26 | db.exec( |
| 17 | 27 | `CREATE TABLE IF NOT EXISTS item ( |
| 18 | 28 | id INTEGER PRIMARY KEY, |