Repository: ariga/atlas
Pull request #650 (2022-03-14 10:07:06)

doc/md: add TiDB to various docs


Patch:
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 836315fa..89cb8678 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -1,10 +1,13 @@
+# # # # # # # # # # # # # # # #
+# CODE GENERATED - DO NOT EDIT
+# # # # # # # # # # # # # # # #
 name: Continuous Integration
 on:
   pull_request:
   push:
     branches:
       - master
-      
+
 jobs:
   lint:
     runs-on: ubuntu-latest
@@ -21,7 +24,7 @@ jobs:
       - uses: actions/checkout@v2.4.0
       - uses: actions/setup-go@v2
         with:
-          go-version: '1.17'
+          go-version: '1.18'
       - uses: actions/cache@v2.1.5
         with:
           path: ~/go/pkg/mod
@@ -31,7 +34,7 @@ jobs:
       - name: run "go generate ./..."
         run: go generate ./...
       - name: run "go generate internal/typedoc"
-        working-directory: internal/typedoc 
+        working-directory: internal/typedoc
         run: go generate ./...
       - name: Verify generated files are checked in properly
         run: |
@@ -46,7 +49,7 @@ jobs:
     runs-on: ubuntu-latest
     strategy:
       matrix:
-        go: ['1.17', '1.16']
+        go: [ '1.17', '1.18' ]
     steps:
       - uses: actions/checkout@v2
       - uses: actions/setup-go@v2
@@ -68,7 +71,8 @@ jobs:
         run: go test -race ./...
         working-directory: cmd
 
-  integration:
+  
+  integration-mysql56:
     runs-on: ubuntu-latest
     services:
       mysql56:
@@ -84,6 +88,24 @@ jobs:
           --health-start-period 10s
           --health-timeout 5s
           --health-retries 10
+    steps:
+      - uses: actions/checkout@v2.3.4
+      - uses: actions/setup-go@v2
+        with:
+          go-version: 1.18
+      - uses: actions/cache@v2.1.6
+        with:
+          path: ~/go/pkg/mod
+          key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
+          restore-keys: |
+            ${{ runner.os }}-go-
+      - name: Run integration tests for mysql56
+        working-directory: internal/integration
+        run: go test -race -count=2 -v -run="MySQL.*/mysql56" -dialect=mysql56 ./...
+  
+  integration-mysql57:
+    runs-on: ubuntu-latest
+    services:
       mysql57:
         image: mysql:5.7.26
         env:
@@ -97,6 +119,24 @@ jobs:
           --health-start-period 10s
           --health-timeout 5s
           --health-retries 10
+    steps:
+      - uses: actions/checkout@v2.3.4
+      - uses: actions/setup-go@v2
+        with:
+          go-version: 1.18
+      - uses: actions/cache@v2.1.6
+        with:
+          path: ~/go/pkg/mod
+          key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
+          restore-keys: |
+            ${{ runner.os }}-go-
+      - name: Run integration tests for mysql57
+        working-directory: internal/integration
+        run: go test -race -count=2 -v -run="MySQL.*/mysql57" -dialect=mysql57 ./...
+  
+  integration-mysql8:
+    runs-on: ubuntu-latest
+    services:
       mysql8:
         image: mysql:8
         env:
@@ -110,8 +150,26 @@ jobs:
           --health-start-period 10s
           --health-timeout 5s
           --health-retries 10
-      maria:
-        image: mariadb
+    steps:
+      - uses: actions/checkout@v2.3.4
+      - uses: actions/setup-go@v2
+        with:
+          go-version: 1.18
+      - uses: actions/cache@v2.1.6
+        with:
+          path: ~/go/pkg/mod
+          key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
+          restore-keys: |
+            ${{ runner.os }}-go-
+      - name: Run integration tests for mysql8
+        working-directory: internal/integration
+        run: go test -race -count=2 -v -run="MySQL.*/mysql8" -dialect=mysql8 ./...
+  
+  integration-maria107:
+    runs-on: ubuntu-latest
+    services:
+      maria107:
+        image: mariadb:10.7
         env:
           MYSQL_DATABASE: test
           MYSQL_ROOT_PASSWORD: pass
@@ -123,6 +181,24 @@ jobs:
           --health-start-period 10s
           --health-timeout 5s
           --health-retries 10
+    steps:
+      - uses: actions/checkout@v2.3.4
+      - uses: actions/setup-go@v2
+        with:
+          go-version: 1.18
+      - uses: actions/cache@v2.1.6
+        with:
+          path: ~/go/pkg/mod
+          key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
+          restore-keys: |
+            ${{ runner.os }}-go-
+      - name: Run integration tests for maria107
+        working-directory: internal/integration
+        run: go test -race -count=2 -v -run="MySQL.*/maria107" -dialect=maria107 ./...
+  
+  integration-maria102:
+    runs-on: ubuntu-latest
+    services:
       maria102:
         image: mariadb:10.2.32
         env:
@@ -136,6 +212,24 @@ jobs:
           --health-start-period 10s
           --health-timeout 5s
           --health-retries 10
+    steps:
+      - uses: actions/checkout@v2.3.4
+      - uses: actions/setup-go@v2
+        with:
+          go-version: 1.18
+      - uses: actions/cache@v2.1.6
+        with:
+          path: ~/go/pkg/mod
+          key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
+          restore-keys: |
+            ${{ runner.os }}-go-
+      - name: Run integration tests for maria102
+        working-directory: internal/integration
+        run: go test -race -count=2 -v -run="MySQL.*/maria102" -dialect=maria102 ./...
+  
+  integration-maria103:
+    runs-on: ubuntu-latest
+    services:
       maria103:
         image: mariadb:10.3.13
         env:
@@ -149,6 +243,24 @@ jobs:
           --health-start-period 10s
           --health-timeout 5s
           --health-retries 10
+    steps:
+      - uses: actions/checkout@v2.3.4
+      - uses: actions/setup-go@v2
+        with:
+          go-version: 1.18
+      - uses: actions/cache@v2.1.6
+        with:
+          path: ~/go/pkg/mod
+          key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
+          restore-keys: |
+            ${{ runner.os }}-go-
+      - name: Run integration tests for maria103
+        working-directory: internal/integration
+        run: go test -race -count=2 -v -run="MySQL.*/maria103" -dialect=maria103 ./...
+  
+  integration-postgres10:
+    runs-on: ubuntu-latest
+    services:
       postgres10:
         image: postgres:10
         env:
@@ -161,6 +273,24 @@ jobs:
           --health-interval 10s
           --health-timeout 5s
           --health-retries 5
+    steps:
+      - uses: actions/checkout@v2.3.4
+      - uses: actions/setup-go@v2
+        with:
+          go-version: 1.18
+      - uses: actions/cache@v2.1.6
+        with:
+          path: ~/go/pkg/mod
+          key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
+          restore-keys: |
+            ${{ runner.os }}-go-
+      - name: Run integration tests for postgres10
+        working-directory: internal/integration
+        run: go test -race -count=2 -v -run="Postgres.*/postgres10" -dialect=postgres10 ./...
+  
+  integration-postgres11:
+    runs-on: ubuntu-latest
+    services:
       postgres11:
         image: postgres:11
         env:
@@ -173,6 +303,24 @@ jobs:
           --health-interval 10s
           --health-timeout 5s
           --health-retries 5
+    steps:
+      - uses: actions/checkout@v2.3.4
+      - uses: actions/setup-go@v2
+        with:
+          go-version: 1.18
+      - uses: actions/cache@v2.1.6
+        with:
+          path: ~/go/pkg/mod
+          key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
+          restore-keys: |
+            ${{ runner.os }}-go-
+      - name: Run integration tests for postgres11
+        working-directory: internal/integration
+        run: go test -race -count=2 -v -run="Postgres.*/postgres11" -dialect=postgres11 ./...
+  
+  integration-postgres12:
+    runs-on: ubuntu-latest
+    services:
       postgres12:
         image: postgres:12.3
         env:
@@ -185,6 +333,24 @@ jobs:
           --health-interval 10s
           --health-timeout 5s
           --health-retries 5
+    steps:
+      - uses: actions/checkout@v2.3.4
+      - uses: actions/setup-go@v2
+        with:
+          go-version: 1.18
+      - uses: actions/cache@v2.1.6
+        with:
+          path: ~/go/pkg/mod
+          key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
+          restore-keys: |
+            ${{ runner.os }}-go-
+      - name: Run integration tests for postgres12
+        working-directory: internal/integration
+        run: go test -race -count=2 -v -run="Postgres.*/postgres12" -dialect=postgres12 ./...
+  
+  integration-postgres13:
+    runs-on: ubuntu-latest
+    services:
       postgres13:
         image: postgres:13.1
         env:
@@ -197,6 +363,24 @@ jobs:
           --health-interval 10s
           --health-timeout 5s
           --health-retries 5
