Repository: ariga/atlas
Pull request #1383 (2023-01-04 10:17:42)

sql: add default migration planners


Patch:
diff --git a/sql/internal/sqlx/plan.go b/sql/internal/sqlx/plan.go
index 14983cba..b3d7f9a2 100644
--- a/sql/internal/sqlx/plan.go
+++ b/sql/internal/sqlx/plan.go
@@ -56,6 +56,23 @@ func ApplyChanges(ctx context.Context, changes []schema.Change, p execPlanner, o
 	return nil
 }
 
+// noRows implements the schema.ExecQuerier for migrate.Driver's without connections.
+// This can be useful to always return no rows for queries, and block any execution.
+type noRows struct{}
+
+// QueryContext implements the sqlx.ExecQuerier interface.
+func (*noRows) QueryContext(context.Context, string, ...interface{}) (*sql.Rows, error) {
+	return nil, sql.ErrNoRows
+}
+
+// ExecContext implements the sqlx.ExecQuerier interface.
+func (*noRows) ExecContext(context.Context, string, ...interface{}) (sql.Result, error) {
+	return nil, errors.New("cannot execute statements without a database connection. use Open to create a new Driver")
+}
+
+// NoRows to be used by differs and planners without a connection.
+var NoRows schema.ExecQuerier = (*noRows)(nil)
+
 // SetReversible sets the Reversible field to
 // true if all planned changes are reversible.
 func SetReversible(p *migrate.Plan) error {
diff --git a/sql/mysql/migrate.go b/sql/mysql/migrate.go
index 5c63f9e4..6fd94544 100644
--- a/sql/mysql/migrate.go
+++ b/sql/mysql/migrate.go
@@ -16,6 +16,11 @@ import (
 	"ariga.io/atlas/sql/schema"
 )
 
+// DefaultPlan provides basic planning capabilities for MySQL dialects.
+// Note, it is recommended to call Open, create a new Driver and use its
+// migrate.PlanApplier when a database connection is available.
+var DefaultPlan migrate.PlanApplier = &planApply{conn: conn{ExecQuerier: sqlx.NoRows, V: "8.0.31"}}
+
 // A planApply provides migration capabilities for schema elements.
 type planApply struct{ conn }
 
@@ -105,11 +110,11 @@ func (s *state) topLevel(changes []schema.Change) ([]schema.Change, error) {
 				b.P("IF NOT EXISTS")
 			}
 			b.Ident(c.S.Name)
-			// Schema was created with CHARSET and it is not the default database character set.
+			// Schema was created with CHARSET, and it is not the default database character set.
 			if a := (schema.Charset{}); sqlx.Has(c.S.Attrs, &a) && a.V != "" && a.V != s.charset {
 				b.P("CHARSET", a.V)
 			}
-			// Schema was created with COLLATE and it is not the default database collation.
+			// Schema was created with COLLATE, and it is not the default database collation.
 			if a := (schema.Collation{}); sqlx.Has(c.S.Attrs, &a) && a.V != "" && a.V != s.collate {
 				b.P("COLLATE", a.V)
 			}
diff --git a/sql/postgres/migrate.go b/sql/postgres/migrate.go
index b5c1ab18..9c37f9af 100644
--- a/sql/postgres/migrate.go
+++ b/sql/postgres/migrate.go
@@ -16,6 +16,11 @@ import (
 	"ariga.io/atlas/sql/schema"
 )
 
+// DefaultPlan provides basic planning capabilities for PostgreSQL dialects.
+// Note, it is recommended to call Open, create a new Driver and use its
+// migrate.PlanApplier when a database connection is available.
+var DefaultPlan migrate.PlanApplier = &planApply{conn: conn{ExecQuerier: sqlx.NoRows}}
+
 // A planApply provides migration capabilities for schema elements.
 type planApply struct{ conn }
 
diff --git a/sql/sqlite/migrate.go b/sql/sqlite/migrate.go
index e8d06de6..f2242e94 100644
--- a/sql/sqlite/migrate.go
+++ b/sql/sqlite/migrate.go
@@ -14,6 +14,11 @@ import (
 	"ariga.io/atlas/sql/schema"
 )
 
+// DefaultPlan provides basic planning capabilities for SQLite dialects.
+// Note, it is recommended to call Open, create a new Driver and use its
+// migrate.PlanApplier when a database connection is available.
+var DefaultPlan migrate.PlanApplier = &planApply{conn: conn{ExecQuerier: sqlx.NoRows}}
+
 // A planApply provides migration capabilities for schema elements.
 type planApply struct{ conn }
 
@@ -474,7 +479,7 @@ func (s *state) alterTable(modify *schema.ModifyTable) error {
 func (s *state) tableSeq(ctx context.Context, add *schema.AddTable) error {
 	var inc AutoIncrement
 	switch pk := add.T.PrimaryKey; {
-	// Sequence was set on the table.
+	// Sequence was set on table attributes.
 	case sqlx.Has(add.T.Attrs, &inc) && inc.Seq > 0:
 	// Sequence was set on table primary-key (a single column PK).
 	case pk != nil && len(pk.Parts) == 1 && pk.Parts[0].C != nil && sqlx.Has(pk.Parts[0].C.Attrs, &inc) && inc.Seq > 0:


Test patch:
diff --git a/sql/mysql/migrate_test.go b/sql/mysql/migrate_test.go
index 428048de..8e2c9533 100644
--- a/sql/mysql/migrate_test.go
+++ b/sql/mysql/migrate_test.go
@@ -981,6 +981,20 @@ func TestPlanChanges(t *testing.T) {
 	}
 }
 
+func TestDefaultPlan(t *testing.T) {
+	changes, err := DefaultPlan.PlanChanges(context.Background(), "plan", []schema.Change{
+		&schema.AddTable{T: schema.NewTable("t1").SetSchema(schema.New("s1")).AddColumns(schema.NewIntColumn("a", "int"))},
+	})
+	require.NoError(t, err)
+	require.Equal(t, 1, len(changes.Changes))
+	require.Equal(t, "CREATE TABLE `s1`.`t1` (`a` int NOT NULL)", changes.Changes[0].Cmd)
+
+	err = DefaultPlan.ApplyChanges(context.Background(), []schema.Change{
+		&schema.AddTable{T: schema.NewTable("t1").AddColumns(schema.NewIntColumn("a", "int"))},
+	})
+	require.EqualError(t, err, `create "t1" table: cannot execute statements without a database connection. use Open to create a new Driver`)
+}
+
 func newMigrate(version string) (migrate.PlanApplier, *mock, error) {
 	db, m, err := sqlmock.New()
 	if err != nil {
diff --git a/sql/postgres/migrate_test.go b/sql/postgres/migrate_test.go
index 83b57e4e..0de9ddba 100644
--- a/sql/postgres/migrate_test.go
+++ b/sql/postgres/migrate_test.go
@@ -1233,3 +1233,17 @@ func TestPlanChanges(t *testing.T) {
 		})
 	}
 }
+
+func TestDefaultPlan(t *testing.T) {
+	changes, err := DefaultPlan.PlanChanges(context.Background(), "plan", []schema.Change{
+		&schema.AddTable{T: schema.NewTable("t1").SetSchema(schema.New("s1")).AddColumns(schema.NewIntColumn("a", "int"))},
+	})
+	require.NoError(t, err)
+	require.Equal(t, 1, len(changes.Changes))
+	require.Equal(t, `CREATE TABLE "s1"."t1" ("a" integer NOT NULL)`, changes.Changes[0].Cmd)
+
+	err = DefaultPlan.ApplyChanges(context.Background(), []schema.Change{
+		&schema.AddTable{T: schema.NewTable("t1").AddColumns(schema.NewIntColumn("a", "int"))},
+	})
+	require.EqualError(t, err, `create "t1" table: cannot execute statements without a database connection. use Open to create a new Driver`)
+}
diff --git a/sql/sqlite/migrate_test.go b/sql/sqlite/migrate_test.go
index 1bab0d8b..d5fed444 100644
--- a/sql/sqlite/migrate_test.go
+++ b/sql/sqlite/migrate_test.go
@@ -433,3 +433,17 @@ func TestPlanChanges(t *testing.T) {
 		})
 	}
 }
+
+func TestDefaultPlan(t *testing.T) {
+	changes, err := DefaultPlan.PlanChanges(context.Background(), "plan", []schema.Change{
+		&schema.AddTable{T: schema.NewTable("t1").AddColumns(schema.NewIntColumn("a", "int"))},
+	})
+	require.NoError(t, err)
+	require.Equal(t, 1, len(changes.Changes))
+	require.Equal(t, "CREATE TABLE `t1` (`a` int NOT NULL)", changes.Changes[0].Cmd)
+
+	err = DefaultPlan.ApplyChanges(context.Background(), []schema.Change{
+		&schema.AddTable{T: schema.NewTable("t1").AddColumns(schema.NewIntColumn("a", "int"))},
+	})
+	require.EqualError(t, err, `create "t1" table: cannot execute statements without a database connection. use Open to create a new Driver`)
+}