Repository: ariga/atlas
Pull request #1019 (2022-08-07 08:27:27)

cmd/atlas: allow storing revisions with zero total


Patch:
diff --git a/cmd/atlas/internal/migrate/ent/revision/revision.go b/cmd/atlas/internal/migrate/ent/revision/revision.go
index ab5a9883..6361a113 100644
--- a/cmd/atlas/internal/migrate/ent/revision/revision.go
+++ b/cmd/atlas/internal/migrate/ent/revision/revision.go
@@ -61,6 +61,8 @@ func ValidColumn(column string) bool {
 }
 
 var (
+	// AppliedValidator is a validator for the "applied" field. It is called by the builders before save.
+	AppliedValidator func(int) error
 	// TotalValidator is a validator for the "total" field. It is called by the builders before save.
 	TotalValidator func(int) error
 )
diff --git a/cmd/atlas/internal/migrate/ent/revision_create.go b/cmd/atlas/internal/migrate/ent/revision_create.go
index e475f93c..fade030e 100644
--- a/cmd/atlas/internal/migrate/ent/revision_create.go
+++ b/cmd/atlas/internal/migrate/ent/revision_create.go
@@ -187,6 +187,11 @@ func (rc *RevisionCreate) check() error {
 	if _, ok := rc.mutation.Applied(); !ok {
 		return &ValidationError{Name: "applied", err: errors.New(`ent: missing required field "Revision.applied"`)}
 	}
+	if v, ok := rc.mutation.Applied(); ok {
+		if err := revision.AppliedValidator(v); err != nil {
+			return &ValidationError{Name: "applied", err: fmt.Errorf(`ent: validator failed for field "Revision.applied": %w`, err)}
+		}
+	}
 	if _, ok := rc.mutation.Total(); !ok {
 		return &ValidationError{Name: "total", err: errors.New(`ent: missing required field "Revision.total"`)}
 	}
diff --git a/cmd/atlas/internal/migrate/ent/revision_update.go b/cmd/atlas/internal/migrate/ent/revision_update.go
index 12383f2e..5aa52c0f 100644
--- a/cmd/atlas/internal/migrate/ent/revision_update.go
+++ b/cmd/atlas/internal/migrate/ent/revision_update.go
@@ -197,6 +197,11 @@ func (ru *RevisionUpdate) ExecX(ctx context.Context) {
 
 // check runs all checks and user-defined validators on the builder.
 func (ru *RevisionUpdate) check() error {
+	if v, ok := ru.mutation.Applied(); ok {
+		if err := revision.AppliedValidator(v); err != nil {
+			return &ValidationError{Name: "applied", err: fmt.Errorf(`ent: validator failed for field "Revision.applied": %w`, err)}
+		}
+	}
 	if v, ok := ru.mutation.Total(); ok {
 		if err := revision.TotalValidator(v); err != nil {
 			return &ValidationError{Name: "total", err: fmt.Errorf(`ent: validator failed for field "Revision.total": %w`, err)}
@@ -516,6 +521,11 @@ func (ruo *RevisionUpdateOne) ExecX(ctx context.Context) {
 
 // check runs all checks and user-defined validators on the builder.
 func (ruo *RevisionUpdateOne) check() error {
+	if v, ok := ruo.mutation.Applied(); ok {
+		if err := revision.AppliedValidator(v); err != nil {
+			return &ValidationError{Name: "applied", err: fmt.Errorf(`ent: validator failed for field "Revision.applied": %w`, err)}
+		}
+	}
 	if v, ok := ruo.mutation.Total(); ok {
 		if err := revision.TotalValidator(v); err != nil {
 			return &ValidationError{Name: "total", err: fmt.Errorf(`ent: validator failed for field "Revision.total": %w`, err)}
diff --git a/cmd/atlas/internal/migrate/ent/runtime.go b/cmd/atlas/internal/migrate/ent/runtime.go
index c3e1864c..ac51c74a 100644
--- a/cmd/atlas/internal/migrate/ent/runtime.go
+++ b/cmd/atlas/internal/migrate/ent/runtime.go
@@ -17,6 +17,10 @@ import (
 func init() {
 	revisionFields := schema.Revision{}.Fields()
 	_ = revisionFields
+	// revisionDescApplied is the schema descriptor for applied field.
+	revisionDescApplied := revisionFields[3].Descriptor()
+	// revision.AppliedValidator is a validator for the "applied" field. It is called by the builders before save.
+	revision.AppliedValidator = revisionDescApplied.Validators[0].(func(int) error)
 	// revisionDescTotal is the schema descriptor for total field.
 	revisionDescTotal := revisionFields[4].Descriptor()
 	// revision.TotalValidator is a validator for the "total" field. It is called by the builders before save.
diff --git a/cmd/atlas/internal/migrate/ent/schema/revision.go b/cmd/atlas/internal/migrate/ent/schema/revision.go
index 68ac9bfb..e4fe6f92 100644
--- a/cmd/atlas/internal/migrate/ent/schema/revision.go
+++ b/cmd/atlas/internal/migrate/ent/schema/revision.go
@@ -30,9 +30,10 @@ func (Revision) Fields() []ent.Field {
 			Immutable(),
 		field.Uint("type").
 			GoType(migrate.RevisionType(0)),
-		field.Int("applied"),
+		field.Int("applied").
+			NonNegative(),
 		field.Int("total").
-			Positive(),
+			NonNegative(),
 		field.Time("executed_at").
 			Immutable(),
 		field.Int64("execution_time").
diff --git a/sql/migrate/migrate.go b/sql/migrate/migrate.go
index 9cc59cf3..9a13c689 100644
--- a/sql/migrate/migrate.go
+++ b/sql/migrate/migrate.go
@@ -84,9 +84,6 @@ type (
 	// StateReader wraps the method for reading a database/schema state.
 	// The types below provides a few builtin options for reading a state
 	// from a migration directory, a static object (e.g. a parsed file).
-	//
-	// In next Go version, the State will be replaced with the following
-	// union type `interface { Realm | Schema }`.
 	StateReader interface {
 		ReadState(ctx context.Context) (*schema.Realm, error)
 	}
diff --git a/sql/sqlite/migrate.go b/sql/sqlite/migrate.go
index 745b144a..71e4c072 100644
--- a/sql/sqlite/migrate.go
+++ b/sql/sqlite/migrate.go
@@ -332,10 +332,7 @@ func (s *state) fks(b *sqlx.Builder, fks ...*schema.ForeignKey) {
 }
 
 func (s *state) copyRows(from *schema.Table, to *schema.Table, changes []schema.Change) (bool, error) {
-	var (
-		args       []interface{}
-		fromC, toC []string
-	)
+	var fromC, toC []string
 	for _, column := range to.Columns {
 		// Skip generated columns in INSERT as they are computed.
 		if sqlx.Has(column.Attrs, &schema.GeneratedExpr{}) {
@@ -376,12 +373,11 @@ func (s *state) copyRows(from *schema.Table, to *schema.Table, changes []schema.
 		case *schema.ModifyColumn:
 			toC = append(toC, column.Name)
 			if !column.Type.Null && column.Default != nil && change.Change.Is(schema.ChangeNull|schema.ChangeDefault) {
-				fromC = append(fromC, fmt.Sprintf("IFNULL(`%[1]s`, ?) AS `%[1]s`", column.Name))
 				x, err := defaultValue(column)
 				if err != nil {
 					return false, err
 				}
-				args = append(args, x)
+				fromC = append(fromC, fmt.Sprintf("IFNULL(`%[1]s`, %s) AS `%[1]s`", column.Name, x))
 			} else {
 				fromC = append(fromC, column.Name)
 			}
@@ -398,7 +394,6 @@ func (s *state) copyRows(from *schema.Table, to *schema.Table, changes []schema.
 				"INSERT INTO `%s` (%s) SELECT %s FROM `%s`",
 				to.Name, identComma(toC), identComma(fromC), from.Name,
 			),
-			Args:    args,
 			Comment: fmt.Sprintf("copy rows from old table %q to new temporary table %q", from.Name, to.Name),
 		})
 	}
@@ -533,7 +528,12 @@ func Build(phrase string) *sqlx.Builder {
 func defaultValue(c *schema.Column) (string, error) {
 	switch x := c.Default.(type) {
 	case *schema.Literal:
-		return sqlx.SingleQuote(x.V)
+		switch c.Type.Type.(type) {
+		case *schema.BoolType, *schema.DecimalType, *schema.IntegerType, *schema.FloatType:
+			return x.V, nil
+		default:
+			return sqlx.SingleQuote(x.V)
+		}
 	case *schema.RawExpr:
 		return x.X, nil
 	default:


Test patch:
diff --git a/cmd/atlas/internal/cmdapi/migrate_test.go b/cmd/atlas/internal/cmdapi/migrate_test.go
index a7612d0a..46cdf474 100644
--- a/cmd/atlas/internal/cmdapi/migrate_test.go
+++ b/cmd/atlas/internal/cmdapi/migrate_test.go
@@ -265,7 +265,7 @@ func TestMigrate_ApplyBaseline(t *testing.T) {
 		"--url", fmt.Sprintf("sqlite://file:%s?cache=shared&_fk=1", filepath.Join(p, "test.db")),
 	)
 	require.NoError(t, err)
-	require.Contains(t, s, "Migrating to version 20220318104615 (2 migrations in total)")
+	require.Contains(t, s, "Migrating to version 20220318104615 from 1 (2 migrations in total)")
 }
 
 func TestMigrate_Diff(t *testing.T) {
diff --git a/sql/sqlite/migrate_test.go b/sql/sqlite/migrate_test.go
index 13918678..78b6516f 100644
--- a/sql/sqlite/migrate_test.go
+++ b/sql/sqlite/migrate_test.go
@@ -191,7 +191,9 @@ func TestPlanChanges(t *testing.T) {
 					users := &schema.Table{
 						Name: "users",
 						Columns: []*schema.Column{
-							{Name: "id", Type: &schema.ColumnType{Type: &schema.IntegerType{T: "bigint"}}},
+							schema.NewIntColumn("id", "bigint"),
+							schema.NewIntColumn("rank", "int").SetDefault(&schema.Literal{V: "1"}),
+							schema.NewStringColumn("nick", "text").SetDefault(&schema.Literal{V: "a8m"}),
 						},
 						Attrs: []schema.Attr{
 							&schema.Check{Expr: "(id <> 0)"},
@@ -200,6 +202,16 @@ func TestPlanChanges(t *testing.T) {
 					return &schema.ModifyTable{
 						T: users,
 						Changes: []schema.Change{
+							&schema.ModifyColumn{
+								From:   schema.NewNullIntColumn("id", "bigint"),
+								To:     users.Columns[1],
+								Change: schema.ChangeNull | schema.ChangeDefault,
+							},
+							&schema.ModifyColumn{
+								From:   schema.NewNullStringColumn("nick", "text"),
+								To:     users.Columns[2],
+								Change: schema.ChangeNull | schema.ChangeDefault,
+							},
 							&schema.DropColumn{
 								C: &schema.Column{Name: "name", Type: &schema.ColumnType{Type: &schema.StringType{T: "varchar(255)"}}},
 							},
@@ -214,8 +226,8 @@ func TestPlanChanges(t *testing.T) {
 				Transactional: true,
 				Changes: []*migrate.Change{
 					{Cmd: "PRAGMA foreign_keys = off"},
-					{Cmd: "CREATE TABLE `new_users` (`id` bigint NOT NULL, CHECK (id <> 0))", Reverse: "DROP TABLE `new_users`"},
-					{Cmd: "INSERT INTO `new_users` (`id`) SELECT `id` FROM `users`"},
+					{Cmd: "CREATE TABLE `new_users` (`id` bigint NOT NULL, `rank` int NOT NULL DEFAULT 1, `nick` text NOT NULL DEFAULT 'a8m', CHECK (id <> 0))", Reverse: "DROP TABLE `new_users`"},
+					{Cmd: "INSERT INTO `new_users` (`id`, `rank`, `nick`) SELECT `id`, IFNULL(`rank`, 1) AS `rank`, IFNULL(`nick`, 'a8m') AS `nick` FROM `users`"},
 					{Cmd: "DROP TABLE `users`"},
 					{Cmd: "ALTER TABLE `new_users` RENAME TO `users`"},
 					{Cmd: "PRAGMA foreign_keys = on"},