Repository: ariga/atlas
Pull request #1494 (2023-03-01 14:02:33)

sql: support planning with indentation 


Patch:
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index a8c0ad84..1c443484 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -167,7 +167,7 @@ jobs:
             ${{ runner.os }}-go-
       - name: Run integration tests for mysql56
         working-directory: internal/integration
-        run: go test -race -count=2 -v -run="MySQL" -version="mysql56" ./...
+        run: go test -race -count=2 -v -run="MySQL" -version="mysql56" -timeout 15m ./...
 
   integration-mysql57:
     runs-on: ubuntu-latest
@@ -198,7 +198,7 @@ jobs:
             ${{ runner.os }}-go-
       - name: Run integration tests for mysql57
         working-directory: internal/integration
-        run: go test -race -count=2 -v -run="MySQL" -version="mysql57" ./...
+        run: go test -race -count=2 -v -run="MySQL" -version="mysql57" -timeout 15m ./...
 
   integration-mysql8:
     runs-on: ubuntu-latest
@@ -229,7 +229,7 @@ jobs:
             ${{ runner.os }}-go-
       - name: Run integration tests for mysql8
         working-directory: internal/integration
-        run: go test -race -count=2 -v -run="MySQL" -version="mysql8" ./...
+        run: go test -race -count=2 -v -run="MySQL" -version="mysql8" -timeout 15m ./...
 
   integration-maria107:
     runs-on: ubuntu-latest
@@ -260,7 +260,7 @@ jobs:
             ${{ runner.os }}-go-
       - name: Run integration tests for maria107
         working-directory: internal/integration
-        run: go test -race -count=2 -v -run="MySQL" -version="maria107" ./...
+        run: go test -race -count=2 -v -run="MySQL" -version="maria107" -timeout 15m ./...
 
   integration-maria102:
     runs-on: ubuntu-latest
@@ -291,7 +291,7 @@ jobs:
             ${{ runner.os }}-go-
       - name: Run integration tests for maria102
         working-directory: internal/integration
-        run: go test -race -count=2 -v -run="MySQL" -version="maria102" ./...
+        run: go test -race -count=2 -v -run="MySQL" -version="maria102" -timeout 15m ./...
 
   integration-maria103:
     runs-on: ubuntu-latest
@@ -322,7 +322,7 @@ jobs:
             ${{ runner.os }}-go-
       - name: Run integration tests for maria103
         working-directory: internal/integration
-        run: go test -race -count=2 -v -run="MySQL" -version="maria103" ./...
+        run: go test -race -count=2 -v -run="MySQL" -version="maria103" -timeout 15m ./...
 
   integration-postgres10:
     runs-on: ubuntu-latest
@@ -352,7 +352,7 @@ jobs:
             ${{ runner.os }}-go-
       - name: Run integration tests for postgres10
         working-directory: internal/integration
-        run: go test -race -count=2 -v -run="Postgres" -version="postgres10" ./...
+        run: go test -race -count=2 -v -run="Postgres" -version="postgres10" -timeout 15m ./...
 
   integration-postgres11:
     runs-on: ubuntu-latest
@@ -382,7 +382,7 @@ jobs:
             ${{ runner.os }}-go-
       - name: Run integration tests for postgres11
         working-directory: internal/integration
-        run: go test -race -count=2 -v -run="Postgres" -version="postgres11" ./...
+        run: go test -race -count=2 -v -run="Postgres" -version="postgres11" -timeout 15m ./...
 
   integration-postgres12:
     runs-on: ubuntu-latest
@@ -412,7 +412,7 @@ jobs:
             ${{ runner.os }}-go-
       - name: Run integration tests for postgres12
         working-directory: internal/integration
-        run: go test -race -count=2 -v -run="Postgres" -version="postgres12" ./...
+        run: go test -race -count=2 -v -run="Postgres" -version="postgres12" -timeout 15m ./...
 
   integration-postgres13:
     runs-on: ubuntu-latest
@@ -442,7 +442,7 @@ jobs:
             ${{ runner.os }}-go-
       - name: Run integration tests for postgres13
         working-directory: internal/integration
-        run: go test -race -count=2 -v -run="Postgres" -version="postgres13" ./...
+        run: go test -race -count=2 -v -run="Postgres" -version="postgres13" -timeout 15m ./...
 
   integration-postgres14:
     runs-on: ubuntu-latest
@@ -472,7 +472,7 @@ jobs:
             ${{ runner.os }}-go-
       - name: Run integration tests for postgres14
         working-directory: internal/integration
-        run: go test -race -count=2 -v -run="Postgres" -version="postgres14" ./...
+        run: go test -race -count=2 -v -run="Postgres" -version="postgres14" -timeout 15m ./...
 
   integration-postgres15:
     runs-on: ubuntu-latest
@@ -502,7 +502,7 @@ jobs:
             ${{ runner.os }}-go-
       - name: Run integration tests for postgres15
         working-directory: internal/integration
