From 117e125d55b4ca30b1e130fffd08472ee59b47dd Mon Sep 17 00:00:00 2001 From: ZacharyZcR Date: Sat, 3 Oct 2026 21:02:04 +0800 Subject: [PATCH] fix(snippets): repair missing note column on SQLite upgrades (#1541) --- src/backend/database/db/index.ts | 2 + .../upgrade/snippets-schema-migration.test.ts | 57 +++++++++++++++++++ .../upgrade/snippets-schema-migration.ts | 19 +++++++ 3 files changed, 78 insertions(+) create mode 100644 src/backend/tests/upgrade/snippets-schema-migration.test.ts create mode 100644 src/backend/upgrade/snippets-schema-migration.ts diff --git a/src/backend/database/db/index.ts b/src/backend/database/db/index.ts index eed378ee5..509545c89 100644 --- a/src/backend/database/db/index.ts +++ b/src/backend/database/db/index.ts @@ -1,3 +1,4 @@ +import { repairSnippetsNoteColumn } from "../../upgrade/snippets-schema-migration.js"; import { getErrorMessage } from "../../utils/error-message.js"; import { drizzle } from "drizzle-orm/better-sqlite3"; import Database from "better-sqlite3"; @@ -782,6 +783,7 @@ const relaxPluginGrantGrantedBy = () => { }; const migrateSchema = () => { + repairSnippetsNoteColumn(sqlite); addColumnIfNotExists("user_preferences", "theme", "TEXT"); addColumnIfNotExists("user_preferences", "font_size", "TEXT"); addColumnIfNotExists("user_preferences", "accent_color", "TEXT"); diff --git a/src/backend/tests/upgrade/snippets-schema-migration.test.ts b/src/backend/tests/upgrade/snippets-schema-migration.test.ts new file mode 100644 index 000000000..bd44d2a90 --- /dev/null +++ b/src/backend/tests/upgrade/snippets-schema-migration.test.ts @@ -0,0 +1,57 @@ +import Database from "better-sqlite3"; +import { readFileSync } from "node:fs"; +import { afterEach, beforeEach, describe, expect, it } from "vitest"; +import { repairSnippetsNoteColumn } from "../../upgrade/snippets-schema-migration.js"; + +let sqlite: Database.Database; +beforeEach(() => { + sqlite = new Database(":memory:"); +}); +afterEach(() => { + sqlite.close(); +}); + +describe("snippet note column upgrade", () => { + it.each(["snippets", "p_snippets_snippets"])( + "repairs %s without losing rows", + (table) => { + sqlite.exec( + `CREATE TABLE "${table}" (id INTEGER PRIMARY KEY, user_id TEXT, content TEXT)`, + ); + sqlite + .prepare(`INSERT INTO "${table}" VALUES (7, 'user', 'echo hello')`) + .run(); + expect(() => sqlite.prepare(`SELECT "is_note" FROM "${table}"`)).toThrow( + /is_note/, + ); + repairSnippetsNoteColumn(sqlite); + if (table === "snippets") { + sqlite.exec( + readFileSync( + "plugins/snippets/migrations/sqlite/0001_adopt_snippets_tables.sql", + "utf8", + ), + ); + } + expect( + sqlite + .prepare("SELECT id, content, is_note FROM p_snippets_snippets") + .get(), + ).toEqual({ id: 7, content: "echo hello", is_note: 0 }); + sqlite.exec("UPDATE p_snippets_snippets SET is_note = 1"); + repairSnippetsNoteColumn(sqlite); + expect( + sqlite.prepare("SELECT is_note FROM p_snippets_snippets").get(), + ).toEqual({ is_note: 1 }); + }, + ); + + it("does not create tables on a fresh install", () => { + repairSnippetsNoteColumn(sqlite); + expect( + sqlite + .prepare("SELECT name FROM sqlite_master WHERE type = 'table'") + .all(), + ).toEqual([]); + }); +}); diff --git a/src/backend/upgrade/snippets-schema-migration.ts b/src/backend/upgrade/snippets-schema-migration.ts new file mode 100644 index 000000000..0326c7d47 --- /dev/null +++ b/src/backend/upgrade/snippets-schema-migration.ts @@ -0,0 +1,19 @@ +import type Database from "better-sqlite3"; + +/** Repair older SQLite tables before adoption, or after an incomplete 2.9 upgrade. */ +export function repairSnippetsNoteColumn(sqlite: Database.Database): void { + for (const table of ["snippets", "p_snippets_snippets"]) { + const columns = sqlite.pragma(`table_info("${table}")`) as Array<{ + name: string; + }>; + if ( + columns.length === 0 || + columns.some(({ name }) => name === "is_note") + ) { + continue; + } + sqlite.exec( + `ALTER TABLE "${table}" ADD COLUMN "is_note" INTEGER NOT NULL DEFAULT 0`, + ); + } +}