diff --git a/database/mysql/README.md b/database/mysql/README.md index 00aca683d..1799e900c 100644 --- a/database/mysql/README.md +++ b/database/mysql/README.md @@ -2,21 +2,22 @@ `mysql://user:password@tcp(host:port)/dbname?query` -| URL Query | WithInstance Config | Description | -|------------|---------------------|-------------| -| `x-migrations-table` | `MigrationsTable` | Name of the migrations table | -| `x-no-lock` | `NoLock` | Set to `true` to skip `GET_LOCK`/`RELEASE_LOCK` statements. Useful for [multi-master MySQL flavors](https://www.percona.com/doc/percona-xtradb-cluster/LATEST/features/pxc-strict-mode.html#explicit-table-locking). Only run migrations from one host when this is enabled. | -| `x-statement-timeout` | `StatementTimeout` | Abort any statement that takes more than the specified number of milliseconds, functionally similar to [Server-side SELECT statement timeouts](https://dev.mysql.com/blog-archive/server-side-select-statement-timeouts/) but enforced by the client. Available for all versions of MySQL, not just >=5.7. | -| `dbname` | `DatabaseName` | The name of the database to connect to | -| `user` | | The user to sign in as | -| `password` | | The user's password | -| `host` | | The host to connect to. | -| `port` | | The port to bind to. | -| `tls` | | TLS / SSL encrypted connection parameter; see [go-sql-driver](https://github.com/go-sql-driver/mysql#tls). Use any name (e.g. `migrate`) if you want to use a custom TLS config (`x-tls-` queries). | -| `x-tls-ca` | | The location of the CA (certificate authority) file. | -| `x-tls-cert` | | The location of the client certicicate file. Must be used with `x-tls-key`. | -| `x-tls-key` | | The location of the private key file. Must be used with `x-tls-cert`. | -| `x-tls-insecure-skip-verify` | | Whether or not to use SSL (true\|false) | +| URL Query | WithInstance Config | Description | +|------------------------------|---------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `x-migrations-table` | `MigrationsTable` | Name of the migrations table | +| `x-no-lock` | `NoLock` | Set to `true` to skip `GET_LOCK`/`RELEASE_LOCK` statements. Useful for [multi-master MySQL flavors](https://www.percona.com/doc/percona-xtradb-cluster/LATEST/features/pxc-strict-mode.html#explicit-table-locking). Only run migrations from one host when this is enabled. | +| `x-statement-timeout` | `StatementTimeout` | Abort any statement that takes more than the specified number of milliseconds, functionally similar to [Server-side SELECT statement timeouts](https://dev.mysql.com/blog-archive/server-side-select-statement-timeouts/) but enforced by the client. Available for all versions of MySQL, not just >=5.7. | +| `x-safe-update` | `SafeUpdate` | Set to `true` if we should respect safe updates (TRUNCATE vs DELETE) | +| `dbname` | `DatabaseName` | The name of the database to connect to | +| `user` | | The user to sign in as | +| `password` | | The user's password | +| `host` | | The host to connect to. | +| `port` | | The port to bind to. | +| `tls` | | TLS / SSL encrypted connection parameter; see [go-sql-driver](https://github.com/go-sql-driver/mysql#tls). Use any name (e.g. `migrate`) if you want to use a custom TLS config (`x-tls-` queries). | +| `x-tls-ca` | | The location of the CA (certificate authority) file. | +| `x-tls-cert` | | The location of the client certicicate file. Must be used with `x-tls-key`. | +| `x-tls-key` | | The location of the private key file. Must be used with `x-tls-cert`. | +| `x-tls-insecure-skip-verify` | | Whether or not to use SSL (true\|false) | ## Use with existing client diff --git a/database/mysql/mysql.go b/database/mysql/mysql.go index e0e18e7a9..cbfd78611 100644 --- a/database/mysql/mysql.go +++ b/database/mysql/mysql.go @@ -43,6 +43,7 @@ type Config struct { MigrationsTable string DatabaseName string NoLock bool + SafeUpdate bool StatementTimeout time.Duration } @@ -253,6 +254,14 @@ func (m *Mysql) Open(url string) (database.Driver, error) { } } + safeUpdateParam, safeUpdate := customParams["x-safe-update"], false + if safeUpdateParam != "" { + safeUpdate, err = strconv.ParseBool(safeUpdateParam) + if err != nil { + return nil, fmt.Errorf("could not parse x-safe-update as bool: %w", err) + } + } + db, err := sql.Open("mysql", config.FormatDSN()) if err != nil { return nil, err @@ -262,6 +271,7 @@ func (m *Mysql) Open(url string) (database.Driver, error) { DatabaseName: config.DBName, MigrationsTable: customParams["x-migrations-table"], NoLock: noLock, + SafeUpdate: safeUpdate, StatementTimeout: time.Duration(statementTimeout) * time.Millisecond, }) if err != nil { @@ -361,7 +371,12 @@ func (m *Mysql) SetVersion(version int, dirty bool) error { return &database.Error{OrigErr: err, Err: "transaction start failed"} } + // Either use DELETE or TRUNCATE, based on safe update selection query := "DELETE FROM `" + m.config.MigrationsTable + "`" + if m.config.SafeUpdate { + query = "TRUNCATE `" + m.config.MigrationsTable + "`" + } + if _, err := tx.ExecContext(context.Background(), query); err != nil { if errRollback := tx.Rollback(); errRollback != nil { err = multierror.Append(err, errRollback) diff --git a/database/mysql/mysql_test.go b/database/mysql/mysql_test.go index 09d95fc9a..0b60be7da 100644 --- a/database/mysql/mysql_test.go +++ b/database/mysql/mysql_test.go @@ -152,6 +152,38 @@ func TestMigrate(t *testing.T) { }) } +func TestMigrateSafeUpdate(t *testing.T) { + dktesting.ParallelTest(t, specs, func(t *testing.T, c dktest.ContainerInfo) { + ip, port, err := c.Port(defaultPort) + if err != nil { + t.Fatal(err) + } + + addr := fmt.Sprintf("mysql://root:root@tcp(%v:%v)/public?x-safe-update=true", ip, port) + p := &Mysql{} + d, err := p.Open(addr) + if err != nil { + t.Fatal(err) + } + defer func() { + if err := d.Close(); err != nil { + t.Error(err) + } + }() + + m, err := migrate.NewWithDatabaseInstance("file://./examples/migrations", "public", d) + if err != nil { + t.Fatal(err) + } + dt.TestMigrate(t, m) + + // check ensureVersionTable + if err := d.(*Mysql).ensureVersionTable(); err != nil { + t.Fatal(err) + } + }) +} + func TestMigrateAnsiQuotes(t *testing.T) { // mysql.SetLogger(mysql.Logger(log.New(io.Discard, "", log.Ltime))) @@ -279,6 +311,17 @@ func TestNoLockWorks(t *testing.T) { }) } +func TestSafeUpdateParamValidation(t *testing.T) { + ip := "127.0.0.1" + port := 3306 + addr := fmt.Sprintf("mysql://root:root@tcp(%v:%v)/public", ip, port) + p := &Mysql{} + _, err := p.Open(addr + "?x-safe-update=not-a-bool") + if !errors.Is(err, strconv.ErrSyntax) { + t.Fatal("Expected syntax error when passing a non-bool as x-safe-update parameter") + } +} + func TestExtractCustomQueryParams(t *testing.T) { testcases := []struct { name string