+    steps:
+      - uses: actions/checkout@v2.3.4
+      - uses: actions/setup-go@v2
+        with:
+          go-version: 1.18
+      - uses: actions/cache@v2.1.6
+        with:
+          path: ~/go/pkg/mod
+          key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
+          restore-keys: |
+            ${{ runner.os }}-go-
+      - name: Run integration tests for postgres13
+        working-directory: internal/integration
+        run: go test -race -count=2 -v -run="Postgres.*/postgres13" -dialect=postgres13 ./...
+  
+  integration-postgres14:
+    runs-on: ubuntu-latest
+    services:
       postgres14:
         image: postgres:14
         env:
@@ -209,21 +393,60 @@ jobs:
           --health-interval 10s
           --health-timeout 5s
           --health-retries 5
+    steps:
+      - uses: actions/checkout@v2.3.4
+      - uses: actions/setup-go@v2
+        with:
+          go-version: 1.18
+      - uses: actions/cache@v2.1.6
+        with:
+          path: ~/go/pkg/mod
+          key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
+          restore-keys: |
+            ${{ runner.os }}-go-
+      - name: Run integration tests for postgres14
+        working-directory: internal/integration
+        run: go test -race -count=2 -v -run="Postgres.*/postgres14" -dialect=postgres14 ./...
+  
+  integration-tidb5:
+    runs-on: ubuntu-latest
+    services:
       tidb5:
         image: pingcap/tidb:v5.4.0
+        
         ports:
           - 4309:4000
+        
+    steps:
+      - uses: actions/checkout@v2.3.4
+      - uses: actions/setup-go@v2
+        with:
+          go-version: 1.18
+      - uses: actions/cache@v2.1.6
+        with:
+          path: ~/go/pkg/mod
+          key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
+          restore-keys: |
+            ${{ runner.os }}-go-
+      - name: Run integration tests for tidb5
+        working-directory: internal/integration
+        run: go test -race -count=2 -v -run="TiDB.*/tidb5" -dialect=tidb5 ./...
+  
+  integration-sqlite:
+    runs-on: ubuntu-latest
+    
     steps:
       - uses: actions/checkout@v2.3.4
       - uses: actions/setup-go@v2
         with:
-          go-version: 1.17
+          go-version: 1.18
       - uses: actions/cache@v2.1.6
         with:
           path: ~/go/pkg/mod
           key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
           restore-keys: |
             ${{ runner.os }}-go-
-      - name: Run integration tests
+      - name: Run integration tests for sqlite
         working-directory: internal/integration
-        run: go test -race -count=2 -timeout=15m ./...
+        run: go test -race -count=2 -v -run="SQLite.*" -dialect=sqlite ./...
+  
\ No newline at end of file
diff --git a/README.md b/README.md
index 0a4190ea..0abb2b8b 100644
--- a/README.md
+++ b/README.md
@@ -12,6 +12,7 @@ be used individually but are designed to work very well together.
 * MariaDB
 * PostgresSQL
 * SQLite
+* TiDB
 
 ### Runs on all platforms:
 * Mac
