From 2516127752124c37a52845b4a28248c6cc48fe71 Mon Sep 17 00:00:00 2001 From: Zezae Oh Date: Wed, 1 Jul 2026 08:24:04 +0900 Subject: [PATCH] fix(sqlite): make column migrations idempotent (#1312) --- internal/store/sqlitestore/schema.go | 64 ++++++++++++++++++++++++++++ 1 file changed, 64 insertions(+) diff --git a/internal/store/sqlitestore/schema.go b/internal/store/sqlitestore/schema.go index 848caf96..b5e9f237 100644 --- a/internal/store/sqlitestore/schema.go +++ b/internal/store/sqlitestore/schema.go @@ -1411,6 +1411,18 @@ func EnsureSchema(db *sql.DB) error { patch = `SELECT 1;` } } + if v == 49 { + patch, err = sqliteCronProviderMigrationPatch(db) + if err != nil { + return fmt.Errorf("inspect cron_jobs provider override columns: %w", err) + } + } + if v == 52 { + patch, err = sqliteUsageEventTokenMigrationPatch(db) + if err != nil { + return fmt.Errorf("inspect usage event token columns: %w", err) + } + } // Migrations that rebuild a table referenced by another table's FK // require foreign_keys=OFF per SQLite altertable ยง7. The pragma is // a no-op inside a transaction, so toggle it around BEGIN/COMMIT. @@ -1502,6 +1514,58 @@ func idempotentColumnMigration(version int) (string, string, bool) { } } +func sqliteCronProviderMigrationPatch(db *sql.DB) (string, error) { + hasProviderID, err := sqliteColumnExists(db, "cron_jobs", "provider_id") + if err != nil { + return "", err + } + hasModel, err := sqliteColumnExists(db, "cron_jobs", "model") + if err != nil { + return "", err + } + + patch := "" + if !hasProviderID { + patch += "ALTER TABLE cron_jobs ADD COLUMN provider_id TEXT REFERENCES llm_providers(id) ON DELETE SET NULL;\n" + } + if !hasModel { + patch += "ALTER TABLE cron_jobs ADD COLUMN model VARCHAR(200);\n" + } + if patch == "" { + patch = "SELECT 1;" + } + return patch, nil +} + +func sqliteUsageEventTokenMigrationPatch(db *sql.DB) (string, error) { + columns := []struct { + table string + name string + }{ + {table: "usage_events", name: "cache_read_tokens"}, + {table: "usage_events", name: "cache_create_tokens"}, + {table: "usage_events", name: "thinking_tokens"}, + {table: "usage_event_rollups", name: "cache_read_tokens"}, + {table: "usage_event_rollups", name: "cache_create_tokens"}, + {table: "usage_event_rollups", name: "thinking_tokens"}, + } + + patch := "" + for _, col := range columns { + hasColumn, err := sqliteColumnExists(db, col.table, col.name) + if err != nil { + return "", err + } + if !hasColumn { + patch += fmt.Sprintf("ALTER TABLE %s ADD COLUMN %s BIGINT NOT NULL DEFAULT 0;\n", col.table, col.name) + } + } + if patch == "" { + patch = "SELECT 1;" + } + return patch, nil +} + func sqliteColumnExists(db *sql.DB, tableName, columnName string) (bool, error) { rows, err := db.Query("PRAGMA table_info(" + tableName + ")") if err != nil {