Repository: ariga/atlas
Pull request #1062 (2022-08-17 07:42:32)

sql/sqltool: support replay of liquibase directories


Patch:
diff --git a/cmd/atlas/internal/cmdapi/migrate.go b/cmd/atlas/internal/cmdapi/migrate.go
index 74b81106..d927e346 100644
--- a/cmd/atlas/internal/cmdapi/migrate.go
+++ b/cmd/atlas/internal/cmdapi/migrate.go
@@ -740,6 +740,7 @@ func dir(create bool) (migrate.Dir, error) {
 	case formatFlyway:
 		f = func() (migrate.Dir, error) { return sqltool.NewFlywayDir(parts[1]) }
 	case formatLiquibase:
+		f = func() (migrate.Dir, error) { return sqltool.NewLiquibaseDir(parts[1]) }
 	case formatDbmate:
 		f = func() (migrate.Dir, error) { return sqltool.NewDBMateDir(parts[1]) }
 	default:
diff --git a/sql/sqltool/tool.go b/sql/sqltool/tool.go
index c2c645a6..4a11658c 100644
--- a/sql/sqltool/tool.go
+++ b/sql/sqltool/tool.go
@@ -355,6 +355,11 @@ func (f FlywayFile) Version() string {
 	return strings.TrimPrefix(strings.SplitN(strings.TrimSuffix(f.Name(), ".sql"), "__", 2)[0], "V")
 }
 
+// NewLiquibaseDir returns a new LiquibaseDir.
+func NewLiquibaseDir(path string) (*migrate.LocalDir, error) {
+	return migrate.NewLocalDir(path)
+}
+
 const (
 	none int = iota // state when parsing goose/dbmate sql file
 	up


Test patch:
diff --git a/internal/integration/script_test.go b/internal/integration/script_test.go
index 43f6374f..d9b97395 100644
--- a/internal/integration/script_test.go
+++ b/internal/integration/script_test.go
@@ -426,6 +426,8 @@ func (t *liteTest) cmdCmpMig(ts *testscript.TestScript, neg bool, args []string)
 	cmdCmpMig(ts, neg, args)
 }
 
+var reLiquibaseChangeset = regexp.MustCompile("--changeset atlas:[0-9]+-[0-9]+")
+
 // cmdCmpMig compares a migration file under migrations with a provided file.
 // If the first argument is a filename that does exist, that file is used for comparison.
 // If there is no file with that name, the argument is parsed to an integer n and the
@@ -443,7 +445,7 @@ func cmdCmpMig(ts *testscript.TestScript, _ bool, args []string) {
 	if _, err := os.Stat(ts.MkAbs(filepath.Join(ver, fname))); err == nil {
 		fname = filepath.Join(ver, fname)
 	}
-	expected := ts.ReadFile(fname)
+	expected := strings.TrimSpace(ts.ReadFile(fname))
 	dir, err := os.ReadDir(ts.MkAbs("migrations"))
 	ts.Check(err)
 	idx, err := strconv.Atoi(args[0])
@@ -454,13 +456,25 @@ func cmdCmpMig(ts *testscript.TestScript, _ bool, args []string) {
 			continue
 		}
 		if current == idx {
-			actual := ts.ReadFile(filepath.Join("migrations", f.Name()))
-			var sb strings.Builder
-			if strings.TrimSpace(actual) == strings.TrimSpace(expected) {
-				return
+			actual := strings.TrimSpace(ts.ReadFile(filepath.Join("migrations", f.Name())))
+			exLines, acLines := strings.Split(actual, "\n"), strings.Split(expected, "\n")
+			if len(exLines) != len(acLines) {
+				var sb strings.Builder
+				ts.Check(diff.Text(f.Name(), args[1], expected, actual, &sb))
+				ts.Fatalf(sb.String())
+			}
+			for i := range exLines {
+				// Skip liquibase changeset comments since they contain a timestamp.
+				if reLiquibaseChangeset.MatchString(acLines[i]) {
+					continue
+				}
+				if exLines[i] != acLines[i] {
+					var sb strings.Builder
+					ts.Check(diff.Text(f.Name(), args[1], expected, actual, &sb))
+					ts.Fatalf(sb.String())
+				}
 			}
-			ts.Check(diff.Text(f.Name(), args[1], expected, actual, &sb))
-			ts.Fatalf(sb.String())
+			return
 		}
 		current++
 	}
diff --git a/internal/integration/testdata/mysql/cli-migrate-diff-format.txt b/internal/integration/testdata/mysql/cli-migrate-diff-format.txt
index 649a0747..6ed058a4 100644
--- a/internal/integration/testdata/mysql/cli-migrate-diff-format.txt
+++ b/internal/integration/testdata/mysql/cli-migrate-diff-format.txt
@@ -39,6 +39,15 @@ atlas migrate diff --dev-url URL --to file://2.hcl --dir-format flyway second
 cmpmig 1 flyway/U2.sql
 cmpmig 3 flyway/V2.sql
 
+rm migrations
+mkdir migrations
+
+atlas migrate diff --dev-url URL --to file://1.hcl --dir-format liquibase first
+cmpmig 0 liquibase/1.sql
+
+atlas migrate diff --dev-url URL --to file://2.hcl --dir-format liquibase second
+cmpmig 1 liquibase/2.sql
+
 -- golang-migrate/1.up.sql --
 -- create "users" table
 CREATE TABLE `users` (`id` bigint NOT NULL AUTO_INCREMENT, PRIMARY KEY (`id`)) CHARSET utf8mb4 COLLATE utf8mb4_general_ci;
@@ -107,6 +116,20 @@ ALTER TABLE `users` ADD COLUMN `email` varchar(100) NULL;
 -- reverse: modify "users" table
 ALTER TABLE `users` DROP COLUMN `email`;
 
+-- liquibase/1.sql --
+--liquibase formatted sql
+--changeset atlas:0-0
+--comment: create "users" table
+CREATE TABLE `users` (`id` bigint NOT NULL AUTO_INCREMENT, PRIMARY KEY (`id`)) CHARSET utf8mb4 COLLATE utf8mb4_general_ci;
+--rollback: DROP TABLE `users`;
+
+-- liquibase/2.sql --
+--liquibase formatted sql
+--changeset atlas:0-0
+--comment: modify "users" table
+ALTER TABLE `users` ADD COLUMN `email` varchar(100) NULL;
+--rollback: ALTER TABLE `users` DROP COLUMN `email`;
+
 -- 1.hcl --
 schema "script_cli_migrate_diff_format" {}
 
diff --git a/sql/sqltool/testdata/liquibase/1_initial.sql b/sql/sqltool/testdata/liquibase/1_initial.sql
new file mode 100644
index 00000000..f7f669c9
--- /dev/null
+++ b/sql/sqltool/testdata/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/sql/sqltool/testdata/liquibase/2_second_migration.sql b/sql/sqltool/testdata/liquibase/2_second_migration.sql
new file mode 100644
index 00000000..90149119
--- /dev/null
+++ b/sql/sqltool/testdata/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/sql/sqltool/testdata/liquibase/atlas.sum b/sql/sqltool/testdata/liquibase/atlas.sum
new file mode 100644
index 00000000..3e8d88d8
--- /dev/null
+++ b/sql/sqltool/testdata/liquibase/atlas.sum
@@ -0,0 +1,3 @@
+h1:6I2+EO1XlJwbhoffBnOS8+i/IP3jlrGgI2DIJvRpcPs=
+1_initial.sql h1:WhCKKuYeV1R6mHBSeM+MNwdlyKFPfSD8I55+pxFBiOA=
+2_second_migration.sql h1:dyEY5EIahGAWgcDS3k5CcD7JGKSyaXL6GeG4AvyZObY=
diff --git a/sql/sqltool/tool_test.go b/sql/sqltool/tool_test.go
index 5cc19e3b..0f9ea99d 100644
--- a/sql/sqltool/tool_test.go
+++ b/sql/sqltool/tool_test.go
@@ -187,6 +187,24 @@ func TestScanners(t *testing.T) {
 				{"CREATE TABLE tbl_2 (col INT);"},
 			},
 		},
+		{
+			name: "liquibase",
+			dir: func() migrate.Dir {
+				d, err := sqltool.NewLiquibaseDir("testdata/liquibase")
+				require.NoError(t, err)
+				return d
+			}(),
+			versions:     []string{"1", "2"},
+			descriptions: []string{"initial", "second_migration"},
+			stmts: [][]string{
+				{
+					"CREATE TABLE post\n(\n    id    int NOT NULL,\n    title text,\n    body  text,\n    PRIMARY KEY (id)\n);",
+					"ALTER TABLE post ADD created_at TIMESTAMP NOT NULL;",
+					"INSERT INTO post (title) VALUES (\n'This is\nmy multiline\n\nvalue');",
+				},
+				{"CREATE TABLE tbl_2 (col INT);"},
+			},
+		},
 		{
 			name: "dbmate",
 			dir: func() migrate.Dir {