Skip to content

Commit

Permalink
Require the code match the exact major and minor version with the schema
Browse files Browse the repository at this point in the history
Before the check, code will try to apply the minor migration change if exists.

We use this behavior so that we can catch hidden minor migration change failures like adding an index
  • Loading branch information
tianzhou committed Oct 14, 2021
1 parent 76cf6c5 commit d28de62
Show file tree
Hide file tree
Showing 2 changed files with 26 additions and 21 deletions.
2 changes: 1 addition & 1 deletion bin/server/cmd/root.go
Expand Up @@ -264,7 +264,7 @@ func initSetting(ctx context.Context, settingService api.SettingService) (*confi
}

func (m *main) Run(ctx context.Context) error {
db := store.NewDB(m.l, m.profile.dsn, m.profile.seedDir, m.profile.forceResetSeed, readonly)
db := store.NewDB(m.l, m.profile.dsn, m.profile.seedDir, m.profile.forceResetSeed, readonly, version)
if err := db.Open(); err != nil {
return fmt.Errorf("cannot open db: %w", err)
}
Expand Down
45 changes: 25 additions & 20 deletions store/sqlite.go
Expand Up @@ -21,29 +21,26 @@ const (
// We use SQLite "PRAGMA user_version" to manage the schema version. The schema version consists of
// major version and minor version. Backward compatible schema change increases the minor version,
// while backward non-compatible schema change increase the majar version.
// MAX_MAJOR_SCHEMA_VERSION defines the maximum major schema version this version of code can handle.
// MAJOR_SCHEMA_VERSION and MAJOR_SCHEMA_VERSION defines the schema version this version of code can handle.
// We reserve 4 least significant digits for minor version.
// e.g.
// 10001 -> Major verion 1, minor version 1
// 10002 -> Major verion 1, minor version 2
// 11001 -> Major verion 1, minor version 1001
// 20001 -> Major verion 2, minor version 1
//
// If MAX_MAJOR_SCHEMA_VERSION is 2, then it can handle version up to 29999 and report error if encountering
// version >= 30000.
//
// The migration file follows the name pattern of {{version_number}}__{{description}}, and inside each migration
// file, the first line is: PRAGMA user_version = {{version_number}};
//
// Notes about rollback
// Though minor version is backward compatible, we require the schema version must match both the MAJOR and MINOR version,
// otherwise, Bytebase will fail to start. We choose this because otherwise failed minor migration changes like adding an
// index is hard to detect.
//
// The migration script is bundled with the code. If the new release contains new migration, it will be applied
// upon startup. It could happen we push out a bad release. If it only involves minor version migration change,
// then it's safe to use the older release because of the backward compatibility guarantee. However, if it
// involves a major version migration change, the rollback is much harder, and we can only do this during
// major app version change (like announce Bytebase 2.0 after 1.0), where we can allocate enough resource for
// the migration. But hopefully, we would never do any major migration change at all. In other words, major
// migration change is very costly and we should do it as the last resort.
MAX_MAJOR_SCHEMA_VERSION = 1
// If the new release requires a higher MAJOR version then the schema file, then the code will abort immediately. We
// will require a separate process to upgrade the schema.
// If the new release requires a higher MINOR version than the schema file, then it will apply the migration upon
// startup.
MAJOR_SCHEMA_VERSION = 1
MINOR_SCHEMA_VERSION = 1
)

// If both debug and sqlite_trace build tags are enabled, then sqliteDriver will be set to "sqlite3_trace" in sqlite_trace.go
Expand Down Expand Up @@ -76,13 +73,16 @@ type DB struct {
// If true, database will be opened in readonly mode
readonly bool

// Bytebase release version
releaseVersion string

// Returns the current time. Defaults to time.Now().
// Can be mocked for tests.
Now func() time.Time
}

// NewDB returns a new instance of DB associated with the given datasource name.
func NewDB(logger *zap.Logger, dsn string, seedDir string, forceResetSeed bool, readonly bool) *DB {
func NewDB(logger *zap.Logger, dsn string, seedDir string, forceResetSeed bool, readonly bool, releaseVersion string) *DB {
if readonly {
pragmaList = append(pragmaList, "mode=ro")
}
Expand All @@ -93,6 +93,7 @@ func NewDB(logger *zap.Logger, dsn string, seedDir string, forceResetSeed bool,
forceResetSeed: forceResetSeed,
readonly: readonly,
Now: time.Now,
releaseVersion: releaseVersion,
}
return db
}
Expand Down Expand Up @@ -223,8 +224,8 @@ func (db *DB) migrate() error {

db.l.Info(fmt.Sprintf("Current schema version before migration: %d.%d", major, minor))

if major > MAX_MAJOR_SCHEMA_VERSION {
return fmt.Errorf("current major schema version %d is higher than the max major schema version %d this code can handle ", major, MAX_MAJOR_SCHEMA_VERSION)
if major != MAJOR_SCHEMA_VERSION {
return fmt.Errorf("current major schema version %d is different from the major schema version %d this release %s expects", major, MAJOR_SCHEMA_VERSION, db.releaseVersion)
}

// Apply migrations
Expand Down Expand Up @@ -259,9 +260,13 @@ func (db *DB) migrate() error {
db.l.Info(fmt.Sprintf("Current schema version after migration: %d.%d", major, minor))

// This is a sanity check to prevent us setting the incorrect user_version in the migration script.
// e.g. We set PRAGMA user_version = 20001 while our code can only handle major version 1.
if major != MAX_MAJOR_SCHEMA_VERSION {
return fmt.Errorf("current schema major version %d does not match the expected schema major version %d after migration, make sure to set the correct PRAGMA user_version in the migration script", major, MAX_MAJOR_SCHEMA_VERSION)
// e.g. We set PRAGMA user_version = 20001 for migration script 20002__add_foo.sql
if major != MAJOR_SCHEMA_VERSION {
return fmt.Errorf("current schema major version %d does not match the expected schema major version %d after migration, make sure to set the correct PRAGMA user_version in the migration script", major, MAJOR_SCHEMA_VERSION)
}

if minor != MINOR_SCHEMA_VERSION {
return fmt.Errorf("current schema minor version %d does not match the expected schema minor version %d after migration, make sure to set the correct PRAGMA user_version in the migration script", minor, MINOR_SCHEMA_VERSION)
}

db.l.Info("Completed database migration.")
Expand Down

0 comments on commit d28de62

Please sign in to comment.