diff --git a/cmd/action/migrate.go b/cmd/action/migrate.go
index 884568eb..c11f6a68 100644
--- a/cmd/action/migrate.go
+++ b/cmd/action/migrate.go
@@ -10,6 +10,7 @@ import (
 	"fmt"
 	"io"
 	"io/ioutil"
+	"os"
 	"strings"
 	"text/template"
 	"time"
@@ -42,14 +43,24 @@ var (
 		Use:   "migrate",
 		Short: "Manage versioned migration files",
 		Long:  "'atlas migrate' wraps several sub-commands for migration management.",
-		PersistentPreRunE: func(*cobra.Command, []string) error {
+		PersistentPreRunE: func(cmd *cobra.Command, _ []string) error {
 			// Migrate commands will not run on a broken migration directory, unless the force flag is given.
 			if !MigrateFlags.Force {
 				dir, err := dir()
 				if err != nil {
 					return err
 				}
-				return migrate.Validate(dir) // TODO(masseelch): tell the user what's wrong
+				if err := migrate.Validate(dir); err != nil {
+					fmt.Fprintf(
+						cmd.ErrOrStderr(),
+						"Error: %s\n\nYou have a checksum error in your migration directory.\n"+
+							"This happens if you manually create or edit a migration file.\n"+
+							"Please check your migration files and run\n\n"+
+							"'atlas migrate hash --force'\n\nto re-hash the contents and resolve the error.\n\n",
+						err,
+					)
+					os.Exit(1)
+				}
 			}
 			return nil
 		},
@@ -66,15 +77,29 @@ the migration directory state to the desired schema. The desired state can be an
 		Args: cobra.MaximumNArgs(1),
 		RunE: CmdMigrateDiffRun,
 	}
+	// MigrateValidateCmd represents the migrate validate command.
+	MigrateValidateCmd = &cobra.Command{
+		Use:   "validate",
+		Short: "Validates the migration directories checksum.",
+		Long: `'atlas migrate validate' computes the integrity hash sum of the migration directory and compares it to 
+the atlas.sum file. If there is a mismatch it will be reported.`,
+		Example: `  atlas migrate validate
+  atlas migrate validate --dir /path/to/migration/directory`,
+		Run: func(*cobra.Command, []string) {},
+	}
 )
 
 func init() {
+	// Add sub-commands.
 	RootCmd.AddCommand(MigrateCmd)
 	MigrateCmd.AddCommand(MigrateDiffCmd)
+	MigrateCmd.AddCommand(MigrateValidateCmd)
+	// Global flags.
 	MigrateCmd.PersistentFlags().StringVarP(&MigrateFlags.DirURL, migrateFlagDir, "", "file://migrations", "select migration directory using DSN format")
 	MigrateCmd.PersistentFlags().StringSliceVarP(&MigrateFlags.Schemas, migrateFlagSchema, "", nil, "set schema names")
 	MigrateCmd.PersistentFlags().BoolVarP(&MigrateFlags.Force, migrateFlagForce, "", false, "force a command to run on a broken migration directory state")
 	MigrateCmd.PersistentFlags().SortFlags = false
+	// Diff flags.
 	MigrateDiffCmd.Flags().StringVarP(&MigrateFlags.DevURL, migrateDiffFlagDevURL, "", "", "[driver://username:password@address/dbname?param=value] select a data source using the DSN format")
 	MigrateDiffCmd.Flags().StringVarP(&MigrateFlags.ToURL, migrateDiffFlagTo, "", "", "[driver://username:password@address/dbname?param=value] select a data source using the DSN format")
 	MigrateDiffCmd.Flags().SortFlags = false
diff --git a/cmd/action/migrations/20220318104614_initial.sql b/cmd/action/migrations/20220318104614_initial.sql
new file mode 100644
index 00000000..ff83d390
--- /dev/null
+++ b/cmd/action/migrations/20220318104614_initial.sql
@@ -0,0 +1,4 @@
+-- add new schema named "atlantis"
+CREATE DATABASE `atlantis`;
+-- create "tbl" table
+CREATE TABLE `atlantis`.`tbl` (`col` int NOT NULL) CHARSET utf8mb4 COLLATE utf8mb4_general_ci;
diff --git a/cmd/action/migrations/atlas.sum b/cmd/action/migrations/atlas.sum
new file mode 100644
index 00000000..c8d3cb5f
--- /dev/null
+++ b/cmd/action/migrations/atlas.sum
@@ -0,0 +1,2 @@
+sum h1:CZFyP06wsoDt78We3f3kGuRqhMnVBe/8mHINP2hE4ik=
+20220318104614_initial.sql h1:X9aVoWzuxCUrcS3tWoxPBUEh5aDeCwAelvo73hca1Ys=
diff --git a/doc/md/cli/reference.md b/doc/md/cli/reference.md
index f98fa215..d126198d 100644
--- a/doc/md/cli/reference.md
+++ b/doc/md/cli/reference.md
@@ -95,6 +95,26 @@ the migration directory state to the desired schema. The desired state can be an
 ```
 
 
+### atlas migrate validate
+
+Validates the migration directories checksum.
+
+#### Usage
+```
+atlas migrate validate
+```
+
+#### Details
+'atlas migrate validate' computes the integrity hash sum of the migration directory and compares it to 
+the atlas.sum file. If there is a mismatch it will be reported.
+
+#### Example
+
+```
+  atlas migrate validate
+  atlas migrate validate --dir /path/to/migration/directory
+```
+
 ## atlas schema
 
 Work with atlas schemas.
diff --git a/doc/md/deployment/intro.md b/doc/md/deployment/intro.md
index 45831fd0..cda47e2d 100644
--- a/doc/md/deployment/intro.md
+++ b/doc/md/deployment/intro.md
@@ -29,8 +29,9 @@ configuration data.
 For now, you can choose any of the supported databases:
 1. SQLite (file) (in-memory is also supported but does not require encryption)
 2. MySQL
-3. MariaDB
-4. PostgresDB
+3. TiDB
+4. MariaDB
+5. PostgresDB
 
 An example for serving Atlas with MySQL persistent storage:
 ```
diff --git a/doc/md/go-api/intro.md b/doc/md/go-api/intro.md
index f7d85c76..4b96405a 100644
--- a/doc/md/go-api/intro.md
+++ b/doc/md/go-api/intro.md
@@ -33,7 +33,7 @@ Atlas currently supports three core capabilities for working with SQL schemas.
 The implementation details for these capabilities vary greatly between the different SQL databases. Atlas currently has
 three supported drivers:
 
-* MySQL (+MariaDB)
+* MySQL (+MariaDB, TiDB)
 * PostgreSQL
 * SQLite
 
diff --git a/internal/ci/ci.tmpl b/internal/ci/ci.tmpl
new file mode 100644
index 00000000..1d8bff5f
--- /dev/null
+++ b/internal/ci/ci.tmpl
@@ -0,0 +1,103 @@
+# # # # # # # # # # # # # # # #
+# CODE GENERATED - DO NOT EDIT
+# # # # # # # # # # # # # # # #
+name: Continuous Integration
+on:
+  pull_request:
+  push:
+    branches:
+      - master
+
+jobs:
+  lint:
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@v2
+      - name: Run linters
+        uses: golangci/golangci-lint-action@v2.5.2
+        with:
+          version: v1.42.1
+          args: --verbose
+  generate-cmp:
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@v2.4.0
+      - uses: actions/setup-go@v2
+        with:
+          go-version: '1.18'
+      - uses: actions/cache@v2.1.5
+        with:
+          path: ~/go/pkg/mod
+          key: {{ "${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}" }}
+          restore-keys: |
+            {{ "${{ runner.os }}-go-" }}
+      - name: run "go generate ./..."
+        run: go generate ./...
+      - name: run "go generate internal/typedoc"
+        working-directory: internal/typedoc
+        run: go generate ./...
+      - name: Verify generated files are checked in properly
+        run: |
+          status=$(git status --porcelain)
+          if [ -n "$status" ]; then
+            echo "you need to run 'go generate ./...' and commit the changes"
+            echo "$status"
+            exit 1
+          fi
+
+  unit:
+    runs-on: ubuntu-latest
+    strategy:
+      matrix:
+        go: [ '1.17', '1.18' ]
+    steps:
+      - uses: actions/checkout@v2
+      - uses: actions/setup-go@v2
+        with:
+          go-version: {{ "${{ matrix.go }}" }}
+      - uses: actions/cache@v2.1.5
+        with:
+          path: ~/go/pkg/mod
+          key: {{ "${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}" }}
+          restore-keys: |
+            {{ "${{ runner.os }}-go-" }}
+      - name: Run sql tests
+        run: go test -race ./...
+        working-directory: sql
+      - name: Run schema tests
+        run: go test -race ./...
+        working-directory: schema
+      - name: Run cli tests
+        run: go test -race ./...
+        working-directory: cmd
+
+  {{ range $ }}
+  integration-{{ .Version }}:
+    runs-on: ubuntu-latest
+    {{ if .Image }}services:
+      {{ .Version }}:
+        image: {{ .Image }}
+        {{ with .Env }}env:{{ range . }}
+          {{ . }}{{ end }}
+        {{- end }}
+        {{ with .Ports }}ports:{{ range . }}
+          - {{ . }}{{ end }}
+        {{- end }}
+        {{ with .Options }}options: >-{{ range . }}
+          {{ . }}{{ end }}
+        {{- end }}{{ end }}
+    steps:
+      - uses: actions/checkout@v2.3.4
+      - uses: actions/setup-go@v2
+        with:
+          go-version: 1.18
+      - uses: actions/cache@v2.1.6
+        with:
+          path: ~/go/pkg/mod
+          key: {{ "${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}" }}
+          restore-keys: |
+            {{ "${{ runner.os }}-go-" }}
+      - name: Run integration tests for {{ .Version }}
+        working-directory: internal/integration
+        run: go test -race -count=2 -v -run="{{ .Regex }}" -dialect={{ .Version }} ./...
+  {{ end }}
\ No newline at end of file
diff --git a/internal/ci/main.go b/internal/ci/main.go
new file mode 100644
index 00000000..9562b498
--- /dev/null
+++ b/internal/ci/main.go
@@ -0,0 +1,158 @@
+package main
+
+import (
+	"bytes"
+	_ "embed"
+	"log"
+	"os"
+	"text/template"
+)
+
+//go:generate go run main.go
+
+// Job defines an integration job to run.
+type Job struct {
+	Version string   // version to test (passed to go test as flag which database dialect/version)
+	Image   string   // name of service
+	Regex   string   // run regex
+	Env     []string // env of service
+	Ports   []string // port mappings
+	Options []string // other options
+}
+
+var (
+	//go:embed ci.tmpl
+	t string
+
+	mysqlOptions = []string{
+		`--health-cmd "mysqladmin ping -ppass"`,
+		`--health-interval 10s`,
+		`--health-start-period 10s`,
+		`--health-timeout 5s`,
+		`--health-retries 10`,
+	}
+	mysqlEnv = []string{
+		"MYSQL_DATABASE: test",
+		"MYSQL_ROOT_PASSWORD: pass",
+	}
+	pgOptions = []string{
+		"--health-cmd pg_isready",
+		"--health-interval 10s",
+		"--health-timeout 5s",
+		"--health-retries 5",
+	}
+	pgEnv = []string{
+		"POSTGRES_DB: test",
+		"POSTGRES_PASSWORD: pass",
+	}
+	jobs = []Job{
+		{
+			Version: "mysql56",
+			Image:   "mysql:5.6.35",
+			Regex:   "MySQL.*/mysql56",
+			Env:     mysqlEnv,
+			Ports:   []string{"3306:3306"},
+			Options: mysqlOptions,
+		},
+		{
+			Version: "mysql57",
+			Image:   "mysql:5.7.26",
+			Regex:   "MySQL.*/mysql57",
+			Env:     mysqlEnv,
+			Ports:   []string{"3307:3306"},
+			Options: mysqlOptions,
+		},
+		{
+			Version: "mysql8",
+			Image:   "mysql:8",
+			Regex:   "MySQL.*/mysql8",
+			Env:     mysqlEnv,
+			Ports:   []string{"3308:3306"},
+			Options: mysqlOptions,
+		},
+		{
+			Version: "maria107",
+			Image:   "mariadb:10.7",
+			Regex:   "MySQL.*/maria107",
+			Env:     mysqlEnv,
+			Ports:   []string{"4306:3306"},
+			Options: mysqlOptions,
+		},
+		{
+			Version: "maria102",
+			Image:   "mariadb:10.2.32",
+			Regex:   "MySQL.*/maria102",
+			Env:     mysqlEnv,
+			Ports:   []string{"4307:3306"},
+			Options: mysqlOptions,
+		},
+		{
+			Version: "maria103",
+			Image:   "mariadb:10.3.13",
+			Regex:   "MySQL.*/maria103",
+			Env:     mysqlEnv,
+			Ports:   []string{"4308:3306"},
+			Options: mysqlOptions,
+		},
+		{
+			Version: "postgres10",
+			Image:   "postgres:10",
+			Regex:   "Postgres.*/postgres10",
+			Env:     pgEnv,
+			Ports:   []string{"5430:5432"},
+			Options: pgOptions,
+		},
+		{
+			Version: "postgres11",
+			Image:   "postgres:11",
+			Regex:   "Postgres.*/postgres11",
+			Env:     pgEnv,
+			Ports:   []string{"5431:5432"},
+			Options: pgOptions,
+		},
+		{
+			Version: "postgres12",
+			Image:   "postgres:12.3",
+			Regex:   "Postgres.*/postgres12",
+			Env:     pgEnv,
+			Ports:   []string{"5432:5432"},
+			Options: pgOptions,
+		},
+		{
+			Version: "postgres13",
+			Image:   "postgres:13.1",
+			Regex:   "Postgres.*/postgres13",
+			Env:     pgEnv,
+			Ports:   []string{"5433:5432"},
+			Options: pgOptions,
+		},
+		{
+			Version: "postgres14",
+			Image:   "postgres:14",
+			Regex:   "Postgres.*/postgres14",
+			Env:     pgEnv,
+			Ports:   []string{"5434:5432"},
+			Options: pgOptions,
+		},
+		{
+			Version: "tidb5",
+			Image:   "pingcap/tidb:v5.4.0",
+			Regex:   "TiDB.*/tidb5",
+			Ports:   []string{"4309:4000"},
+		},
+		{
+			Version: "sqlite",
+			Regex:   "SQLite.*",
+		},
+	}
+)
+
+func main() {
+	var buf bytes.Buffer
+	if err := template.Must(template.New("").Parse(t)).Execute(&buf, jobs); err != nil {
+		log.Fatalln(err)
+	}
+	if err := os.WriteFile("../../.github/workflows/ci.yml", buf.Bytes(), 0600); err != nil {
+		log.Fatalln(err)
+	}
+}
diff --git a/internal/integration/docker-compose.yaml b/internal/integration/docker-compose.yaml
index 1c24aad7..e9787e1c 100644
--- a/internal/integration/docker-compose.yaml
+++ b/internal/integration/docker-compose.yaml
@@ -122,7 +122,7 @@ services:
     ports:
       - 4308:3306
 
-# Default DB test, No Password
+  # Default DB test, No Password
   tidb5:
     platform: linux/amd64
     image: pingcap/tidb:v5.4.0
diff --git a/internal/integration/go.sum b/internal/integration/go.sum
index 594c5c40..f3e30862 100644
--- a/internal/integration/go.sum
+++ b/internal/integration/go.sum
@@ -82,6 +82,7 @@ github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghf
 github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
 github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
 github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
+github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e h1:fY5BOSpyZCqRo5OhCuC+XN+r/bBCmeuuJtjz+bCNIf8=
 github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
 github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
 github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6Dob7S7YxXgwXpfOuvO54S+tGdZdw9fuRZt25Ag=
@@ -277,6 +278,7 @@ github.com/lib/pq v1.10.4 h1:SO9z7FRPzA03QhHKJrH5BXA6HU1rS4V2nIVrrNC1iYk=
 github.com/lib/pq v1.10.4/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
 github.com/lyft/protoc-gen-star v0.5.3/go.mod h1:V0xaHgaf5oCCqmcxYcWiDfTiKsZsRc87/1qhoTACD8w=
 github.com/magiconair/properties v1.8.5/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60=
+github.com/manifoldco/promptui v0.9.0 h1:3V4HzJk1TtXW1MTZMP7mdlwbBpIinw3HztaIlYthEiA=
 github.com/manifoldco/promptui v0.9.0/go.mod h1:ka04sppxSGFAtxX0qhlYQjISsg9mR4GWtQEhdbn6Pgg=
 github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
 github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
@@ -297,6 +299,7 @@ github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3N
 github.com/miekg/dns v1.1.26/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso=
 github.com/miekg/dns v1.1.41/go.mod h1:p6aan82bvRIyn+zDIv9xYNUpwa73JcSh9BKwknJysuI=
 github.com/mitchellh/cli v1.1.0/go.mod h1:xcISNoH86gajksDmfB23e/pu+B+GeFRMYmoHXxx3xhI=
+github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
 github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
 github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=
 github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo=
@@ -352,9 +355,11 @@ github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasO
 github.com/spf13/afero v1.3.3/go.mod h1:5KUK8ByomD5Ti5Artl0RtHeI5pTF7MIDuXL3yY520V4=
 github.com/spf13/afero v1.6.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I=
 github.com/spf13/cast v1.4.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
+github.com/spf13/cobra v1.3.0 h1:R7cSvGu+Vv+qX0gW5R/85dx2kmmJT5z5NM8ifdYjdn0=
 github.com/spf13/cobra v1.3.0/go.mod h1:BrRVncBjOJa/eUcVVm9CE+oC6as8k+VYr4NY7WCi9V4=
 github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo=
 github.com/spf13/pflag v1.0.2/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
+github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
 github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
 github.com/spf13/viper v1.10.0/go.mod h1:SoyBPwAtKDzypXNDFKN5kzH7ppppbGZtls1UpIy5AsM=
 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=


Test patch:
diff --git a/cmd/action/migrate_test.go b/cmd/action/migrate_test.go
index ba50be43..b11b09dc 100644
--- a/cmd/action/migrate_test.go
+++ b/cmd/action/migrate_test.go
@@ -7,6 +7,7 @@ package action
 import (
 	"fmt"
 	"os"
+	"os/exec"
 	"path/filepath"
 	"strings"
 	"testing"
@@ -15,9 +16,12 @@ import (
 	"github.com/stretchr/testify/require"
 )
 
-func TestMigrate_Diff(t *testing.T) {
+func TestMigrate(t *testing.T) {
 	_, err := runCmd(RootCmd, "migrate")
 	require.NoError(t, err)
+}
+
+func TestMigrate_Diff(t *testing.T) {
 	p := t.TempDir()
 
 	// Expect no clean dev error.
@@ -45,6 +49,28 @@ func TestMigrate_Diff(t *testing.T) {
 	require.FileExists(t, filepath.Join(p, "atlas.sum"))
 }
 
+func TestMigrate_Validate(t *testing.T) {
+	s, err := runCmd(RootCmd, "migrate", "validate", "--dir", "file://migrations")
+	require.Zero(t, s)
+	require.NoError(t, err)
+}
+
+func TestMigrate_ValidateError(t *testing.T) {
+	if os.Getenv("DO_VALIDATE") == "1" {
+		runCmd(RootCmd, "migrate", "validate", "--dir", "file://migrations")
+		return
+	}
+	require.NoError(t, os.WriteFile("migrations/new.sql", []byte("contents"), 0600))
+	defer os.Remove("migrations/new.sql")
+	cmd := exec.Command(os.Args[0], "-test.run=TestMigrate_ValidateError") //nolint:gosec
+	cmd.Env = append(os.Environ(), "DO_VALIDATE=1")
+	err := cmd.Run()
+	if err, ok := err.(*exec.ExitError); ok && !err.Success() {
+		return
+	}
+	t.Fatalf("process ran with err %v, want exist status 1", err)
+}
+
 const hcl = `
 schema "main" {
 }
diff --git a/cmd/action/mux_test.go b/cmd/action/mux_test.go
index 0545f846..d4749b7b 100644
--- a/cmd/action/mux_test.go
+++ b/cmd/action/mux_test.go
@@ -11,6 +11,7 @@ import (
 	"log"
 	"net"
 	"os"
+	"sync/atomic"
 	"testing"
 
 	"ariga.io/atlas/cmd/action"
@@ -178,9 +179,9 @@ func TestMux_OpenAtlas(t *testing.T) {
 	})
 }
 
-func mockServer(t *testing.T) (*int, net.Listener) {
+func mockServer(t *testing.T) (*int64, net.Listener) {
 	var (
-		calls  int
+		calls  int64
 		l, err = net.Listen("tcp", "localhost:")
 	)
 	require.NoError(t, err)
@@ -190,7 +191,7 @@ func mockServer(t *testing.T) (*int, net.Listener) {
 			if err != nil {
 				return
 			}
-			calls++
+			atomic.AddInt64(&calls, 1)
 			require.NoError(t, conn.Close())
 		}
 	}()
diff --git a/internal/integration/integration_test.go b/internal/integration/integration_test.go
index d1770305..26511afe 100644
--- a/internal/integration/integration_test.go
+++ b/internal/integration/integration_test.go
@@ -4,10 +4,12 @@ import (
 	"bytes"
 	"context"
 	"database/sql"
+	"flag"
 	"io"
 	"io/ioutil"
 	"os"
 	"os/exec"
+	"path/filepath"
 	"strings"
 	"testing"
 	"time"
@@ -21,6 +23,22 @@ import (
 	"github.com/stretchr/testify/require"
 )
 
+func TestMain(m *testing.M) {
+	var dialect string
+	flag.StringVar(&dialect, "dialect", "", "[mysql56, postgres10, tidb5, ...] what dialect (version) to test")
+	flag.Parse()
+	var dbs []io.Closer
+	dbs = append(dbs, myInit(dialect)...)
+	dbs = append(dbs, pgInit(dialect)...)
+	dbs = append(dbs, tidbInit(dialect)...)
+	defer func() {
+		for _, db := range dbs {
+			db.Close()
+		}
+	}()
+	os.Exit(m.Run())
+}
+
 // T holds the elements common between dialect tests.
 type T interface {
 	testing.TB
@@ -175,12 +193,9 @@ func testCLISchemaInspect(t T, h string, dsn string, unmarshaler schemaspec.Unma
 func testCLIMultiSchemaApply(t T, h string, dsn string, schemas []string, unmarshaler schemaspec.Unmarshaler) {
 	// Required to have a clean "stderr" while running first time.
 	err := exec.Command("go", "run", "-mod=mod", "ariga.io/atlas/cmd/atlas").Run()
-	f := "atlas.hcl"
+	f := filepath.Join(t.TempDir(), "atlas.hcl")
 	err = ioutil.WriteFile(f, []byte(h), 0644)
 	require.NoError(t, err)
-	t.Cleanup(func() {
-		os.Remove("atlas.hcl")
-	})
 	require.NoError(t, err)
 	var expected schema.Realm
 	err = unmarshaler.UnmarshalSpec([]byte(h), &expected)
@@ -189,7 +204,7 @@ func testCLIMultiSchemaApply(t T, h string, dsn string, schemas []string, unmars
 		"schema",
 		"apply",
 		"-f",
-		"atlas.hcl",
+		f,
 		"-d",
 		dsn,
 		"-s",
@@ -238,10 +253,9 @@ func testCLISchemaApply(t T, h string, dsn string) {
 	err := exec.Command("go", "run", "-mod=mod", "ariga.io/atlas/cmd/atlas").Run()
 	require.NoError(t, err)
 	t.dropTables("users")
-	f := "atlas.hcl"
+	f := filepath.Join(t.TempDir(), "atlas.hcl")
 	err = ioutil.WriteFile(f, []byte(h), 0644)
 	require.NoError(t, err)
-	defer os.Remove(f)
 	cmd := exec.Command("go", "run", "ariga.io/atlas/cmd/atlas",
 		"schema",
 		"apply",
@@ -272,10 +286,9 @@ func testCLISchemaApplyDry(t T, h string, dsn string) {
 	err := exec.Command("go", "run", "-mod=mod", "ariga.io/atlas/cmd/atlas").Run()
 	require.NoError(t, err)
 	t.dropTables("users")
-	f := "atlas.hcl"
+	f := filepath.Join(t.TempDir(), "atlas.hcl")
 	err = ioutil.WriteFile(f, []byte(h), 0644)
 	require.NoError(t, err)
-	defer os.Remove(f)
 	cmd := exec.Command("go", "run", "ariga.io/atlas/cmd/atlas",
 		"schema",
 		"apply",
@@ -307,10 +320,9 @@ func testCLISchemaApplyAutoApprove(t T, h string, dsn string) {
 	err := exec.Command("go", "run", "-mod=mod", "ariga.io/atlas/cmd/atlas").Run()
 	require.NoError(t, err)
 	t.dropTables("users")
-	f := "atlas.hcl"
+	f := filepath.Join(t.TempDir(), "atlas.hcl")
 	err = ioutil.WriteFile(f, []byte(h), 0644)
 	require.NoError(t, err)
-	defer os.Remove(f)
 	cmd := exec.Command("go", "run", "ariga.io/atlas/cmd/atlas",
 		"schema",
 		"apply",
diff --git a/internal/integration/mysql_test.go b/internal/integration/mysql_test.go
index 4bd3f94e..b38ce364 100644
--- a/internal/integration/mysql_test.go
+++ b/internal/integration/mysql_test.go
@@ -8,8 +8,9 @@ import (
 	"context"
 	"database/sql"
 	"fmt"
+	"io"
+	"log"
 	"strings"
-	"sync"
 	"testing"
 
 	"ariga.io/atlas/sql/mysql"
@@ -28,22 +29,21 @@ type myTest struct {
 	port    int
 }
 
-var myTests struct {
-	sync.Once
+var myTests = struct {
 	drivers map[string]*myTest
+	ports   map[string]int
+}{
+	drivers: make(map[string]*myTest),
+	ports: map[string]int{"mysql56": 3306,
+		"mysql57":  3307,
+		"mysql8":   3308,
+		"maria107": 4306,
+		"maria102": 4307,
+		"maria103": 4308,
+	},
 }
 
 func myRun(t *testing.T, fn func(*myTest)) {
-	myTests.Do(func() {
-		myTests.drivers = make(map[string]*myTest)
-		for version, port := range map[string]int{"56": 3306, "57": 3307, "8": 3308, "Maria107": 4306, "Maria102": 4307, "Maria103": 4308} {
-			db, err := sql.Open("mysql", fmt.Sprintf("root:pass@tcp(localhost:%d)/test?parseTime=True", port))
-			require.NoError(t, err)
-			drv, err := mysql.Open(db)
-			require.NoError(t, err)
-			myTests.drivers[version] = &myTest{db: db, drv: drv, version: version, port: port}
-		}
-	})
 	for version, tt := range myTests.drivers {
 		t.Run(version, func(t *testing.T) {
 			tt := &myTest{T: t, db: tt.db, drv: tt.drv, version: version, port: tt.port}
@@ -52,6 +52,31 @@ func myRun(t *testing.T, fn func(*myTest)) {
 	}
 }
 
+func myInit(dialect string) []io.Closer {
+	var cs []io.Closer
+	if dialect != "" {
+		p, ok := myTests.ports[dialect]
+		if ok {
+			myTests.ports = map[string]int{dialect: p}
+		} else {
+			myTests.ports = make(map[string]int)
+		}
+	}
+	for version, port := range myTests.ports {
+		db, err := sql.Open("mysql", fmt.Sprintf("root:pass@tcp(localhost:%d)/test?parseTime=True", port))
+		if err != nil {
+			log.Fatalln(err)
+		}
+		cs = append(cs, db)
+		drv, err := mysql.Open(db)
+		if err != nil {
+			log.Fatalln(err)
+		}
+		myTests.drivers[version] = &myTest{db: db, drv: drv, version: version, port: port}
+	}
+	return cs
+}
+
 func TestMySQL_AddDropTable(t *testing.T) {
 	myRun(t, func(t *myTest) {
 		testAddDrop(t)
@@ -550,6 +575,13 @@ func TestMySQL_CLI(t *testing.T) {
 			testCLISchemaDiff(t, t.dsn("test"))
 		})
 	})
+	t.Run("SchemaApplyAutoApprove", func(t *testing.T) {
+		myRun(t, func(t *myTest) {
+			attrs := t.defaultAttrs()
+			charset, collate := attrs[0].(*schema.Charset), attrs[1].(*schema.Collation)
+			testCLISchemaApplyAutoApprove(t, fmt.Sprintf(h, charset.V, collate.V), t.dsn("test"))
+		})
+	})
 }
 
 func TestMySQL_CLI_MultiSchema(t *testing.T) {
@@ -721,31 +753,31 @@ create table atlas_types_sanity
 					{
 						Name: "tInt",
 						Type: &schema.ColumnType{Type: &schema.IntegerType{T: "int", Unsigned: false},
-							Raw: t.valueByVersion(map[string]string{"8": "int"}, "int(10)"), Null: false},
+							Raw: t.valueByVersion(map[string]string{"mysql8": "int"}, "int(10)"), Null: false},
 						Default: &schema.Literal{V: "4"},
 					},
 					{
 						Name: "tTinyInt",
 						Type: &schema.ColumnType{Type: &schema.IntegerType{T: "tinyint", Unsigned: false},
-							Raw: t.valueByVersion(map[string]string{"8": "tinyint"}, "tinyint(10)"), Null: true},
+							Raw: t.valueByVersion(map[string]string{"mysql8": "tinyint"}, "tinyint(10)"), Null: true},
 						Default: &schema.Literal{V: "8"},
 					},
 					{
 						Name: "tSmallInt",
 						Type: &schema.ColumnType{Type: &schema.IntegerType{T: "smallint", Unsigned: false},
-							Raw: t.valueByVersion(map[string]string{"8": "smallint"}, "smallint(10)"), Null: true},
+							Raw: t.valueByVersion(map[string]string{"mysql8": "smallint"}, "smallint(10)"), Null: true},
 						Default: &schema.Literal{V: "2"},
 					},
 					{
 						Name: "tMediumInt",
 						Type: &schema.ColumnType{Type: &schema.IntegerType{T: "mediumint", Unsigned: false},
-							Raw: t.valueByVersion(map[string]string{"8": "mediumint"}, "mediumint(10)"), Null: true},
+							Raw: t.valueByVersion(map[string]string{"mysql8": "mediumint"}, "mediumint(10)"), Null: true},
 						Default: &schema.Literal{V: "11"},
 					},
 					{
 						Name: "tBigInt",
 						Type: &schema.ColumnType{Type: &schema.IntegerType{T: "bigint", Unsigned: false},
-							Raw: t.valueByVersion(map[string]string{"8": "bigint"}, "bigint(10)"), Null: true},
+							Raw: t.valueByVersion(map[string]string{"mysql8": "bigint"}, "bigint(10)"), Null: true},
 						Default: &schema.Literal{V: "4"},
 					},
 					{
@@ -867,8 +899,8 @@ create table atlas_types_sanity
 					},
 					{
 						Name: "tYear",
-						Type: &schema.ColumnType{Type: &schema.TimeType{T: "year", Precision: t.intByVersion(map[string]int{"8": 0}, 4)},
-							Raw: t.valueByVersion(map[string]string{"8": "year"}, "year(4)"), Null: true},
+						Type: &schema.ColumnType{Type: &schema.TimeType{T: "year", Precision: t.intByVersion(map[string]int{"mysql8": 0}, 4)},
+							Raw: t.valueByVersion(map[string]string{"mysql8": "year"}, "year(4)"), Null: true},
 					},
 					{
 						Name: "tVarchar",
@@ -894,13 +926,13 @@ create table atlas_types_sanity
 						Name: "tVarBinary",
 						Type: &schema.ColumnType{Type: &schema.BinaryType{T: "varbinary", Size: 30},
 							Raw: "varbinary(30)", Null: true},
-						Default: &schema.Literal{V: t.valueByVersion(map[string]string{"8": "0x546974616E"}, t.quoted("Titan"))},
+						Default: &schema.Literal{V: t.valueByVersion(map[string]string{"mysql8": "0x546974616E"}, t.quoted("Titan"))},
 					},
 					{
 						Name: "tBinary",
 						Type: &schema.ColumnType{Type: &schema.BinaryType{T: "binary", Size: 5},
 							Raw: "binary(5)", Null: true},
-						Default: &schema.Literal{V: t.valueByVersion(map[string]string{"8": "0x546974616E"}, t.quoted("Titan"))},
+						Default: &schema.Literal{V: t.valueByVersion(map[string]string{"mysql8": "0x546974616E"}, t.quoted("Titan"))},
 					},
 					{
 						Name: "tBlob",
@@ -1014,8 +1046,8 @@ create table atlas_types_sanity
 					{
 						Name: "tGeometryCollection",
 						Type: &schema.ColumnType{Type: &schema.SpatialType{T: t.valueByVersion(
-							map[string]string{"8": "geomcollection"}, "geometrycollection")},
-							Raw: t.valueByVersion(map[string]string{"8": "geomcollection"},
+							map[string]string{"mysql8": "geomcollection"}, "geometrycollection")},
+							Raw: t.valueByVersion(map[string]string{"mysql8": "geomcollection"},
 								"geometrycollection"), Null: true},
 					},
 				},
@@ -1033,7 +1065,7 @@ create table atlas_types_sanity
 ) CHARSET = latin1 COLLATE latin1_swedish_ci;
 `
 		myRun(t, func(t *myTest) {
-			if t.version == "56" {
+			if t.version == "mysql56" {
 				return
 			}
 			t.dropTables(n)
@@ -1046,7 +1078,7 @@ create table atlas_types_sanity
 			expected := schema.Table{
 				Name: n,
 				Attrs: func() []schema.Attr {
-					if t.version == "Maria107" {
+					if t.version == "maria107" {
 						return []schema.Attr{
 							&schema.Charset{V: "latin1"},
 							&schema.Collation{V: "latin1_swedish_ci"},
@@ -1063,9 +1095,9 @@ create table atlas_types_sanity
 					func() *schema.Column {
 						c := &schema.Column{Name: "tJSON", Type: &schema.ColumnType{Type: &schema.JSONType{T: "json"}, Raw: "json", Null: true}}
 						switch t.version {
-						case "Maria107":
+						case "maria107":
 							c.Attrs = []schema.Attr{}
-						case "Maria102", "Maria103":
+						case "maria102", "maria103":
 							c.Type.Raw = "longtext"
 							c.Type.Type = &schema.StringType{T: "longtext"}
 							c.Attrs = []schema.Attr{
@@ -1275,8 +1307,8 @@ func (t *myTest) loadTable(name string) *schema.Table {
 	return table
 }
 
-func (t *myTest) mariadb() bool { return strings.HasPrefix(t.version, "Maria") }
-func (t *myTest) tidb() bool    { return strings.HasPrefix(t.version, "TiDB") }
+func (t *myTest) mariadb() bool { return strings.HasPrefix(t.version, "maria") }
+func (t *myTest) tidb() bool    { return strings.HasPrefix(t.version, "tidb") }
 
 // defaultConfig returns the default charset and
 // collation configuration based on the MySQL version.
@@ -1286,13 +1318,13 @@ func (t *myTest) defaultAttrs() []schema.Attr {
 		collation = "latin1_swedish_ci"
 	)
 	switch {
-	case strings.Contains(t.version, "TiDB"):
+	case strings.Contains(t.version, "tidb"):
 		charset = "utf8mb4"
 		collation = "utf8mb4_bin"
-	case t.version == "8":
+	case t.version == "mysql8":
 		charset = "utf8mb4"
 		collation = "utf8mb4_0900_ai_ci"
-	case t.version == "Maria107":
+	case t.version == "maria107":
 		charset = "utf8mb4"
 		collation = "utf8mb4_general_ci"
 	}
diff --git a/internal/integration/postgres_test.go b/internal/integration/postgres_test.go
index 8b6dc47b..9ebaba65 100644
--- a/internal/integration/postgres_test.go
+++ b/internal/integration/postgres_test.go
@@ -8,8 +8,9 @@ import (
 	"context"
 	"database/sql"
 	"fmt"
+	"io"
+	"log"
 	"strings"
-	"sync"
 	"testing"
 
 	"ariga.io/atlas/sql/mysql"
@@ -29,22 +30,21 @@ type pgTest struct {
 	port    int
 }
 
-var pgTests struct {
-	sync.Once
+var pgTests = struct {
 	drivers map[string]*pgTest
+	ports   map[string]int
+}{
+	drivers: make(map[string]*pgTest),
+	ports: map[string]int{
+		"postgres10": 5430,
+		"postgres11": 5431,
+		"postgres12": 5432,
+		"postgres13": 5433,
+		"postgres14": 5434,
+	},
 }
 
 func pgRun(t *testing.T, fn func(*pgTest)) {
-	pgTests.Do(func() {
-		pgTests.drivers = make(map[string]*pgTest)
-		for version, port := range map[string]int{"10": 5430, "11": 5431, "12": 5432, "13": 5433, "14": 5434} {
-			db, err := sql.Open("postgres", fmt.Sprintf("host=localhost port=%d user=postgres dbname=test password=pass sslmode=disable", port))
-			require.NoError(t, err)
-			drv, err := postgres.Open(db)
-			require.NoError(t, err)
-			pgTests.drivers[version] = &pgTest{db: db, drv: drv, version: version, port: port}
-		}
-	})
 	for version, tt := range pgTests.drivers {
 		t.Run(version, func(t *testing.T) {
 			tt := &pgTest{T: t, db: tt.db, drv: tt.drv, version: version, port: tt.port}
@@ -53,6 +53,31 @@ func pgRun(t *testing.T, fn func(*pgTest)) {
 	}
 }
 
+func pgInit(dialect string) []io.Closer {
+	var cs []io.Closer
+	if dialect != "" {
+		p, ok := pgTests.ports[dialect]
+		if ok {
+			pgTests.ports = map[string]int{dialect: p}
+		} else {
+			pgTests.ports = make(map[string]int)
+		}
+	}
+	for version, port := range pgTests.ports {
+		db, err := sql.Open("postgres", fmt.Sprintf("host=localhost port=%d user=postgres dbname=test password=pass sslmode=disable", port))
+		if err != nil {
+			log.Fatalln(err)
+		}
+		cs = append(cs, db)
+		drv, err := postgres.Open(db)
+		if err != nil {
+			log.Fatalln(err)
+		}
+		pgTests.drivers[version] = &pgTest{db: db, drv: drv, version: version, port: port}
+	}
+	return cs
+}
+
 func TestPostgres_AddDropTable(t *testing.T) {
 	pgRun(t, func(t *pgTest) {
 		testAddDrop(t)
@@ -484,6 +509,11 @@ func TestPostgres_CLI(t *testing.T) {
 			testCLISchemaDiff(t, t.dsn())
 		})
 	})
+	t.Run("SchemaApplyAutoApprove", func(t *testing.T) {
+		pgRun(t, func(t *pgTest) {
+			testCLISchemaApplyAutoApprove(t, h, t.dsn())
+		})
+	})
 }
 
 func TestPostgres_CLI_MultiSchema(t *testing.T) {
@@ -635,12 +665,12 @@ create table atlas_types_sanity
 				{
 					Name:    "tBit",
 					Type:    &schema.ColumnType{Type: &postgres.BitType{T: "bit", Len: 10}, Raw: "bit", Null: true},
-					Default: &schema.RawExpr{X: t.valueByVersion(map[string]string{"10": "B'100'::\"bit\""}, "'100'::\"bit\"")},
+					Default: &schema.RawExpr{X: t.valueByVersion(map[string]string{"postgres10": "B'100'::\"bit\""}, "'100'::\"bit\"")},
 				},
 				{
 					Name:    "tBitVar",
 					Type:    &schema.ColumnType{Type: &postgres.BitType{T: "bit varying", Len: 10}, Raw: "bit varying", Null: true},
-					Default: &schema.RawExpr{X: t.valueByVersion(map[string]string{"10": "B'100'::\"bit\""}, "'100'::\"bit\"")},
+					Default: &schema.RawExpr{X: t.valueByVersion(map[string]string{"postgres10": "B'100'::\"bit\""}, "'100'::\"bit\"")},
 				},
 				{
 					Name:    "tBoolean",
diff --git a/internal/integration/script_test.go b/internal/integration/script_test.go
index d31a6797..83c8e85e 100644
--- a/internal/integration/script_test.go
+++ b/internal/integration/script_test.go
@@ -174,11 +174,15 @@ func (t *myTest) cmdCmpShow(ts *testscript.TestScript, _ bool, args []string) {
 
 func (t *pgTest) cmdCmpShow(ts *testscript.TestScript, _ bool, args []string) {
 	cmdCmpShow(ts, args, func(schema, name string) (string, error) {
-		buf, err := exec.Command("docker", "ps", "-qa", "-f", fmt.Sprintf("name=postgres%s", t.version)).CombinedOutput()
+		buf, err := exec.Command("docker", "ps", "-qa", "-f", fmt.Sprintf("publish=%d", t.port)).CombinedOutput()
 		if err != nil {
 			return "", fmt.Errorf("get container id %q: %v", buf, err)
 		}
-		cmd := exec.Command("docker", "exec", string(bytes.TrimSpace(buf)), "psql", "-U", "postgres", "-d", "test", "-c", fmt.Sprintf(`\d %s.%s`, schema, name))
+		buf = bytes.TrimSpace(buf)
+		if len(bytes.Split(buf, []byte("\n"))) > 1 {
+			return "", fmt.Errorf("multiple container ids found: %q", buf)
+		}
+		cmd := exec.Command("docker", "exec", string(buf), "psql", "-U", "postgres", "-d", "test", "-c", fmt.Sprintf(`\d %s.%s`, schema, name))
 		// Use "cmd.String" to debug command.
 		buf, err = cmd.CombinedOutput()
 		if err != nil {
diff --git a/internal/integration/sqlite_test.go b/internal/integration/sqlite_test.go
index 9668951e..f78e0c78 100644
--- a/internal/integration/sqlite_test.go
+++ b/internal/integration/sqlite_test.go
@@ -30,6 +30,7 @@ type liteTest struct {
 }
 
 func liteRun(t *testing.T, fn func(test *liteTest)) {
+	t.Parallel()
 	f := path.Join(t.TempDir(), strings.ReplaceAll(t.Name(), "/", "_"))
 	db, err := sql.Open("sqlite3", fmt.Sprintf("file:%s?cache=shared&_fk=1", f))
 	require.NoError(t, err)
@@ -457,6 +458,11 @@ func TestSQLite_CLI(t *testing.T) {
 			testCLISchemaDiff(t, t.dsn())
 		})
 	})
+	t.Run("SchemaApplyAutoApprove", func(t *testing.T) {
+		liteRun(t, func(t *liteTest) {
+			testCLISchemaApplyAutoApprove(t, h, t.dsn())
+		})
+	})
 }
 
 func TestSQLite_Sanity(t *testing.T) {
diff --git a/internal/integration/testdata/mysql/autoincrement.txt b/internal/integration/testdata/mysql/autoincrement.txt
index 8f1f7279..f1570920 100644
--- a/internal/integration/testdata/mysql/autoincrement.txt
+++ b/internal/integration/testdata/mysql/autoincrement.txt
@@ -30,7 +30,7 @@ CREATE TABLE `users` (
   PRIMARY KEY (`id`)
 )
 
--- 8/1.sql --
+-- mysql8/1.sql --
 CREATE TABLE `users` (
   `id` bigint NOT NULL AUTO_INCREMENT,
   PRIMARY KEY (`id`)
@@ -58,7 +58,7 @@ CREATE TABLE `users` (
   PRIMARY KEY (`id`)
 ) AUTO_INCREMENT=1000
 
--- 8/2.sql --
+-- mysql8/2.sql --
 CREATE TABLE `users` (
   `id` bigint NOT NULL AUTO_INCREMENT,
   PRIMARY KEY (`id`)
@@ -86,7 +86,7 @@ CREATE TABLE `users` (
   PRIMARY KEY (`id`)
 ) AUTO_INCREMENT=2000
 
--- 8/3.sql --
+-- mysql8/3.sql --
 CREATE TABLE `users` (
   `id` bigint NOT NULL AUTO_INCREMENT,
   PRIMARY KEY (`id`)
diff --git a/internal/integration/testdata/mysql/column-add-drop.txt b/internal/integration/testdata/mysql/column-add-drop.txt
index 4b253dd2..2329c80b 100644
--- a/internal/integration/testdata/mysql/column-add-drop.txt
+++ b/internal/integration/testdata/mysql/column-add-drop.txt
@@ -50,7 +50,7 @@ CREATE TABLE `users` (
   PRIMARY KEY (`id`)
 )
 
--- 8/1.sql --
+-- mysql8/1.sql --
 CREATE TABLE `users` (
   `id` int NOT NULL,
   PRIMARY KEY (`id`)
@@ -82,7 +82,7 @@ CREATE TABLE `users` (
   PRIMARY KEY (`id`)
 )
 
--- 8/2.sql --
+-- mysql8/2.sql --
 CREATE TABLE `users` (
   `id` int NOT NULL,
   `name` text NOT NULL,
diff --git a/internal/integration/testdata/mysql/column-bool.txt b/internal/integration/testdata/mysql/column-bool.txt
index e9b5bf9a..0620485e 100644
--- a/internal/integration/testdata/mysql/column-bool.txt
+++ b/internal/integration/testdata/mysql/column-bool.txt
@@ -82,7 +82,7 @@ CREATE TABLE `users` (
   `c` tinyint(1) NOT NULL
 )
 
--- 8/3.sql --
+-- mysql8/3.sql --
 CREATE TABLE `users` (
   `a` tinyint(1) NOT NULL,
   `b` tinyint NOT NULL,
diff --git a/internal/integration/testdata/mysql/column-time-precision-mysql.txt b/internal/integration/testdata/mysql/column-time-precision-mysql.txt
index 93335667..69e0671d 100644
--- a/internal/integration/testdata/mysql/column-time-precision-mysql.txt
+++ b/internal/integration/testdata/mysql/column-time-precision-mysql.txt
@@ -1,5 +1,5 @@
 # Each test runs on a clean database.
-only 56 57 8
+only mysql56 mysql57 mysql8
 
 # Apply schema "1.hcl" on fresh database.
 apply 1.hcl
diff --git a/internal/integration/testdata/mysql/foreign-key-add.txt b/internal/integration/testdata/mysql/foreign-key-add.txt
index 88368a22..8d547a0e 100644
--- a/internal/integration/testdata/mysql/foreign-key-add.txt
+++ b/internal/integration/testdata/mysql/foreign-key-add.txt
@@ -149,7 +149,7 @@ CREATE TABLE `posts` (
   CONSTRAINT `owner_id` FOREIGN KEY (`author_id`) REFERENCES `users` (`id`) ON DELETE SET NULL
 )
 
--- 8/2.sql --
+-- mysql8/2.sql --
 CREATE TABLE `users` (
   `id` int NOT NULL,
   PRIMARY KEY (`id`)
diff --git a/internal/integration/testdata/mysql/foreign-key-modify-action.txt b/internal/integration/testdata/mysql/foreign-key-modify-action.txt
index 7e45dd6a..29589875 100644
--- a/internal/integration/testdata/mysql/foreign-key-modify-action.txt
+++ b/internal/integration/testdata/mysql/foreign-key-modify-action.txt
@@ -57,7 +57,7 @@ CREATE TABLE `posts` (
   CONSTRAINT `owner_id` FOREIGN KEY (`author_id`) REFERENCES `users` (`id`) ON UPDATE SET NULL
 )
 
--- 8/1.sql --
+-- mysql8/1.sql --
 CREATE TABLE `users` (
   `id` int NOT NULL,
   PRIMARY KEY (`id`)
@@ -119,7 +119,7 @@ CREATE TABLE `posts` (
   CONSTRAINT `owner_id` FOREIGN KEY (`author_id`) REFERENCES `users` (`id`) ON DELETE CASCADE ON UPDATE NO ACTION
 )
 
--- 8/2.sql --
+-- mysql8/2.sql --
 CREATE TABLE `users` (
   `id` int NOT NULL,
   PRIMARY KEY (`id`)
diff --git a/internal/integration/testdata/mysql/index-add-drop.txt b/internal/integration/testdata/mysql/index-add-drop.txt
index 41a9850d..4ac88de3 100644
--- a/internal/integration/testdata/mysql/index-add-drop.txt
+++ b/internal/integration/testdata/mysql/index-add-drop.txt
@@ -36,7 +36,7 @@ CREATE TABLE `users` (
   `rank` bigint(20) NOT NULL
 )
 
--- 8/1.sql --
+-- mysql8/1.sql --
 CREATE TABLE `users` (
   `rank` bigint NOT NULL
 )
@@ -64,7 +64,7 @@ CREATE TABLE `users` (
   UNIQUE KEY `rank_idx` (`rank`)
 )
 
--- 8/2.sql --
+-- mysql8/2.sql --
 CREATE TABLE `users` (
   `rank` bigint NOT NULL,
   UNIQUE KEY `rank_idx` (`rank`)
@@ -93,7 +93,7 @@ CREATE TABLE `users` (
   KEY `rank_idx` (`rank`)
 )
 
--- 8/3.sql --
+-- mysql8/3.sql --
 CREATE TABLE `users` (
   `rank` bigint NOT NULL,
   KEY `rank_idx` (`rank`)
diff --git a/internal/integration/testdata/mysql/index-desc.txt b/internal/integration/testdata/mysql/index-desc.txt
index 7fc5c916..0c3baa3a 100644
--- a/internal/integration/testdata/mysql/index-desc.txt
+++ b/internal/integration/testdata/mysql/index-desc.txt
@@ -1,7 +1,7 @@
 # Each test runs on a clean database.
 
 # Run this test only on MySQL 8 as it is not supported by other versions.
-only 8
+only mysql8
 
 # Apply schema "1.hcl" on fresh database.
 apply 1.hcl
diff --git a/internal/integration/testdata/mysql/index-expr.txt b/internal/integration/testdata/mysql/index-expr.txt
index d39bcb64..7df43a19 100644
--- a/internal/integration/testdata/mysql/index-expr.txt
+++ b/internal/integration/testdata/mysql/index-expr.txt
@@ -1,5 +1,5 @@
 # Run this test only on MySQL 8 as it is not supported by other versions.
-only 8
+only mysql8
 
 apply 1.hcl
 cmpshow users 1.sql
diff --git a/internal/integration/tidb_test.go b/internal/integration/tidb_test.go
index e0833968..98fc62bb 100644
--- a/internal/integration/tidb_test.go
+++ b/internal/integration/tidb_test.go
@@ -8,6 +8,8 @@ import (
 	"context"
 	"database/sql"
 	"fmt"
+	"io"
+	"log"
 	"testing"
 
 	"ariga.io/atlas/sql/mysql"
@@ -23,16 +25,46 @@ import (
 	"github.com/stretchr/testify/require"
 )
 
+var tidbTests = struct {
+	drivers map[string]*myTest
+	ports   map[string]int
+}{
+	drivers: make(map[string]*myTest),
+	ports:   map[string]int{"tidb5": 4309},
+}
+
+func tidbInit(dialect string) []io.Closer {
+	var cs []io.Closer
+	if dialect != "" {
+		p, ok := tidbTests.ports[dialect]
+		if ok {
+			tidbTests.ports = map[string]int{dialect: p}
+		} else {
+			tidbTests.ports = make(map[string]int)
+		}
+	}
+	for version, port := range tidbTests.ports {
+		db, err := sql.Open("mysql", fmt.Sprintf("root@tcp(localhost:%d)/test?parseTime=True", port))
+		if err != nil {
+			log.Fatalln(err)
+		}
+		cs = append(cs, db)
+		drv, err := mysql.Open(db)
+		if err != nil {
+			log.Fatalln(err)
+		}
+		tidbTests.drivers[version] = &myTest{db: db, drv: drv, version: version, port: port}
+	}
+	return cs
+}
+
 func tidbRun(t *testing.T, fn func(*myTest)) {
-	port := 4309
-	db, err := sql.Open("mysql", fmt.Sprintf("root@tcp(localhost:%d)/test?parseTime=True", port))
-	require.NoError(t, err)
-	drv, err := mysql.Open(db)
-	require.NoError(t, err)
-	t.Run("TiDB5", func(t *testing.T) {
-		tt := &myTest{T: t, db: db, drv: drv, version: "TiDB5", port: port}
-		fn(tt)
-	})
+	for version, tt := range tidbTests.drivers {
+		t.Run(version, func(t *testing.T) {
+			tt := &myTest{T: t, db: tt.db, drv: tt.drv, version: version, port: tt.port}
+			fn(tt)
+		})
+	}
 }
 
 func TestTiDB_AddDropTable(t *testing.T) {
@@ -723,31 +755,31 @@ create table atlas_types_sanity
 					{
 						Name: "tInt",
 						Type: &schema.ColumnType{Type: &schema.IntegerType{T: "int", Unsigned: false},
-							Raw: t.valueByVersion(map[string]string{"8": "int"}, "int(10)"), Null: false},
+							Raw: t.valueByVersion(map[string]string{"mysql8": "int"}, "int(10)"), Null: false},
 						Default: &schema.Literal{V: "4"},
 					},
 					{
 						Name: "tTinyInt",
 						Type: &schema.ColumnType{Type: &schema.IntegerType{T: "tinyint", Unsigned: false},
-							Raw: t.valueByVersion(map[string]string{"8": "tinyint"}, "tinyint(10)"), Null: true},
+							Raw: t.valueByVersion(map[string]string{"mysql8": "tinyint"}, "tinyint(10)"), Null: true},
 						Default: &schema.Literal{V: "8"},
 					},
 					{
 						Name: "tSmallInt",
 						Type: &schema.ColumnType{Type: &schema.IntegerType{T: "smallint", Unsigned: false},
-							Raw: t.valueByVersion(map[string]string{"8": "smallint"}, "smallint(10)"), Null: true},
+							Raw: t.valueByVersion(map[string]string{"mysql8": "smallint"}, "smallint(10)"), Null: true},
 						Default: &schema.Literal{V: "2"},
 					},
 					{
 						Name: "tMediumInt",
 						Type: &schema.ColumnType{Type: &schema.IntegerType{T: "mediumint", Unsigned: false},
-							Raw: t.valueByVersion(map[string]string{"8": "mediumint"}, "mediumint(10)"), Null: true},
+							Raw: t.valueByVersion(map[string]string{"mysql8": "mediumint"}, "mediumint(10)"), Null: true},
 						Default: &schema.Literal{V: "11"},
 					},
 					{
 						Name: "tBigInt",
 						Type: &schema.ColumnType{Type: &schema.IntegerType{T: "bigint", Unsigned: false},
-							Raw: t.valueByVersion(map[string]string{"8": "bigint"}, "bigint(10)"), Null: true},
+							Raw: t.valueByVersion(map[string]string{"mysql8": "bigint"}, "bigint(10)"), Null: true},
 						Default: &schema.Literal{V: "4"},
 					},
 					{
@@ -839,8 +871,8 @@ create table atlas_types_sanity
 					},
 					{
 						Name: "tYear",
-						Type: &schema.ColumnType{Type: &schema.TimeType{T: "year", Precision: t.intByVersion(map[string]int{"8": 0}, 4)},
-							Raw: t.valueByVersion(map[string]string{"8": "year"}, "year(4) unsigned"), Null: true},
+						Type: &schema.ColumnType{Type: &schema.TimeType{T: "year", Precision: t.intByVersion(map[string]int{"mysql8": 0}, 4)},
+							Raw: t.valueByVersion(map[string]string{"mysql8": "year"}, "year(4) unsigned"), Null: true},
 					},
 					{
 						Name: "tVarchar",
@@ -866,13 +898,13 @@ create table atlas_types_sanity
 						Name: "tVarBinary",
 						Type: &schema.ColumnType{Type: &schema.BinaryType{T: "varbinary", Size: 30},
 							Raw: "varbinary(30)", Null: true},
-						Default: &schema.Literal{V: t.valueByVersion(map[string]string{"8": "0x546974616E"}, t.quoted("Titan"))},
+						Default: &schema.Literal{V: t.valueByVersion(map[string]string{"mysql8": "0x546974616E"}, t.quoted("Titan"))},
 					},
 					{
 						Name: "tBinary",
 						Type: &schema.ColumnType{Type: &schema.BinaryType{T: "binary", Size: 5},
 							Raw: "binary(5)", Null: true},
-						Default: &schema.Literal{V: t.valueByVersion(map[string]string{"8": "0x546974616E"}, t.quoted("Titan"))},
+						Default: &schema.Literal{V: t.valueByVersion(map[string]string{"mysql8": "0x546974616E"}, t.quoted("Titan"))},
 					},
 					{
 						Name: "tBlob",
@@ -1019,7 +1051,7 @@ create table atlas_types_sanity
 					&schema.Column{
 						Name: "tBigInt2",
 						Type: &schema.ColumnType{Type: &schema.IntegerType{T: "bigint", Unsigned: false},
-							Raw: t.valueByVersion(map[string]string{"8": "bigint"}, "bigint(10)"), Null: true},
+							Raw: t.valueByVersion(map[string]string{"mysql8": "bigint"}, "bigint(10)"), Null: true},
 						Default: &schema.Literal{V: "4"},
 					},
 				},