-        run: go test -race -count=2 -v -run="Postgres" -version="postgres15" ./...
+        run: go test -race -count=2 -v -run="Postgres" -version="postgres15" -timeout 15m ./...
 
   integration-tidb5:
     runs-on: ubuntu-latest
@@ -526,7 +526,7 @@ jobs:
             ${{ runner.os }}-go-
       - name: Run integration tests for tidb5
         working-directory: internal/integration
-        run: go test -race -count=2 -v -run="TiDB" -version="tidb5" ./...
+        run: go test -race -count=2 -v -run="TiDB" -version="tidb5" -timeout 15m ./...
 
   integration-tidb6:
     runs-on: ubuntu-latest
@@ -550,7 +550,7 @@ jobs:
             ${{ runner.os }}-go-
       - name: Run integration tests for tidb6
         working-directory: internal/integration
-        run: go test -race -count=2 -v -run="TiDB" -version="tidb6" ./...
+        run: go test -race -count=2 -v -run="TiDB" -version="tidb6" -timeout 15m ./...
 
   integration-sqlite:
     runs-on: ubuntu-latest
@@ -568,7 +568,7 @@ jobs:
             ${{ runner.os }}-go-
       - name: Run integration tests for sqlite
         working-directory: internal/integration
-        run: go test -race -count=2 -v -run="SQLite.*" -version="sqlite" ./...
+        run: go test -race -count=2 -v -run="SQLite.*" -version="sqlite" -timeout 15m ./...
 
   integration-cockroach:
     runs-on: ubuntu-latest
@@ -592,5 +592,5 @@ jobs:
             ${{ runner.os }}-go-
       - name: Run integration tests for cockroach
         working-directory: internal/integration
-        run: go test -race -count=2 -v -run="Cockroach" -version="cockroach" ./...
+        run: go test -race -count=2 -v -run="Cockroach" -version="cockroach" -timeout 15m ./...
 
diff --git a/internal/ci/ci.tmpl b/internal/ci/ci.tmpl
index 7860b101..87111a1f 100644
--- a/internal/ci/ci.tmpl
+++ b/internal/ci/ci.tmpl
@@ -167,5 +167,5 @@ jobs:
             {{ "${{ runner.os }}-go-" }}
       - name: Run integration tests for {{ .Version }}
         working-directory: internal/integration
-        run: go test -race -count=2 -v -run="{{ .Regex }}" -version="{{ .Version }}" ./...
+        run: go test -race -count=2 -v -run="{{ .Regex }}" -version="{{ .Version }}" -timeout 15m ./...
 {{ end }}
diff --git a/sql/internal/sqlx/sqlx.go b/sql/internal/sqlx/sqlx.go
index 018b2f90..6ecc7c37 100644
--- a/sql/internal/sqlx/sqlx.go
+++ b/sql/internal/sqlx/sqlx.go
@@ -216,6 +216,8 @@ type Builder struct {
 	bytes.Buffer
 	QuoteChar byte    // quoting identifiers
 	Schema    *string // schema qualifier
+	Indent    string  // indentation string
+	level     int     // current indentation level
 }
 
 // P writes a list of phrases to the builder separated and
@@ -267,6 +269,32 @@ func (b *Builder) Table(t *schema.Table) *Builder {
 	return b
 }
 
