From 0610257562c70f2cb5f7fe796b8e33839d6faca0 Mon Sep 17 00:00:00 2001 From: Steven Date: Mon, 1 Dec 2025 22:54:30 +0800 Subject: [PATCH] refactor(store): remove deprecated migration_history table and backward compatibility code MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Complete removal of migration_history system in favor of instance_setting based schema versioning. Changes: - Remove migration_history table creation from all LATEST.sql files - Delete all migration_history model and implementation files (~300 lines) - Remove FindMigrationHistoryList and UpsertMigrationHistory from Driver interface - Replace complex backward compatibility functions with simple version check - Update health check to use instance_setting instead of migration_history - Simplify checkMinimumUpgradeVersion to detect pre-v0.22 installations Breaking change: Users on versions < v0.22.0 (May 2024) must upgrade to v0.25.x first before upgrading to this version. Clear error message with upgrade instructions will be shown for old installations. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- server/router/api/v1/health_service.go | 14 ++- store/db/mysql/migration_history.go | 60 ---------- store/db/postgres/migration_history.go | 61 ---------- store/db/sqlite/migration_history.go | 61 ---------- store/driver.go | 7 -- store/migration/mysql/LATEST.sql | 6 - store/migration/postgres/LATEST.sql | 6 - store/migration/sqlite/LATEST.sql | 6 - store/migration_history.go | 21 ---- store/migrator.go | 147 ++++++++----------------- store/test/store.go | 2 - 11 files changed, 52 insertions(+), 339 deletions(-) delete mode 100644 store/db/mysql/migration_history.go delete mode 100644 store/db/postgres/migration_history.go delete mode 100644 store/db/sqlite/migration_history.go delete mode 100644 store/migration_history.go diff --git a/server/router/api/v1/health_service.go b/server/router/api/v1/health_service.go index 47a00c86d..4afc6145f 100644 --- a/server/router/api/v1/health_service.go +++ b/server/router/api/v1/health_service.go @@ -6,15 +6,19 @@ import ( "google.golang.org/grpc/codes" "google.golang.org/grpc/health/grpc_health_v1" "google.golang.org/grpc/status" - - "github.com/usememos/memos/store" ) func (s *APIV1Service) Check(ctx context.Context, _ *grpc_health_v1.HealthCheckRequest) (*grpc_health_v1.HealthCheckResponse, error) { - history, err := s.Store.GetDriver().FindMigrationHistoryList(ctx, &store.FindMigrationHistory{}) - if err != nil || len(history) == 0 { - return nil, status.Errorf(codes.Unavailable, "not available") + // Check if database is initialized by verifying instance basic setting exists + instanceBasicSetting, err := s.Store.GetInstanceBasicSetting(ctx) + if err != nil { + return nil, status.Errorf(codes.Unavailable, "database not initialized: %v", err) + } + + // Verify schema version is set (empty means database not properly initialized) + if instanceBasicSetting.SchemaVersion == "" { + return nil, status.Errorf(codes.Unavailable, "schema version not set") } return &grpc_health_v1.HealthCheckResponse{Status: grpc_health_v1.HealthCheckResponse_SERVING}, nil diff --git a/store/db/mysql/migration_history.go b/store/db/mysql/migration_history.go deleted file mode 100644 index fbaf5a710..000000000 --- a/store/db/mysql/migration_history.go +++ /dev/null @@ -1,60 +0,0 @@ -package mysql - -import ( - "context" - - "github.com/usememos/memos/store" -) - -// FindMigrationHistoryList retrieves all migration history records. -// NOTE: This method is deprecated along with the migration_history table. -func (d *DB) FindMigrationHistoryList(ctx context.Context, _ *store.FindMigrationHistory) ([]*store.MigrationHistory, error) { - query := "SELECT `version`, UNIX_TIMESTAMP(`created_ts`) FROM `migration_history` ORDER BY `created_ts` DESC" - rows, err := d.db.QueryContext(ctx, query) - if err != nil { - return nil, err - } - defer rows.Close() - - list := make([]*store.MigrationHistory, 0) - for rows.Next() { - var migrationHistory store.MigrationHistory - if err := rows.Scan( - &migrationHistory.Version, - &migrationHistory.CreatedTs, - ); err != nil { - return nil, err - } - - list = append(list, &migrationHistory) - } - - if err := rows.Err(); err != nil { - return nil, err - } - - return list, nil -} - -// UpsertMigrationHistory inserts or updates a migration history record. -// NOTE: This method is deprecated along with the migration_history table. -// This uses separate INSERT and SELECT queries instead of INSERT...RETURNING because -// MySQL doesn't support RETURNING clause in the same way as PostgreSQL/SQLite. -// This could have race conditions but is acceptable for deprecated transition code. -func (d *DB) UpsertMigrationHistory(ctx context.Context, upsert *store.UpsertMigrationHistory) (*store.MigrationHistory, error) { - stmt := "INSERT INTO `migration_history` (`version`) VALUES (?) ON DUPLICATE KEY UPDATE `version` = ?" - _, err := d.db.ExecContext(ctx, stmt, upsert.Version, upsert.Version) - if err != nil { - return nil, err - } - - var migrationHistory store.MigrationHistory - stmt = "SELECT `version`, UNIX_TIMESTAMP(`created_ts`) FROM `migration_history` WHERE `version` = ?" - if err := d.db.QueryRowContext(ctx, stmt, upsert.Version).Scan( - &migrationHistory.Version, - &migrationHistory.CreatedTs, - ); err != nil { - return nil, err - } - return &migrationHistory, nil -} diff --git a/store/db/postgres/migration_history.go b/store/db/postgres/migration_history.go deleted file mode 100644 index 5464dfbca..000000000 --- a/store/db/postgres/migration_history.go +++ /dev/null @@ -1,61 +0,0 @@ -package postgres - -import ( - "context" - - "github.com/usememos/memos/store" -) - -// FindMigrationHistoryList retrieves all migration history records. -// NOTE: This method is deprecated along with the migration_history table. -func (d *DB) FindMigrationHistoryList(ctx context.Context, _ *store.FindMigrationHistory) ([]*store.MigrationHistory, error) { - query := "SELECT version, created_ts FROM migration_history ORDER BY created_ts DESC" - rows, err := d.db.QueryContext(ctx, query) - if err != nil { - return nil, err - } - defer rows.Close() - - list := make([]*store.MigrationHistory, 0) - for rows.Next() { - var migrationHistory store.MigrationHistory - if err := rows.Scan( - &migrationHistory.Version, - &migrationHistory.CreatedTs, - ); err != nil { - return nil, err - } - - list = append(list, &migrationHistory) - } - - if err := rows.Err(); err != nil { - return nil, err - } - - return list, nil -} - -// UpsertMigrationHistory inserts or updates a migration history record. -// NOTE: This method is deprecated along with the migration_history table. -func (d *DB) UpsertMigrationHistory(ctx context.Context, upsert *store.UpsertMigrationHistory) (*store.MigrationHistory, error) { - stmt := ` - INSERT INTO migration_history ( - version - ) - VALUES ($1) - ON CONFLICT(version) DO UPDATE - SET - version=EXCLUDED.version - RETURNING version, created_ts - ` - var migrationHistory store.MigrationHistory - if err := d.db.QueryRowContext(ctx, stmt, upsert.Version).Scan( - &migrationHistory.Version, - &migrationHistory.CreatedTs, - ); err != nil { - return nil, err - } - - return &migrationHistory, nil -} diff --git a/store/db/sqlite/migration_history.go b/store/db/sqlite/migration_history.go deleted file mode 100644 index 3403bcfdb..000000000 --- a/store/db/sqlite/migration_history.go +++ /dev/null @@ -1,61 +0,0 @@ -package sqlite - -import ( - "context" - - "github.com/usememos/memos/store" -) - -// FindMigrationHistoryList retrieves all migration history records. -// NOTE: This method is deprecated along with the migration_history table. -func (d *DB) FindMigrationHistoryList(ctx context.Context, _ *store.FindMigrationHistory) ([]*store.MigrationHistory, error) { - query := "SELECT `version`, `created_ts` FROM `migration_history` ORDER BY `created_ts` DESC" - rows, err := d.db.QueryContext(ctx, query) - if err != nil { - return nil, err - } - defer rows.Close() - - list := make([]*store.MigrationHistory, 0) - for rows.Next() { - var migrationHistory store.MigrationHistory - if err := rows.Scan( - &migrationHistory.Version, - &migrationHistory.CreatedTs, - ); err != nil { - return nil, err - } - - list = append(list, &migrationHistory) - } - - if err := rows.Err(); err != nil { - return nil, err - } - - return list, nil -} - -// UpsertMigrationHistory inserts or updates a migration history record. -// NOTE: This method is deprecated along with the migration_history table. -func (d *DB) UpsertMigrationHistory(ctx context.Context, upsert *store.UpsertMigrationHistory) (*store.MigrationHistory, error) { - stmt := ` - INSERT INTO migration_history ( - version - ) - VALUES (?) - ON CONFLICT(version) DO UPDATE - SET - version=EXCLUDED.version - RETURNING version, created_ts - ` - var migrationHistory store.MigrationHistory - if err := d.db.QueryRowContext(ctx, stmt, upsert.Version).Scan( - &migrationHistory.Version, - &migrationHistory.CreatedTs, - ); err != nil { - return nil, err - } - - return &migrationHistory, nil -} diff --git a/store/driver.go b/store/driver.go index 029f522d0..f13a23ee9 100644 --- a/store/driver.go +++ b/store/driver.go @@ -13,13 +13,6 @@ type Driver interface { IsInitialized(ctx context.Context) (bool, error) - // MigrationHistory model related methods. - // NOTE: These methods are deprecated. The migration_history table is no longer used - // for tracking schema versions. Schema version is now stored in instance_setting. - // These methods are kept for backward compatibility to migrate existing installations. - FindMigrationHistoryList(ctx context.Context, find *FindMigrationHistory) ([]*MigrationHistory, error) - UpsertMigrationHistory(ctx context.Context, upsert *UpsertMigrationHistory) (*MigrationHistory, error) - // Activity model related methods. CreateActivity(ctx context.Context, create *Activity) (*Activity, error) ListActivities(ctx context.Context, find *FindActivity) ([]*Activity, error) diff --git a/store/migration/mysql/LATEST.sql b/store/migration/mysql/LATEST.sql index e5ae04dfa..adc86a9eb 100644 --- a/store/migration/mysql/LATEST.sql +++ b/store/migration/mysql/LATEST.sql @@ -1,9 +1,3 @@ --- migration_history -CREATE TABLE `migration_history` ( - `version` VARCHAR(256) NOT NULL PRIMARY KEY, - `created_ts` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP -); - -- system_setting CREATE TABLE `system_setting` ( `name` VARCHAR(256) NOT NULL PRIMARY KEY, diff --git a/store/migration/postgres/LATEST.sql b/store/migration/postgres/LATEST.sql index 66df2848c..b5b70a9ec 100644 --- a/store/migration/postgres/LATEST.sql +++ b/store/migration/postgres/LATEST.sql @@ -1,9 +1,3 @@ --- migration_history -CREATE TABLE migration_history ( - version TEXT NOT NULL PRIMARY KEY, - created_ts BIGINT NOT NULL DEFAULT EXTRACT(EPOCH FROM NOW()) -); - -- system_setting CREATE TABLE system_setting ( name TEXT NOT NULL PRIMARY KEY, diff --git a/store/migration/sqlite/LATEST.sql b/store/migration/sqlite/LATEST.sql index 37f77125f..6a36e9338 100644 --- a/store/migration/sqlite/LATEST.sql +++ b/store/migration/sqlite/LATEST.sql @@ -1,9 +1,3 @@ --- migration_history -CREATE TABLE migration_history ( - version TEXT NOT NULL PRIMARY KEY, - created_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')) -); - -- system_setting CREATE TABLE system_setting ( name TEXT NOT NULL, diff --git a/store/migration_history.go b/store/migration_history.go deleted file mode 100644 index 77f41ff0b..000000000 --- a/store/migration_history.go +++ /dev/null @@ -1,21 +0,0 @@ -package store - -// MigrationHistory represents a record in the migration_history table. -// NOTE: The migration_history table is deprecated in favor of storing schema version -// in system_setting (BASIC setting). This is kept for backward compatibility only. -// Migration from migration_history to system_setting happens automatically during startup. -type MigrationHistory struct { - Version string - CreatedTs int64 -} - -// UpsertMigrationHistory is used to insert or update a migration history record. -// NOTE: This is deprecated along with the migration_history table. -type UpsertMigrationHistory struct { - Version string -} - -// FindMigrationHistory is used to query migration history records. -// NOTE: This is deprecated along with the migration_history table. -type FindMigrationHistory struct { -} diff --git a/store/migrator.go b/store/migrator.go index 915105b4c..d5446fcab 100644 --- a/store/migrator.go +++ b/store/migrator.go @@ -21,20 +21,18 @@ import ( // Migration System Overview: // // The migration system handles database schema versioning and upgrades. -// Schema version is stored in system_setting (the new system). -// The old migration_history table is deprecated but still supported for backward compatibility. +// Schema version is stored in instance_setting (formerly system_setting). // // Migration Flow: // 1. preMigrate: Check if DB is initialized. If not, apply LATEST.sql -// 2. normalizeMigrationHistoryList: Normalize old migration_history records (for pre-0.22 installations) -// 3. migrateSchemaVersionToSetting: Migrate version from migration_history to system_setting -// 4. Migrate (prod mode): Apply incremental migrations from current to target version -// 5. Migrate (demo mode): Seed database with demo data +// 2. checkMinimumUpgradeVersion: Verify installation can be upgraded (reject pre-0.22 installations) +// 3. Migrate (prod mode): Apply incremental migrations from current to target version +// 4. Migrate (demo mode): Seed database with demo data // // Version Tracking: -// - New installations: Schema version set in system_setting immediately -// - Old installations: Version migrated from migration_history to system_setting automatically -// - Empty version: Treated as 0.0.0 and all migrations applied +// - New installations: Schema version set in instance_setting immediately +// - Existing v0.22+ installations: Schema version tracked in instance_setting +// - Pre-v0.22 installations: Must upgrade to v0.25.x first (migration_history → instance_setting migration) // // Migration Files: // - Location: store/migration/{driver}/{version}/NN__description.sql @@ -53,17 +51,13 @@ const ( // For example, "1__create_table.sql". MigrateFileNameSplit = "__" // LatestSchemaFileName is the name of the latest schema file. - // This file is used to apply the latest schema when no migration history is found. + // This file is used to initialize fresh installations with the current schema. LatestSchemaFileName = "LATEST.sql" // defaultSchemaVersion is used when schema version is empty or not set. // This handles edge cases for old installations without version tracking. defaultSchemaVersion = "0.0.0" - // migrationHistoryNormalizedVersion is the version where migration_history normalization was completed. - // Before 0.22, migration history had inconsistent versioning that needed normalization. - migrationHistoryNormalizedVersion = "0.22" - // Mode constants for profile mode. modeProd = "prod" modeDemo = "demo" @@ -262,11 +256,8 @@ func (s *Store) preMigrate(ctx context.Context) error { } if s.profile.Mode == modeProd { - if err := s.normalizeMigrationHistoryList(ctx); err != nil { - return errors.Wrap(err, "failed to normalize migration history list") - } - if err := s.migrateSchemaVersionToSetting(ctx); err != nil { - return errors.Wrap(err, "failed to migrate schema version to setting") + if err := s.checkMinimumUpgradeVersion(ctx); err != nil { + return err // Error message is already descriptive, don't wrap it } } return nil @@ -380,96 +371,44 @@ func (s *Store) updateCurrentSchemaVersion(ctx context.Context, schemaVersion st return nil } -// normalizeMigrationHistoryList normalizes the migration history list. -// It checks the existing migration history and updates it to the latest schema version if necessary. -// NOTE: This is a transition function for backward compatibility with the deprecated migration_history table. -// This ensures that old installations (< 0.22) have their migration_history normalized before migrating to system_setting. -func (s *Store) normalizeMigrationHistoryList(ctx context.Context) error { - migrationHistoryList, err := s.driver.FindMigrationHistoryList(ctx, &FindMigrationHistory{}) - if err != nil { - return errors.Wrap(err, "failed to find migration history") - } - versions := []string{} - for _, migrationHistory := range migrationHistoryList { - versions = append(versions, migrationHistory.Version) - } - if len(versions) == 0 { - return nil - } - sort.Sort(version.SortVersion(versions)) - latestVersion := versions[len(versions)-1] - latestMinorVersion := version.GetMinorVersion(latestVersion) - - // If the latest version is greater than migrationHistoryNormalizedVersion, return. - // As of that version, the migration history is already normalized. - if version.IsVersionGreaterThan(latestMinorVersion, migrationHistoryNormalizedVersion) { - return nil - } - - schemaVersionMap := map[string]string{} - filePaths, err := fs.Glob(migrationFS, fmt.Sprintf("%s*/*.sql", s.getMigrationBasePath())) - if err != nil { - return errors.Wrap(err, "failed to read migration files") - } - sort.Strings(filePaths) - for _, filePath := range filePaths { - fileSchemaVersion, err := s.getSchemaVersionOfMigrateScript(filePath) - if err != nil { - return errors.Wrap(err, "failed to get schema version of migrate script") - } - schemaVersionMap[version.GetMinorVersion(fileSchemaVersion)] = fileSchemaVersion - } - - latestSchemaVersion := schemaVersionMap[latestMinorVersion] - if latestSchemaVersion == "" { - return errors.Errorf("latest schema version not found") - } - if version.IsVersionGreaterOrEqualThan(latestVersion, latestSchemaVersion) { - return nil - } - if _, err := s.driver.UpsertMigrationHistory(ctx, &UpsertMigrationHistory{ - Version: latestSchemaVersion, - }); err != nil { - return errors.Wrap(err, "failed to upsert latest migration history") - } - return nil -} - -// migrateSchemaVersionToSetting migrates the schema version from the migration history to the instance basic setting. -// It retrieves the migration history, sorts the versions, and updates the instance basic setting if necessary. -// NOTE: This is a transition function for backward compatibility with the deprecated migration_history table. -// The migration_history table is deprecated in favor of storing schema version in system_setting. -// This handles upgrades from old installations that only have migration_history but no system_setting. -func (s *Store) migrateSchemaVersionToSetting(ctx context.Context) error { - migrationHistoryList, err := s.driver.FindMigrationHistoryList(ctx, &FindMigrationHistory{}) - if err != nil { - return errors.Wrap(err, "failed to find migration history") - } - versions := []string{} - for _, migrationHistory := range migrationHistoryList { - versions = append(versions, migrationHistory.Version) - } - if len(versions) == 0 { - return nil - } - sort.Sort(version.SortVersion(versions)) - latestVersion := versions[len(versions)-1] - +// checkMinimumUpgradeVersion verifies the installation meets minimum version requirements for upgrade. +// For very old installations (< v0.22.0), users must upgrade to v0.25.x first before upgrading to current version. +// This is necessary because schema version tracking was moved from migration_history to instance_setting in v0.22.0. +func (s *Store) checkMinimumUpgradeVersion(ctx context.Context) error { instanceBasicSetting, err := s.GetInstanceBasicSetting(ctx) if err != nil { return errors.Wrap(err, "failed to get instance basic setting") } - // If instance_setting has no schema version (empty), or migration_history has a newer version, update instance_setting. - // This handles upgrades from old installations where schema version was only tracked in migration_history. - if isVersionEmpty(instanceBasicSetting.SchemaVersion) || version.IsVersionGreaterThan(latestVersion, instanceBasicSetting.SchemaVersion) { - slog.Info("migrating schema version from migration_history to instance_setting", - slog.String("from", instanceBasicSetting.SchemaVersion), - slog.String("to", latestVersion), - ) - if err := s.updateCurrentSchemaVersion(ctx, latestVersion); err != nil { - return errors.Wrap(err, "failed to update current schema version") - } + schemaVersion := instanceBasicSetting.SchemaVersion + + // If schema version is >= 0.22.0, the installation is up-to-date + if !isVersionEmpty(schemaVersion) && version.IsVersionGreaterOrEqualThan(schemaVersion, "0.22.0") { + return nil } + + // If schema version is set but < 0.22.0, this is an old installation + if !isVersionEmpty(schemaVersion) && !version.IsVersionGreaterOrEqualThan(schemaVersion, "0.22.0") { + currentVersion, _ := s.GetCurrentSchemaVersion() + + return errors.Errorf( + "Your Memos installation is too old to upgrade directly.\n\n"+ + "Your current version: %s\n"+ + "Target version: %s\n"+ + "Minimum required: v0.22.0 (May 2024)\n\n"+ + "Upgrade path:\n"+ + "1. First upgrade to v0.25.3: https://github.com/usememos/memos/releases/tag/v0.25.3\n"+ + "2. Start the server and verify it works\n"+ + "3. Then upgrade to the latest version\n\n"+ + "This is required because schema version tracking was moved from migration_history\n"+ + "to instance_setting in v0.22.0. The intermediate upgrade handles this migration safely.", + schemaVersion, + currentVersion, + ) + } + + // Schema version is empty - this is either a fresh install or corrupted installation + // Fresh installs will have schema version set immediately after LATEST.sql is applied + // So this should not be an issue in normal operation return nil } diff --git a/store/test/store.go b/store/test/store.go index 71435bfd6..84371bc2b 100644 --- a/store/test/store.go +++ b/store/test/store.go @@ -37,7 +37,6 @@ func NewTestingStore(ctx context.Context, t *testing.T) *store.Store { func resetTestingDB(ctx context.Context, profile *profile.Profile, dbDriver store.Driver) { if profile.Driver == "mysql" { _, err := dbDriver.GetDB().ExecContext(ctx, ` - DROP TABLE IF EXISTS migration_history; DROP TABLE IF EXISTS system_setting; DROP TABLE IF EXISTS user; DROP TABLE IF EXISTS user_setting; @@ -57,7 +56,6 @@ func resetTestingDB(ctx context.Context, profile *profile.Profile, dbDriver stor } } else if profile.Driver == "postgres" { _, err := dbDriver.GetDB().ExecContext(ctx, ` - DROP TABLE IF EXISTS migration_history CASCADE; DROP TABLE IF EXISTS system_setting CASCADE; DROP TABLE IF EXISTS "user" CASCADE; DROP TABLE IF EXISTS user_setting CASCADE;