package provider import ( "context" "database/sql" "errors" "fmt" "github.com/pressly/goose/v3/database" "github.com/pressly/goose/v3/lock" ) const ( // DefaultTablename is the default name of the database table used to track history of applied // migrations. It can be overridden using the [WithTableName] option when creating a new // provider. DefaultTablename = "goose_db_version" ) // ProviderOption is a configuration option for a goose provider. type ProviderOption interface { apply(*config) error } // WithStore configures the provider with a custom [database.Store] implementation. // // By default, the provider uses the [database.NewStore] function to create a store backed by the // given dialect. However, this option allows users to provide their own implementation or call // [database.NewStore] with custom options, such as setting the table name. // // Example: // // // Create a store with a custom table name. // store, err := database.NewStore(database.DialectPostgres, "my_custom_table_name") // if err != nil { // return err // } // // Create a provider with the custom store. // provider, err := goose.NewProvider("", db, nil, goose.WithStore(store)) // if err != nil { // return err // } func WithStore(store database.Store) ProviderOption { return configFunc(func(c *config) error { if c.store != nil { return fmt.Errorf("store already set: %T", c.store) } if store == nil { return errors.New("store must not be nil") } if store.Tablename() == "" { return errors.New("store implementation must set the table name") } c.store = store return nil }) } // WithVerbose enables verbose logging. func WithVerbose(b bool) ProviderOption { return configFunc(func(c *config) error { c.verbose = b return nil }) } // WithSessionLocker enables locking using the provided SessionLocker. // // If WithSessionLocker is not called, locking is disabled. func WithSessionLocker(locker lock.SessionLocker) ProviderOption { return configFunc(func(c *config) error { if c.lockEnabled { return errors.New("lock already enabled") } if c.sessionLocker != nil { return errors.New("session locker already set") } if locker == nil { return errors.New("session locker must not be nil") } c.lockEnabled = true c.sessionLocker = locker return nil }) } // WithExcludes excludes the given file names from the list of migrations. // // If WithExcludes is called multiple times, the list of excludes is merged. func WithExcludes(excludes []string) ProviderOption { return configFunc(func(c *config) error { for _, name := range excludes { c.excludes[name] = true } return nil }) } // GoMigrationFunc is a user-defined Go migration, registered using the option [WithGoMigration]. type GoMigrationFunc struct { // One of the following must be set: Run func(context.Context, *sql.Tx) error // -- OR -- RunNoTx func(context.Context, *sql.DB) error } // WithGoMigration registers a Go migration with the given version. // // If WithGoMigration is called multiple times with the same version, an error is returned. Both up // and down [GoMigration] may be nil. But if set, exactly one of Run or RunNoTx functions must be // set. func WithGoMigration(version int64, up, down *GoMigrationFunc) ProviderOption { return configFunc(func(c *config) error { if version < 1 { return errors.New("version must be greater than zero") } if _, ok := c.registered[version]; ok { return fmt.Errorf("go migration with version %d already registered", version) } // Allow nil up/down functions. This enables users to apply "no-op" migrations, while // versioning them. if up != nil { if up.Run == nil && up.RunNoTx == nil { return fmt.Errorf("go migration with version %d must have an up function", version) } if up.Run != nil && up.RunNoTx != nil { return fmt.Errorf("go migration with version %d must not have both an up and upNoTx function", version) } } if down != nil { if down.Run == nil && down.RunNoTx == nil { return fmt.Errorf("go migration with version %d must have a down function", version) } if down.Run != nil && down.RunNoTx != nil { return fmt.Errorf("go migration with version %d must not have both a down and downNoTx function", version) } } c.registered[version] = &goMigration{ up: up, down: down, } return nil }) } // WithAllowedMissing allows the provider to apply missing (out-of-order) migrations. By default, // goose will raise an error if it encounters a missing migration. // // Example: migrations 1,3 are applied and then version 2,6 are introduced. If this option is true, // then goose will apply 2 (missing) and 6 (new) instead of raising an error. The final order of // applied migrations will be: 1,3,2,6. Out-of-order migrations are always applied first, followed // by new migrations. func WithAllowedMissing(b bool) ProviderOption { return configFunc(func(c *config) error { c.allowMissing = b return nil }) } // WithDisabledVersioning disables versioning. Disabling versioning allows applying migrations // without tracking the versions in the database schema table. Useful for tests, seeding a database // or running ad-hoc queries. By default, goose will track all versions in the database schema // table. func WithDisabledVersioning(b bool) ProviderOption { return configFunc(func(c *config) error { c.disableVersioning = b return nil }) } type config struct { store database.Store verbose bool excludes map[string]bool // Go migrations registered by the user. These will be merged/resolved with migrations from the // filesystem and init() functions. registered map[int64]*goMigration // Locking options lockEnabled bool sessionLocker lock.SessionLocker // Feature disableVersioning bool allowMissing bool } type configFunc func(*config) error func (f configFunc) apply(cfg *config) error { return f(cfg) }