-
Notifications
You must be signed in to change notification settings - Fork 510
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(experimental): add internal migrate package and SessionLocker in…
…terface (#606)
- Loading branch information
Showing
12 changed files
with
723 additions
and
3 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
// Package migrate defines a Migration struct and implements the migration logic for executing Go | ||
// and SQL migrations. | ||
// | ||
// - For Go migrations, only *sql.Tx and *sql.DB are supported. *sql.Conn is not supported. | ||
// - For SQL migrations, all three are supported. | ||
// | ||
// Lastly, SQL migrations are lazily parsed. This means that the SQL migration is parsed the first | ||
// time it is executed. | ||
package migrate |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,166 @@ | ||
package migrate | ||
|
||
import ( | ||
"context" | ||
"database/sql" | ||
"errors" | ||
"fmt" | ||
|
||
"github.com/pressly/goose/v3/internal/sqlextended" | ||
) | ||
|
||
type Migration struct { | ||
// Fullpath is the full path to the migration file. | ||
// | ||
// Example: /path/to/migrations/123_create_users_table.go | ||
Fullpath string | ||
// Version is the version of the migration. | ||
Version int64 | ||
// Type is the type of migration. | ||
Type MigrationType | ||
// A migration is either a Go migration or a SQL migration, but never both. | ||
// | ||
// Note, the SQLParsed field is used to determine if the SQL migration has been parsed. This is | ||
// an optimization to avoid parsing the SQL migration if it is never required. Also, the | ||
// majority of the time migrations are incremental, so it is likely that the user will only want | ||
// to run the last few migrations, and there is no need to parse ALL prior migrations. | ||
// | ||
// Exactly one of these fields will be set: | ||
Go *Go | ||
// -- or -- | ||
SQLParsed bool | ||
SQL *SQL | ||
} | ||
|
||
type MigrationType int | ||
|
||
const ( | ||
TypeGo MigrationType = iota + 1 | ||
TypeSQL | ||
) | ||
|
||
func (t MigrationType) String() string { | ||
switch t { | ||
case TypeGo: | ||
return "go" | ||
case TypeSQL: | ||
return "sql" | ||
default: | ||
// This should never happen. | ||
return "unknown" | ||
} | ||
} | ||
|
||
func (m *Migration) UseTx() bool { | ||
switch m.Type { | ||
case TypeGo: | ||
return m.Go.UseTx | ||
case TypeSQL: | ||
return m.SQL.UseTx | ||
default: | ||
// This should never happen. | ||
panic("unknown migration type: use tx") | ||
} | ||
} | ||
|
||
func (m *Migration) IsEmpty(direction bool) bool { | ||
switch m.Type { | ||
case TypeGo: | ||
return m.Go.IsEmpty(direction) | ||
case TypeSQL: | ||
return m.SQL.IsEmpty(direction) | ||
default: | ||
// This should never happen. | ||
panic("unknown migration type: is empty") | ||
} | ||
} | ||
|
||
func (m *Migration) GetSQLStatements(direction bool) ([]string, error) { | ||
if m.Type != TypeSQL { | ||
return nil, fmt.Errorf("expected sql migration, got %s: no sql statements", m.Type) | ||
} | ||
if m.SQL == nil { | ||
return nil, errors.New("sql migration has not been initialized") | ||
} | ||
if !m.SQLParsed { | ||
return nil, errors.New("sql migration has not been parsed") | ||
} | ||
if direction { | ||
return m.SQL.UpStatements, nil | ||
} | ||
return m.SQL.DownStatements, nil | ||
} | ||
|
||
type Go struct { | ||
// We used an explicit bool instead of relying on a pointer because registered funcs may be nil. | ||
// These are still valid Go and versioned migrations, but they are just empty. | ||
// | ||
// For example: goose.AddMigration(nil, nil) | ||
UseTx bool | ||
|
||
// Only one of these func pairs will be set: | ||
UpFn, DownFn func(context.Context, *sql.Tx) error | ||
// -- or -- | ||
UpFnNoTx, DownFnNoTx func(context.Context, *sql.DB) error | ||
} | ||
|
||
func (g *Go) IsEmpty(direction bool) bool { | ||
if direction { | ||
return g.UpFn == nil && g.UpFnNoTx == nil | ||
} | ||
return g.DownFn == nil && g.DownFnNoTx == nil | ||
} | ||
|
||
func (g *Go) run(ctx context.Context, tx *sql.Tx, direction bool) error { | ||
var fn func(context.Context, *sql.Tx) error | ||
if direction { | ||
fn = g.UpFn | ||
} else { | ||
fn = g.DownFn | ||
} | ||
if fn != nil { | ||
return fn(ctx, tx) | ||
} | ||
return nil | ||
} | ||
|
||
func (g *Go) runNoTx(ctx context.Context, db *sql.DB, direction bool) error { | ||
var fn func(context.Context, *sql.DB) error | ||
if direction { | ||
fn = g.UpFnNoTx | ||
} else { | ||
fn = g.DownFnNoTx | ||
} | ||
if fn != nil { | ||
return fn(ctx, db) | ||
} | ||
return nil | ||
} | ||
|
||
type SQL struct { | ||
UseTx bool | ||
UpStatements []string | ||
DownStatements []string | ||
} | ||
|
||
func (s *SQL) IsEmpty(direction bool) bool { | ||
if direction { | ||
return len(s.UpStatements) == 0 | ||
} | ||
return len(s.DownStatements) == 0 | ||
} | ||
|
||
func (s *SQL) run(ctx context.Context, db sqlextended.DBTxConn, direction bool) error { | ||
var statements []string | ||
if direction { | ||
statements = s.UpStatements | ||
} else { | ||
statements = s.DownStatements | ||
} | ||
for _, stmt := range statements { | ||
if _, err := db.ExecContext(ctx, stmt); err != nil { | ||
return err | ||
} | ||
} | ||
return nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,75 @@ | ||
package migrate | ||
|
||
import ( | ||
"bytes" | ||
"io" | ||
"io/fs" | ||
|
||
"github.com/pressly/goose/v3/internal/sqlparser" | ||
) | ||
|
||
// ParseSQL parses all SQL migrations in BOTH directions. If a migration has already been parsed, it | ||
// will not be parsed again. | ||
// | ||
// Important: This function will mutate SQL migrations. | ||
func ParseSQL(fsys fs.FS, debug bool, migrations []*Migration) error { | ||
for _, m := range migrations { | ||
if m.Type == TypeSQL && !m.SQLParsed { | ||
parsedSQLMigration, err := parseSQL(fsys, m.Fullpath, parseAll, debug) | ||
if err != nil { | ||
return err | ||
} | ||
m.SQLParsed = true | ||
m.SQL = parsedSQLMigration | ||
} | ||
} | ||
return nil | ||
} | ||
|
||
// parse is used to determine which direction to parse the SQL migration. | ||
type parse int | ||
|
||
const ( | ||
// parseAll parses all SQL statements in BOTH directions. | ||
parseAll parse = iota + 1 | ||
// parseUp parses all SQL statements in the UP direction. | ||
parseUp | ||
// parseDown parses all SQL statements in the DOWN direction. | ||
parseDown | ||
) | ||
|
||
func parseSQL(fsys fs.FS, filename string, p parse, debug bool) (*SQL, error) { | ||
r, err := fsys.Open(filename) | ||
if err != nil { | ||
return nil, err | ||
} | ||
by, err := io.ReadAll(r) | ||
if err != nil { | ||
return nil, err | ||
} | ||
if err := r.Close(); err != nil { | ||
return nil, err | ||
} | ||
s := new(SQL) | ||
if p == parseAll || p == parseUp { | ||
s.UpStatements, s.UseTx, err = sqlparser.ParseSQLMigration( | ||
bytes.NewReader(by), | ||
sqlparser.DirectionUp, | ||
debug, | ||
) | ||
if err != nil { | ||
return nil, err | ||
} | ||
} | ||
if p == parseAll || p == parseDown { | ||
s.DownStatements, s.UseTx, err = sqlparser.ParseSQLMigration( | ||
bytes.NewReader(by), | ||
sqlparser.DirectionDown, | ||
debug, | ||
) | ||
if err != nil { | ||
return nil, err | ||
} | ||
} | ||
return s, nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,53 @@ | ||
package migrate | ||
|
||
import ( | ||
"context" | ||
"database/sql" | ||
"fmt" | ||
"path/filepath" | ||
) | ||
|
||
// Run runs the migration inside of a transaction. | ||
func (m *Migration) Run(ctx context.Context, tx *sql.Tx, direction bool) error { | ||
switch m.Type { | ||
case TypeSQL: | ||
if m.SQL == nil || !m.SQLParsed { | ||
return fmt.Errorf("tx: sql migration has not been parsed") | ||
} | ||
return m.SQL.run(ctx, tx, direction) | ||
case TypeGo: | ||
return m.Go.run(ctx, tx, direction) | ||
} | ||
// This should never happen. | ||
return fmt.Errorf("tx: failed to run migration %s: neither sql or go", filepath.Base(m.Fullpath)) | ||
} | ||
|
||
// RunNoTx runs the migration without a transaction. | ||
func (m *Migration) RunNoTx(ctx context.Context, db *sql.DB, direction bool) error { | ||
switch m.Type { | ||
case TypeSQL: | ||
if m.SQL == nil || !m.SQLParsed { | ||
return fmt.Errorf("db: sql migration has not been parsed") | ||
} | ||
return m.SQL.run(ctx, db, direction) | ||
case TypeGo: | ||
return m.Go.runNoTx(ctx, db, direction) | ||
} | ||
// This should never happen. | ||
return fmt.Errorf("db: failed to run migration %s: neither sql or go", filepath.Base(m.Fullpath)) | ||
} | ||
|
||
// RunConn runs the migration without a transaction using the provided connection. | ||
func (m *Migration) RunConn(ctx context.Context, conn *sql.Conn, direction bool) error { | ||
switch m.Type { | ||
case TypeSQL: | ||
if m.SQL == nil || !m.SQLParsed { | ||
return fmt.Errorf("conn: sql migration has not been parsed") | ||
} | ||
return m.SQL.run(ctx, conn, direction) | ||
case TypeGo: | ||
return fmt.Errorf("conn: go migrations are not supported with *sql.Conn") | ||
} | ||
// This should never happen. | ||
return fmt.Errorf("conn: failed to run migration %s: neither sql or go", filepath.Base(m.Fullpath)) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.