+// IndentIn adds one indentation in.
+func (b *Builder) IndentIn() *Builder {
+	b.level++
+	return b
+}
+
+// IndentOut removed one indentation level.
+func (b *Builder) IndentOut() *Builder {
+	b.level--
+	return b
+}
+
+// NL adds line break and prefix the new line with
+// indentation in case indentation is enabled.
+func (b *Builder) NL() *Builder {
+	if b.Indent != "" {
+		if b.lastByte() == ' ' {
+			b.rewriteLastByte('\n')
+		} else {
+			b.WriteByte('\n')
+		}
+		b.WriteString(strings.Repeat(b.Indent, b.level))
+	}
+	return b
+}
+
 // Comma writes a comma in case the buffer is not empty, or
 // replaces the last char if it is a whitespace.
 func (b *Builder) Comma() *Builder {
@@ -294,6 +322,13 @@ func (b *Builder) MapComma(x any, f func(i int, b *Builder)) *Builder {
 	return b
 }
 
+// MapIndent is like MapComma, but writes a new line before each element.
+func (b *Builder) MapIndent(x any, f func(i int, b *Builder)) *Builder {
+	return b.MapComma(x, func(i int, b *Builder) {
+		f(i, b.NL())
+	})
+}
+
 // MapCommaErr is like MapComma, but returns an error if f returns an error.
 func (b *Builder) MapCommaErr(x any, f func(i int, b *Builder) error) error {
 	s := reflect.ValueOf(x)
@@ -308,6 +343,13 @@ func (b *Builder) MapCommaErr(x any, f func(i int, b *Builder) error) error {
 	return nil
 }
 
+// MapIndentErr is like MapCommaErr, but writes a new line before each element.
+func (b *Builder) MapIndentErr(x any, f func(i int, b *Builder) error) error {
+	return b.MapCommaErr(x, func(i int, b *Builder) error {
+		return f(i, b.NL())
+	})
+}
+
 // Wrap wraps the written string with parentheses.
 func (b *Builder) Wrap(f func(b *Builder)) *Builder {
 	b.WriteByte('(')
@@ -320,6 +362,16 @@ func (b *Builder) Wrap(f func(b *Builder)) *Builder {
 	return b
 }
 
+// WrapIndent is like Wrap but with extra level of indentation.
+func (b *Builder) WrapIndent(f func(b *Builder)) *Builder {
+	return b.Wrap(func(b *Builder) {
+		b.IndentIn()
+		f(b)
+		b.IndentOut()
+		b.NL()
+	})
+}
+
 // Clone returns a duplicate of the builder.
 func (b *Builder) Clone() *Builder {
 	return &Builder{
diff --git a/sql/migrate/migrate.go b/sql/migrate/migrate.go
index 08b2a2a6..31ac2f6e 100644
--- a/sql/migrate/migrate.go
+++ b/sql/migrate/migrate.go
@@ -99,6 +99,9 @@ type (
 		// PlanWithSchemaQualifier allows setting a custom schema to prefix
 		// tables and other resources. An empty string indicates no qualifier.
 		SchemaQualifier *string
+		// Indent is the string to use for indentation.
+		// If empty, no indentation is used.
+		Indent string
 	}
 
 	// PlanOption allows configuring a drivers' plan using functional arguments.
diff --git a/sql/mysql/migrate.go b/sql/mysql/migrate.go
index f9d1b8d1..98d598f1 100644
--- a/sql/mysql/migrate.go
+++ b/sql/mysql/migrate.go
@@ -224,32 +224,32 @@ func (s *state) addTable(add *schema.AddTable) error {
 	if len(add.T.Columns) == 0 {
 		return fmt.Errorf("table %q has no columns", add.T.Name)
 	}
-	b.Wrap(func(b *sqlx.Builder) {
-		b.MapComma(add.T.Columns, func(i int, b *sqlx.Builder) {
+	b.WrapIndent(func(b *sqlx.Builder) {
+		b.MapIndent(add.T.Columns, func(i int, b *sqlx.Builder) {
 			if err := s.column(b, add.T, add.T.Columns[i]); err != nil {
 				errs = append(errs, err.Error())
 			}
 		})
 		if pk := add.T.PrimaryKey; pk != nil {
-			b.Comma().P("PRIMARY KEY")
+			b.Comma().NL().P("PRIMARY KEY")
 			indexTypeParts(b, pk)
 		}
 		if len(add.T.Indexes) > 0 {
 			b.Comma()
 		}
-		b.MapComma(add.T.Indexes, func(i int, b *sqlx.Builder) {
+		b.MapIndent(add.T.Indexes, func(i int, b *sqlx.Builder) {
 			idx := add.T.Indexes[i]
 			index(b, idx)
 		})
 		if len(add.T.ForeignKeys) > 0 {
 			b.Comma()
-			if err := s.fks(b, add.T.ForeignKeys...); err != nil {
+			if err := s.fks(b.MapIndentErr, add.T.ForeignKeys...); err != nil {
 				errs = append(errs, err.Error())
 			}
 		}
 		for _, attr := range add.T.Attrs {
 			if c, ok := attr.(*schema.Check); ok {
-				b.Comma()
+				b.Comma().NL()
 				s.check(b, c)
 			}
 		}
@@ -407,7 +407,7 @@ func (s *state) alterTable(t *schema.Table, changes []schema.Change) error {
 				reverse = append(reverse, &schema.ModifyPrimaryKey{From: change.To, To: change.From, Change: change.Change})
 			case *schema.AddForeignKey:
 				b.P("ADD")
-				if err := s.fks(b, change.F); err != nil {
+				if err := s.fks(b.MapCommaErr, change.F); err != nil {
 					return err
 				}
 				reverse = append(reverse, &schema.DropForeignKey{F: change.F})
@@ -603,8 +603,8 @@ func indexTypeParts(b *sqlx.Builder, idx *schema.Index) {
 	})
 }
 
-func (s *state) fks(b *sqlx.Builder, fks ...*schema.ForeignKey) error {
-	return b.MapCommaErr(fks, func(i int, b *sqlx.Builder) error {
+func (s *state) fks(commaF func(any, func(int, *sqlx.Builder) error) error, fks ...*schema.ForeignKey) error {
+	return commaF(fks, func(i int, b *sqlx.Builder) error {
 		fk := fks[i]
 		if fk.Symbol != "" {
 			b.P("CONSTRAINT").Ident(fk.Symbol)
@@ -724,7 +724,7 @@ func (s *state) columnDefault(b *sqlx.Builder, c *schema.Column) {
 
 // Build instantiates a new builder and writes the given phrase to it.
 func (s *state) Build(phrases ...string) *sqlx.Builder {
-	b := &sqlx.Builder{QuoteChar: '`', Schema: s.SchemaQualifier}
+	b := &sqlx.Builder{QuoteChar: '`', Schema: s.SchemaQualifier, Indent: s.Indent}
 	return b.P(phrases...)
 }
 
diff --git a/sql/postgres/migrate.go b/sql/postgres/migrate.go
index 657d8015..798b5d98 100644
--- a/sql/postgres/migrate.go
+++ b/sql/postgres/migrate.go
@@ -152,14 +152,14 @@ func (s *state) addTable(ctx context.Context, add *schema.AddTable) error {
 		b.P("IF NOT EXISTS")
 	}
 	b.Table(add.T)
-	b.Wrap(func(b *sqlx.Builder) {
-		b.MapComma(add.T.Columns, func(i int, b *sqlx.Builder) {
+	b.WrapIndent(func(b *sqlx.Builder) {
+		b.MapIndent(add.T.Columns, func(i int, b *sqlx.Builder) {
 			if err := s.column(b, add.T, add.T.Columns[i]); err != nil {
 				errs = append(errs, err.Error())
 			}
 		})
 		if pk := add.T.PrimaryKey; pk != nil {
-			b.Comma().P("PRIMARY KEY")
+			b.Comma().NL().P("PRIMARY KEY")
 			if err := s.index(b, pk); err != nil {
 				errs = append(errs, err.Error())
 			}
@@ -170,7 +170,7 @@ func (s *state) addTable(ctx context.Context, add *schema.AddTable) error {
 		}
 		for _, attr := range add.T.Attrs {
 			if c, ok := attr.(*schema.Check); ok {
-				b.Comma()
+				b.Comma().NL()
 				check(b, c)
 			}
 		}
@@ -1054,7 +1054,7 @@ func (s *state) index(b *sqlx.Builder, idx *schema.Index) error {
 }
 
 func (s *state) fks(b *sqlx.Builder, fks ...*schema.ForeignKey) {
-	b.MapComma(fks, func(i int, b *sqlx.Builder) {
+	b.MapIndent(fks, func(i int, b *sqlx.Builder) {
 		fk := fks[i]
 		if fk.Symbol != "" {
 			b.P("CONSTRAINT").Ident(fk.Symbol)
@@ -1086,7 +1086,7 @@ func (s *state) append(c ...*migrate.Change) {
 
 // Build instantiates a new builder and writes the given phrase to it.
 func (s *state) Build(phrases ...string) *sqlx.Builder {
-	b := &sqlx.Builder{QuoteChar: '"', Schema: s.SchemaQualifier}
+	b := &sqlx.Builder{QuoteChar: '"', Schema: s.SchemaQualifier, Indent: s.Indent}
 	return b.P(phrases...)
 }
 
diff --git a/sql/sqlite/migrate.go b/sql/sqlite/migrate.go
index f2242e94..dc1468f5 100644
--- a/sql/sqlite/migrate.go
+++ b/sql/sqlite/migrate.go
@@ -105,15 +105,15 @@ func (s *state) addTable(ctx context.Context, add *schema.AddTable) error {
 	if sqlx.Has(add.Extra, &schema.IfNotExists{}) {
 		b.P("IF NOT EXISTS")
 	}
-	b.Wrap(func(b *sqlx.Builder) {
-		b.MapComma(add.T.Columns, func(i int, b *sqlx.Builder) {
+	b.WrapIndent(func(b *sqlx.Builder) {
+		b.MapIndent(add.T.Columns, func(i int, b *sqlx.Builder) {
 			if err := s.column(b, add.T.Columns[i]); err != nil {
 				errs = append(errs, err.Error())
 			}
 		})
 		// Primary keys with auto-increment are inlined on the column definition.
 		if pk := add.T.PrimaryKey; pk != nil && !autoincPK(pk) {
-			b.Comma().P("PRIMARY KEY")
+			b.Comma().NL().P("PRIMARY KEY")
 			s.indexParts(b, pk.Parts)
 		}
 		if len(add.T.ForeignKeys) > 0 {
@@ -122,7 +122,7 @@ func (s *state) addTable(ctx context.Context, add *schema.AddTable) error {
 		}
 		for _, attr := range add.T.Attrs {
 			if c, ok := attr.(*schema.Check); ok {
-				b.Comma()
+				b.Comma().NL()
 				check(b, c)
 			}
 		}
@@ -332,7 +332,7 @@ func (s *state) indexParts(b *sqlx.Builder, parts []*schema.IndexPart) {
 }
 
 func (s *state) fks(b *sqlx.Builder, fks ...*schema.ForeignKey) {
-	b.MapComma(fks, func(i int, b *sqlx.Builder) {
+	b.MapIndent(fks, func(i int, b *sqlx.Builder) {
 		fk := fks[i]
 		if fk.Symbol != "" {
 			b.P("CONSTRAINT").Ident(fk.Symbol)
@@ -548,7 +548,7 @@ func autoincPK(pk *schema.Index) bool {
 
 // Build instantiates a new builder and writes the given phrase to it.
 func (s *state) Build(phrases ...string) *sqlx.Builder {
-	b := &sqlx.Builder{QuoteChar: '`', Schema: s.SchemaQualifier}
+	b := &sqlx.Builder{QuoteChar: '`', Schema: s.SchemaQualifier, Indent: s.Indent}
 	return b.P(phrases...)
 }
 


Test patch:
diff --git a/sql/mysql/migrate_test.go b/sql/mysql/migrate_test.go
index 64d93ee1..08d65872 100644
--- a/sql/mysql/migrate_test.go
+++ b/sql/mysql/migrate_test.go
@@ -7,6 +7,7 @@ package mysql
 import (
 	"context"
 	"strconv"
+	"strings"
 	"testing"
 
 	"ariga.io/atlas/sql/internal/sqltest"
@@ -1094,6 +1095,217 @@ func TestDefaultPlan(t *testing.T) {
 	require.EqualError(t, err, `create "t1" table: cannot execute statements without a database connection. use Open to create a new Driver`)
 }
 
+func TestIndentedPlan(t *testing.T) {
+	tests := []struct {
+		T   *schema.Table
+		Cmd string
+	}{
+		{
+			T: schema.NewTable("t1").
+				SetSchema(schema.New("s1")).
+				AddColumns(schema.NewIntColumn("a", "int")),
+			Cmd: join(
+				"CREATE TABLE `s1`.`t1` (",
+				"  `a` int NOT NULL",
+				")",
+			),
+		},
+		{
+			T: schema.NewTable("t1").
+				AddColumns(
+					schema.NewIntColumn("a", "int"),
+					schema.NewIntColumn("b", "int"),
+				),
+			Cmd: join(
+				"CREATE TABLE `t1` (",
+				"  `a` int NOT NULL,",
+				"  `b` int NOT NULL",
+				")",
+			),
+		},
+		{
+			T: schema.NewTable("t1").
+				AddColumns(
+					schema.NewIntColumn("a", "int"),
+					schema.NewIntColumn("b", "int"),
+				).
+				SetPrimaryKey(
+					schema.NewPrimaryKey(schema.NewIntColumn("id", "int")),
+				),
+			Cmd: join(
+				"CREATE TABLE `t1` (",
+				"  `a` int NOT NULL,",
+				"  `b` int NOT NULL,",
+				"  `id` int NOT NULL,",
+				"  PRIMARY KEY (`id`)",
+				")",
+			),
+		},
+		{
+			T: schema.NewTable("t1").
+				AddColumns(
+					schema.NewIntColumn("a", "int"),
+					schema.NewIntColumn("b", "int"),
+				).
+				AddIndexes(
+					schema.NewIndex("idx").
+						AddColumns(
+							schema.NewIntColumn("a", "int"),
+							schema.NewIntColumn("b", "int"),
+						),
+				),
+			Cmd: join(
+				"CREATE TABLE `t1` (",
+				"  `a` int NOT NULL,",
+				"  `b` int NOT NULL,",
+				"  INDEX `idx` (`a`, `b`)",
+				")",
+			),
+		},
+		{
+			T: schema.NewTable("t1").
+				AddColumns(
+					schema.NewIntColumn("a", "int"),
+					schema.NewIntColumn("b", "int"),
+				).
+				SetPrimaryKey(
+					schema.NewPrimaryKey(schema.NewIntColumn("id", "int")),
+				).
+				AddIndexes(
+					schema.NewIndex("idx").
+						AddColumns(
+							schema.NewIntColumn("a", "int"),
+							schema.NewIntColumn("b", "int"),
+						),
+				),
+			Cmd: join(
+				"CREATE TABLE `t1` (",
+				"  `a` int NOT NULL,",
+				"  `b` int NOT NULL,",
+				"  `id` int NOT NULL,",
+				"  PRIMARY KEY (`id`),",
+				"  INDEX `idx` (`a`, `b`)",
+				")",
+			),
+		},
+		{
+			T: schema.NewTable("t1").
+				AddColumns(
+					schema.NewIntColumn("a", "int"),
+					schema.NewIntColumn("b", "int"),
+				).
+				AddIndexes(
+					schema.NewIndex("idx1").
+						AddColumns(
+							schema.NewIntColumn("a", "int"),
+							schema.NewIntColumn("b", "int"),
+						),
+					schema.NewIndex("idx2").
+						AddColumns(
+							schema.NewIntColumn("a", "int"),
+							schema.NewIntColumn("b", "int"),
+						),
+				),
+			Cmd: join(
+				"CREATE TABLE `t1` (",
+				"  `a` int NOT NULL,",
+				"  `b` int NOT NULL,",
+				"  INDEX `idx1` (`a`, `b`),",
+				"  INDEX `idx2` (`a`, `b`)",
+				")",
+			),
+		},
+		{
+			T: schema.NewTable("t1").
+				AddColumns(
+					schema.NewIntColumn("a", "int"),
+					schema.NewIntColumn("b", "int"),
+				).
+				AddIndexes(
+					schema.NewIndex("idx1").
+						AddColumns(
+							schema.NewIntColumn("a", "int"),
+							schema.NewIntColumn("b", "int"),
+						),
+					schema.NewIndex("idx2").
+						AddColumns(
+							schema.NewIntColumn("a", "int"),
+							schema.NewIntColumn("b", "int"),
+						),
+				).
+				AddForeignKeys(
+					schema.NewForeignKey("fk1").
+						AddColumns(schema.NewIntColumn("a", "int")).
+						SetRefTable(schema.NewTable("t2")).
+						AddRefColumns(schema.NewIntColumn("a", "int")),
+					schema.NewForeignKey("fk2").
+						AddColumns(schema.NewIntColumn("a", "int")).
+						SetRefTable(schema.NewTable("t2")).
+						AddRefColumns(schema.NewIntColumn("a", "int")),
+				),
+			Cmd: join(
+				"CREATE TABLE `t1` (",
+				"  `a` int NOT NULL,",
+				"  `b` int NOT NULL,",
+				"  INDEX `idx1` (`a`, `b`),",
+				"  INDEX `idx2` (`a`, `b`),",
+				"  CONSTRAINT `fk1` FOREIGN KEY (`a`) REFERENCES `t2` (`a`),",
+				"  CONSTRAINT `fk2` FOREIGN KEY (`a`) REFERENCES `t2` (`a`)",
+				")",
+			),
+		},
+		{
+			T: schema.NewTable("t1").
+				AddColumns(
+					schema.NewIntColumn("a", "int"),
+					schema.NewIntColumn("b", "int"),
+				).
+				SetPrimaryKey(
+					schema.NewPrimaryKey(schema.NewIntColumn("id", "int")),
+				).
+				AddForeignKeys(
+					schema.NewForeignKey("fk1").
+						AddColumns(schema.NewIntColumn("a", "int")).
+						SetRefTable(schema.NewTable("t2")).
+						AddRefColumns(schema.NewIntColumn("a", "int")),
+					schema.NewForeignKey("fk2").
+						AddColumns(schema.NewIntColumn("a", "int")).
+						SetRefTable(schema.NewTable("t2")).
+						AddRefColumns(schema.NewIntColumn("a", "int")),
+				).
+				AddChecks(
+					schema.NewCheck().SetName("ck1").SetExpr("a > 0"),
+					schema.NewCheck().SetName("ck2").SetExpr("a > 0"),
+				).
+				SetComment("Comment"),
+			Cmd: join(
+				"CREATE TABLE `t1` (",
+				"  `a` int NOT NULL,",
+				"  `b` int NOT NULL,",
+				"  `id` int NOT NULL,",
+				"  PRIMARY KEY (`id`),",
+				"  CONSTRAINT `fk1` FOREIGN KEY (`a`) REFERENCES `t2` (`a`),",
+				"  CONSTRAINT `fk2` FOREIGN KEY (`a`) REFERENCES `t2` (`a`),",
+				"  CONSTRAINT `ck1` CHECK (a > 0),",
+				"  CONSTRAINT `ck2` CHECK (a > 0)",
+				`) COMMENT "Comment"`,
+			),
+		},
+	}
+	for i, tt := range tests {
+		t.Run(strconv.Itoa(i), func(t *testing.T) {
+			db, _, err := newMigrate("8.0.16")
+			require.NoError(t, err)
+			plan, err := db.PlanChanges(context.Background(), "wantPlan", []schema.Change{&schema.AddTable{T: tt.T}}, func(opts *migrate.PlanOptions) {
+				opts.Indent = "  "
+			})
+			require.NoError(t, err)
+			require.Len(t, plan.Changes, 1)
+			require.Equal(t, tt.Cmd, plan.Changes[0].Cmd)
+		})
+	}
+}
+
 func newMigrate(version string) (migrate.PlanApplier, *mock, error) {
 	db, m, err := sqlmock.New()
 	if err != nil {
@@ -1107,3 +1319,5 @@ func newMigrate(version string) (migrate.PlanApplier, *mock, error) {
 	}
 	return drv, mk, nil
 }
+
+func join(lines ...string) string { return strings.Join(lines, "\n") }
diff --git a/sql/postgres/migrate_test.go b/sql/postgres/migrate_test.go
index 361891c9..0575904a 100644
--- a/sql/postgres/migrate_test.go
+++ b/sql/postgres/migrate_test.go
@@ -1370,3 +1370,117 @@ func TestDefaultPlan(t *testing.T) {
 	})
 	require.EqualError(t, err, `create "t1" table: cannot execute statements without a database connection. use Open to create a new Driver`)
 }
+
+func TestIndentedPlan(t *testing.T) {
+	tests := []struct {
+		T   *schema.Table
+		Cmd string
+	}{
+		{
+			T: schema.NewTable("t1").
+				AddColumns(schema.NewIntColumn("a", "int")),
+			Cmd: `CREATE TABLE "t1" (
+  "a" integer NOT NULL
+)`,
+		},
+		{
+			T: schema.NewTable("t1").
+				AddColumns(
+					schema.NewIntColumn("a", "int"),
+					schema.NewIntColumn("b", "int"),
+				),
+			Cmd: `CREATE TABLE "t1" (
+  "a" integer NOT NULL,
+  "b" integer NOT NULL
+)`,
+		},
+		{
+			T: schema.NewTable("t1").
+				AddColumns(
+					schema.NewIntColumn("a", "int"),
+					schema.NewIntColumn("b", "int"),
+				).
+				SetPrimaryKey(
+					schema.NewPrimaryKey(schema.NewIntColumn("id", "int")),
+				),
+			Cmd: `CREATE TABLE "t1" (
+  "a" integer NOT NULL,
+  "b" integer NOT NULL,
+  "id" integer NOT NULL,
+  PRIMARY KEY ("id")
+)`,
+		},
+		{
+			T: schema.NewTable("t1").
+				AddColumns(
+					schema.NewIntColumn("a", "int"),
+					schema.NewIntColumn("b", "int"),
+				).
+				AddForeignKeys(
+					schema.NewForeignKey("fk1").
+						AddColumns(schema.NewIntColumn("a", "int")).
+						SetRefTable(schema.NewTable("t2")).
+						AddRefColumns(schema.NewIntColumn("a", "int")),
+					schema.NewForeignKey("fk2").
+						AddColumns(schema.NewIntColumn("a", "int")).
+						SetRefTable(schema.NewTable("t2")).
+						AddRefColumns(schema.NewIntColumn("a", "int")),
+				),
+			Cmd: `CREATE TABLE "t1" (
+  "a" integer NOT NULL,
+  "b" integer NOT NULL,
+  CONSTRAINT "fk1" FOREIGN KEY ("a") REFERENCES "t2" ("a"),
+  CONSTRAINT "fk2" FOREIGN KEY ("a") REFERENCES "t2" ("a")
+)`,
+		},
+		{
+			T: schema.NewTable("t1").
+				AddColumns(
+					schema.NewIntColumn("a", "int"),
+					schema.NewIntColumn("b", "int"),
+				).
+				SetPrimaryKey(
+					schema.NewPrimaryKey(schema.NewIntColumn("id", "int")),
+				).
+				AddForeignKeys(
+					schema.NewForeignKey("fk1").
+						AddColumns(schema.NewIntColumn("a", "int")).
+						SetRefTable(schema.NewTable("t2")).
+						AddRefColumns(schema.NewIntColumn("a", "int")),
+					schema.NewForeignKey("fk2").
+						AddColumns(schema.NewIntColumn("a", "int")).
+						SetRefTable(schema.NewTable("t2")).
+						AddRefColumns(schema.NewIntColumn("a", "int")),
+				).
+				AddChecks(
+					schema.NewCheck().SetName("ck1").SetExpr("a > 0"),
+					schema.NewCheck().SetName("ck2").SetExpr("a > 0"),
+				),
+			Cmd: `CREATE TABLE "t1" (
+  "a" integer NOT NULL,
+  "b" integer NOT NULL,
+  "id" integer NOT NULL,
+  PRIMARY KEY ("id"),
+  CONSTRAINT "fk1" FOREIGN KEY ("a") REFERENCES "t2" ("a"),
+  CONSTRAINT "fk2" FOREIGN KEY ("a") REFERENCES "t2" ("a"),
+  CONSTRAINT "ck1" CHECK (a > 0),
+  CONSTRAINT "ck2" CHECK (a > 0)
+)`,
+		},
+	}
+	for i, tt := range tests {
+		t.Run(strconv.Itoa(i), func(t *testing.T) {
+			db, mk, err := sqlmock.New()
+			require.NoError(t, err)
+			mock{mk}.version("130000")
+			drv, err := Open(db)
+			require.NoError(t, err)
+			plan, err := drv.PlanChanges(context.Background(), "wantPlan", []schema.Change{&schema.AddTable{T: tt.T}}, func(opts *migrate.PlanOptions) {
+				opts.Indent = "  "
+			})
+			require.NoError(t, err)
+			require.Len(t, plan.Changes, 1)
+			require.Equal(t, tt.Cmd, plan.Changes[0].Cmd)
+		})
+	}
+}
diff --git a/sql/sqlite/migrate_test.go b/sql/sqlite/migrate_test.go
index d5fed444..d7bb91c3 100644
--- a/sql/sqlite/migrate_test.go
+++ b/sql/sqlite/migrate_test.go
@@ -8,6 +8,7 @@ import (
 	"context"
 	"database/sql"
 	"strconv"
+	"strings"
 	"testing"
 
 	"ariga.io/atlas/sql/internal/sqltest"
@@ -447,3 +448,129 @@ func TestDefaultPlan(t *testing.T) {
 	})
 	require.EqualError(t, err, `create "t1" table: cannot execute statements without a database connection. use Open to create a new Driver`)
 }
+
+func TestIndentedPlan(t *testing.T) {
+	tests := []struct {
+		T   *schema.Table
+		Cmd string
+	}{
+		{
+			T: schema.NewTable("t1").
+				AddColumns(schema.NewIntColumn("a", "int")),
+			Cmd: join(
+				"CREATE TABLE `t1` (",
+				"  `a` int NOT NULL",
+				")",
+			),
+		},
+		{
+			T: schema.NewTable("t1").
+				AddColumns(
+					schema.NewIntColumn("a", "int"),
+					schema.NewIntColumn("b", "int"),
+				),
+			Cmd: join(
+				"CREATE TABLE `t1` (",
+				"  `a` int NOT NULL,",
+				"  `b` int NOT NULL",
+				")",
+			),
+		},
+		{
+			T: schema.NewTable("t1").
+				AddColumns(
+					schema.NewIntColumn("a", "int"),
+					schema.NewIntColumn("b", "int"),
+				).
+				SetPrimaryKey(
+					schema.NewPrimaryKey(schema.NewIntColumn("id", "int")),
+				),
+			Cmd: join(
+				"CREATE TABLE `t1` (",
+				"  `a` int NOT NULL,",
+				"  `b` int NOT NULL,",
+				"  `id` int NOT NULL,",
+				"  PRIMARY KEY (`id`)",
+				")",
+			),
+		},
+		{
+			T: schema.NewTable("t1").
+				AddColumns(
+					schema.NewIntColumn("a", "int"),
+					schema.NewIntColumn("b", "int"),
+				).
+				AddForeignKeys(
+					schema.NewForeignKey("fk1").
+						AddColumns(schema.NewIntColumn("a", "int")).
+						SetRefTable(schema.NewTable("t2")).
+						AddRefColumns(schema.NewIntColumn("a", "int")),
+					schema.NewForeignKey("fk2").
+						AddColumns(schema.NewIntColumn("a", "int")).
+						SetRefTable(schema.NewTable("t2")).
+						AddRefColumns(schema.NewIntColumn("a", "int")),
+				),
+			Cmd: join(
+				"CREATE TABLE `t1` (",
+				"  `a` int NOT NULL,",
+				"  `b` int NOT NULL,",
+				"  CONSTRAINT `fk1` FOREIGN KEY (`a`) REFERENCES `t2` (`a`),",
+				"  CONSTRAINT `fk2` FOREIGN KEY (`a`) REFERENCES `t2` (`a`)",
+				")",
+			),
+		},
+		{
+			T: schema.NewTable("t1").
+				AddColumns(
+					schema.NewIntColumn("a", "int"),
+					schema.NewIntColumn("b", "int"),
+				).
+				SetPrimaryKey(
+					schema.NewPrimaryKey(schema.NewIntColumn("id", "int")),
+				).
+				AddForeignKeys(
+					schema.NewForeignKey("fk1").
+						AddColumns(schema.NewIntColumn("a", "int")).
+						SetRefTable(schema.NewTable("t2")).
+						AddRefColumns(schema.NewIntColumn("a", "int")),
+					schema.NewForeignKey("fk2").
+						AddColumns(schema.NewIntColumn("a", "int")).
+						SetRefTable(schema.NewTable("t2")).
+						AddRefColumns(schema.NewIntColumn("a", "int")),
+				).
+				AddChecks(
+					schema.NewCheck().SetName("ck1").SetExpr("a > 0"),
+					schema.NewCheck().SetName("ck2").SetExpr("a > 0"),
+				),
+			Cmd: join(
+				"CREATE TABLE `t1` (",
+				"  `a` int NOT NULL,",
+				"  `b` int NOT NULL,",
+				"  `id` int NOT NULL,",
+				"  PRIMARY KEY (`id`),",
+				"  CONSTRAINT `fk1` FOREIGN KEY (`a`) REFERENCES `t2` (`a`),",
+				"  CONSTRAINT `fk2` FOREIGN KEY (`a`) REFERENCES `t2` (`a`),",
+				"  CONSTRAINT `ck1` CHECK (a > 0),",
+				"  CONSTRAINT `ck2` CHECK (a > 0)",
+				`)`,
+			),
+		},
+	}
+	for i, tt := range tests {
+		t.Run(strconv.Itoa(i), func(t *testing.T) {
+			db, mk, err := sqlmock.New()
+			require.NoError(t, err)
+			mock{mk}.systemVars("3.36.0")
+			drv, err := Open(db)
+			require.NoError(t, err)
+			plan, err := drv.PlanChanges(context.Background(), "wantPlan", []schema.Change{&schema.AddTable{T: tt.T}}, func(opts *migrate.PlanOptions) {
+				opts.Indent = "  "
+			})
+			require.NoError(t, err)
+			require.Len(t, plan.Changes, 1)
+			require.Equal(t, tt.Cmd, plan.Changes[0].Cmd)
+		})
+	}
+}
+
+func join(lines ...string) string { return strings.Join(lines, "\n") }