diff --git a/atlasexec/atlas.go b/atlasexec/atlas.go index 28c9df5..949ec4b 100644 --- a/atlasexec/atlas.go +++ b/atlasexec/atlas.go @@ -39,7 +39,7 @@ type ( } // TriggerType defines the type for the "trigger_type" enum field. TriggerType string - // ExecutionOrder define how Atlas computes and executes pending migration files to the database. + // MigrateExecOrder define how Atlas computes and executes pending migration files to the database. // See: https://atlasgo.io/versioned/apply#execution-order MigrateExecOrder string // DeployRunContext describes what triggered this command (e.g., GitHub Action, v1.2.3) @@ -63,6 +63,21 @@ type ( DryRun bool Vars Vars } + // MigrateDownParams are the parameters for the `migrate down` command. + MigrateDownParams struct { + Env string + ConfigURL string + Context *DeployRunContext + DirURL string + URL string + RevisionsSchema string + ToVersion string + Vars Vars + + // Not yet supported + // DryRun bool + // TxMode string + } // MigrateStatusParams are the parameters for the `migrate status` command. MigrateStatusParams struct { Env string @@ -169,9 +184,9 @@ func NewClient(workingDir, execPath string) (_ *Client, err error) { // }) // return err // }) -func (t Client) WithWorkDir(dir string, fn func(*Client) error) error { - t.workingDir = dir - return fn(&t) +func (c *Client) WithWorkDir(dir string, fn func(*Client) error) error { + c.workingDir = dir + return fn(c) } // Login runs the 'login' command. @@ -282,6 +297,44 @@ func (c *Client) MigrateApplySlice(ctx context.Context, params *MigrateApplyPara return jsonDecodeErr[MigrateApply](newMigrateApplyError)(c.runCommand(ctx, args)) } +// MigrateDown runs the 'migrate down' command. +func (c *Client) MigrateDown(ctx context.Context, params *MigrateDownParams) (*MigrateDown, error) { + args := []string{"migrate", "down", "--format", "{{ json . }}"} + if params.Env != "" { + args = append(args, "--env", params.Env) + } + if params.ConfigURL != "" { + args = append(args, "--config", params.ConfigURL) + } + if params.Context != nil { + buf, err := json.Marshal(params.Context) + if err != nil { + return nil, err + } + args = append(args, "--context", string(buf)) + } + if params.URL != "" { + args = append(args, "--url", params.URL) + } + if params.DirURL != "" { + args = append(args, "--dir", params.DirURL) + } + if params.RevisionsSchema != "" { + args = append(args, "--revisions-schema", params.RevisionsSchema) + } + if params.ToVersion != "" { + args = append(args, "--to-version", params.ToVersion) + } + args = append(args, params.Vars.AsArgs()...) + r, err := c.runCommand(ctx, args) + if cliErr := (cliError{}); errors.As(err, &cliErr) && cliErr.stderr == "" { + r = strings.NewReader(cliErr.stdout) + err = nil + } + // NOTE: This command only support one result. + return firstResult(jsonDecode[MigrateDown](r, err)) +} + // SchemaApply runs the 'schema apply' command. func (c *Client) SchemaApply(ctx context.Context, params *SchemaApplyParams) (*SchemaApply, error) { return firstResult(c.SchemaApplySlice(ctx, params)) diff --git a/atlasexec/atlas_models.go b/atlasexec/atlas_models.go index ec1d478..94c2387 100644 --- a/atlasexec/atlas_models.go +++ b/atlasexec/atlas_models.go @@ -38,6 +38,18 @@ type ( // but by Atlas, e.g. when committing or rolling back a transaction. Error string `json:"Error,omitempty"` } + // MigrateDown contains a summary of a migration down attempt on a database. + MigrateDown struct { + Planned []File `json:"Planned,omitempty"` // Pending migration files + Applied []*AppliedFile `json:"Applied,omitempty"` // Applied files + Current string `json:"Current,omitempty"` // Current migration version + Target string `json:"Target,omitempty"` // Target migration version + Start time.Time + End time.Time + // Error is set even then, if it was not caused by a statement in a migration file, + // but by Atlas, e.g. when committing or rolling back a transaction. + Error string `json:"Error,omitempty"` + } // MigrateStatus contains a summary of the migration status of a database. MigrateStatus struct { Available []File `json:"Available,omitempty"` // Available migration files @@ -142,6 +154,11 @@ type ( MigrateApplyError struct { Result []*MigrateApply } + // MigrateDownError is returned when an error occurred + // during a migration down attempt. + MigrateDownError struct { + Result []*MigrateDown + } // SchemaApplyError is returned when an error occurred // during a schema applying attempt. SchemaApplyError struct { @@ -152,6 +169,9 @@ type ( // Error implements the error interface. func (e *MigrateApplyError) Error() string { return last(e.Result).Error } +// Error implements the error interface. +func (e *MigrateDownError) Error() string { return last(e.Result).Error } + // Error implements the error interface. func (e *SchemaApplyError) Error() string { return last(e.Result).Error } @@ -169,6 +189,11 @@ func (r *SummaryReport) DiagnosticsCount() int { func newMigrateApplyError(r []*MigrateApply) error { return &MigrateApplyError{Result: r} } + +func newMigrateDownError(r []*MigrateDown) error { + return &MigrateDownError{Result: r} +} + func newSchemaApplyError(r []*SchemaApply) error { return &SchemaApplyError{Result: r} } diff --git a/atlasexec/atlas_test.go b/atlasexec/atlas_test.go index 11d518a..595f546 100644 --- a/atlasexec/atlas_test.go +++ b/atlasexec/atlas_test.go @@ -841,3 +841,54 @@ func TestMigrateApply(t *testing.T) { }) } } + +func TestMigrateDown(t *testing.T) { + wd, err := os.Getwd() + require.NoError(t, err) + // Mock the client with a script that just prints the arguments to stderr and + // exit with an error code. + c, err := atlasexec.NewClient(t.TempDir(), filepath.Join(wd, "./mock-args.sh")) + require.NoError(t, err) + + for _, tt := range []struct { + name string + params *atlasexec.MigrateDownParams + expect string + }{ + { + name: "no params", + params: &atlasexec.MigrateDownParams{}, + expect: "migrate down --format {{ json . }}", + }, + { + name: "with env", + params: &atlasexec.MigrateDownParams{ + Env: "test", + }, + expect: "migrate down --format {{ json . }} --env test", + }, + { + name: "with url", + params: &atlasexec.MigrateDownParams{ + URL: "sqlite://file?_fk=1&cache=shared&mode=memory", + }, + expect: "migrate down --format {{ json . }} --url sqlite://file?_fk=1&cache=shared&mode=memory", + }, + { + name: "with target version", + params: &atlasexec.MigrateDownParams{ + ToVersion: "12345", + }, + expect: "migrate down --format {{ json . }} --to-version 12345", + }, + } { + t.Run(tt.name, func(t *testing.T) { + _, err := c.MigrateDown(context.Background(), tt.params) + require.Error(t, err) + // The script mock-args.sh exit with an error code. + // So, our atlasexec.MigrateApply should return a cliError. + // Which contains all output from the script (both stdout and stderr). + require.Equal(t, tt.expect, err.Error()) + }) + } +}