From 0d2b5623bb09e249c62de9a8b97eff1d1c263d11 Mon Sep 17 00:00:00 2001 From: ZacharyZcR Date: Mon, 9 Nov 2026 16:34:50 +0800 Subject: [PATCH] fix: add the 2.8 sync columns to homepage and vault tables on upgrade 2.8 added sync_id to dashboard_service_links, homepage_items and vault_profiles, and updated_at to dashboard_service_links, on start. 2.9 handed those tables to plugins and dropped that code, so a SQLite database upgraded from 2.5 straight to 2.9 or later never got the columns. The plugins read sync_id from their adopted tables, and sync logged "no such column: sync_id" on every pass while skipping that data. Run the existing snippets repair over these tables too, under both their legacy and adopted names, and fill a sync_id for every row. The repair is renamed to match. Fixes Termix-SSH/Plugin-Sync#8 --- src/backend/database/db/index.ts | 4 +- ....test.ts => legacy-plugin-columns.test.ts} | 43 ++++++++++++++++--- ...-migration.ts => legacy-plugin-columns.ts} | 21 ++++++++- 3 files changed, 58 insertions(+), 10 deletions(-) rename src/backend/tests/upgrade/{snippets-schema-migration.test.ts => legacy-plugin-columns.test.ts} (62%) rename src/backend/upgrade/{snippets-schema-migration.ts => legacy-plugin-columns.ts} (65%) diff --git a/src/backend/database/db/index.ts b/src/backend/database/db/index.ts index bc6dfe0c5d..0b71cf989a 100644 --- a/src/backend/database/db/index.ts +++ b/src/backend/database/db/index.ts @@ -1,4 +1,4 @@ -import { repairSnippetsColumns } from "../../upgrade/snippets-schema-migration.js"; +import { repairLegacyPluginColumns } from "../../upgrade/legacy-plugin-columns.js"; import { getErrorMessage } from "../../utils/error-message.js"; import { drizzle } from "drizzle-orm/better-sqlite3"; import Database from "better-sqlite3"; @@ -1024,7 +1024,7 @@ const enforceUniqueUsernames = () => { }; const migrateSchema = () => { - repairSnippetsColumns(sqlite); + repairLegacyPluginColumns(sqlite); addColumnIfNotExists("api_keys", "scopes", "TEXT"); addColumnIfNotExists("user_preferences", "theme", "TEXT"); addColumnIfNotExists("user_preferences", "font_size", "TEXT"); diff --git a/src/backend/tests/upgrade/snippets-schema-migration.test.ts b/src/backend/tests/upgrade/legacy-plugin-columns.test.ts similarity index 62% rename from src/backend/tests/upgrade/snippets-schema-migration.test.ts rename to src/backend/tests/upgrade/legacy-plugin-columns.test.ts index 56232ec60b..f5b533288c 100644 --- a/src/backend/tests/upgrade/snippets-schema-migration.test.ts +++ b/src/backend/tests/upgrade/legacy-plugin-columns.test.ts @@ -1,6 +1,6 @@ import Database from "better-sqlite3"; import { afterEach, beforeEach, describe, expect, it } from "vitest"; -import { repairSnippetsColumns } from "../../upgrade/snippets-schema-migration.js"; +import { repairLegacyPluginColumns } from "../../upgrade/legacy-plugin-columns.js"; let sqlite: Database.Database; beforeEach(() => { @@ -23,7 +23,7 @@ describe("snippet column upgrade", () => { expect(() => sqlite.prepare(`SELECT "is_note" FROM "${table}"`)).toThrow( /is_note/, ); - repairSnippetsColumns(sqlite); + repairLegacyPluginColumns(sqlite); if (table === "snippets") { // What the plugin migration does to the legacy table. sqlite.exec(`ALTER TABLE "snippets" RENAME TO "p_snippets_snippets"`); @@ -34,7 +34,7 @@ describe("snippet column upgrade", () => { .get(), ).toEqual({ id: 7, content: "echo hello", is_note: 0 }); sqlite.exec("UPDATE p_snippets_snippets SET is_note = 1"); - repairSnippetsColumns(sqlite); + repairLegacyPluginColumns(sqlite); expect( sqlite.prepare("SELECT is_note FROM p_snippets_snippets").get(), ).toEqual({ is_note: 1 }); @@ -42,7 +42,7 @@ describe("snippet column upgrade", () => { ); it("does not create tables on a fresh install", () => { - repairSnippetsColumns(sqlite); + repairLegacyPluginColumns(sqlite); expect( sqlite .prepare("SELECT name FROM sqlite_master WHERE type = 'table'") @@ -61,7 +61,7 @@ describe("snippet column upgrade", () => { `INSERT INTO "p_snippets_snippets" VALUES (1, 'u', 'a', 'ls'), (2, 'u', 'b', 'pwd')`, ); sqlite.exec(`INSERT INTO "snippet_folders" VALUES (1, 'u', 'ops')`); - repairSnippetsColumns(sqlite); + repairLegacyPluginColumns(sqlite); const snippets = sqlite .prepare( @@ -84,11 +84,42 @@ describe("snippet column upgrade", () => { const before = sqlite .prepare(`SELECT "sync_id" FROM "p_snippets_snippets" ORDER BY id`) .all(); - repairSnippetsColumns(sqlite); + repairLegacyPluginColumns(sqlite); expect( sqlite .prepare(`SELECT "sync_id" FROM "p_snippets_snippets" ORDER BY id`) .all(), ).toEqual(before); }); + + it("adds the 2.8 columns to homepage and vault tables adopted from 2.5", () => { + // 2.5.1's columns, renamed by the plugins' adoption migrations. + sqlite.exec(` + CREATE TABLE "p_homepage_homepage_items" (id INTEGER PRIMARY KEY, user_id TEXT, type_id TEXT, title TEXT, config TEXT, folder_id INTEGER, created_at TEXT, updated_at TEXT); + CREATE TABLE "p_homepage_dashboard_service_links" (id INTEGER PRIMARY KEY, user_id TEXT, label TEXT, url TEXT, "order" INTEGER, created_at TEXT); + CREATE TABLE "p_vault_profiles" (id INTEGER PRIMARY KEY, user_id TEXT, name TEXT, vault_addr TEXT, ssh_role TEXT); + INSERT INTO "p_homepage_homepage_items" (id, user_id, type_id) VALUES (1, 'u', 'clock'); + INSERT INTO "p_homepage_dashboard_service_links" (id, user_id, label, url) VALUES (1, 'u', 'Wiki', 'https://wiki'); + INSERT INTO "p_vault_profiles" (id, user_id, name) VALUES (1, 'u', 'prod'); + `); + repairLegacyPluginColumns(sqlite); + + for (const table of [ + "p_homepage_homepage_items", + "p_homepage_dashboard_service_links", + "p_vault_profiles", + ]) { + const row = sqlite.prepare(`SELECT "sync_id" FROM "${table}"`).get() as { + sync_id: string; + }; + expect(row.sync_id).toMatch(/^[0-9a-f]{32}$/); + } + expect( + sqlite + .prepare( + `SELECT "label", "updated_at" FROM "p_homepage_dashboard_service_links"`, + ) + .get(), + ).toEqual({ label: "Wiki", updated_at: null }); + }); }); diff --git a/src/backend/upgrade/snippets-schema-migration.ts b/src/backend/upgrade/legacy-plugin-columns.ts similarity index 65% rename from src/backend/upgrade/snippets-schema-migration.ts rename to src/backend/upgrade/legacy-plugin-columns.ts index 3a8b8088e9..cc824388f7 100644 --- a/src/backend/upgrade/snippets-schema-migration.ts +++ b/src/backend/upgrade/legacy-plugin-columns.ts @@ -15,15 +15,32 @@ const FOLDER_COLUMNS: Array<[string, string]> = [ ["sync_id", "TEXT"], ]; +// 2.8 added these to core's tables on start. 2.9 left the tables to plugins +// and dropped that code, so a database that skipped 2.8 never got them. +const SYNC_ID: Array<[string, string]> = [["sync_id", "TEXT"]]; +const SERVICE_LINK_COLUMNS: Array<[string, string]> = [ + ["sync_id", "TEXT"], + ["updated_at", "TEXT"], +]; + const TABLES: Array<[string, Array<[string, string]>]> = [ ["snippets", SNIPPET_COLUMNS], ["p_snippets_snippets", SNIPPET_COLUMNS], ["snippet_folders", FOLDER_COLUMNS], ["p_snippets_snippet_folders", FOLDER_COLUMNS], + ["homepage_items", SYNC_ID], + ["p_homepage_homepage_items", SYNC_ID], + ["dashboard_service_links", SERVICE_LINK_COLUMNS], + ["p_homepage_dashboard_service_links", SERVICE_LINK_COLUMNS], + ["vault_profiles", SYNC_ID], + ["p_vault_profiles", SYNC_ID], ]; -/** Repair older SQLite tables before adoption, or after an incomplete 2.9 upgrade. */ -export function repairSnippetsColumns(sqlite: Database.Database): void { +/** + * Repairs older SQLite tables plugins adopt, before adoption or after an + * upgrade that skipped the release adding their columns. + */ +export function repairLegacyPluginColumns(sqlite: Database.Database): void { for (const [table, wanted] of TABLES) { const columns = sqlite.pragma(`table_info("${table}")`) as Array<{ name: string;