Repository: ariga/atlas
Pull request #1153 (2022-09-19 23:05:08)

sql/postgres: Add support for concurrent indexes
In the process of resolving https://github.com/ent/ent/issues/1624 I've added support for creating indexes concurrently.


Patch:
diff --git a/README.md b/README.md
index d7b7e289..3a5ae5e2 100644
--- a/README.md
+++ b/README.md
@@ -18,7 +18,7 @@
      </p>
 </div>
 
-Atlas CLI is an open source tool that helps developers manage their database schemas by applying modern DevOps principles. Contrary to existing tools, Atlas intelligently plans schema migrations for you. Atlas users can use the [Atlas DDL](https://atlasgo.io/knowledge/ddl#hcl) (data definition language) to describe their desired database schema and use the command-line tool to plan and apply the migrations to their systems.
+Atlas CLI is an open source tool that helps developers manage their database schemas by applying modern DevOps principles. Contrary to existing tools, Atlas intelligently plans schema migrations for you. Atlas users can use the [Atlas DDL](https://atlasgo.io/guides/ddl#hcl) (data definition language) to describe their desired database schema and use the command-line tool to plan and apply the migrations to their systems.
 
 ## Quick Installation
 
diff --git a/cmd/atlas/internal/cmdapi/migrate.go b/cmd/atlas/internal/cmdapi/migrate.go
index 91f4cd15..6f7fd654 100644
--- a/cmd/atlas/internal/cmdapi/migrate.go
+++ b/cmd/atlas/internal/cmdapi/migrate.go
@@ -39,18 +39,17 @@ const (
 	migrateFlagDevURL           = "dev-url"
 	migrateFlagDir              = "dir"
 	migrateFlagDirFormat        = "dir-format"
-	migrateFlagForce            = "force"
 	migrateFlagLog              = "log"
 	migrateFlagRevisionsSchema  = "revisions-schema"
 	migrateFlagDryRun           = "dry-run"
 	migrateFlagTo               = "to"
+	migrateFlagFrom             = "from"
 	migrateFlagSchema           = "schema"
 	migrateLintLatest           = "latest"
 	migrateLintGitDir           = "git-dir"
 	migrateLintGitBase          = "git-base"
 	migrateDiffQualifier        = "qualifier"
 	migrateApplyAllowDirty      = "allow-dirty"
-	migrateApplyFromVersion     = "from"
 	migrateApplyBaselineVersion = "baseline"
 	migrateApplyTxMode          = "tx-mode"
 )
@@ -65,7 +64,6 @@ var (
 		DirURL         string
 		DirFormat      string
 		RevisionSchema string
-		Force          bool
 		Apply          struct {
 			DryRun          bool
 			LogFormat       string
@@ -77,6 +75,10 @@ var (
 		Diff struct {
 			Qualifier string // optional table qualifier
 		}
+		Import struct {
+			FromURL string
+			ToURL   string
+		}
 		Lint struct {
 			Format  string // log formatting
 			Latest  uint   // latest N migration files
@@ -93,17 +95,14 @@ var (
 			if err := migrateFlagsFromEnv(cmd, nil); err != nil {
 				return err
 			}
-			// Migrate commands will not run on a broken migration directory, unless the force flag is given.
-			if !MigrateFlags.Force {
-				dir, err := dir(false)
-				if err != nil {
-					return err
-				}
-				if err := migrate.Validate(dir); err != nil {
-					printChecksumErr(cmd.OutOrStderr())
-					cmd.SilenceUsage = true
-					return err
-				}
+			dir, err := dir(false)
+			if err != nil {
+				return err
+			}
+			if err := migrate.Validate(dir); err != nil {
+				printChecksumErr(cmd.OutOrStderr())
+				cmd.SilenceUsage = true
+				return err
 			}
 			return nil
 		},
@@ -145,17 +144,14 @@ directory state to the desired schema. The desired state can be another connecte
 			if err := migrateFlagsFromEnv(cmd, nil); err != nil {
 				return err
 			}
-			// Migrate commands will not run on a broken migration directory, unless the force flag is given.
-			if !MigrateFlags.Force {
-				dir, err := dir(true)
-				if err != nil {
-					return err
-				}
-				if err := migrate.Validate(dir); err != nil {
-					printChecksumErr(cmd.OutOrStderr())
-					cmd.SilenceUsage = true
-					return err
-				}
+			dir, err := dir(true)
+			if err != nil {
+				return err
+			}
+			if err := migrate.Validate(dir); err != nil {
+				printChecksumErr(cmd.OutOrStderr())
+				cmd.SilenceUsage = true
+				return err
 			}
 			return nil
 		},
@@ -167,8 +163,34 @@ directory state to the desired schema. The desired state can be another connecte
 		Short: "Hash (re-)creates an integrity hash file for the migration directory.",
 		Long: `'atlas migrate hash' computes the integrity hash sum of the migration directory and stores it in the atlas.sum file.
 This command should be used whenever a manual change in the migration directory was made.`,
-		Example: `  atlas migrate hash --force`,
-		RunE:    CmdMigrateHashRun,
+		Example:           `  atlas migrate hash`,
+		PersistentPreRunE: migrateFlagsFromEnv,
+		RunE:              CmdMigrateHashRun,
+	}
+	// MigrateImportCmd represents the 'atlas migrate import' command.
+	MigrateImportCmd = &cobra.Command{
+		Use:     "import",
+		Short:   "Import a migration directory from another migration management tool to the Atlas format.",
+		Example: `  atlas migrate import --dir-format liquibase --from file:///path/to/source/directory --to file:///path/to/migration/directory`,
+		// Validate the source directory. Consider a directory with no sum file valid, since it might be an import
+		// from an existing project.
+		PersistentPreRunE: func(cmd *cobra.Command, _ []string) error {
+			if err := migrateFlagsFromEnv(cmd, nil); err != nil {
+				return err
+			}
+			MigrateFlags.DirURL = MigrateFlags.Import.FromURL
+			dir, err := dir(false)
+			if err != nil {
+				return err
+			}
+			if err := migrate.Validate(dir); err != nil && !errors.Is(err, migrate.ErrChecksumNotFound) {
+				printChecksumErr(cmd.OutOrStderr())
+				cmd.SilenceUsage = true
+				return err
+			}
+			return nil
+		},
+		RunE: CmdMigrateImportRun,
 	}
 	// MigrateNewCmd represents the 'atlas migrate new' command.
 	MigrateNewCmd = &cobra.Command{
@@ -201,7 +223,7 @@ files are executed on the connected database in order to validate SQL semantics.
   atlas migrate validate --env dev`,
 		RunE: CmdMigrateValidateRun,
 	}
-	// MigrateLintCmd represents the 'atlas migrate Lint' command.
+	// MigrateLintCmd represents the 'atlas migrate lint' command.
 	MigrateLintCmd = &cobra.Command{
 		Use:   "lint",
 		Short: "Run analysis on the migration directory",
@@ -226,6 +248,7 @@ func init() {
 	MigrateCmd.AddCommand(MigrateValidateCmd)
 	MigrateCmd.AddCommand(MigrateStatusCmd)
 	MigrateCmd.AddCommand(MigrateLintCmd)
+	MigrateCmd.AddCommand(MigrateImportCmd)
 	// Reusable flags.
 	urlFlag := func(f *string, name, short string, set *pflag.FlagSet) {
 		set.StringVarP(f, name, short, "", "[driver://username:password@address/dbname?param=value] select a database using the URL format")
@@ -233,26 +256,27 @@ func init() {
 	revisionsFlag := func(set *pflag.FlagSet) {
 		set.StringVarP(&MigrateFlags.RevisionSchema, migrateFlagRevisionsSchema, "", "", "schema name where the revisions table resides")
 	}
+	dirURLFlag := func(f *string, name, short string, set *pflag.FlagSet) {
+		set.StringVarP(f, name, short, "file://migrations", "select migration directory using URL format")
+	}
 	// Global flags.
-	MigrateCmd.PersistentFlags().StringVarP(&MigrateFlags.DirURL, migrateFlagDir, "", "file://migrations", "select migration directory using URL format")
+	dirURLFlag(&MigrateFlags.DirURL, migrateFlagDir, "", MigrateCmd.PersistentFlags())
 	MigrateCmd.PersistentFlags().StringSliceVarP(&MigrateFlags.Schemas, migrateFlagSchema, "", nil, "set schema names")
 	MigrateCmd.PersistentFlags().StringVarP(&MigrateFlags.DirFormat, migrateFlagDirFormat, "", formatAtlas, "set migration file format")
-	MigrateCmd.PersistentFlags().BoolVarP(&MigrateFlags.Force, migrateFlagForce, "", false, "force a command to run on a broken migration directory state")
 	MigrateCmd.PersistentFlags().SortFlags = false
 	// Apply flags.
 	MigrateApplyCmd.Flags().StringVarP(&MigrateFlags.Apply.LogFormat, migrateFlagLog, "", logFormatTTY, "log format to use")
 	revisionsFlag(MigrateApplyCmd.Flags())
 	MigrateApplyCmd.Flags().BoolVarP(&MigrateFlags.Apply.DryRun, migrateFlagDryRun, "", false, "do not actually execute any SQL but show it on screen")
-	MigrateApplyCmd.Flags().StringVarP(&MigrateFlags.Apply.FromVersion, migrateApplyFromVersion, "", "", "calculate pending files from the given version (including it)")
+	MigrateApplyCmd.Flags().StringVarP(&MigrateFlags.Apply.FromVersion, migrateFlagFrom, "", "", "calculate pending files from the given version (including it)")
 	MigrateApplyCmd.Flags().StringVarP(&MigrateFlags.Apply.BaselineVersion, migrateApplyBaselineVersion, "", "", "start the first migration after the given baseline version")
 	MigrateApplyCmd.Flags().StringVarP(&MigrateFlags.Apply.TxMode, migrateApplyTxMode, "", txModeFile, "set transaction mode [none, file, all]")
 	MigrateApplyCmd.Flags().BoolVarP(&MigrateFlags.Apply.AllowDirty, migrateApplyAllowDirty, "", false, "allow start working on a non-clean database")
 	urlFlag(&MigrateFlags.URL, migrateFlagURL, "u", MigrateApplyCmd.Flags())
 	MigrateApplyCmd.Flags().SortFlags = false
 	cobra.CheckErr(MigrateApplyCmd.MarkFlagRequired(migrateFlagURL))
-	MigrateApplyCmd.MarkFlagsMutuallyExclusive(migrateApplyFromVersion, migrateApplyBaselineVersion)
+	MigrateApplyCmd.MarkFlagsMutuallyExclusive(migrateFlagFrom, migrateApplyBaselineVersion)
 	cobra.CheckErr(MigrateApplyCmd.Flags().MarkHidden(migrateFlagDirFormat))
-	cobra.CheckErr(MigrateApplyCmd.Flags().MarkHidden(migrateFlagForce))
 	cobra.CheckErr(MigrateApplyCmd.Flags().MarkHidden(migrateFlagSchema))
 	// Diff flags.
 	urlFlag(&MigrateFlags.DevURL, migrateFlagDevURL, "", MigrateDiffCmd.Flags())
@@ -261,11 +285,18 @@ func init() {
 	MigrateDiffCmd.Flags().SortFlags = false
 	cobra.CheckErr(MigrateDiffCmd.MarkFlagRequired(migrateFlagDevURL))
 	cobra.CheckErr(MigrateDiffCmd.MarkFlagRequired(migrateFlagTo))
+	// Import flags.
+	dirURLFlag(&MigrateFlags.Import.FromURL, migrateFlagFrom, "", MigrateImportCmd.Flags())
+	dirURLFlag(&MigrateFlags.Import.ToURL, migrateFlagTo, "", MigrateImportCmd.Flags())
+	MigrateImportCmd.Flags().SortFlags = false
 	// Validate flags.
 	urlFlag(&MigrateFlags.DevURL, migrateFlagDevURL, "", MigrateValidateCmd.Flags())
 	// Status flags.
 	urlFlag(&MigrateFlags.URL, migrateFlagURL, "u", MigrateStatusCmd.Flags())
 	revisionsFlag(MigrateStatusCmd.Flags())
+	// Hash flags.
+	MigrateHashCmd.Flags().Bool("force", false, "")
+	cobra.CheckErr(MigrateHashCmd.Flags().MarkDeprecated("force", "you can safely omit it."))
 	// Lint flags.
 	urlFlag(&MigrateFlags.DevURL, migrateFlagDevURL, "", MigrateLintCmd.Flags())
 	MigrateLintCmd.PersistentFlags().StringVarP(&MigrateFlags.Lint.Format, migrateFlagLog, "", "", "custom logging using a Go template")
@@ -623,6 +654,84 @@ func CmdMigrateHashRun(*cobra.Command, []string) error {
 	return migrate.WriteSumFile(dir, sum)
 }
 
+// CmdMigrateImportRun is the command executed when running the CLI with 'migrate import' args.
+func CmdMigrateImportRun(cmd *cobra.Command, _ []string) error {
+	if MigrateFlags.DirFormat == formatAtlas {
+		return fmt.Errorf("cannot import a migration directory already in %q format", formatAtlas)
+	}
+	MigrateFlags.DirURL = MigrateFlags.Import.FromURL
+	src, err := dir(false)
+	if err != nil {
+		return err
+	}
+	MigrateFlags.DirFormat = formatAtlas
+	MigrateFlags.DirURL = MigrateFlags.Import.ToURL
+	trgt, err := dir(true)
+	if err != nil {
+		return err
+	}
+	// Target must be empty.
+	ff, err := trgt.Files()
+	switch {
+	case err != nil:
+		return err
+	case len(ff) != 0:
+		return errors.New("target migration directory must be empty")
+	}
+	ff, err = src.Files()
+	switch {
+	case err != nil:
+		return err
+	case len(ff) == 0:
+		fmt.Fprint(cmd.OutOrStderr(), "nothing to import")
+		cmd.SilenceUsage = true
+		return nil
+	}
+	// Fix version numbers for Flyway repeatable migrations.
+	if _, ok := src.(*sqltool.FlywayDir); ok {
+		sqltool.SetRepeatableVersion(ff)
+	}
+	// Extract the statements for each of the migration files, add them to a plan to format with the
+	// migrate.DefaultFormatter.
+	for _, f := range ff {
+		stmts, err := f.StmtDecls()
+		if err != nil {
+			return err
+		}
+		plan := &migrate.Plan{
+			Version: f.Version(),
+			Name:    f.Desc(),
+			Changes: make([]*migrate.Change, len(stmts)),
+		}
+		var buf strings.Builder
+		for i, s := range stmts {
+			for _, c := range s.Comments {
+				buf.WriteString(c)
+				if !strings.HasSuffix(c, "\n") {
+					buf.WriteString("\n")
+				}
+			}
+			buf.WriteString(strings.TrimSuffix(s.Text, ";"))
+			plan.Changes[i] = &migrate.Change{Cmd: buf.String()}
+			buf.Reset()
+		}
+		files, err := migrate.DefaultFormatter.Format(plan)
+		if err != nil {
+			return err
+		}
+		for _, f := range files {
+			if err := trgt.WriteFile(f.Name(), f.Bytes()); err != nil {
+				return err
+			}
+		}
+	}
+	sum, err := trgt.Checksum()
+	if err != nil {
+		return err
+	}
+	return migrate.WriteSumFile(trgt, sum)
+}
+
 // CmdMigrateNewRun is the command executed when running the CLI with 'migrate new' args.
 func CmdMigrateNewRun(_ *cobra.Command, args []string) error {
 	dir, err := dir(true)
@@ -848,7 +957,7 @@ func printChecksumErr(out io.Writer) {
 This happens if you manually create or edit a migration file.
 Please check your migration files and run
 
-'atlas migrate hash --force'
+'atlas migrate hash'
 
 to re-hash the contents and resolve the error
 
diff --git a/cmd/atlas/internal/lint/lint.go b/cmd/atlas/internal/lint/lint.go
index 3e7c386f..9e369963 100644
--- a/cmd/atlas/internal/lint/lint.go
+++ b/cmd/atlas/internal/lint/lint.go
@@ -13,7 +13,6 @@ import (
 	"strings"
 
 	"ariga.io/atlas/cmd/atlas/internal/sqlparse"
-	"ariga.io/atlas/cmd/atlas/internal/sqlparse/parseutil"
 	"ariga.io/atlas/sql/migrate"
 	"ariga.io/atlas/sql/schema"
 	"ariga.io/atlas/sql/sqlcheck"
@@ -28,7 +27,6 @@ type (
 	}
 
 	// A ChangeLoader takes a set of migration files and will create multiple schema.Changes out of it.
-	// It will also label migration files as either "generated" or "handcrafted".
 	ChangeLoader interface {
 		// LoadChanges converts each of the given migration files into one Changes.
 		LoadChanges(context.Context, []migrate.File) (*Changes, error)
@@ -108,8 +106,7 @@ func (d *GitChangeDetector) DetectChanges(ctx context.Context) ([]migrate.File,
 		args = append(args, "-C", d.work)
 	}
 	args = append(args, "--no-pager", "diff", "--name-only", "--diff-filter=A", d.base, "HEAD", d.path)
-	buf, err := exec.CommandContext(ctx, "git", args...).
-		CombinedOutput()
+	buf, err := exec.CommandContext(ctx, "git", args...).CombinedOutput()
 	if err != nil {
 		return nil, nil, fmt.Errorf("git diff: %w", err)
 	}
@@ -217,7 +214,7 @@ func (d *DevLoader) LoadChanges(ctx context.Context, base, files []migrate.File)
 			File:   f,
 			Parser: sqlparse.ParserFor(d.Dev.Name),
 		}
-		stmts, err := parseutil.StmtDecls(f)
+		stmts, err := f.StmtDecls()
 		if err != nil {
 			return nil, &FileError{File: f.Name(), Err: fmt.Errorf("scanning statements: %w", err)}
 		}
diff --git a/cmd/atlas/internal/sqlparse/parseutil/parseutil.go b/cmd/atlas/internal/sqlparse/parseutil/parseutil.go
index c7055474..15c4322f 100644
--- a/cmd/atlas/internal/sqlparse/parseutil/parseutil.go
+++ b/cmd/atlas/internal/sqlparse/parseutil/parseutil.go
@@ -6,9 +6,6 @@
 package parseutil
 
 import (
-	"bytes"
-	"fmt"
-
 	"ariga.io/atlas/sql/migrate"
 	"ariga.io/atlas/sql/schema"
 
@@ -66,7 +63,7 @@ func RenameTable(changes schema.Changes, r *Rename) schema.Changes {
 
 // MatchStmtBefore reports if the file contains any statement that matches the predicate before the given position.
 func MatchStmtBefore(f migrate.File, pos int, p func(*migrate.Stmt) (bool, error)) (bool, error) {
-	stmts, err := StmtDecls(f)
+	stmts, err := f.StmtDecls()
 	if err != nil {
 		return false, err
 	}
@@ -88,37 +85,6 @@ func MatchStmtBefore(f migrate.File, pos int, p func(*migrate.Stmt) (bool, error
 	return false, nil
 }
 
-// StmtDecls returns the statement declarations of a file.
-func StmtDecls(f migrate.File) ([]*migrate.Stmt, error) {
-	if s, ok := f.(interface {
-		StmtDecls() ([]*migrate.Stmt, error)
-	}); ok {
-		return s.StmtDecls()
-	}
-	s1, err := f.Stmts()
-	if err != nil {
-		return nil, err
-	}
-	s2 := make([]*migrate.Stmt, len(s1))
-	for i := range s1 {
-		p, err := pos(f, s1[i])
-		if err != nil {
-			return nil, err
-		}
-		s2[i] = &migrate.Stmt{Pos: p, Text: s1[i]}
-	}
-	return s2, nil
-}
-
-// pos returns the position of a statement in migration file.
-func pos(f migrate.File, stmt string) (int, error) {
-	i := bytes.Index(f.Bytes(), []byte(stmt))
-	if i == -1 {
-		return 0, fmt.Errorf("statement %q was not found in %q", stmt, f.Bytes())
-	}
-	return i, nil
-}
-
 func max(i, j int) int {
 	if i > j {
 		return i
diff --git a/doc/md/atlas-schema/sql.mdx b/doc/md/atlas-schema/sql.mdx
index 3c85d3ee..b52819e7 100644
--- a/doc/md/atlas-schema/sql.mdx
+++ b/doc/md/atlas-schema/sql.mdx
@@ -9,7 +9,7 @@ import TabItem from '@theme/TabItem';
 
 The [sqlspec](https://pkg.go.dev/ariga.io/atlas@master/sql/sqlspec) package defines
 the resource types used to describe an SQL database schema. Used with the
-[Atlas HCL](/knowledge/ddl.md#hcl) syntax, it is easy to compose documents describing the desired
+[Atlas HCL](/guides/ddl.md#hcl) syntax, it is easy to compose documents describing the desired
 state of a SQL database.
 
 ## Schema
@@ -165,7 +165,7 @@ table "metrics" {
 
 In some cases, an Atlas DDL document may contain multiple tables of the same name. This usually happens
 when the same table name appears in two different schemas. In these cases, the table names must be
-disambiguated by using resource [qualifers](/knowledge/ddl.md#qualifiers). The following document describes a
+disambiguated by using resource [qualifers](/guides/ddl.md#qualifiers). The following document describes a
 database that contains two schemas named `a` and `b`, and both of them contain a table named `users`.
 
 ```hcl
diff --git a/doc/md/cloud/getting-started.md b/doc/md/cloud/getting-started.md
new file mode 100644
index 00000000..dc892847
--- /dev/null
+++ b/doc/md/cloud/getting-started.md
@@ -0,0 +1,281 @@
+---
+id: getting-started
+title: Getting Started with Ariga Cloud
+sidebar_label: Getting Started
+---
+import Tabs from '@theme/Tabs';
+import TabItem from '@theme/TabItem';
+import Discord from '../../website/src/assets/icons/discord-white.svg'
+
+[Ariga Cloud](https://ariga.cloud) is an online platform that allows developers to view their Atlas projects and track
+their [Atlas GitHub Action](/integrations/github-actions) CI runs. The cloud platform gives full visibility into your
+Atlas schema, by displaying an entity relation diagram (ERD) that shows the schema changes, as well as the entire
+database schema.
+
+### Signing Up
+
+![Login](https://release.ariga.io/images/assets/login1.png)
+1. To get started with [Ariga Cloud](https://ariga.cloud/), create an account by clicking 'sign up' on the homepage.
+2. In the sign up screen, enter your work email and choose a password.
+3. Next, you will receive an email to verify your account. From your email, [Ariga Cloud](https://ariga.cloud/)
+will open in a new tab, and you will need to sign in to access your account.
+4. Once signed in, you will be prompted to create an organization. After creating the organization, you will be able
+to invite team members to join. Choose a meaningful name for the organization, as it will also be your subdomain.
+For example, "Acme Corp" will be available at "acme-corp.ariga.cloud".
+
+:::note
+Currently, the system only allows users to sign up for **one** organization. If you wish to create multiple
+organizations under the same user, you can create a [task-specific email address](https://support.google.com/a/users/answer/9308648?hl=en)
+(for Google users only). Create multiple emails that all link back to your regular address by adding a plus sign and
+any word before the @ sign in your address.
+:::
+
+### Connecting to the Atlas GitHub action
+At first you will notice that your projects and CI runs pages are empty. In order to connect the organization
+to your GitHub repository, you will need to setup the Atlas GitHub action on your repository by following these steps:
+
+:::note
+If you already have the Atlas GitHub action set up, you may skip step 4. In step 5, only add
+`ariga-token: ${{ secrets.ARIGA_TOKEN }}` to your yaml file.
+:::
+
+1. From the Settings page, generate an access token under 'Tokens'.
+2. On your GitHub repo, under the 'Settings' section, click on 'Secrets' > 'Actions' to create a new repository secret.
+3. Name your secret (for example, ARIGA_TOKEN) and paste the generated token from step 2.
+4. Install the Atlas GitHub Action by adding a file named `.github/workflows/atlas-ci.yaml` to your repo.
+5. Based on the type of database you are using, copy the following code into the workflow definition file. Set up
+the `ariga-token` input parameter to the secret name you chose in the previous step, and ensure your
+mainline branch and migration directory path are configured correctly:
+
+<Tabs
+defaultValue="mysql"
+values={[
+{label: 'MySQL', value: 'mysql'},
+{label: 'PostgreSQL', value: 'postgres'},
+{label: 'MariaDB', value: 'maria'},
+]}>
+<TabItem value="mysql">
+
+```yaml
+name: Atlas CI
+on:
+  # Run whenever code is changed in the master branch,
+  # change this to your root branch.
+  push:
+    branches:
+    // highlight-next-line
+      - master
+  # Run on PRs where something changed under the `path/to/migration/dir/` directory.
+  pull_request:
+    paths:
+    // highlight-next-line
+      - 'path/to/migration/dir/*'
+jobs:
+  lint:
+    services:
+      # Spin up a mysql:8.0.29 container to be used as the dev-database for analysis.
+      mysql:
+        image: mysql:8.0.29
+        env:
+          MYSQL_ROOT_PASSWORD: pass
+          MYSQL_DATABASE: test
+        ports:
+          - "3307:3306"
+        options: >-
+          --health-cmd "mysqladmin ping -ppass"
+          --health-interval 10s
+          --health-start-period 10s
+          --health-timeout 5s
+          --health-retries 10
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@v3.0.1
+        with:
+          fetch-depth: 0 # Mandatory unless "latest" is set below.
+      - uses: ariga/atlas-action@v0
+        with:
+        // highlight-next-line
+          dir: 'path/to/migrations'
+          dir-format: atlas # Or: golang-migrate, goose, dbmate, flyway, liquibase
+          dev-url: mysql://root:pass@localhost:3307/test
+          // highlight-next-line
+          ariga-token: ${{ secrets.ARIGA_TOKEN }}
+```
+
+
+</TabItem>
+<TabItem value="postgres">
+
+```yaml
+name: Atlas CI
+on:
+  # Run whenever code is changed in the master branch,
+  # change this to your root branch.
+  push:
+    branches:
+    // highlight-next-line
+      - master
+  # Run on PRs where something changed under the `path/to/migration/dir/` directory.
+  pull_request:
+    paths:
+    // highlight-next-line
+      - 'path/to/migration/dir/*'
+jobs:
+  lint:
+    services:
+      # Spin up a postgres:14 container to be used as the dev-database for analysis.
+      postgres14:
+        image: postgres:14
+        env:
+          POSTGRES_DB: test
+          POSTGRES_PASSWORD: pass
+        ports:
+          - 5430:5432
+        options: >-
+          --health-cmd pg_isready
+          --health-interval 10s
+          --health-timeout 5s
+          --health-retries 5
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@v3.0.1
+        with:
+          fetch-depth: 0 # Mandatory unless "latest" is set below.
+      - uses: ariga/atlas-action@v0
+        with:
+        // highlight-next-line
+          dir: 'path/to/migrations'
+          dir-format: atlas # Or: golang-migrate, goose, dbmate, flyway, liquibase
+          dev-url: postgres://postgres:pass@localhost:5430/test?sslmode=disable
+        // highlight-next-line
+          ariga-token: ${{ secrets.ARIGA_TOKEN }}
+```
+
+</TabItem>
+<TabItem value="maria">
+
+```yaml
+name: Atlas CI
+on:
+  # Run whenever code is changed in the master branch,
+  # change this to your root branch.
+  push:
+    branches:
+    // highlight-next-line
+      - master
+  # Run on PRs where something changed under the `path/to/migration/dir/` directory.
+  pull_request:
+    paths:
+    // highlight-next-line
+      - 'path/to/migration/dir/*'
+jobs:
+  lint:
+    services:
+      # Spin up a maria:10.7 container to be used as the dev-database for analysis.
+      maria107:
+        image: mariadb:10.7
+        env:
+          MYSQL_DATABASE: test
+          MYSQL_ROOT_PASSWORD: pass
+        ports:
+          - 4306:3306
+        options: >-
+          --health-cmd "mysqladmin ping -ppass"
+          --health-interval 10s
+          --health-start-period 10s
+          --health-timeout 5s
+          --health-retries 10
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@v3.0.1
+        with:
+          fetch-depth: 0 # Mandatory unless "latest" is set below.
+      - uses: ariga/atlas-action@v0
+        with:
+        // highlight-next-line
+          dir: 'path/to/migrations'
+          dir-format: atlas # Or: golang-migrate, goose, dbmate, flyway, liquibase
+          dev-url: maria://root:pass@localhost:4306/test
+        // highlight-next-line
+          ariga-token: ${{ secrets.ARIGA_TOKEN }}
+```
+</TabItem>
+
+</Tabs>
+
+6. After merging the workflow to your mainline branch, the workflow will be triggered.
+7. Refresh Ariga Cloud and your project will appear!
+
+![Setup Projects](https://release.ariga.io/images/assets/setup-projects.png)
+
+### Viewing CI Runs
+In the system, you can view all the CI runs that were triggered by the Atlas GitHub workflow.
+
+Each run includes the following:
+- A summary of the run.
+- SQL statements that were analyzed.
+- An ERD that shows the changes made to the schema, as well as a full view.
+
+A run can complete in one of three ways:
+
+<Tabs
+defaultValue="successful"
+values={[
+{label: 'Successful', value: 'successful'},
+{label: 'Issues Found', value: 'issues'},
+{label: 'Failed', value: 'failed'},
+]}>
+
+<TabItem value="successful">
+
+![Successful Run](https://release.ariga.io/images/assets/successful-run.png)
+
+The CI ran successfully and no errors or issues were found in your SQL statements or Atlas sum file.
+
+</TabItem>
+
+<TabItem value = "issues">
+
+![Issues Found Run](https://release.ariga.io/images/assets/issues-found.png)
+
+In cases where your SQL statements _might_ cause a failure in production, the CI run will be labeled as 'issues
+found'. In this example, we can see that the column `name` was created as non-nullable. The CI is letting us know that
+this has a chance of causing a failure, because if there is a row that exists in this table that has a null `name`
+value, this migration will for a fact fail in production.
+The report also makes sure to reference the specific data-dependent check that was found
+[MF103](https://atlasgo.io/lint/analyzers#MF103), in this example).
+</TabItem>
+
+<TabItem value = "failed">
+
+![Failed Run](https://release.ariga.io/images/assets/failed-run2.png)
+The CI run can fail for multiple reasons: incorrect SQL statements, wrong configuration, and more.
+In this example, we can see the CI has failed due to an SQL statement that attempts to drop a table. Because this
+is dangerous and will result in loss of data, the CI will automatically fail any `drop` statements.
+However, users can disable this by configuring the destructive analyzer in the
+[`atlas.hcl`](https://atlasgo.io/atlas-schema/projects) file:
+
+```hcl title="atlas.hcl"
+lint {
+  //highlight-start
+  destructive {
+    error = false
+  }
+  //highlight-end
+}
+```
+</TabItem>
+</Tabs>
+
+### Inviting Members
+Under 'Settings' > 'Members', you can invite team members to your organization.
+These members will receive an email with a link to Ariga Cloud, and will be required to sign up with the same email
+in order to access the organization.
+
+### Regenerating Tokens
+It is possible to regenerate the access token, however once you do so the old token will be **invalidated**.
+When choosing to regenerate the token, you must remember to copy the new one into your GitHub project's 'Secrets'.
+
+:::info
+For more help, reach out to us on our [Discord server](https://discord.gg/zZ6sWVg6NT).
+:::
diff --git a/doc/md/knowledge/ddl.md b/doc/md/guides/ddl.md
similarity index 99%
rename from doc/md/knowledge/ddl.md
rename to doc/md/guides/ddl.md
index 37667a20..a43a6ecf 100644
--- a/doc/md/knowledge/ddl.md
+++ b/doc/md/guides/ddl.md
@@ -1,6 +1,6 @@
 ---
 id: ddl
-slug: /knowledge/ddl
+slug: /guides/ddl
 title: Data Definition Language
 ---
 
diff --git a/doc/md/knowledge/knowledge.md b/doc/md/guides/guides.md
similarity index 55%
rename from doc/md/knowledge/knowledge.md
rename to doc/md/guides/guides.md
index 63315797..b14bb6f2 100644
--- a/doc/md/knowledge/knowledge.md
+++ b/doc/md/guides/guides.md
@@ -1,12 +1,12 @@
 ---
-title: Knowledge Center
-id: knowledge
-slug: /knowledge
+title: Guides
+id: guides
+slug: /guides
 ---
 
-# Knowledge Center
+# Guides
 
-Welcome to the Atlas Knowledge Center.
+Welcome to the Atlas Guides.
 
 ## Databases
 
@@ -18,5 +18,4 @@ Welcome to the Atlas Knowledge Center.
 
 [Serial Type Columns](postgres/serial-columns.md)
 
-
-
+[Partial Indexes](postgres/partial-indexes.md)
\ No newline at end of file
diff --git a/doc/md/knowledge/mysql/generated-columns.md b/doc/md/guides/mysql/generated-columns.md
similarity index 99%
rename from doc/md/knowledge/mysql/generated-columns.md
rename to doc/md/guides/mysql/generated-columns.md
index c0c2e6b7..7b091129 100644
--- a/doc/md/knowledge/mysql/generated-columns.md
+++ b/doc/md/guides/mysql/generated-columns.md
@@ -1,7 +1,7 @@
 ---
 id: generated-columns
 title: Generated Columns in MySQL with Atlas
-slug: /knowledge/mysql/generated-columns
+slug: /guides/mysql/generated-columns
 ---
 
 **MySQL** is a popular open-source relational database. **Generated columns** are a feature of MySQL that allows you to
diff --git a/doc/md/guides/postgres/partial-indexes.md b/doc/md/guides/postgres/partial-indexes.md
new file mode 100644
index 00000000..fa570e8c
--- /dev/null
+++ b/doc/md/guides/postgres/partial-indexes.md
@@ -0,0 +1,307 @@
+---
+id: partial-indexes
+title: Partial Indexes in PostgreSQL
+slug: /guides/postgres/partial-indexes
+---
+
+### Overview of Partial Indexes
+
+#### What are Partial Indexes?
+
+With PostgreSQL, users may create _partial indexes_, which are types of indexes that exist on a subset of a table, rather than the entire table itself. If used correctly, partial indexes improve performance and reduce costs, all while minimizing the amount of storage space they take up on the disk.
+
+#### Why do we need them?
+
+Let's demonstrate a case where partial indexes may be useful by contrasting them with a non-partial index. ​​If you have many records in an indexed table, the number of records the index needs to track also grows. If the index grows in size, the disk space needed to store the index itself increases as well.
+In many tables, different records are not accessed with uniform frequency. A subset of a table's records might not be searched very frequently or not searched at all. Records take up precious space in your index whether they are queried or not, and are updated when a new entry is added to the field.
+
+Partial indexes come into the picture to filter unsearched values and give you, as an engineer, a tool to index only what's important.
+
+:::info
+You can learn more about partial indexes in PostgreSQL [here](https://www.postgresql.org/docs/current/indexes-partial.html)
+:::
+
+#### Advantages of using Partial Indexes
+In cases where we know ahead of time the access pattern to a table and can reduce the size of an index by making it partial:
+1. Response time for SELECT operations is improved because the database searches through a smaller index.
+2. On average, response time for UPDATE operations is also improved as the index is not going to get updated in all cases.
+3. Index is smaller in size and can fit into RAM more easily. s
+4. Less space is required to store the index on disk.
+
+#### Basic PostgreSQL syntax for using Partial Index
+
+```sql
+CREATE INDEX 
+    index_name
+ON 
+    table_name(column_list)
+WHERE 
+    condition;
+```
+
+#### Example of Non-partial Index vs Partial Index in PostgreSQL
+
+Let's see this in action by creating a table with the following command:
+
+```sql
+CREATE TABLE "vaccination_data" (
+  id SERIAL PRIMARY KEY,
+  country varchar(20),
+  title varchar(10),
+  names varchar(20),
+  vaccinated varchar(3)
+);
+```
+
+Here is how a portion of the table might look like after inserting values:
+
+```sql
+SELECT * FROM vaccination_data;
+```
+```console title="Output"
+ id  |      country       | title |    names    | vaccinated 
+-----+--------------------+-------+-------------+------------
+   1 | Poland             | Mr.   | Teagan      | No
+   2 | Ukraine            | Ms.   | Alden       | No
+   3 | Ukraine            | Mr.   | Ima         | No
+   4 | Colombia           | Mr.   | Lawrence    | Yes
+   5 | Turkey             | Mrs.  | Keegan      | No
+   6 | China              | Mrs.  | Kylan       | No
+   7 | Netherlands        | Dr.   | Howard      | No
+...
+ 289690 | Russian Federation | Mrs.  | Ray     | Yes
+ 289689 | Austria            | Dr.   | Lenore  | Yes
+ 289688 | Sweden             | Dr.   | Walker  | Yes
+ 289687 | Turkey             | Dr.   | Emerson | No
+ 289686 | Vietnam            | Dr.   | Addison | Yes
+
+(289686 rows)
+```
+
+In the following example, suppose we want a list of doctors from India that have taken the vaccine. If we want to use normal index, we can create it on the “vaccinated” column with the following command:
+
+```sql
+CREATE INDEX vaccinated_idx ON vaccination_data(vaccinated);
+```
+```console title="Output"
+CREATE INDEX
+Time: 333.891 ms
+```
+
+Now, let's check the performance of querying data of doctors from India that have taken the vaccine with the following command:
+
+```sql
+EXPLAIN ANALYZE
+SELECT  
+        *
+FROM    
+        vaccination_data
+WHERE
+        vaccinated = 'Yes' AND country = 'India' AND title = 'Dr.';
+```
+```console title="Output"
+QUERY PLAN                                                           
+---------------------------------------------------------------------------
+ Bitmap Heap Scan on vaccination_data  (cost=758.64..4053.40 rows=699 width=25) (actual time=4.142..16.212 rows=582 loops=1)
+   Recheck Cond: ((vaccinated)::text = 'Yes'::text)
+   Filter: (((country)::text = 'India'::text) AND ((title)::text = 'Dr.'::text))
+   Rows Removed by Filter: 69334
+   Heap Blocks: exact=1337
+   ->  Bitmap Index Scan on vaccinated_idx  (cost=0.00..758.46 rows=69072 width=0) (actual time=3.940
+..3.941 rows=69916 loops=1)
+         Index Cond: ((vaccinated)::text = 'Yes'::text)
+ Planning Time: 0.188 ms
+ Execution Time: 16.292 ms
+(9 rows)
+```
+
+:::info
+The EXPLAIN command is used for understanding the performance of a query. You can learn more about usage of EXPLAIN command with ANALYZE option [here](https://www.postgresql.org/docs/14/using-explain.html#USING-EXPLAIN-ANALYZE)
+:::
+
+Notice that total Execution Time is 16.292ms. Also, let's check the index size with the following command:
+
+```sql
+SELECT pg_size_pretty(pg_relation_size('vaccinated_idx'));
+```
+```console title="Output"
+ pg_size_pretty 
+----------------
+ 1984 kB
+(1 row)
+```
+
+Now, suppose we want to accelerate the same query using the partial index. Let's begin by dropping the existing index that we created earlier:
+
+```sql
+DROP INDEX vaccinated_idx;
+```
+```console title="Output"
+DROP INDEX
+Time: 7.183 ms
+```
+
+In the following command, we have created an index with a WHERE clause that precisely describes list of doctors from India that have taken the vaccine.
+
+```sql
+CREATE INDEX 
+    vaccinated_idx
+ON 
+    vaccination_data(vaccinated)
+WHERE 
+    vaccinated = 'Yes' AND country = 'India' AND title = 'Dr.';
+```
+```console title="Output"
+CREATE INDEX
+Time: 94.567 ms
+```
+
+Notice that the partial index with the WHERE clause is created in 94.567ms, compared to the 333.891ms taken for the non-partial index on the 'vaccinated' column.
+Let's check the performance of querying list of doctors from India that have taken the vaccine again, using the following command:
+
+```sql
+EXPLAIN ANALYZE
+SELECT
+        *
+FROM    
+        vaccination_data
+WHERE
+        vaccinated = 'Yes' AND country = 'India' AND title = 'Dr.';
+```
+```console title="Output"
+QUERY PLAN                                                              
+---------------------------------------------------------------------------
+ Index Scan using vaccinated_idx on vaccination_data  (cost=0.15..1455.12 rows=699 width=25) (actual time=0.015..0.704 rows=582 loops=1)
+ Planning Time: 0.442 ms
+ Execution Time: 0.880 ms
+(3 rows)
+```
+
+Observe that total execution time has dropped significantly and is now only 0.880ms, compared to 16.292ms achieved by using a non-partial index on the 'vaccinated' column. Once again, let's check the index size with the following command:
+
+```sql
+SELECT pg_size_pretty(pg_relation_size('vaccinated_idx'));
+```
+```console title="Output"
+ pg_size_pretty 
+----------------
+ 16 kB
+(1 row)
+```
+
+As we can observe, the index size for the partial index takes significantly less space (16kb) compared to the non-partial index that we created earlier on the 'vaccinated' column (1984kb).
+
+Here is a summary from our tests:
+
+| Parameter                                    | Non-partial Index       | Partial Index           | Ratio of change(%) |
+|:-------------------------------------------- |:----------------------- |:----------------------- |:------------------ |
+| Estimated start-up cost                      | 758.64 arbitrary units  | 0.15 arbitrary units    | 99.9% reduced cost |
+| Estimated total cost                         | 4053.40 arbitrary units | 1455.12 arbitrary units | 64.1% reduced cost |
+| Time to create index                         | 333.891ms               | 94.567ms                | 71.6% less time    |
+| Execution time for query with “WHERE” clause | 16.292ms                | 0.880ms                 | 94.5% less time    |
+| Size of index                                | 1984kb                  | 16kb                    | 99.1% less space   |
+(Note: The results will vary, ​​depending on the data that is stored in the database)
+
+We have seen that creating a partial index is a better choice where only a small subset of the values stored in the database are accessed frequently. Now, let's see how we can easily manage partial indexes using Atlas.
+
+### Managing Partial Indexes is easy with Atlas
+
+Managing partial indexes and database schemas in PostgreSQL can be confusing and error-prone. Atlas is an open-source project which allows us to manage our database using a simple and easy-to-understand declarative syntax (similar to Terraform). We will now learn how to manage partial indexes using Atlas.
+
+:::info
+If you are just getting started, install the latest version of Atlas using the guide to [setting up Atlas](https://atlasgo.io/cli/getting-started/setting-up).
+:::
+
+#### Managing Partial Index in Atlas
+
+We will first use the `atlas schema inspect` command to get an HCL representation of the table which we created earlier by using the Atlas CLI:
+
+```console
+atlas schema inspect -u "postgres://postgres:mysecretpassword@localhost:5432/vaccination_data?sslmode=disable" > schema.hcl
+```
+```hcl title="schema.hcl"
+table "vaccination_data" {
+  schema = schema.public
+  column "id" {
+    null = false
+    type = serial
+  }
+  column "country" {
+    null    = true
+    type    = character_varying(20)
+  }
+  column "title" {
+    null    = true
+    type    = character_varying(10)
+  }
+  column "names" {
+    null    = true
+    type    = character_varying(20)
+  }
+  column "vaccinated" {
+    null    = true
+    type    = character_varying(3)
+  }
+  primary_key {
+    columns = [column.id]
+  }
+}
+schema "public" {
+}
+```
+
+Now, lets add the following index definition to the file:
+
+```hcl
+  index "vaccinated_idx" {
+    columns = [column.vaccinated]
+    where   = "(vaccinated::text = 'Yes'::text AND country::text = 'India'::text AND title::text = 'Dr.'::text)"
+  }
+```
+
+Save and apply the schema changes on the database by using the following command:
+
+```console
+atlas schema apply -u "postgres://postgres:mysecretpassword@localhost:5432/vaccination_data?sslmode=disable" -f schema.hcl
+```
+
+Atlas generates the necessary SQL statements to add the new partial index to the database schema. Press Enter while the `Apply` option is highlighted to apply the changes:
+
+```console
+-- Planned Changes:
+-- Create index "vaccinated_idx" to table: "vaccination_data"
+CREATE INDEX "vaccinated_idx" ON "public"."vaccination_data" ("vaccinated") WHERE (vaccinated::text = 'Yes'::text AND country::text = 'India'::text AND title::text = 'Dr.'::text)
+✔ Apply
+  Abort
+```
+
+To verify that our new index was created, open the database command line tool from previous step and run:
+
+```sql
+SELECT
+    indexname,
+    indexdef
+FROM
+    pg_indexes
+WHERE
+    tablename = 'vaccination_data';
+```
+```console title="Output"
+[ RECORD 1 ]
+indexname | vaccinated_idx
+indexdef  | CREATE INDEX vaccinated_idx ON public.vaccination_data USING btree (vaccinated) WHERE (((vaccinated)::text = 'Yes'::text) AND ((country)::text = 'India'::text) AND ((title)::text = 'Dr.'::text))
+```
+
+Amazing! Our new partial index is now created!
+
+### Limitation of using Partial Index
+
+Partial indexes are useful in cases where we know ahead of time that a table is most frequently queried with a certain `WHERE` clause.  As applications evolve, access patterns to the database also change. Consequently, we may find ourselves in a situation where our index no longer covers many queries, causing them to become resource consuming and slow.
+
+### Conclusion
+
+In this section, we learned about PostgreSQL partial indexes and how we can easily create partial indexes in our database by using Atlas.
+
+## Need More Help?​
+
+[Join the Ariga Discord Server](https://discord.gg/zZ6sWVg6NT) for early access to features and the ability to provide exclusive feedback that improves your Database Management Tooling.
\ No newline at end of file
diff --git a/doc/md/knowledge/postgres/serial-columns.md b/doc/md/guides/postgres/serial-columns.md
similarity index 99%
rename from doc/md/knowledge/postgres/serial-columns.md
rename to doc/md/guides/postgres/serial-columns.md
index f03c0fdf..24278b1f 100644
--- a/doc/md/knowledge/postgres/serial-columns.md
+++ b/doc/md/guides/postgres/serial-columns.md
@@ -1,7 +1,7 @@
 ---
 id: serial-columns
 title: Serial Type Columns in PostgreSQL
-slug: /knowledge/postgres/serial-columns
+slug: /guides/postgres/serial-columns
 ---
 
 PostgreSQL allows creating columns of types `smallserial`, `serial`, and `bigserial`. These types are not
diff --git a/doc/md/reference.md b/doc/md/reference.md
index 5617b108..d99e5afb 100644
--- a/doc/md/reference.md
+++ b/doc/md/reference.md
@@ -163,9 +163,31 @@ This command should be used whenever a manual change in the migration directory
 #### Example
 
 ```
-  atlas migrate hash --force
+  atlas migrate hash
 ```
 
+### atlas migrate import
+
+Import a migration directory from another migration management tool to the Atlas format.
+
+#### Usage
+```
+atlas migrate import [flags]
+```
+
+#### Example
+
+```
+  atlas migrate import --dir-format liquibase --from file:///path/to/source/directory --to file:///path/to/migration/directory
+```
+#### Flags
+```
+      --from string   select migration directory using URL format (default "file://migrations")
+      --to string     select migration directory using URL format (default "file://migrations")
+
+```
+
+
 ### atlas migrate lint
 
 Run analysis on the migration directory
diff --git a/doc/md/versioned/import.mdx b/doc/md/versioned/import.mdx
new file mode 100644
index 00000000..49996366
--- /dev/null
+++ b/doc/md/versioned/import.mdx
@@ -0,0 +1,188 @@
+---
+id: import
+slug: /versioned/import
+title: Migration Directory Import
+---
+
+import Tabs from '@theme/Tabs';
+import TabItem from '@theme/TabItem';
+
+Atlas supports the generation of [custom migration file formats](/versioned/diff#generate-migrations-with-custom-formats)
+for a variety of existing migration management tools, e.g. [Flyway](https://flywaydb.org/) or
+[golang-migrate/migrate](https://github.com/golang-migrate/migrate). But Atlas has its own format as well and provides
+a convenient command to import existing migration directories of supported tools into the Atlas format.
+
+### Flags
+When using `atlas migrate import` to import a migration directory, users must supply multiple parameters:
+* `--from` the [URL](/concepts/url) to the migration directory to import.
+* `--to` the URL of the migration directory to save imported migration files into, by default it is `file://migrations`.
+* `--dir-format` one of `[golang-migrate, goose, flyway, liquibase, dbmate]` to specifiy the migration file format of the
+migration directory to import.
+
+### Limitations
+
+Importing an existing migration directory has some limitations:
+
+#### Comments not directly preceding a SQL statement will get lost.
+
+<Tabs
+defaultValue="source"
+values={[
+{label: 'source.sql', value: 'source'},
+{label: 'imported.sql', value: 'target'},
+]}>
+<TabItem value="source">
+
+```sql
+-- This comment will get lost
+
+-- This will be preserved
+/*
+    This will be preserved as well
+/*
+CREATE TABLE `users` (
+    `id` bigint(20) NOT NULL AUTO_INCREMENT,
+    `age` bigint(20) NOT NULL,
+    `name` varchar(255) COLLATE utf8mb4_bin NOT NULL,
+    PRIMARY KEY (`id`),
+    UNIQUE KEY `age` (`age`)
+); -- This will get lost.
+```
+
+</TabItem>
+<TabItem value="target">
+
+```sql
+-- This will be preserved
+/*
+    This will be preserved as well
+/*
+CREATE TABLE `users` (
+    `id` bigint(20) NOT NULL AUTO_INCREMENT,
+    `age` bigint(20) NOT NULL,
+    `name` varchar(255) COLLATE utf8mb4_bin NOT NULL,
+    PRIMARY KEY (`id`),
+    UNIQUE KEY `age` (`age`)
+);
+```
+
+</TabItem>
+</Tabs>
+
+#### Rollback migrations will not get imported.
+
+Atlas does not have the concept of rollback migrations. Therefore migrations to undo an applied migration, often called
+"down" or "undo" migrations, will not be imported into the new migration directory. For migration formats having the rollback
+migration part of one file separated by some directive, the rollback parts are stripped away.
+
+<Tabs
+defaultValue="source"
+values={[
+{label: 'source.sql', value: 'source'},
+{label: 'imported.sql', value: 'target'},
+]}>
+<TabItem value="source">
+
+```sql
+-- +goose Up
+CREATE TABLE `users` (
+    `id` bigint(20) NOT NULL AUTO_INCREMENT,
+    `age` bigint(20) NOT NULL,
+    `name` varchar(255) COLLATE utf8mb4_bin NOT NULL,
+    PRIMARY KEY (`id`),
+    UNIQUE KEY `age` (`age`)
+);
+
+-- +goose Down
+DROP TABLE `users`;
+```
+
+</TabItem>
+<TabItem value="target">
+
+```sql
+CREATE TABLE `users` (
+    `id` bigint(20) NOT NULL AUTO_INCREMENT,
+    `age` bigint(20) NOT NULL,
+    `name` varchar(255) COLLATE utf8mb4_bin NOT NULL,
+    PRIMARY KEY (`id`),
+    UNIQUE KEY `age` (`age`)
+);
+```
+
+</TabItem>
+</Tabs>
+
+#### Repeatable Migrations
+
+Flyway has the concept of repeatable migrations, however, Atlas does not. In Flyway repeatable migrations are run last,
+if their contents did change. Atlas tries to reproduce this behavior by creating versioned migrations out of each
+repeatable migration file found and giving them the character `R` as version suffix.
+
+<Tabs
+defaultValue="users"
+values={[
+{label: 'V1__users.sql', value: 'users'},
+{label: 'R__users_view.sql', value: 'users_view'},
+{label: '1_users.sql', value: 'users_imported'},
+{label: '1R_users_view.sql', value: 'users_view_imported'},
+]}>
+<TabItem value="users">
+
+```sql
+CREATE TABLE `users` (
+    `id` bigint(20) NOT NULL AUTO_INCREMENT,
+    `age` bigint(20) NOT NULL,
+    `name` varchar(255) COLLATE utf8mb4_bin NOT NULL,
+    PRIMARY KEY (`id`),
+    UNIQUE KEY `age` (`age`)
+);
+```
+
+</TabItem>
+<TabItem value="users_view">
+
+```sql
+CREATE VIEW `users_over_30` AS SELECT * FROM `users` where `age` > 30;
+```
+
+</TabItem>
+<TabItem value="users_imported">
+
+```sql
+CREATE TABLE `users` (
+    `id` bigint(20) NOT NULL AUTO_INCREMENT,
+    `age` bigint(20) NOT NULL,
+    `name` varchar(255) COLLATE utf8mb4_bin NOT NULL,
+    PRIMARY KEY (`id`),
+    UNIQUE KEY `age` (`age`)
+);
+```
+
+</TabItem>
+<TabItem value="users_view_imported">
+
+```sql
+CREATE VIEW `users_over_30` AS SELECT * FROM `users` where `age` > 30;
+```
+
+</TabItem>
+</Tabs>
+
+### Examples
+
+Import existing `golang-migrate/migrate` migration directory:
+```shell
+atlas migrate import \
+  --from "file://migrations" \
+  --to "file://atlas-migrations" \
+  -- dir-format "golang-migrate"
+```
+
+Import existing Flyway migration directory:
+```shell
+atlas migrate import \
+  --from "file://migrations" \
+  --to "file://atlas-migrations" \
+  -- dir-format "flyway"
+```
diff --git a/doc/md/versioned/new.mdx b/doc/md/versioned/new.mdx
index 83a6f722..419766b2 100644
--- a/doc/md/versioned/new.mdx
+++ b/doc/md/versioned/new.mdx
@@ -169,7 +169,7 @@ ensure the integrity of the migration directory and force developers to deal wit
 situations where migration order or contents was modified after the fact. 
 
 After manually editing the contents of a newly created migration file, the checksums for
-the directory must be recalculated. This can be done by running `atlas migrate hash --force`
+the directory must be recalculated. This can be done by running `atlas migrate hash`
 command.
 
 ### Examples
diff --git a/doc/website/docusaurus.config.js b/doc/website/docusaurus.config.js
index d31d2bbe..0842d6a2 100644
--- a/doc/website/docusaurus.config.js
+++ b/doc/website/docusaurus.config.js
@@ -59,8 +59,8 @@ module.exports = {
           position: 'left',
         },
         {
-          to: 'knowledge',
-          label: 'Knowledge Center',
+          to: 'guides',
+          label: 'Guides',
           position: 'left',
         },
         {
@@ -77,10 +77,10 @@ module.exports = {
           "title": "Docs",
           "items": [
             {"label": "Getting Started", "to": "getting-started"},
-            {"label": "Data Definition Language ", "to": "knowledge/ddl"},
+            {"label": "Data Definition Language ", "to": "guides/ddl"},
             {"label": "CLI Reference", "to": "cli-reference"},
             {"label": "Blog", "to": "blog"},
-            {"label": "Knowledge Center", "to": "knowledge"},
+            {"label": "Guides", "to": "guides"},
             {"label": "About", "to": "about"},
             {"label": "GoDoc", "to": "https://pkg.go.dev/ariga.io/atlas"},
           ]
@@ -155,7 +155,7 @@ module.exports = {
             from: '/dev-database',
           },
           {
-            to: '/knowledge/ddl',
+            to: '/guides/ddl',
             from: ['/ddl/intro', '/concepts/ddl'],
           },
           {
@@ -174,6 +174,26 @@ module.exports = {
             to: '/atlas-schema/sql-resources',
             from: '/ddl/sql',
           },
+          {
+            to: '/guides',
+            from: '/knowledge',
+          },
+          {
+            to: '/guides/mysql/generated-columns',
+            from: '/knowledge/mysql/generated-columns',
+          },
+          {
+            to: '/guides/postgres/partial-indexes',
+            from: '/knowledge/postgres/partial-indexes',
+          },
+          {
+            to: '/guides/postgres/serial-columns',
+            from: '/knowledge/postgres/serial-columns',
+          },
+          {
+            to: '/guides/ddl',
+            from: '/knowledge/ddl',
+          },          
         ],
       },
     ],
diff --git a/doc/website/sidebars.js b/doc/website/sidebars.js
index fc308b56..75683a5e 100644
--- a/doc/website/sidebars.js
+++ b/doc/website/sidebars.js
@@ -38,6 +38,7 @@ module.exports = {
                 {type: 'doc', id: 'versioned/lint', label: 'Migration Linting'},
                 {type: 'doc', id: 'versioned/new', label: 'Manual Migrations'},
                 {type: 'doc', id: 'versioned/apply', label: 'Migration Applying'},
+                {type: 'doc', id: 'versioned/import', label: 'Migration Import'},
             ]
         },
         {
@@ -62,6 +63,14 @@ module.exports = {
                 {type: 'doc', id: 'concepts/migration-directory-integrity', label: 'Directory Integrity'},
             ],
         },
+        {
+            type: 'category',
+            label: 'Cloud',
+            collapsed: false,
+            items: [
+                {type: 'doc', id: 'cloud/getting-started', label: 'Getting Started'},
+            ],
+        },
         {
             type: 'category',
             label: 'Integrations',
@@ -81,10 +90,10 @@ module.exports = {
             id: 'cli-reference'
         }
     ],
-    knowledge: [
+    guides: [
         {
             type: 'doc',
-            id: 'knowledge/knowledge'
+            id: 'guides/guides'
         },
         {
             type: 'category',
@@ -93,7 +102,7 @@ module.exports = {
             items: [
                 {
                   type: 'doc',
-                  id: 'knowledge/mysql/generated-columns',
+                  id: 'guides/mysql/generated-columns',
                   label: 'Generated Columns'
                 },
             ],
@@ -105,14 +114,19 @@ module.exports = {
             items: [
                 {
                     type: 'doc',
-                    id: 'knowledge/postgres/serial-columns',
+                    id: 'guides/postgres/serial-columns',
                     label: 'Serial Type Columns'
                 },
+                {
+                    type: 'doc',
+                    id: 'guides/postgres/partial-indexes',
+                    label: 'Partial Indexes'
+                },
             ],
         },
         {
             type: 'doc',
-            id: 'knowledge/ddl'
+            id: 'guides/ddl'
         },
     ],
     about: [
diff --git a/doc/website/src/pages/index.js b/doc/website/src/pages/index.js
index a933ac0c..4f8914c7 100644
--- a/doc/website/src/pages/index.js
+++ b/doc/website/src/pages/index.js
@@ -16,8 +16,8 @@ function Nav() {
             </Link>
         </li>
         <li className={index.linkItem}>
-            <Link to="/knowledge">
-                Knowledge Center
+            <Link to="/guides">
+                Guides
             </Link>
         </li>
         <li className={index.linkItem}>
diff --git a/sql/migrate/dir.go b/sql/migrate/dir.go
index 42356c15..34bf3859 100644
--- a/sql/migrate/dir.go
+++ b/sql/migrate/dir.go
@@ -54,6 +54,8 @@ type (
 		Bytes() []byte
 		// Stmts returns the set of SQL statements this file holds.
 		Stmts() ([]string, error)
+		// StmtDecls returns the set of SQL statements this file holds alongside its preceding comments.
+		StmtDecls() ([]*Stmt, error)
 	}
 )
 
@@ -173,7 +175,7 @@ func (f LocalFile) Version() string {
 
 // Stmts returns the SQL statement exists in the local file.
 func (f LocalFile) Stmts() ([]string, error) {
-	s, err := stmts(string(f.b))
+	s, err := Stmts(string(f.b))
 	if err != nil {
 		return nil, err
 	}
@@ -184,10 +186,9 @@ func (f LocalFile) Stmts() ([]string, error) {
 	return stmts, nil
 }
 
-// StmtDecls returns the all statement declarations exist
-// in the local file.
+// StmtDecls returns the all statement declarations exist in the local file.
 func (f LocalFile) StmtDecls() ([]*Stmt, error) {
-	return stmts(string(f.b))
+	return Stmts(string(f.b))
 }
 
 // Bytes returns local file data.
@@ -203,9 +204,9 @@ var (
 		templates: []struct{ N, C *template.Template }{
 			{
 				N: template.Must(template.New("").Funcs(templateFuncs).Parse(
-					"{{ now }}{{ with .Name }}_{{ . }}{{ end }}.sql",
+					"{{ with .Version }}{{ . }}{{ else }}{{ now }}{{ end }}{{ with .Name }}_{{ . }}{{ end }}.sql",
 				)),
-				C: template.Must(template.New("").Funcs(templateFuncs).Parse(
+				C: template.Must(template.New("").Parse(
 					`{{ range .Changes }}{{ with .Comment }}-- {{ println . }}{{ end }}{{ printf "%s;\n" .Cmd }}{{ end }}`,
 				)),
 			},
diff --git a/sql/migrate/lex.go b/sql/migrate/lex.go
index b9c8877e..539b9421 100644
--- a/sql/migrate/lex.go
+++ b/sql/migrate/lex.go
@@ -41,9 +41,8 @@ func (s *Stmt) Directive(name string) (ds []string) {
 	return
 }
 
-// stmts provides a generic implementation for extracting
-// SQL statements from the given file contents.
-func stmts(input string) ([]*Stmt, error) {
+// Stmts provides a generic implementation for extracting SQL statements from the given file contents.
+func Stmts(input string) ([]*Stmt, error) {
 	var stmts []*Stmt
 	l, err := newLex(input)
 	if err != nil {
@@ -84,7 +83,7 @@ func newLex(input string) (*lex, error) {
 		}
 		parts := strings.SplitN(input, "\n", 2)
 		if len(parts) == 1 {
-			return nil, fmt.Errorf("not input found after delimiter %q", d)
+			return nil, fmt.Errorf("no input found after delimiter %q", d)
 		}
 		l.input = parts[1]
 	}
diff --git a/sql/migrate/migrate.go b/sql/migrate/migrate.go
index 3d279490..d2198c61 100644
--- a/sql/migrate/migrate.go
+++ b/sql/migrate/migrate.go
@@ -22,8 +22,8 @@ type (
 	// drivers to indicate if the changeset is transactional (can be rolled-back) and
 	// reversible (a down file can be generated to it).
 	Plan struct {
-		// Name of the plan. Provided by the user or auto-generated.
-		Name string
+		// Version and Name of the plan. Provided by the user or auto-generated.
+		Version, Name string
 
 		// Reversible describes if the changeset is reversible.
 		Reversible bool
diff --git a/sql/mysql/driver.go b/sql/mysql/driver.go
index 53d50316..27303eae 100644
--- a/sql/mysql/driver.go
+++ b/sql/mysql/driver.go
@@ -169,7 +169,7 @@ func (d *Driver) CheckClean(ctx context.Context, revT *migrate.TableIdent) error
 		return err
 	}
 	if s != nil {
-		if len(s.Tables) == 0 || ((revT.Schema == "" || s.Name == revT.Schema) && len(s.Tables) == 1 && s.Tables[0].Name == revT.Name) {
+		if len(s.Tables) == 0 || (revT != nil && (revT.Schema == "" || s.Name == revT.Schema) && len(s.Tables) == 1 && s.Tables[0].Name == revT.Name) {
 			return nil
 		}
 		return &migrate.NotCleanError{Reason: fmt.Sprintf("found table %q in schema %q", s.Tables[0].Name, s.Name)}
diff --git a/sql/postgres/driver.go b/sql/postgres/driver.go
index 2343e377..61d41192 100644
--- a/sql/postgres/driver.go
+++ b/sql/postgres/driver.go
@@ -226,7 +226,7 @@ func (d *Driver) CheckClean(ctx context.Context, revT *migrate.TableIdent) error
 		switch s, err := d.InspectSchema(ctx, d.schema, nil); {
 		case err != nil:
 			return err
-		case len(s.Tables) == 0, (revT.Schema == "" || s.Name == revT.Schema) && len(s.Tables) == 1 && s.Tables[0].Name == revT.Name:
+		case len(s.Tables) == 0, (revT != nil && revT.Schema == "" || s.Name == revT.Schema) && len(s.Tables) == 1 && s.Tables[0].Name == revT.Name:
 			return nil
 		default:
 			return &migrate.NotCleanError{Reason: fmt.Sprintf("found table %q in schema %q", s.Tables[0].Name, s.Name)}
diff --git a/sql/postgres/inspect.go b/sql/postgres/inspect.go
index 4585ecfb..146d5402 100644
--- a/sql/postgres/inspect.go
+++ b/sql/postgres/inspect.go
@@ -772,6 +772,13 @@ type (
 		Columns []*schema.Column
 	}
 
+	// Concurrently describes the CONCURRENTLY clause to instruct Postgres
+	// to build or drop the index concurrently without blocking the current table.
+	// https://www.postgresql.org/docs/current/sql-createindex.html#SQL-CREATEINDEX-CONCURRENTLY
+	Concurrently struct {
+		schema.Attr
+	}
+
 	// NoInherit attribute defines the NO INHERIT flag for CHECK constraint.
 	// https://postgresql.org/docs/current/catalog-pg-constraint.html
 	NoInherit struct {
diff --git a/sql/postgres/migrate.go b/sql/postgres/migrate.go
index 8739a68f..17c2dfe8 100644
--- a/sql/postgres/migrate.go
+++ b/sql/postgres/migrate.go
@@ -787,6 +787,9 @@ func (s *state) addIndexes(t *schema.Table, indexes ...*schema.Index) {
 			b.P("UNIQUE")
 		}
 		b.P("INDEX")
+		if c := (Concurrently{}); sqlx.Has(idx.Attrs, &c) {
+			b.P("CONCURRENTLY")
+		}
 		if idx.Name != "" {
 			b.Ident(idx.Name)
 		}
@@ -797,6 +800,9 @@ func (s *state) addIndexes(t *schema.Table, indexes ...*schema.Index) {
 			Comment: fmt.Sprintf("create index %q to table: %q", idx.Name, t.Name),
 			Reverse: func() string {
 				b := s.Build("DROP INDEX")
+				if c := (Concurrently{}); sqlx.Has(idx.Attrs, &c) {
+					b.P("CONCURRENTLY")
+				}
 				// Unlike MySQL, the DROP command is not attached to ALTER TABLE.
 				// Therefore, we print indexes with their qualified name, because
 				// the connection that executes the statements may not be attached
@@ -949,7 +955,7 @@ func (s *state) index(b *sqlx.Builder, idx *schema.Index) {
 	}
 	for _, attr := range idx.Attrs {
 		switch attr.(type) {
-		case *schema.Comment, *ConType, *IndexType, *IndexInclude, *IndexPredicate, *IndexStorageParams:
+		case *schema.Comment, *ConType, *IndexType, *IndexInclude, *Concurrently, *IndexPredicate, *IndexStorageParams:
 		default:
 			panic(fmt.Sprintf("unexpected index attribute: %T", attr))
 		}
diff --git a/sql/sqlite/driver.go b/sql/sqlite/driver.go
index dfea83bb..06844b1f 100644
--- a/sql/sqlite/driver.go
+++ b/sql/sqlite/driver.go
@@ -125,7 +125,7 @@ func (d *Driver) CheckClean(ctx context.Context, revT *migrate.TableIdent) error
 		return migrate.NotCleanError{Reason: fmt.Sprintf("found schema %q", r.Schemas[0].Name)}
 	case n == 1 && len(r.Schemas[0].Tables) > 1:
 		return migrate.NotCleanError{Reason: fmt.Sprintf("found multiple tables: %d", len(r.Schemas[0].Tables))}
-	case n == 1 && len(r.Schemas[0].Tables) == 1 && r.Schemas[0].Tables[0].Name != revT.Name:
+	case n == 1 && len(r.Schemas[0].Tables) == 1 && (revT == nil || r.Schemas[0].Tables[0].Name != revT.Name):
 		return migrate.NotCleanError{Reason: fmt.Sprintf("found table %q", r.Schemas[0].Tables[0].Name)}
 	}
 	return nil
diff --git a/sql/sqltool/tool.go b/sql/sqltool/tool.go
index 186e42c8..d6a5b321 100644
--- a/sql/sqltool/tool.go
+++ b/sql/sqltool/tool.go
@@ -15,6 +15,7 @@ import (
 	"strings"
 	"text/template"
 	"time"
+	"unicode"
 
 	"ariga.io/atlas/sql/migrate"
 )
@@ -128,7 +129,7 @@ func NewGooseDir(path string) (*GooseDir, error) {
 	return &GooseDir{dir}, nil
 }
 
-// Files implements Scanner.Files. It looks for all files with up.sql suffix and orders them by filename.
+// Files looks for all files with .sql suffix and orders them by filename.
 func (d *GooseDir) Files() ([]migrate.File, error) {
 	files, err := d.LocalDir.Files()
 	if err != nil {
@@ -140,76 +141,80 @@ func (d *GooseDir) Files() ([]migrate.File, error) {
 	return files, nil
 }
 
-// Stmts implements Scanner.Stmts. It understands the migration format used by pressly/goose sql migration files.
-func (f *GooseFile) Stmts() ([]string, error) {
+// StmtDecls understands the migration format used by pressly/goose sql migration files.
+func (f *GooseFile) StmtDecls() ([]*migrate.Stmt, error) {
+	// Atlas custom delimiter is per file, goose has pragma do mark start and end of a delimiter.
+	// In order to use the Atlas lexer, we define a custom delimiter for the source SQL and edit it to use the
+	// custom delimiter.
+	const delim = "-- ATLAS_DELIM_END"
 	var (
-		state, line int
-		stmts       []string
-		buf         strings.Builder
-		sc          = bufio.NewScanner(bytes.NewReader(f.Bytes()))
+		state, lineCount int
+		lines            = []string{"-- atlas:delimiter " + delim, ""}
+		sc               = bufio.NewScanner(bytes.NewReader(f.Bytes()))
 	)
+Scan:
 	for sc.Scan() {
-		line++
-		s := sc.Text()
-		// Handle pragmas.
-		if strings.HasPrefix(s, goosePragma) {
-			switch strings.TrimSpace(strings.TrimPrefix(s, goosePragma)) {
+		lineCount++
+		line := sc.Text()
+		// Handle goose custom delimiters.
+		if strings.HasPrefix(line, goosePragma) {
+			switch strings.TrimSpace(strings.TrimPrefix(line, goosePragma)) {
 			case "Up":
 				switch state {
 				case none: // found the "up" part of the file
 					state = up
 				default:
-					return nil, unexpectedPragmaErr(f, line, "Up")
+					return nil, unexpectedPragmaErr(f, lineCount, "Up")
 				}
 			case "Down":
 				switch state {
 				case up: // found the "down" part
-					if rest := strings.TrimSpace(buf.String()); len(rest) > 0 {
-						return nil, unexpectedPragmaErr(f, line, "Down")
-					}
-					return stmts, nil
+					break Scan
 				default:
-					return nil, unexpectedPragmaErr(f, line, "Down")
+					return nil, unexpectedPragmaErr(f, lineCount, "Down")
 				}
 			case "StatementBegin":
 				switch state {
 				case up:
 					state = begin // begin of a statement
 				default:
-					return nil, unexpectedPragmaErr(f, line, "StatementBegin")
+					return nil, unexpectedPragmaErr(f, lineCount, "StatementBegin")
 				}
 			case "StatementEnd":
 				switch state {
 				case begin:
 					state = end // end of a statement
 				default:
-					return nil, unexpectedPragmaErr(f, line, "StatementEnd")
+					return nil, unexpectedPragmaErr(f, lineCount, "StatementEnd")
 				}
 			}
 		}
 		// Write the line of the statement.
-		if !reGoosePragma.MatchString(s) {
-			if _, err := buf.WriteString(s + "\n"); err != nil {
-				return nil, err
+		if !reGoosePragma.MatchString(line) && state != end {
+			// end of statement if line ends with semicolon
+			line = strings.TrimRightFunc(line, unicode.IsSpace)
+			lines = append(lines, line)
+			if state == up && strings.HasSuffix(line, ";") && !strings.HasPrefix(line, "--") {
+				lines = append(lines, delim)
 			}
 		}
-		switch state {
-		case up: // end of statement if line ends with semicolon
-			if s := strings.TrimSpace(s); strings.HasSuffix(s, ";") && !strings.HasPrefix(s, "--") {
-				stmts = append(stmts, strings.TrimSpace(buf.String()))
-				buf.Reset()
-			}
-		case end: // end of statement marked by goosePragma
-			stmts = append(stmts, strings.TrimSpace(buf.String()))
-			buf.Reset()
-			state = up // back in up block
+		if state == end {
+			state = up
+			lines = append(lines, delim)
 		}
 	}
-	if err := sc.Err(); err != nil {
-		return nil, fmt.Errorf("sql/migrate: scanning migration %q: %w", f.Name(), err)
+	return migrate.Stmts(strings.Join(lines, "\n"))
+}
+
+// Stmts understands the migration format used by pressly/goose sql migration files.
+func (f *GooseFile) Stmts() ([]string, error) {
+	s, err := f.StmtDecls()
+	if err != nil {
+		return nil, err
 	}
-	if state == none {
-		return nil, fmt.Errorf("sql/migrate: empty migration %q", f.Name())
+	stmts := make([]string, len(s))
+	for i := range s {
+		stmts[i] = s[i].Text
 	}
 	return stmts, nil
 }
@@ -231,7 +236,7 @@ func NewDBMateDir(path string) (*DBMateDir, error) {
 	return &DBMateDir{dir}, nil
 }
 
-// Files implements Scanner.Files. It looks for all files with up.sql suffix and orders them by filename.
+// Files looks for all files with up.sql suffix and orders them by filename.
 func (d *DBMateDir) Files() ([]migrate.File, error) {
 	files, err := d.LocalDir.Files()
 	if err != nil {
@@ -243,55 +248,43 @@ func (d *DBMateDir) Files() ([]migrate.File, error) {
 	return files, nil
 }
 
-// Stmts implements Scanner.Stmts. It understands the migration format used by pressly/DBMate sql migration files.
-func (f *DBMateFile) Stmts() ([]string, error) {
+// StmtDecls understands the migration format used by amacneil/dbmate sql migration files.
+func (f *DBMateFile) StmtDecls() ([]*migrate.Stmt, error) {
 	var (
-		state, line int
-		stmts       []string
-		buf         strings.Builder
-		sc          = bufio.NewScanner(bytes.NewReader(f.Bytes()))
+		state, lineCount int
+		lines            []string
+		sc               = bufio.NewScanner(bytes.NewReader(f.Bytes()))
 	)
+Scan:
 	for sc.Scan() {
-		line++
-		s := sc.Text()
+		lineCount++
+		line := sc.Text()
 		// Handle pragmas.
-		if strings.HasPrefix(s, dbmatePragma) {
-			switch strings.TrimSpace(strings.TrimPrefix(s, dbmatePragma)) {
+		if strings.HasPrefix(line, dbmatePragma) {
+			switch strings.TrimSpace(strings.TrimPrefix(line, dbmatePragma)) {
 			case "up":
-				switch state {
-				case none: // found the "up" part of the file
-					state = up
-				default:
-					return nil, unexpectedPragmaErr(f, line, "up")
-				}
+				state = up
 			case "down":
-				switch state {
-				case up: // found the "down" part
-					if rest := strings.TrimSpace(buf.String()); len(rest) > 0 {
-						return nil, unexpectedPragmaErr(f, line, "down")
-					}
-					return stmts, nil
-				default:
-					return nil, unexpectedPragmaErr(f, line, "down")
-				}
+				break Scan
 			}
 		}
 		// Write the line of the statement.
-		if !reDBMatePragma.MatchString(s) {
-			if _, err := buf.WriteString(s + "\n"); err != nil {
-				return nil, err
-			}
-		}
-		if s := strings.TrimSpace(s); strings.HasSuffix(s, ";") && !strings.HasPrefix(s, "--") {
-			stmts = append(stmts, strings.TrimSpace(buf.String()))
-			buf.Reset()
+		if !reDBMatePragma.MatchString(line) && state == up {
+			lines = append(lines, line)
 		}
 	}
-	if err := sc.Err(); err != nil {
-		return nil, fmt.Errorf("sql/migrate: scanning migration %q: %w", f.Name(), err)
+	return migrate.Stmts(strings.Join(lines, "\n"))
+}
+
+// Stmts understands the migration format used by amacneil/dbmate sql migration files.
+func (f *DBMateFile) Stmts() ([]string, error) {
+	s, err := f.StmtDecls()
+	if err != nil {
+		return nil, err
 	}
-	if state == none {
-		return nil, fmt.Errorf("sql/migrate: empty migration %q", f.Name())
+	stmts := make([]string, len(s))
+	for i := range s {
+		stmts[i] = s[i].Text
 	}
 	return stmts, nil
 }
@@ -368,6 +361,43 @@ func (f FlywayFile) Version() string {
 	return flywayVersion(f.Name())
 }
 
+// SetRepeatableVersion iterates over the migration files and assigns repeatable migrations a version number since
+// Atlas does not have the concept of repeatable migrations. Each repeatable migration file gets assigned the version
+// of the preceding migration file (or 0) followed by an 'R'.
+func SetRepeatableVersion(ff []migrate.File) {
+	// First find the index of the first repeatable migration file (if any).
+	var (
+		v   string // last versioned migration version
+		idx = func() int {
+			for i, f := range ff {
+				if f.Version() == "" {
+					return i
+				}
+			}
+			return -1
+		}()
+	)
+	switch idx {
+	case -1:
+		// No repeatable migration does exist.
+		return
+	case 0:
+		// There is no preceding migration. Use Version "0".
+		v = "0"
+	default:
+		v = ff[idx-1].Version()
+	}
+	if v != "" {
+		// Every migration file following the first repeatable found are repeatable as well.
+		for i, f := range ff[idx:] {
+			ff[idx+i] = &FlywayFile{migrate.NewLocalFile(
+				fmt.Sprintf("V%sR__%s", v, f.Desc()),
+				f.Bytes(),
+			)}
+		}
+	}
+}
+
 // LiquibaseDir wraps migrate.LocalDir and provides a migrate.Scanner implementation able to understand files
 // generated by the LiquibaseFormatter for migration directory replaying.
 type LiquibaseDir struct{ *migrate.LocalDir }
@@ -382,7 +412,7 @@ func NewLiquibaseDir(path string) (*LiquibaseDir, error) {
 }
 
 const (
-	none int = iota // state when parsing goose/dbmate sql file
+	none int = iota
 	up
 	begin
 	end


Test patch:
diff --git a/cmd/atlas/internal/cmdapi/migrate_test.go b/cmd/atlas/internal/cmdapi/migrate_test.go
index f5d163d4..5dfccd55 100644
--- a/cmd/atlas/internal/cmdapi/migrate_test.go
+++ b/cmd/atlas/internal/cmdapi/migrate_test.go
@@ -9,7 +9,6 @@ import (
 	"errors"
 	"fmt"
 	"io"
-	"io/ioutil"
 	"net/url"
 	"os"
 	"os/exec"
@@ -36,6 +35,39 @@ func TestMigrate(t *testing.T) {
 	require.NoError(t, err)
 }
 
+func TestMigrate_Import(t *testing.T) {
+	for _, tool := range []string{"dbmate", "flyway", "golang-migrate", "goose", "liquibase"} {
+		p := t.TempDir()
+		t.Run(tool, func(t *testing.T) {
+			path := filepath.FromSlash("testdata/import/" + tool)
+			out, err := runCmd(
+				Root, "migrate", "import",
+				"--from", "file://"+path,
+				"--to", "file://"+p,
+				"--dir-format", tool,
+			)
+			require.NoError(t, err)
+			require.Zero(t, out)
+
+			path += "_gold"
+			ex, err := os.ReadDir(path)
+			require.NoError(t, err)
+			ac, err := os.ReadDir(p)
+			require.NoError(t, err)
+			require.Equal(t, len(ex)+1, len(ac)) // sum file
+
+			for i := range ex {
+				e, err := os.ReadFile(filepath.Join(path, ex[i].Name()))
+				require.NoError(t, err)
+				a, err := os.ReadFile(filepath.Join(p, ex[i].Name()))
+				require.NoError(t, err)
+
+				require.Equal(t, string(e), string(a))
+			}
+		})
+	}
+}
+
 func TestMigrate_Apply(t *testing.T) {
 	var (
 		p   = t.TempDir()
@@ -195,7 +227,7 @@ func TestMigrate_Apply(t *testing.T) {
 		require.NoError(t, os.RemoveAll("testdata/sqlite3"))
 	})
 	sed(t, "s/col_2/col_5/g", "testdata/sqlite3/20220318104615_second.sql")
-	_, err = runCmd(Root, "migrate", "hash", "--force", "--dir", "file://testdata/sqlite3")
+	_, err = runCmd(Root, "migrate", "hash", "--dir", "file://testdata/sqlite3")
 	require.NoError(t, err)
 	s, err = runCmd(
 		Root, "migrate", "apply",
@@ -207,7 +239,7 @@ func TestMigrate_Apply(t *testing.T) {
 	// Fixing the migration file will finish without errors.
 	sed(t, "s/col_5/col_2/g", "testdata/sqlite3/20220318104615_second.sql")
 	sed(t, "s/asdasd //g", "testdata/sqlite3/20220318104615_second.sql")
-	_, err = runCmd(Root, "migrate", "hash", "--force", "--dir", "file://testdata/sqlite3")
+	_, err = runCmd(Root, "migrate", "hash", "--dir", "file://testdata/sqlite3")
 	require.NoError(t, err)
 	s, err = runCmd(
 		Root, "migrate", "apply",
@@ -288,7 +320,6 @@ func TestMigrate_Diff(t *testing.T) {
 	to := hclURL(t)
 
 	// Will create migration directory if not existing.
-	MigrateFlags.Force = false
 	_, err := runCmd(
 		Root, "migrate", "diff",
 		"name",
@@ -432,7 +463,7 @@ func TestMigrate_Validate(t *testing.T) {
 	p := t.TempDir()
 	require.NoError(t, os.WriteFile(filepath.Join(p, "1_initial.sql"), []byte("create table t1 (c1 int)"), 0644))
 	require.NoError(t, os.WriteFile(filepath.Join(p, "2_second.sql"), []byte("create table t2 (c2 int)"), 0644))
-	_, err = runCmd(Root, "migrate", "hash", "--force", "--dir", "file://"+p)
+	_, err = runCmd(Root, "migrate", "hash", "--dir", "file://"+p)
 	require.NoError(t, err)
 	s, err = runCmd(
 		Root, "migrate", "validate",
@@ -452,15 +483,20 @@ func TestMigrate_Hash(t *testing.T) {
 	require.Zero(t, s)
 	require.NoError(t, err)
 
+	// Prints a warning if --force flag is still used.
+	s, err = runCmd(Root, "migrate", "hash", "--dir", "file://testdata/mysql", "--force")
+	require.NoError(t, err)
+	require.Equal(t, "Flag --force has been deprecated, you can safely omit it.\n", s)
+
 	p := t.TempDir()
 	err = copyFile(filepath.Join("testdata", "mysql", "20220318104614_initial.sql"), filepath.Join(p, "20220318104614_initial.sql"))
 	require.NoError(t, err)
 
-	s, err = runCmd(Root, "migrate", "hash", "--dir", "file://"+p, "--force")
+	s, err = runCmd(Root, "migrate", "hash", "--dir", "file://"+p)
 	require.Zero(t, s)
 	require.NoError(t, err)
 	require.FileExists(t, filepath.Join(p, "atlas.sum"))
-	d, err := ioutil.ReadFile(filepath.Join(p, "atlas.sum"))
+	d, err := os.ReadFile(filepath.Join(p, "atlas.sum"))
 	require.NoError(t, err)
 	dir, err := migrate.NewLocalDir(p)
 	require.NoError(t, err)
diff --git a/cmd/atlas/internal/cmdapi/testdata/import/dbmate/1_initial.sql b/cmd/atlas/internal/cmdapi/testdata/import/dbmate/1_initial.sql
new file mode 100644
index 00000000..c8d052f7
--- /dev/null
+++ b/cmd/atlas/internal/cmdapi/testdata/import/dbmate/1_initial.sql
@@ -0,0 +1,26 @@
+-- migrate:up
+CREATE TABLE post
+(
+    id    int NOT NULL,
+    title text,
+    body  text,
+    PRIMARY KEY (id)
+);
+
+/*
+ Multiline comment ...
+ */
+ALTER TABLE post ADD created_at TIMESTAMP NOT NULL;
+
+-- Normal comment
+-- With a second line
+INSERT INTO post (title) VALUES (
+'This is
+my multiline
+
+value');
+
+-- migrate:down
+
+
+DROP TABLE post;
\ No newline at end of file
diff --git a/cmd/atlas/internal/cmdapi/testdata/import/dbmate/2_second_migration.sql b/cmd/atlas/internal/cmdapi/testdata/import/dbmate/2_second_migration.sql
new file mode 100644
index 00000000..f9fa036e
--- /dev/null
+++ b/cmd/atlas/internal/cmdapi/testdata/import/dbmate/2_second_migration.sql
@@ -0,0 +1,9 @@
+
+
+
+-- migrate:up
+
+
+
+
+CREATE TABLE tbl_2 (col INT);
\ No newline at end of file
diff --git a/cmd/atlas/internal/cmdapi/testdata/import/dbmate_gold/1_initial.sql b/cmd/atlas/internal/cmdapi/testdata/import/dbmate_gold/1_initial.sql
new file mode 100644
index 00000000..dc708379
--- /dev/null
+++ b/cmd/atlas/internal/cmdapi/testdata/import/dbmate_gold/1_initial.sql
@@ -0,0 +1,18 @@
+CREATE TABLE post
+(
+    id    int NOT NULL,
+    title text,
+    body  text,
+    PRIMARY KEY (id)
+);
+/*
+ Multiline comment ...
+ */
+ALTER TABLE post ADD created_at TIMESTAMP NOT NULL;
+-- Normal comment
+-- With a second line
+INSERT INTO post (title) VALUES (
+'This is
+my multiline
+
+value');
diff --git a/cmd/atlas/internal/cmdapi/testdata/import/dbmate_gold/2_second_migration.sql b/cmd/atlas/internal/cmdapi/testdata/import/dbmate_gold/2_second_migration.sql
new file mode 100644
index 00000000..4f63b825
--- /dev/null
+++ b/cmd/atlas/internal/cmdapi/testdata/import/dbmate_gold/2_second_migration.sql
@@ -0,0 +1,1 @@
+CREATE TABLE tbl_2 (col INT);
diff --git a/cmd/atlas/internal/cmdapi/testdata/import/flyway/B2__baseline.sql b/cmd/atlas/internal/cmdapi/testdata/import/flyway/B2__baseline.sql
new file mode 100644
index 00000000..ea6e1080
--- /dev/null
+++ b/cmd/atlas/internal/cmdapi/testdata/import/flyway/B2__baseline.sql
@@ -0,0 +1,14 @@
+CREATE TABLE post
+(
+    id    int NOT NULL,
+    title text,
+    body  text,
+    created_at TIMESTAMP NOT NULL
+    PRIMARY KEY (id)
+);
+
+INSERT INTO post (title, created_at) VALUES (
+'This is
+my multiline
+
+value', NOW());
\ No newline at end of file
diff --git a/cmd/atlas/internal/cmdapi/testdata/import/flyway/R__views.sql b/cmd/atlas/internal/cmdapi/testdata/import/flyway/R__views.sql
new file mode 100644
index 00000000..7e166592
--- /dev/null
+++ b/cmd/atlas/internal/cmdapi/testdata/import/flyway/R__views.sql
@@ -0,0 +1,1 @@
+CREATE VIEW `my_view` AS SELECT * FROM `post`;
\ No newline at end of file
diff --git a/cmd/atlas/internal/cmdapi/testdata/import/flyway/U1__initial.sql b/cmd/atlas/internal/cmdapi/testdata/import/flyway/U1__initial.sql
new file mode 100644
index 00000000..1b52286d
--- /dev/null
+++ b/cmd/atlas/internal/cmdapi/testdata/import/flyway/U1__initial.sql
@@ -0,0 +1,1 @@
+DROP TABLE tbl;
\ No newline at end of file
diff --git a/cmd/atlas/internal/cmdapi/testdata/import/flyway/V1__initial.sql b/cmd/atlas/internal/cmdapi/testdata/import/flyway/V1__initial.sql
new file mode 100644
index 00000000..7ec5bf7c
--- /dev/null
+++ b/cmd/atlas/internal/cmdapi/testdata/import/flyway/V1__initial.sql
@@ -0,0 +1,16 @@
+-- comment
+CREATE TABLE post
+(
+    id    int NOT NULL,
+    title text,
+    body  text,
+    PRIMARY KEY (id)
+);
+
+ALTER TABLE post ADD created_at TIMESTAMP NOT NULL;
+
+INSERT INTO post (title, created_at) VALUES (
+'This is
+my multiline
+
+value', NOW());
diff --git a/cmd/atlas/internal/cmdapi/testdata/import/flyway/V2__second_migration.sql b/cmd/atlas/internal/cmdapi/testdata/import/flyway/V2__second_migration.sql
new file mode 100644
index 00000000..f9fa036e
--- /dev/null
+++ b/cmd/atlas/internal/cmdapi/testdata/import/flyway/V2__second_migration.sql
@@ -0,0 +1,9 @@
+
+
+
+-- migrate:up
+
+
+
+
+CREATE TABLE tbl_2 (col INT);
\ No newline at end of file
diff --git a/cmd/atlas/internal/cmdapi/testdata/import/flyway/V3__third_migration.sql b/cmd/atlas/internal/cmdapi/testdata/import/flyway/V3__third_migration.sql
new file mode 100644
index 00000000..040fa194
--- /dev/null
+++ b/cmd/atlas/internal/cmdapi/testdata/import/flyway/V3__third_migration.sql
@@ -0,0 +1,1 @@
+ALTER TABLE tbl_2 ADD col_1 INTEGER NOT NULL;
\ No newline at end of file
diff --git a/cmd/atlas/internal/cmdapi/testdata/import/flyway_gold/2_baseline.sql b/cmd/atlas/internal/cmdapi/testdata/import/flyway_gold/2_baseline.sql
new file mode 100644
index 00000000..cc17a444
--- /dev/null
+++ b/cmd/atlas/internal/cmdapi/testdata/import/flyway_gold/2_baseline.sql
@@ -0,0 +1,13 @@
+CREATE TABLE post
+(
+    id    int NOT NULL,
+    title text,
+    body  text,
+    created_at TIMESTAMP NOT NULL
+    PRIMARY KEY (id)
+);
+INSERT INTO post (title, created_at) VALUES (
+'This is
+my multiline
+
+value', NOW());
diff --git a/cmd/atlas/internal/cmdapi/testdata/import/flyway_gold/3R_views.sql b/cmd/atlas/internal/cmdapi/testdata/import/flyway_gold/3R_views.sql
new file mode 100644
index 00000000..4eb5f230
--- /dev/null
+++ b/cmd/atlas/internal/cmdapi/testdata/import/flyway_gold/3R_views.sql
@@ -0,0 +1,1 @@
+CREATE VIEW `my_view` AS SELECT * FROM `post`;
diff --git a/cmd/atlas/internal/cmdapi/testdata/import/flyway_gold/3_third_migration.sql b/cmd/atlas/internal/cmdapi/testdata/import/flyway_gold/3_third_migration.sql
new file mode 100644
index 00000000..f0e11b98
--- /dev/null
+++ b/cmd/atlas/internal/cmdapi/testdata/import/flyway_gold/3_third_migration.sql
@@ -0,0 +1,1 @@
+ALTER TABLE tbl_2 ADD col_1 INTEGER NOT NULL;
diff --git a/cmd/atlas/internal/cmdapi/testdata/import/golang-migrate/1_initial.down.sql b/cmd/atlas/internal/cmdapi/testdata/import/golang-migrate/1_initial.down.sql
new file mode 100644
index 00000000..1b52286d
--- /dev/null
+++ b/cmd/atlas/internal/cmdapi/testdata/import/golang-migrate/1_initial.down.sql
@@ -0,0 +1,1 @@
+DROP TABLE tbl;
\ No newline at end of file
diff --git a/cmd/atlas/internal/cmdapi/testdata/import/golang-migrate/1_initial.up.sql b/cmd/atlas/internal/cmdapi/testdata/import/golang-migrate/1_initial.up.sql
new file mode 100644
index 00000000..08d0187d
--- /dev/null
+++ b/cmd/atlas/internal/cmdapi/testdata/import/golang-migrate/1_initial.up.sql
@@ -0,0 +1,4 @@
+CREATE TABLE tbl
+(
+    col INT
+);
\ No newline at end of file
diff --git a/cmd/atlas/internal/cmdapi/testdata/import/golang-migrate/2_second_migration.down.sql b/cmd/atlas/internal/cmdapi/testdata/import/golang-migrate/2_second_migration.down.sql
new file mode 100644
index 00000000..670ad7e8
--- /dev/null
+++ b/cmd/atlas/internal/cmdapi/testdata/import/golang-migrate/2_second_migration.down.sql
@@ -0,0 +1,1 @@
+DROP TABLE tbl_2;
\ No newline at end of file
diff --git a/cmd/atlas/internal/cmdapi/testdata/import/golang-migrate/2_second_migration.up.sql b/cmd/atlas/internal/cmdapi/testdata/import/golang-migrate/2_second_migration.up.sql
new file mode 100644
index 00000000..b398c9c9
--- /dev/null
+++ b/cmd/atlas/internal/cmdapi/testdata/import/golang-migrate/2_second_migration.up.sql
@@ -0,0 +1,1 @@
+CREATE TABLE tbl_2 (col INT);
\ No newline at end of file
diff --git a/cmd/atlas/internal/cmdapi/testdata/import/golang-migrate_gold/1_initial.sql b/cmd/atlas/internal/cmdapi/testdata/import/golang-migrate_gold/1_initial.sql
new file mode 100644
index 00000000..41ab1bba
--- /dev/null
+++ b/cmd/atlas/internal/cmdapi/testdata/import/golang-migrate_gold/1_initial.sql
@@ -0,0 +1,4 @@
+CREATE TABLE tbl
+(
+    col INT
+);
diff --git a/cmd/atlas/internal/cmdapi/testdata/import/golang-migrate_gold/2_second_migration.sql b/cmd/atlas/internal/cmdapi/testdata/import/golang-migrate_gold/2_second_migration.sql
new file mode 100644
index 00000000..4f63b825
--- /dev/null
+++ b/cmd/atlas/internal/cmdapi/testdata/import/golang-migrate_gold/2_second_migration.sql
@@ -0,0 +1,1 @@
+CREATE TABLE tbl_2 (col INT);
diff --git a/cmd/atlas/internal/cmdapi/testdata/import/goose/1_initial.sql b/cmd/atlas/internal/cmdapi/testdata/import/goose/1_initial.sql
new file mode 100644
index 00000000..111699b5
--- /dev/null
+++ b/cmd/atlas/internal/cmdapi/testdata/import/goose/1_initial.sql
@@ -0,0 +1,19 @@
+-- +goose Up
+CREATE TABLE post
+(
+    id    int NOT NULL,
+    title text,
+    body  text,
+    PRIMARY KEY (id)
+);
+
+ALTER TABLE post ADD created_at TIMESTAMP NOT NULL;
+
+INSERT INTO post (title) VALUES (
+'This is
+my multiline
+
+value');
+
+-- +goose Down
+DROP TABLE post;
\ No newline at end of file
diff --git a/cmd/atlas/internal/cmdapi/testdata/import/goose/2_second_migration.sql b/cmd/atlas/internal/cmdapi/testdata/import/goose/2_second_migration.sql
new file mode 100644
index 00000000..4d8a4123
--- /dev/null
+++ b/cmd/atlas/internal/cmdapi/testdata/import/goose/2_second_migration.sql
@@ -0,0 +1,31 @@
+
+
+
+-- +goose Up
+
+
+ALTER TABLE post ADD updated_at TIMESTAMP NOT NULL;
+
+-- +goose StatementBegin
+-- Comment for the function declaration.
+CREATE
+OR REPLACE FUNCTION histories_partition_creation( DATE, DATE )
+returns void AS $$
+DECLARE
+create_query text;
+BEGIN
+FOR create_query IN
+SELECT 'CREATE TABLE IF NOT EXISTS histories_'
+           || TO_CHAR(d, 'YYYY_MM')
+           || ' ( CHECK( created_at >= timestamp '''
+           || TO_CHAR(d, 'YYYY-MM-DD 00:00:00')
+           || ''' AND created_at < timestamp '''
+           || TO_CHAR(d + INTERVAL '1 month', 'YYYY-MM-DD 00:00:00')
+           || ''' ) ) inherits ( histories );'
+FROM generate_series($1, $2, '1 month') AS d LOOP
+    EXECUTE create_query;
+END LOOP;  -- LOOP END
+END;         -- FUNCTION END
+$$
+language plpgsql;
+-- +goose StatementEnd
\ No newline at end of file
diff --git a/cmd/atlas/internal/cmdapi/testdata/import/goose_gold/1_initial.sql b/cmd/atlas/internal/cmdapi/testdata/import/goose_gold/1_initial.sql
new file mode 100644
index 00000000..69d7aff7
--- /dev/null
+++ b/cmd/atlas/internal/cmdapi/testdata/import/goose_gold/1_initial.sql
@@ -0,0 +1,13 @@
+CREATE TABLE post
+(
+    id    int NOT NULL,
+    title text,
+    body  text,
+    PRIMARY KEY (id)
+);
+ALTER TABLE post ADD created_at TIMESTAMP NOT NULL;
+INSERT INTO post (title) VALUES (
+'This is
+my multiline
+
+value');
diff --git a/cmd/atlas/internal/cmdapi/testdata/import/goose_gold/2_second_migration.sql b/cmd/atlas/internal/cmdapi/testdata/import/goose_gold/2_second_migration.sql
new file mode 100644
index 00000000..3fc7f1a3
--- /dev/null
+++ b/cmd/atlas/internal/cmdapi/testdata/import/goose_gold/2_second_migration.sql
@@ -0,0 +1,22 @@
+ALTER TABLE post ADD updated_at TIMESTAMP NOT NULL;
+-- Comment for the function declaration.
+CREATE
+OR REPLACE FUNCTION histories_partition_creation( DATE, DATE )
+returns void AS $$
+DECLARE
+create_query text;
+BEGIN
+FOR create_query IN
+SELECT 'CREATE TABLE IF NOT EXISTS histories_'
+           || TO_CHAR(d, 'YYYY_MM')
+           || ' ( CHECK( created_at >= timestamp '''
+           || TO_CHAR(d, 'YYYY-MM-DD 00:00:00')
+           || ''' AND created_at < timestamp '''
+           || TO_CHAR(d + INTERVAL '1 month', 'YYYY-MM-DD 00:00:00')
+           || ''' ) ) inherits ( histories );'
+FROM generate_series($1, $2, '1 month') AS d LOOP
+    EXECUTE create_query;
+END LOOP;  -- LOOP END
+END;         -- FUNCTION END
+$$
+language plpgsql;
diff --git a/cmd/atlas/internal/cmdapi/testdata/import/liquibase/1_initial.sql b/cmd/atlas/internal/cmdapi/testdata/import/liquibase/1_initial.sql
new file mode 100644
index 00000000..f7f669c9
--- /dev/null
+++ b/cmd/atlas/internal/cmdapi/testdata/import/liquibase/1_initial.sql
@@ -0,0 +1,22 @@
+--liquibase formatted sql
+
+--changeset atlas:1-1
+CREATE TABLE post
+(
+    id    int NOT NULL,
+    title text,
+    body  text,
+    PRIMARY KEY (id)
+);
+--rollback: DROP TABLE post;
+
+--changeset atlas:1-2
+ALTER TABLE post ADD created_at TIMESTAMP NOT NULL;
+--rollback: ALTER TABLE post DROP created_at;
+
+--changeset atlas:1-3
+INSERT INTO post (title) VALUES (
+'This is
+my multiline
+
+value');
diff --git a/cmd/atlas/internal/cmdapi/testdata/import/liquibase/2_second_migration.sql b/cmd/atlas/internal/cmdapi/testdata/import/liquibase/2_second_migration.sql
new file mode 100644
index 00000000..90149119
--- /dev/null
+++ b/cmd/atlas/internal/cmdapi/testdata/import/liquibase/2_second_migration.sql
@@ -0,0 +1,5 @@
+--liquibase formatted sql
+
+--changeset atlas:2-1
+CREATE TABLE tbl_2 (col INT);
+--rollback DROP TABLE tbl_2;
diff --git a/cmd/atlas/internal/cmdapi/testdata/import/liquibase_gold/1_initial.sql b/cmd/atlas/internal/cmdapi/testdata/import/liquibase_gold/1_initial.sql
new file mode 100644
index 00000000..43096281
--- /dev/null
+++ b/cmd/atlas/internal/cmdapi/testdata/import/liquibase_gold/1_initial.sql
@@ -0,0 +1,16 @@
+--changeset atlas:1-1
+CREATE TABLE post
+(
+    id    int NOT NULL,
+    title text,
+    body  text,
+    PRIMARY KEY (id)
+);
+--changeset atlas:1-2
+ALTER TABLE post ADD created_at TIMESTAMP NOT NULL;
+--changeset atlas:1-3
+INSERT INTO post (title) VALUES (
+'This is
+my multiline
+
+value');
diff --git a/cmd/atlas/internal/cmdapi/testdata/import/liquibase_gold/2_second_migration.sql b/cmd/atlas/internal/cmdapi/testdata/import/liquibase_gold/2_second_migration.sql
new file mode 100644
index 00000000..7cfe154e
--- /dev/null
+++ b/cmd/atlas/internal/cmdapi/testdata/import/liquibase_gold/2_second_migration.sql
@@ -0,0 +1,2 @@
+--changeset atlas:2-1
+CREATE TABLE tbl_2 (col INT);
diff --git a/cmd/atlas/internal/lint/lint_test.go b/cmd/atlas/internal/lint/lint_test.go
index 8726f5cb..2f9f2b53 100644
--- a/cmd/atlas/internal/lint/lint_test.go
+++ b/cmd/atlas/internal/lint/lint_test.go
@@ -216,3 +216,14 @@ func (f testFile) Bytes() []byte {
 func (f testFile) Stmts() ([]string, error) {
 	return strings.Split(string(f.Bytes()), "\n"), nil
 }
+
+func (f testFile) StmtDecls() (stmts []*migrate.Stmt, err error) {
+	s, err := f.Stmts()
+	if err != nil {
+		return nil, err
+	}
+	for _, s := range s {
+		stmts = append(stmts, &migrate.Stmt{Text: s})
+	}
+	return
+}
diff --git a/internal/integration/testdata/mysql/cli-migrate-apply.txt b/internal/integration/testdata/mysql/cli-migrate-apply.txt
index 0837473a..ca0965a6 100644
--- a/internal/integration/testdata/mysql/cli-migrate-apply.txt
+++ b/internal/integration/testdata/mysql/cli-migrate-apply.txt
@@ -3,9 +3,9 @@ only mysql
 ! atlas migrate apply
 stderr 'Error: checksum file not found'
 stdout 'You have a checksum error in your migration directory.'
-stdout 'atlas migrate hash --force'
+stdout 'atlas migrate hash'
 
-atlas migrate hash --force
+atlas migrate hash
 
 # Apply all of them
 atlas migrate apply --url URL --revisions-schema $db
diff --git a/internal/integration/testdata/postgres/cli-migrate-apply.txt b/internal/integration/testdata/postgres/cli-migrate-apply.txt
index 276455f4..220700af 100644
--- a/internal/integration/testdata/postgres/cli-migrate-apply.txt
+++ b/internal/integration/testdata/postgres/cli-migrate-apply.txt
@@ -1,9 +1,9 @@
 ! atlas migrate apply
 stderr 'Error: checksum file not found'
 stdout 'You have a checksum error in your migration directory.'
-stdout 'atlas migrate hash --force'
+stdout 'atlas migrate hash'
 
-atlas migrate hash --force
+atlas migrate hash
 
 # Apply all of them
 atlas migrate apply --url URL --revisions-schema $db
@@ -45,7 +45,7 @@ clearSchema
 
 # Move the broken migration into the migrations directory and check the different transaction modes.
 cp broken.sql migrations/4_fourth.sql
-atlas migrate hash --force
+atlas migrate hash
 
 ! atlas migrate apply --url URL --revisions-schema $db --tx-mode invalid
 stderr 'unknown tx-mode "invalid"'
@@ -70,7 +70,7 @@ cmp stdout empty.hcl
 
 # If the broken migration is gone, we can apply everything without any problems.
 rm migrations/4_fourth.sql
-atlas migrate hash --force
+atlas migrate hash
 
 atlas migrate apply --url URL --revisions-schema $db
 cmpshow users users.sql
@@ -83,7 +83,7 @@ clearSchema
 # Test --tx-mode file
 
 cp broken.sql migrations/4_fourth.sql
-atlas migrate hash --force
+atlas migrate hash
 
 ! atlas migrate apply --url URL --revisions-schema $db --tx-mode file
 stderr 'executing statement "THIS IS A FAILING STATEMENT;" from version "4"'
@@ -97,7 +97,7 @@ cmp stdout empty.hcl
 
 # If the broken migration is gone, we can apply everything without any problems.
 rm migrations/4_fourth.sql
-atlas migrate hash --force
+atlas migrate hash
 
 atlas migrate apply --url URL --revisions-schema $db
 cmpshow users users.sql
@@ -110,7 +110,7 @@ clearSchema
 # Test --tx-mode none
 
 cp broken.sql migrations/4_fourth.sql
-atlas migrate hash --force
+atlas migrate hash
 
 ! atlas migrate apply --url URL --revisions-schema $db --tx-mode none
 stderr 'executing statement "THIS IS A FAILING STATEMENT;" from version "4"'
diff --git a/internal/integration/testdata/postgres/cli-migrate-status.txt b/internal/integration/testdata/postgres/cli-migrate-status.txt
index 8fd772f7..d2c3802f 100644
--- a/internal/integration/testdata/postgres/cli-migrate-status.txt
+++ b/internal/integration/testdata/postgres/cli-migrate-status.txt
@@ -1,5 +1,5 @@
 # make sure sum file is correct
-atlas migrate hash --force
+atlas migrate hash
 
 # for clean database
 atlas migrate status --url URL --revisions-schema $db
diff --git a/internal/integration/testdata/sqlite/cli-migrate-apply.txt b/internal/integration/testdata/sqlite/cli-migrate-apply.txt
index 4838864a..946a53d6 100644
--- a/internal/integration/testdata/sqlite/cli-migrate-apply.txt
+++ b/internal/integration/testdata/sqlite/cli-migrate-apply.txt
@@ -1,9 +1,9 @@
 ! atlas migrate apply
 stderr 'Error: checksum file not found'
 stdout 'You have a checksum error in your migration directory.'
-stdout 'atlas migrate hash --force'
+stdout 'atlas migrate hash'
 
-atlas migrate hash --force
+atlas migrate hash
 
 # Apply all of them
 atlas migrate apply --url URL
@@ -39,7 +39,7 @@ clearSchema
 
 # Move the broken migration into the migrations directory and check the different transaction modes.
 cp broken.sql migrations/3_third.sql
-atlas migrate hash --force
+atlas migrate hash
 
 ! atlas migrate apply --url URL --tx-mode invalid
 stderr 'unknown tx-mode "invalid"'
@@ -64,7 +64,7 @@ cmp stdout empty.hcl
 
 # If the broken migration is gone, we can apply everything without any problems.
 rm migrations/3_third.sql
-atlas migrate hash --force
+atlas migrate hash
 
 atlas migrate apply --url URL --revisions-schema $db
 cmpshow users users.sql
@@ -77,7 +77,7 @@ clearSchema
 # Test --tx-mode file
 
 cp broken.sql migrations/3_third.sql
-atlas migrate hash --force
+atlas migrate hash
 
 ! atlas migrate apply --url URL --tx-mode file
 stderr 'executing statement "THIS IS A FAILING STATEMENT;" from version "3"'
@@ -91,7 +91,7 @@ cmp stdout empty.hcl
 
 # If the broken migration is gone, we can apply everything without any problems.
 rm migrations/3_third.sql
-atlas migrate hash --force
+atlas migrate hash
 
 atlas migrate apply --url URL --revisions-schema $db
 cmpshow users users.sql
@@ -104,7 +104,7 @@ clearSchema
 # Test --tx-mode none
 
 cp broken.sql migrations/3_third.sql
-atlas migrate hash --force
+atlas migrate hash
 
 ! atlas migrate apply --url URL --tx-mode none
 stderr 'executing statement "THIS IS A FAILING STATEMENT;" from version "3"'
diff --git a/internal/integration/testdata/sqlite/cli-migrate-project.txt b/internal/integration/testdata/sqlite/cli-migrate-project.txt
index af1fde05..46f29f86 100644
--- a/internal/integration/testdata/sqlite/cli-migrate-project.txt
+++ b/internal/integration/testdata/sqlite/cli-migrate-project.txt
@@ -11,7 +11,7 @@ exec touch migrations/2.sql
 ! atlas migrate validate --env local
 stderr 'Error: checksum mismatch'
 
-atlas migrate hash --force --env local
+atlas migrate hash --env local
 atlas migrate validate --env local
 
 -- atlas.hcl --
diff --git a/sql/postgres/migrate_test.go b/sql/postgres/migrate_test.go
index 81111291..e2789dc0 100644
--- a/sql/postgres/migrate_test.go
+++ b/sql/postgres/migrate_test.go
@@ -395,6 +395,17 @@ func TestPlanChanges(t *testing.T) {
 									},
 								},
 							},
+							&schema.AddIndex{
+								I: &schema.Index{
+									Name: "concurrently",
+									Parts: []*schema.IndexPart{
+										{C: users.Columns[0]},
+									},
+									Attrs: []schema.Attr{
+										&Concurrently{},
+									},
+								},
+							},
 						},
 					}
 				}(),
@@ -419,6 +430,10 @@ func TestPlanChanges(t *testing.T) {
 						Cmd:     `CREATE INDEX "include_key" ON "users" ("id") INCLUDE ("a", "b")`,
 						Reverse: `DROP INDEX "include_key"`,
 					},
+					{
+						Cmd:     `CREATE INDEX CONCURRENTLY "concurrently" ON "users" ("id")`,
+						Reverse: `DROP INDEX CONCURRENTLY "concurrently"`,
+					},
 					{
 						Cmd:     `COMMENT ON COLUMN "users" ."name" IS 'foo'`,
 						Reverse: `COMMENT ON COLUMN "users" ."name" IS ''`,
diff --git a/sql/sqltool/testdata/dbmate/1_initial.sql b/sql/sqltool/testdata/dbmate/1_initial.sql
index 83ea267e..e70711c2 100644
--- a/sql/sqltool/testdata/dbmate/1_initial.sql
+++ b/sql/sqltool/testdata/dbmate/1_initial.sql
@@ -7,6 +7,9 @@ CREATE TABLE post
     PRIMARY KEY (id)
 );
 
+/*
+ Multiline comment ...
+ */
 ALTER TABLE post ADD created_at TIMESTAMP NOT NULL;
 
 INSERT INTO post (title) VALUES (
@@ -16,4 +19,6 @@ my multiline
 value');
 
 -- migrate:down
+
+
 DROP TABLE post;
\ No newline at end of file
diff --git a/sql/sqltool/testdata/goose/2_second_migration.sql b/sql/sqltool/testdata/goose/2_second_migration.sql
index 416c9c5a..4d8a4123 100644
--- a/sql/sqltool/testdata/goose/2_second_migration.sql
+++ b/sql/sqltool/testdata/goose/2_second_migration.sql
@@ -4,9 +4,10 @@
 -- +goose Up
 
 
-
+ALTER TABLE post ADD updated_at TIMESTAMP NOT NULL;
 
 -- +goose StatementBegin
+-- Comment for the function declaration.
 CREATE
 OR REPLACE FUNCTION histories_partition_creation( DATE, DATE )
 returns void AS $$
diff --git a/sql/sqltool/tool_test.go b/sql/sqltool/tool_test.go
index 5450ce07..9ba415a2 100644
--- a/sql/sqltool/tool_test.go
+++ b/sql/sqltool/tool_test.go
@@ -166,7 +166,10 @@ func TestScanners(t *testing.T) {
 					"ALTER TABLE post ADD created_at TIMESTAMP NOT NULL;",
 					"INSERT INTO post (title) VALUES (\n'This is\nmy multiline\n\nvalue');",
 				},
-				{"CREATE\nOR REPLACE FUNCTION histories_partition_creation( DATE, DATE )\nreturns void AS $$\nDECLARE\ncreate_query text;\nBEGIN\nFOR create_query IN\nSELECT 'CREATE TABLE IF NOT EXISTS histories_'\n           || TO_CHAR(d, 'YYYY_MM')\n           || ' ( CHECK( created_at >= timestamp '''\n           || TO_CHAR(d, 'YYYY-MM-DD 00:00:00')\n           || ''' AND created_at < timestamp '''\n           || TO_CHAR(d + INTERVAL '1 month', 'YYYY-MM-DD 00:00:00')\n           || ''' ) ) inherits ( histories );'\nFROM generate_series($1, $2, '1 month') AS d LOOP\n    EXECUTE create_query;\nEND LOOP;  -- LOOP END\nEND;         -- FUNCTION END\n$$\nlanguage plpgsql;"},
+				{
+					"ALTER TABLE post ADD updated_at TIMESTAMP NOT NULL;",
+					"CREATE\nOR REPLACE FUNCTION histories_partition_creation( DATE, DATE )\nreturns void AS $$\nDECLARE\ncreate_query text;\nBEGIN\nFOR create_query IN\nSELECT 'CREATE TABLE IF NOT EXISTS histories_'\n           || TO_CHAR(d, 'YYYY_MM')\n           || ' ( CHECK( created_at >= timestamp '''\n           || TO_CHAR(d, 'YYYY-MM-DD 00:00:00')\n           || ''' AND created_at < timestamp '''\n           || TO_CHAR(d + INTERVAL '1 month', 'YYYY-MM-DD 00:00:00')\n           || ''' ) ) inherits ( histories );'\nFROM generate_series($1, $2, '1 month') AS d LOOP\n    EXECUTE create_query;\nEND LOOP;  -- LOOP END\nEND;         -- FUNCTION END\n$$\nlanguage plpgsql;",
+				},
 			},
 		},
 		{