github.com/iasthc/atlas/cmd/atlas@v0.0.0-20230523071841-73246df3f88d/internal/cmdapi/migrate_test.go (about)

     1  // Copyright 2021-present The Atlas Authors. All rights reserved.
     2  // This source code is licensed under the Apache 2.0 license found
     3  // in the LICENSE file in the root directory of this source tree.
     4  
     5  package cmdapi
     6  
     7  import (
     8  	"context"
     9  	"database/sql"
    10  	"encoding/base64"
    11  	"encoding/json"
    12  	"errors"
    13  	"fmt"
    14  	"io"
    15  	"net/http"
    16  	"net/http/httptest"
    17  	"net/url"
    18  	"os"
    19  	"os/exec"
    20  	"path/filepath"
    21  	"runtime"
    22  	"strings"
    23  	"testing"
    24  	"time"
    25  
    26  	"github.com/iasthc/atlas/cmd/atlas/internal/cloudapi"
    27  	"github.com/iasthc/atlas/cmd/atlas/internal/cmdlog"
    28  	migrate2 "github.com/iasthc/atlas/cmd/atlas/internal/migrate"
    29  	"github.com/iasthc/atlas/sql/migrate"
    30  	"github.com/iasthc/atlas/sql/schema"
    31  	"github.com/iasthc/atlas/sql/sqlclient"
    32  	"github.com/iasthc/atlas/sql/sqlite"
    33  	_ "github.com/iasthc/atlas/sql/sqlite"
    34  	_ "github.com/iasthc/atlas/sql/sqlite/sqlitecheck"
    35  
    36  	"github.com/fatih/color"
    37  	"github.com/google/uuid"
    38  	_ "github.com/mattn/go-sqlite3"
    39  	"github.com/stretchr/testify/require"
    40  )
    41  
    42  func TestMigrate(t *testing.T) {
    43  	_, err := runCmd(migrateCmd())
    44  	require.NoError(t, err)
    45  }
    46  
    47  func TestMigrate_Import(t *testing.T) {
    48  	for _, tool := range []string{"dbmate", "flyway", "golang-migrate", "goose", "liquibase"} {
    49  		p := t.TempDir()
    50  		t.Run(tool, func(t *testing.T) { // remove this once --dir-format is removed. Test is kept to ensure BC.
    51  			path := filepath.FromSlash("testdata/import/" + tool)
    52  			out, err := runCmd(
    53  				migrateImportCmd(),
    54  				"--from", "file://"+path,
    55  				"--to", "file://"+p,
    56  				"--dir-format", tool,
    57  			)
    58  			require.NoError(t, err)
    59  			require.Zero(t, out)
    60  
    61  			path += "_gold"
    62  			ex, err := os.ReadDir(path)
    63  			require.NoError(t, err)
    64  			ac, err := os.ReadDir(p)
    65  			require.NoError(t, err)
    66  			require.Equal(t, len(ex)+1, len(ac)) // sum file
    67  
    68  			for i := range ex {
    69  				e, err := os.ReadFile(filepath.Join(path, ex[i].Name()))
    70  				require.NoError(t, err)
    71  				a, err := os.ReadFile(filepath.Join(p, ex[i].Name()))
    72  				require.NoError(t, err)
    73  				require.Equal(t, string(e), string(a))
    74  			}
    75  		})
    76  		p = t.TempDir()
    77  		t.Run(tool, func(t *testing.T) {
    78  			path := filepath.FromSlash("testdata/import/" + tool)
    79  			out, err := runCmd(
    80  				migrateImportCmd(),
    81  				"--from", fmt.Sprintf("file://%s?format=%s", path, tool),
    82  				"--to", "file://"+p,
    83  			)
    84  			require.NoError(t, err)
    85  			require.Zero(t, out)
    86  
    87  			path += "_gold"
    88  			ex, err := os.ReadDir(path)
    89  			require.NoError(t, err)
    90  			ac, err := os.ReadDir(p)
    91  			require.NoError(t, err)
    92  			require.Equal(t, len(ex)+1, len(ac)) // sum file
    93  
    94  			for i := range ex {
    95  				e, err := os.ReadFile(filepath.Join(path, ex[i].Name()))
    96  				require.NoError(t, err)
    97  				a, err := os.ReadFile(filepath.Join(p, ex[i].Name()))
    98  				require.NoError(t, err)
    99  				require.Equal(t, string(e), string(a))
   100  			}
   101  		})
   102  	}
   103  }
   104  
   105  func TestMigrate_Apply(t *testing.T) {
   106  	var (
   107  		p   = t.TempDir()
   108  		ctx = context.Background()
   109  	)
   110  	// Disable text coloring in testing
   111  	// to assert on string matching.
   112  	color.NoColor = true
   113  
   114  	// Fails on empty directory.
   115  	s, err := runCmd(
   116  		migrateApplyCmd(),
   117  		"--dir", "file://"+p,
   118  		"-u", openSQLite(t, ""),
   119  	)
   120  	require.NoError(t, err)
   121  	require.Equal(t, "No migration files to execute\n", s)
   122  
   123  	// Fails on directory without sum file.
   124  	require.NoError(t, os.Rename(
   125  		filepath.FromSlash("testdata/sqlite/atlas.sum"),
   126  		filepath.FromSlash("testdata/sqlite/atlas.sum.bak"),
   127  	))
   128  	t.Cleanup(func() {
   129  		os.Rename(filepath.FromSlash("testdata/sqlite/atlas.sum.bak"), filepath.FromSlash("testdata/sqlite/atlas.sum"))
   130  	})
   131  
   132  	_, err = runCmd(
   133  		migrateApplyCmd(),
   134  		"--dir", "file://testdata/sqlite",
   135  		"--url", openSQLite(t, ""),
   136  	)
   137  	require.ErrorIs(t, err, migrate.ErrChecksumNotFound)
   138  	require.NoError(t, os.Rename(
   139  		filepath.FromSlash("testdata/sqlite/atlas.sum.bak"),
   140  		filepath.FromSlash("testdata/sqlite/atlas.sum"),
   141  	))
   142  
   143  	// A lock will prevent execution.
   144  	sqlclient.Register(
   145  		"sqlitelockapply",
   146  		sqlclient.OpenerFunc(func(ctx context.Context, u *url.URL) (*sqlclient.Client, error) {
   147  			client, err := sqlclient.Open(ctx, strings.Replace(u.String(), u.Scheme, "sqlite", 1))
   148  			if err != nil {
   149  				return nil, err
   150  			}
   151  			client.Driver = &sqliteLockerDriver{client.Driver}
   152  			return client, nil
   153  		}),
   154  		sqlclient.RegisterDriverOpener(func(db schema.ExecQuerier) (migrate.Driver, error) {
   155  			drv, err := sqlite.Open(db)
   156  			if err != nil {
   157  				return nil, err
   158  			}
   159  			return &sqliteLockerDriver{drv}, nil
   160  		}),
   161  	)
   162  	f, err := os.Create(filepath.Join(p, "test.db"))
   163  	require.NoError(t, err)
   164  	require.NoError(t, f.Close())
   165  
   166  	s, err = runCmd(
   167  		migrateApplyCmd(),
   168  		"--dir", "file://testdata/sqlite",
   169  		"--url", fmt.Sprintf("sqlitelockapply://file:%s?cache=shared&_fk=1", filepath.Join(p, "test.db")),
   170  	)
   171  	require.ErrorIs(t, err, errLock)
   172  	require.True(t, strings.HasPrefix(s, "Error: acquiring database lock: "+errLock.Error()))
   173  
   174  	// Apply zero throws error.
   175  	for _, n := range []string{"-1", "0"} {
   176  		_, err = runCmd(
   177  			migrateApplyCmd(),
   178  			"--dir", "file://testdata/sqlite",
   179  			"--url", fmt.Sprintf("sqlite://file:%s?cache=shared&_fk=1", filepath.Join(p, "test.db")),
   180  			"--", n,
   181  		)
   182  		require.EqualError(t, err, fmt.Sprintf("cannot apply '%s' migration files", n))
   183  	}
   184  
   185  	// Will work and print stuff to the console.
   186  	s, err = runCmd(
   187  		migrateApplyCmd(),
   188  		"--dir", "file://testdata/sqlite",
   189  		"--url", fmt.Sprintf("sqlite://file:%s?cache=shared&_fk=1", filepath.Join(p, "test.db")),
   190  		"1",
   191  	)
   192  	require.NoError(t, err)
   193  	require.Contains(t, s, "20220318104614")                           // log to version
   194  	require.Contains(t, s, "CREATE TABLE tbl (`col` int NOT NULL);")   // logs statement
   195  	require.NotContains(t, s, "ALTER TABLE `tbl` ADD `col_2` bigint;") // does not execute second file
   196  	require.Contains(t, s, "1 migrations")                             // logs amount of migrations
   197  	require.Contains(t, s, "1 sql statements")
   198  
   199  	// Transactions will be wrapped per file. If the second file has an error, first still is applied.
   200  	s, err = runCmd(
   201  		migrateApplyCmd(),
   202  		"--dir", "file://testdata/sqlite2",
   203  		"--url", fmt.Sprintf("sqlite://file:%s?cache=shared&_fk=1", filepath.Join(p, "test2.db")),
   204  	)
   205  	require.Error(t, err)
   206  	require.Contains(t, s, "20220318104614")                           // log to version
   207  	require.Contains(t, s, "CREATE TABLE tbl (`col` int NOT NULL);")   // logs statement
   208  	require.Contains(t, s, "ALTER TABLE `tbl` ADD `col_2` bigint;")    // does execute first stmt first second file
   209  	require.Contains(t, s, "ALTER TABLE `tbl` ADD `col_3` bigint;")    // does execute second stmt first second file
   210  	require.NotContains(t, s, "ALTER TABLE `tbl` ADD `col_4` bigint;") // but not third
   211  	require.Contains(t, s, "1 migrations ok (1 with errors)")          // logs amount of migrations
   212  	require.Contains(t, s, "2 sql statements ok (1 with errors)")      // logs amount of statement
   213  	require.Contains(t, s, "near \"asdasd\": syntax error")            // logs error summary
   214  
   215  	c, err := sqlclient.Open(ctx, fmt.Sprintf("sqlite://file:%s?cache=shared&_fk=1", filepath.Join(p, "test2.db")))
   216  	require.NoError(t, err)
   217  	t.Cleanup(func() {
   218  		require.NoError(t, c.Close())
   219  	})
   220  	sch, err := c.InspectSchema(ctx, "", nil)
   221  	tbl, ok := sch.Table("tbl")
   222  	require.True(t, ok)
   223  	_, ok = tbl.Column("col_2")
   224  	require.False(t, ok)
   225  	_, ok = tbl.Column("col_3")
   226  	require.False(t, ok)
   227  	rrw, err := migrate2.NewEntRevisions(ctx, c)
   228  	require.NoError(t, err)
   229  	revs, err := rrw.ReadRevisions(ctx)
   230  	require.NoError(t, err)
   231  	require.Len(t, revs, 1)
   232  
   233  	// Running again will pick up the failed statement and try it again.
   234  	s, err = runCmd(
   235  		migrateApplyCmd(),
   236  		"--dir", "file://testdata/sqlite2",
   237  		"--url", fmt.Sprintf("sqlite://file:%s?cache=shared&_fk=1", filepath.Join(p, "test2.db")),
   238  	)
   239  	require.Error(t, err)
   240  	require.Contains(t, s, "20220318104614")                            // currently applied version
   241  	require.Contains(t, s, "20220318104615")                            // retry second (partially applied)
   242  	require.NotContains(t, s, "CREATE TABLE tbl (`col` int NOT NULL);") // will not attempt stmts from first file
   243  	require.Contains(t, s, "ALTER TABLE `tbl` ADD `col_2` bigint;")     // picks up first statement
   244  	require.Contains(t, s, "ALTER TABLE `tbl` ADD `col_3` bigint;")     // does execute second stmt first second file
   245  	require.NotContains(t, s, "ALTER TABLE `tbl` ADD `col_4` bigint;")  // but not third
   246  	require.Contains(t, s, "0 migrations ok (1 with errors)")           // logs amount of migrations
   247  	require.Contains(t, s, "1 sql statements ok (1 with errors)")       // logs amount of statement
   248  	require.Contains(t, s, "near \"asdasd\": syntax error")             // logs error summary
   249  
   250  	// Editing an applied line will raise error.
   251  	s, err = runCmd(
   252  		migrateApplyCmd(),
   253  		"--dir", "file://testdata/sqlite2",
   254  		"--url", fmt.Sprintf("sqlite://file:%s?cache=shared&_fk=1", filepath.Join(p, "test2.db")),
   255  		"--tx-mode", "none",
   256  	)
   257  	t.Cleanup(func() {
   258  		_ = os.RemoveAll("testdata/sqlite3")
   259  	})
   260  	require.NoError(t, exec.Command("cp", "-r", "testdata/sqlite2", "testdata/sqlite3").Run())
   261  	sed(t, "s/col_2/col_5/g", "testdata/sqlite3/20220318104615_second.sql")
   262  	_, err = runCmd(migrateHashCmd(), "--dir", "file://testdata/sqlite3")
   263  	require.NoError(t, err)
   264  	s, err = runCmd(
   265  		migrateApplyCmd(),
   266  		"--dir", "file://testdata/sqlite3",
   267  		"--url", fmt.Sprintf("sqlite://file:%s?cache=shared&_fk=1", filepath.Join(p, "test2.db")),
   268  	)
   269  	require.ErrorAs(t, err, &migrate.HistoryChangedError{})
   270  
   271  	// Fixing the migration file will finish without errors.
   272  	sed(t, "s/col_5/col_2/g", "testdata/sqlite3/20220318104615_second.sql")
   273  	sed(t, "s/asdasd //g", "testdata/sqlite3/20220318104615_second.sql")
   274  	_, err = runCmd(migrateHashCmd(), "--dir", "file://testdata/sqlite3")
   275  	require.NoError(t, err)
   276  	s, err = runCmd(
   277  		migrateApplyCmd(),
   278  		"--dir", "file://testdata/sqlite3",
   279  		"--url", fmt.Sprintf("sqlite://file:%s?cache=shared&_fk=1", filepath.Join(p, "test2.db")),
   280  	)
   281  	require.NoError(t, err)
   282  	require.Contains(t, s, "20220318104615")                        // retry second (partially applied)
   283  	require.Contains(t, s, "ALTER TABLE `tbl` ADD `col_3` bigint;") // does execute second stmt first second file
   284  	require.Contains(t, s, "ALTER TABLE `tbl` ADD `col_4` bigint;") // does execute second stmt first second file
   285  	require.Contains(t, s, "1 migrations")                          // logs amount of migrations
   286  	require.Contains(t, s, "2")                                     // logs amount of statement
   287  	require.NotContains(t, s, "Error: Execution had errors:")       // logs error summary
   288  	require.NotContains(t, s, "near \"asdasd\": syntax error")      // logs error summary
   289  
   290  	// Running again will report database being in clean state.
   291  	s, err = runCmd(
   292  		migrateApplyCmd(),
   293  		"--dir", "file://testdata/sqlite3",
   294  		"--url", fmt.Sprintf("sqlite://file:%s?cache=shared&_fk=1", filepath.Join(p, "test2.db")),
   295  	)
   296  	require.NoError(t, err)
   297  	require.Equal(t, "No migration files to execute\n", s)
   298  
   299  	// Dry run will print the statements in second migration file without executing them.
   300  	// No changes to the revisions will be done.
   301  	s, err = runCmd(
   302  		migrateApplyCmd(),
   303  		"--dir", "file://testdata/sqlite",
   304  		"--url", fmt.Sprintf("sqlite://file:%s?cache=shared&_fk=1", filepath.Join(p, "test.db")),
   305  		"--dry-run",
   306  		"1",
   307  	)
   308  	require.NoError(t, err)
   309  	require.Contains(t, s, "20220318104615")                        // log to version
   310  	require.Contains(t, s, "ALTER TABLE `tbl` ADD `col_2` bigint;") // logs statement
   311  	c1, err := sqlclient.Open(ctx, fmt.Sprintf("sqlite://file:%s?cache=shared&_fk=1", filepath.Join(p, "test.db")))
   312  	require.NoError(t, err)
   313  	t.Cleanup(func() {
   314  		require.NoError(t, c1.Close())
   315  	})
   316  	sch, err = c1.InspectSchema(ctx, "", nil)
   317  	tbl, ok = sch.Table("tbl")
   318  	require.True(t, ok)
   319  	_, ok = tbl.Column("col_2")
   320  	require.False(t, ok)
   321  	rrw, err = migrate2.NewEntRevisions(ctx, c1)
   322  	require.NoError(t, err)
   323  	revs, err = rrw.ReadRevisions(ctx)
   324  	require.NoError(t, err)
   325  	require.Len(t, revs, 1)
   326  
   327  	// Prerequisites for testing missing migration behavior.
   328  	c1, err = sqlclient.Open(ctx, fmt.Sprintf("sqlite://file:%s?cache=shared&_fk=1", filepath.Join(p, "test3.db")))
   329  	require.NoError(t, err)
   330  	t.Cleanup(func() {
   331  		require.NoError(t, c1.Close())
   332  	})
   333  	require.NoError(t, os.Rename(
   334  		"testdata/sqlite3/20220318104615_second.sql",
   335  		"testdata/sqlite3/20220318104616_second.sql",
   336  	))
   337  	_, err = runCmd(migrateHashCmd(), "--dir", "file://testdata/sqlite3")
   338  	require.NoError(t, err)
   339  	rrw, err = migrate2.NewEntRevisions(ctx, c1)
   340  	require.NoError(t, err)
   341  	require.NoError(t, rrw.Migrate(ctx))
   342  
   343  	// No changes if the last revision has a greater version than the last migration.
   344  	require.NoError(t, rrw.WriteRevision(ctx, &migrate.Revision{Version: "zzz"}))
   345  	s, err = runCmd(
   346  		migrateApplyCmd(),
   347  		"--dir", "file://testdata/sqlite3",
   348  		"--url", fmt.Sprintf("sqlite://file:%s?cache=shared&_fk=1", filepath.Join(p, "test3.db")),
   349  	)
   350  	require.NoError(t, err)
   351  	require.Equal(t, "No migration files to execute\n", s)
   352  
   353  	// If the revision is before the last but after the first migration, only the last one is pending.
   354  	_, err = c1.ExecContext(ctx, "DROP table `atlas_schema_revisions`")
   355  	require.NoError(t, err)
   356  	s, err = runCmd(
   357  		migrateApplyCmd(), "1",
   358  		"--dir", "file://testdata/sqlite3",
   359  		"--url", fmt.Sprintf("sqlite://file:%s?cache=shared&_fk=1", filepath.Join(p, "test3.db")),
   360  	)
   361  	require.NoError(t, rrw.WriteRevision(ctx, &migrate.Revision{Version: "20220318104615"}))
   362  	require.NoError(t, err)
   363  	s, err = runCmd(
   364  		migrateApplyCmd(),
   365  		"--dir", "file://testdata/sqlite3",
   366  		"--url", fmt.Sprintf("sqlite://file:%s?cache=shared&_fk=1", filepath.Join(p, "test3.db")),
   367  	)
   368  	require.NoError(t, err)
   369  	require.NotContains(t, s, "20220318104614")                     // log to version
   370  	require.Contains(t, s, "20220318104616")                        // log to version
   371  	require.Contains(t, s, "ALTER TABLE `tbl` ADD `col_2` bigint;") // logs statement
   372  
   373  	// If the revision is before every migration file, every file is pending.
   374  	_, err = c1.ExecContext(ctx, "DROP table `atlas_schema_revisions`; DROP table `tbl`;")
   375  	require.NoError(t, err)
   376  	require.NoError(t, rrw.Migrate(ctx))
   377  	require.NoError(t, rrw.WriteRevision(ctx, &migrate.Revision{Version: "1"}))
   378  	require.NoError(t, err)
   379  	s, err = runCmd(
   380  		migrateApplyCmd(),
   381  		"--dir", "file://testdata/sqlite3",
   382  		"--url", fmt.Sprintf("sqlite://file:%s?cache=shared&_fk=1", filepath.Join(p, "test3.db")),
   383  	)
   384  	require.NoError(t, err)
   385  	require.Contains(t, s, "20220318104614")                         // log to version
   386  	require.Contains(t, s, "20220318104616")                         // log to version
   387  	require.Contains(t, s, "CREATE TABLE tbl (`col` int NOT NULL);") // logs statement
   388  	require.Contains(t, s, "ALTER TABLE `tbl` ADD `col_2` bigint;")  // logs statement
   389  
   390  	// If the revision is partially applied, error out.
   391  	require.NoError(t, rrw.WriteRevision(ctx, &migrate.Revision{Version: "z", Description: "z", Total: 1}))
   392  	require.NoError(t, err)
   393  	_, err = runCmd(
   394  		migrateApplyCmd(),
   395  		"--dir", "file://testdata/sqlite3",
   396  		"--url", fmt.Sprintf("sqlite://file:%s?cache=shared&_fk=1", filepath.Join(p, "test3.db")),
   397  	)
   398  	require.EqualError(t, err, migrate.MissingMigrationError{Version: "z", Description: "z"}.Error())
   399  }
   400  
   401  func TestMigrate_ApplyMultiEnv(t *testing.T) {
   402  	t.Run("FromVars", func(t *testing.T) {
   403  		p := t.TempDir()
   404  		h := `
   405  variable "urls" {
   406    type = list(string)
   407  }
   408  
   409  env "local" {
   410    for_each = toset(var.urls)
   411    url = each.value
   412    dev = "sqlite://ci?mode=memory&cache=shared&_fk=1"
   413    migration {
   414      dir = "file://testdata/sqlite"
   415    }
   416  }
   417  `
   418  		path := filepath.Join(p, "atlas.hcl")
   419  		err := os.WriteFile(path, []byte(h), 0600)
   420  		require.NoError(t, err)
   421  		cmd := migrateCmd()
   422  		cmd.AddCommand(migrateApplyCmd())
   423  		s, err := runCmd(
   424  			cmd, "apply",
   425  			"-c", "file://"+path,
   426  			"--env", "local",
   427  			"--var", fmt.Sprintf("urls=sqlite://file:%s?cache=shared&_fk=1", filepath.Join(p, "test1.db")),
   428  			"--var", fmt.Sprintf("urls=sqlite://file:%s?cache=shared&_fk=1", filepath.Join(p, "test2.db")),
   429  		)
   430  		require.NoError(t, err)
   431  		require.Equal(t, 2, strings.Count(s, "Migrating to version 20220318104615 (2 migrations in total)"), "execution per environment")
   432  		_, err = os.Stat(filepath.Join(p, "test1.db"))
   433  		require.NoError(t, err)
   434  		_, err = os.Stat(filepath.Join(p, "test2.db"))
   435  		require.NoError(t, err)
   436  	})
   437  
   438  	t.Run("FromDataSrc", func(t *testing.T) {
   439  		var (
   440  			h = `
   441  variable "url" {
   442    type = string
   443  }
   444  
   445  locals {
   446    string = "%test"
   447    bool   = true
   448    int    = 1
   449  }
   450  
   451  data "sql" "tenants" {
   452    url   = var.url
   453    query = <<EOS
   454  SELECT name FROM tenants
   455  	WHERE mode LIKE ? AND active = ? AND created = ?
   456  EOS
   457    # Pass all types of arguments.
   458    args  = [local.string, local.bool, local.int]
   459  }
   460  
   461  env "local" {
   462    for_each = toset(data.sql.tenants.values)
   463    url = "sqlite://file:${each.value}?cache=shared&_fk=1"
   464    dev = "sqlite://ci?mode=memory&cache=shared&_fk=1"
   465    migration {
   466      dir = "file://testdata/sqlite"
   467    }
   468    log {
   469      migrate {
   470        apply = format(
   471          "{{ json . | json_merge %q }}",
   472          jsonencode({
   473            Tenant: each.value
   474          })
   475        )
   476      }
   477    }
   478  }
   479  `
   480  			p    = t.TempDir()
   481  			path = filepath.Join(p, "atlas.hcl")
   482  			dbs  = []string{filepath.Join(p, "test1.db"), filepath.Join(p, "test2.db"), filepath.Join(p, "test3.db")}
   483  		)
   484  		err := os.WriteFile(path, []byte(h), 0600)
   485  		require.NoError(t, err)
   486  		db, err := sql.Open("sqlite3", fmt.Sprintf("file:%s?cache=shared&_fk=1", filepath.Join(p, "tenants.db")))
   487  		require.NoError(t, err)
   488  		_, err = db.Exec("CREATE TABLE `tenants` (`name` TEXT, `mode` TEXT DEFAULT 'test', `active` BOOL DEFAULT TRUE, `created` INT DEFAULT 1);")
   489  		require.NoError(t, err)
   490  		_, err = db.Exec("INSERT INTO `tenants` (`name`) VALUES (?), (?), (?)", dbs[0], dbs[1], dbs[2])
   491  		require.NoError(t, err)
   492  
   493  		cmd := migrateCmd()
   494  		cmd.AddCommand(migrateApplyCmd())
   495  		s, err := runCmd(
   496  			cmd, "apply",
   497  			"-c", "file://"+path,
   498  			"--env", "local",
   499  			"--var", fmt.Sprintf("url=sqlite://file:%s?cache=shared&_fk=1", filepath.Join(p, "tenants.db")),
   500  		)
   501  		require.NoError(t, err)
   502  		require.Equal(t, 3, strings.Count(s, `"Tenant"`))
   503  		require.Equal(t, 3, strings.Count(s, `"Applied":[{"Applied":["CREATE TABLE tbl`), "execution per environment")
   504  		for i := range dbs {
   505  			_, err = os.Stat(dbs[i])
   506  			require.NoError(t, err)
   507  		}
   508  
   509  		cmd = migrateCmd()
   510  		cmd.AddCommand(migrateApplyCmd())
   511  		s, err = runCmd(
   512  			cmd, "apply",
   513  			"-c", "file://"+path,
   514  			"--env", "local",
   515  			"--var", fmt.Sprintf("url=sqlite://file:%s?cache=shared&_fk=1", filepath.Join(p, "tenants.db")),
   516  		)
   517  		require.NoError(t, err)
   518  		for _, s := range strings.Split(s, "\n") {
   519  			var r struct {
   520  				Tenant string
   521  				cmdlog.MigrateApply
   522  			}
   523  			require.NoError(t, json.Unmarshal([]byte(s), &r))
   524  			require.Empty(t, r.Pending)
   525  			require.Empty(t, r.Applied)
   526  			require.NotEmpty(t, r.Tenant)
   527  			require.Equal(t, "sqlite3", r.Driver)
   528  		}
   529  		_, err = db.Exec("INSERT INTO `tenants` (`name`) VALUES (NULL)")
   530  		require.NoError(t, err)
   531  		_, err = runCmd(
   532  			cmd, "apply",
   533  			"-c", "file://"+path,
   534  			"--env", "local",
   535  			"--var", fmt.Sprintf("url=sqlite://file:%s?cache=shared&_fk=1", filepath.Join(p, "tenants.db")),
   536  		)
   537  		// Rows should represent real and consistent values.
   538  		require.EqualError(t, err, "data.sql.tenants: unsupported row type: <nil>")
   539  
   540  		_, err = db.Exec("DELETE FROM `tenants`")
   541  		require.NoError(t, err)
   542  		s, err = runCmd(
   543  			cmd, "apply",
   544  			"-c", "file://"+path,
   545  			"--env", "local",
   546  			"--var", fmt.Sprintf("url=sqlite://file:%s?cache=shared&_fk=1", filepath.Join(p, "tenants.db")),
   547  		)
   548  		// Empty list is expanded to zero blocks.
   549  		require.EqualError(t, err, `env "local" not defined in project file`)
   550  	})
   551  
   552  	t.Run("TemplateDir", func(t *testing.T) {
   553  		var (
   554  			h = `
   555  variable "path" {
   556    type = string
   557  }
   558  
   559  data "template_dir" "migrations" {
   560    path = var.path
   561    vars = {
   562      Env = atlas.env
   563    }
   564  }
   565  
   566  env "dev" {
   567    url = "sqlite://${atlas.env}?mode=memory&_fk=1"
   568    migration {
   569      dir = data.template_dir.migrations.url
   570    }
   571  }
   572  
   573  env "prod" {
   574    url = "sqlite://${atlas.env}?mode=memory&_fk=1"
   575    migration {
   576      dir = data.template_dir.migrations.url
   577    }
   578  }
   579  `
   580  			p    = t.TempDir()
   581  			path = filepath.Join(p, "atlas.hcl")
   582  		)
   583  		err := os.WriteFile(path, []byte(h), 0600)
   584  		require.NoError(t, err)
   585  		for _, e := range []string{"dev", "prod"} {
   586  			cmd := migrateCmd()
   587  			cmd.AddCommand(migrateApplyCmd())
   588  			s, err := runCmd(
   589  				cmd, "apply",
   590  				"-c", "file://"+path,
   591  				"--env", e,
   592  				"--var", "path=testdata/templatedir",
   593  			)
   594  			require.NoError(t, err)
   595  			require.Contains(t, s, "Migrating to version 2 (2 migrations in total):")
   596  			require.Contains(t, s, fmt.Sprintf("create table %s1 (c text);", e))
   597  			require.Contains(t, s, fmt.Sprintf("create table %s2 (c text);", e))
   598  			require.Contains(t, s, fmt.Sprintf("create table users_%s2 (c text);", e))
   599  		}
   600  	})
   601  }
   602  
   603  func TestMigrate_ApplyTxMode(t *testing.T) {
   604  	for _, mode := range []string{"none", "file", "all"} {
   605  		t.Run(mode, func(t *testing.T) {
   606  			p := t.TempDir()
   607  			// Apply the first 2 migrations.
   608  			s, err := runCmd(
   609  				migrateApplyCmd(),
   610  				"--dir", "file://testdata/sqlitetx",
   611  				"--url", fmt.Sprintf("sqlite://file:%s?cache=shared&_fk=1", filepath.Join(p, "test.db")),
   612  				"--tx-mode", mode,
   613  				"2",
   614  			)
   615  			require.NoError(t, err)
   616  			require.NotEmpty(t, s)
   617  			db, err := sql.Open("sqlite3", fmt.Sprintf("file:%s?cache=shared&_fk=1", filepath.Join(p, "test.db")))
   618  			require.NoError(t, err)
   619  			var n int
   620  			require.NoError(t, db.QueryRow("SELECT COUNT(*) FROM `friendships`").Scan(&n))
   621  			require.Equal(t, 2, n)
   622  
   623  			// Apply the rest.
   624  			s, err = runCmd(
   625  				migrateApplyCmd(),
   626  				"--dir", "file://testdata/sqlitetx",
   627  				"--url", fmt.Sprintf("sqlite://file:%s?cache=shared&_fk=1", filepath.Join(p, "test.db")),
   628  				"--tx-mode", mode,
   629  			)
   630  			require.NoError(t, err)
   631  			require.NoError(t, db.QueryRow("SELECT COUNT(*) FROM `friendships`").Scan(&n))
   632  			require.Equal(t, 2, n)
   633  
   634  			// For transactions check that the foreign keys are checked before the transaction is committed.
   635  			if mode != "none" {
   636  				// Apply the first 2 migrations for the faulty one.
   637  				s, err = runCmd(
   638  					migrateApplyCmd(),
   639  					"--dir", "file://testdata/sqlitetx2",
   640  					"--url", fmt.Sprintf("sqlite://file:%s?cache=shared&_fk=1", filepath.Join(p, "test_2.db")),
   641  					"--tx-mode", mode,
   642  					"2",
   643  				)
   644  				require.NoError(t, err)
   645  				require.NotEmpty(t, s)
   646  				db, err = sql.Open("sqlite3", fmt.Sprintf("file:%s?cache=shared&_fk=1", filepath.Join(p, "test_2.db")))
   647  				require.NoError(t, err)
   648  				require.NoError(t, db.QueryRow("SELECT COUNT(*) FROM `friendships`").Scan(&n))
   649  				require.Equal(t, 2, n)
   650  
   651  				// Add an existing constraint.
   652  				c, err := sqlclient.Open(context.Background(), fmt.Sprintf("sqlite://file:%s?cache=shared&_fk=1", filepath.Join(p, "test_2.db")))
   653  				require.NoError(t, err)
   654  				_, err = c.ExecContext(context.Background(), "PRAGMA foreign_keys = off; INSERT INTO `friendships` (`user_id`, `friend_id`) VALUES (3,3);PRAGMA foreign_keys = on;")
   655  				require.NoError(t, err)
   656  				require.NoError(t, db.QueryRow("SELECT COUNT(*) FROM `friendships`").Scan(&n))
   657  				require.Equal(t, 3, n)
   658  
   659  				// Apply the rest, expect it to fail due to constraint error, but only the new one is reported.
   660  				s, err = runCmd(
   661  					migrateApplyCmd(),
   662  					"--dir", "file://testdata/sqlitetx2",
   663  					"--url", fmt.Sprintf("sqlite://file:%s?cache=shared&_fk=1", filepath.Join(p, "test_2.db")),
   664  					"--tx-mode", mode,
   665  				)
   666  				require.EqualError(t, err, "sql/sqlite: foreign key mismatch: [{tbl:friendships ref:users row:4 index:1}]")
   667  				require.NoError(t, db.QueryRow("SELECT COUNT(*) FROM `friendships`").Scan(&n))
   668  				require.Equal(t, 3, n) // was rolled back
   669  			}
   670  		})
   671  	}
   672  }
   673  
   674  func TestMigrate_ApplyTxModeDirective(t *testing.T) {
   675  	for _, mode := range []string{txModeNone, txModeFile} {
   676  		u := openSQLite(t, "")
   677  		_, err := runCmd(
   678  			migrateApplyCmd(),
   679  			"--dir", "file://testdata/sqlitetx3",
   680  			"--url", u,
   681  			"--tx-mode", mode,
   682  		)
   683  		require.EqualError(t, err, `sql/migrate: execute: executing statement "INSERT INTO t1 VALUES (1), (1);" from version "20220925094021": UNIQUE constraint failed: t1.a`)
   684  		db, err := sql.Open("sqlite3", strings.TrimPrefix(u, "sqlite://"))
   685  		require.NoError(t, err)
   686  		var n int
   687  		require.NoError(t, db.QueryRow("SELECT COUNT(*) FROM sqlite_master WHERE name IN ('atlas_schema_revisions', 'users', 't1')").Scan(&n))
   688  		require.Equal(t, 3, n)
   689  		require.NoError(t, db.Close())
   690  	}
   691  
   692  	_, err := runCmd(
   693  		migrateApplyCmd(),
   694  		"--dir", "file://testdata/sqlitetx3",
   695  		"--url", "sqlite://txmode?mode=memory&_fk=1",
   696  		"--tx-mode", txModeAll,
   697  	)
   698  	require.EqualError(t, err, `cannot set txmode directive to "none" in "20220925094021_second.sql" when txmode "all" is set globally`)
   699  
   700  	s, err := runCmd(
   701  		migrateApplyCmd(),
   702  		"--dir", "file://testdata/sqlitetx4",
   703  		"--url", "sqlite://txmode?mode=memory&_fk=1",
   704  		"--tx-mode", txModeAll,
   705  		"--log", "{{ .Error }}",
   706  	)
   707  	require.EqualError(t, err, `unknown txmode "unknown" found in file directive "20220925094021_second.sql"`)
   708  	// Errors should be attached to the report.
   709  	require.Equal(t, s, `unknown txmode "unknown" found in file directive "20220925094021_second.sql"`)
   710  }
   711  
   712  func TestMigrate_ApplyBaseline(t *testing.T) {
   713  	t.Run("FromFlags", func(t *testing.T) {
   714  		p := t.TempDir()
   715  		// Run migration with baseline should store this revision in the database.
   716  		s, err := runCmd(
   717  			migrateApplyCmd(),
   718  			"--dir", "file://testdata/baseline1",
   719  			"--baseline", "1",
   720  			"--url", fmt.Sprintf("sqlite://file:%s?cache=shared&_fk=1", filepath.Join(p, "test1.db")),
   721  		)
   722  		require.NoError(t, err)
   723  		require.Contains(t, s, "No migration files to execute")
   724  		// Next run without baseline should run the migration from the baseline.
   725  		s, err = runCmd(
   726  			migrateApplyCmd(),
   727  			"--dir", "file://testdata/baseline1",
   728  			"--url", fmt.Sprintf("sqlite://file:%s?cache=shared&_fk=1", filepath.Join(p, "test1.db")),
   729  		)
   730  		require.NoError(t, err)
   731  		require.Contains(t, s, "No migration files to execute")
   732  
   733  		// Multiple migration files with baseline.
   734  		s, err = runCmd(
   735  			migrateApplyCmd(),
   736  			"--dir", "file://testdata/baseline2",
   737  			"--baseline", "1",
   738  			"--url", fmt.Sprintf("sqlite://file:%s?cache=shared&_fk=1", filepath.Join(p, "test2.db")),
   739  		)
   740  		require.NoError(t, err)
   741  		require.Contains(t, s, "Migrating to version 20220318104615 from 1 (2 migrations in total)")
   742  
   743  		// Run all migration files and skip baseline.
   744  		s, err = runCmd(
   745  			migrateApplyCmd(),
   746  			"--dir", "file://testdata/baseline2",
   747  			"--url", fmt.Sprintf("sqlite://file:%s?cache=shared&_fk=1", filepath.Join(p, "test3.db")),
   748  		)
   749  		require.NoError(t, err)
   750  		require.Contains(t, s, "Migrating to version 20220318104615 (3 migrations in total)")
   751  	})
   752  
   753  	t.Run("FromConfig", func(t *testing.T) {
   754  		const h = `
   755  env "local" {
   756    migration {
   757      baseline = "1"
   758    }
   759  }`
   760  		p := t.TempDir()
   761  		path := filepath.Join(p, "atlas.hcl")
   762  		err := os.WriteFile(path, []byte(h), 0600)
   763  		require.NoError(t, err)
   764  		cmd := migrateCmd()
   765  		cmd.AddCommand(migrateApplyCmd())
   766  		s, err := runCmd(
   767  			cmd, "apply",
   768  			"-c", "file://"+path,
   769  			"--env", "local",
   770  			"--dir", "file://testdata/baseline1",
   771  			"--url", fmt.Sprintf("sqlite://file:%s?cache=shared&_fk=1", filepath.Join(p, "test1.db")),
   772  		)
   773  		require.NoError(t, err)
   774  		require.Contains(t, s, "No migration files to execute")
   775  
   776  		cmd = migrateCmd()
   777  		cmd.AddCommand(migrateApplyCmd())
   778  		s, err = runCmd(
   779  			cmd, "apply",
   780  			"-c", "file://"+path,
   781  			"--env", "local",
   782  			"--dir", "file://testdata/baseline2",
   783  			"--url", fmt.Sprintf("sqlite://file:%s?cache=shared&_fk=1", filepath.Join(p, "test2.db")),
   784  		)
   785  		require.NoError(t, err)
   786  		require.Contains(t, s, "Migrating to version 20220318104615 from 1 (2 migrations in total)")
   787  	})
   788  }
   789  
   790  func TestMigrate_ApplyCloudReport(t *testing.T) {
   791  	var (
   792  		dir    migrate.MemDir
   793  		status int
   794  		report cloudapi.ReportMigrationInput
   795  		srv    = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   796  			var m struct {
   797  				Query     string `json:"query"`
   798  				Variables struct {
   799  					Input json.RawMessage `json:"input"`
   800  				} `json:"variables"`
   801  			}
   802  			require.NoError(t, json.NewDecoder(r.Body).Decode(&m))
   803  			switch {
   804  			case strings.Contains(m.Query, "query"):
   805  				// Checksum before archiving.
   806  				hf, err := dir.Checksum()
   807  				require.NoError(t, err)
   808  				ht, err := hf.MarshalText()
   809  				require.NoError(t, err)
   810  				require.NoError(t, dir.WriteFile(migrate.HashFileName, ht))
   811  				// Archive and send.
   812  				arc, err := migrate.ArchiveDir(&dir)
   813  				require.NoError(t, err)
   814  				fmt.Fprintf(w, `{"data":{"dir":{"content":%q}}}`, base64.StdEncoding.EncodeToString(arc))
   815  			case strings.Contains(m.Query, "mutation"):
   816  				if status != 0 {
   817  					w.WriteHeader(status)
   818  				}
   819  				require.NoError(t, json.Unmarshal(m.Variables.Input, &report))
   820  			default:
   821  				t.Fatalf("unexpected query: %s", m.Query)
   822  			}
   823  		}))
   824  		h = `
   825  variable "cloud_url" {
   826    type = string
   827  }
   828  
   829  atlas {
   830    cloud {
   831      token   = "token"
   832      url     = var.cloud_url
   833      project = "example"
   834    }
   835  }
   836  
   837  data "remote_dir" "migrations" {
   838    name = "migrations"
   839  }
   840  
   841  env {
   842    name = atlas.env
   843    migration {
   844      dir = data.remote_dir.migrations.url
   845    }
   846  }
   847  `
   848  		u    = openSQLite(t, "")
   849  		p    = t.TempDir()
   850  		path = filepath.Join(p, "atlas.hcl")
   851  	)
   852  	t.Cleanup(srv.Close)
   853  	require.NoError(t, os.WriteFile(path, []byte(h), 0600))
   854  
   855  	t.Run("NoPendingFiles", func(t *testing.T) {
   856  		cmd := migrateCmd()
   857  		cmd.AddCommand(migrateApplyCmd())
   858  		s, err := runCmd(
   859  			cmd, "apply",
   860  			"-c", "file://"+path,
   861  			"--env", "local",
   862  			"--url", u,
   863  			"--var", "cloud_url="+srv.URL,
   864  		)
   865  		require.NoError(t, err)
   866  		require.Equal(t, "No migration files to execute\n", s)
   867  		require.NotEmpty(t, report.Target.ID)
   868  		_, err = uuid.Parse(report.Target.ID)
   869  		require.NoError(t, err, "target id is not a valid uuid")
   870  		require.False(t, report.StartTime.IsZero())
   871  		require.False(t, report.EndTime.IsZero())
   872  		require.Equal(t, cloudapi.ReportMigrationInput{
   873  			ProjectName:  "example",
   874  			DirName:      "migrations",
   875  			EnvName:      "local",
   876  			AtlasVersion: "Atlas CLI - development",
   877  			StartTime:    report.StartTime,
   878  			EndTime:      report.EndTime,
   879  			Files:        []cloudapi.DeployedFileInput{},
   880  			Target: cloudapi.DeployedTargetInput{
   881  				ID:     report.Target.ID, // generated uuid
   882  				Schema: "main",
   883  				URL:    u,
   884  			},
   885  			Log: "No migration files to execute\n",
   886  		}, report)
   887  	})
   888  
   889  	t.Run("WithFiles", func(t *testing.T) {
   890  		require.NoError(t, dir.WriteFile("1.sql", []byte("create table foo (id int)")))
   891  		require.NoError(t, dir.WriteFile("2.sql", []byte("create table bar (id int)")))
   892  
   893  		cmd := migrateCmd()
   894  		cmd.AddCommand(migrateApplyCmd())
   895  		s, err := runCmd(
   896  			cmd, "apply",
   897  			"-c", "file://"+path,
   898  			"--env", "local",
   899  			"--url", u,
   900  			"--var", "cloud_url="+srv.URL,
   901  		)
   902  		require.NoError(t, err)
   903  		// Reporting does not affect the output.
   904  		require.True(t, strings.HasPrefix(s, "Migrating to version 2 (2 migrations in total):"))
   905  		require.True(t, strings.HasSuffix(s, "  -- 2 migrations \n  -- 2 sql statements\n"))
   906  		require.Equal(t, "", report.FromVersion, "from empty database")
   907  		require.Equal(t, "2", report.ToVersion)
   908  		require.Equal(t, "2", report.CurrentVersion)
   909  		require.Len(t, report.Files, 2)
   910  		for i, n := range []string{"1.sql", "2.sql"} {
   911  			require.Equal(t, n, report.Files[i].Name)
   912  			require.Equal(t, 1, report.Files[i].Applied)
   913  			require.Zero(t, report.Files[i].Skipped)
   914  			require.Nil(t, report.Files[i].Error)
   915  		}
   916  	})
   917  
   918  	t.Run("PrintError", func(t *testing.T) {
   919  		status = http.StatusInternalServerError
   920  		require.NoError(t, dir.WriteFile("3.sql", []byte("create table baz (id int)")))
   921  		cmd := migrateCmd()
   922  		cmd.AddCommand(migrateApplyCmd())
   923  		s, err := runCmd(
   924  			cmd, "apply",
   925  			"-c", "file://"+path,
   926  			"--env", "local",
   927  			"--url", u,
   928  			"--var", "cloud_url="+srv.URL,
   929  		)
   930  		require.NoError(t, err)
   931  		// Reporting error should not affect the migration execution.
   932  		require.True(t, strings.HasSuffix(s, "  -- 1 migrations \n  -- 1 sql statements\nError: unexpected status code: 500\n"))
   933  
   934  		// Custom logging.
   935  		cmd = migrateCmd()
   936  		cmd.AddCommand(migrateApplyCmd())
   937  		s, err = runCmd(
   938  			cmd, "apply",
   939  			"-c", "file://"+path,
   940  			"--env", "local",
   941  			"--url", u,
   942  			"--var", "cloud_url="+srv.URL,
   943  			"--format", "{{ .Env.Driver }}",
   944  		)
   945  		require.NoError(t, err)
   946  		require.Equal(t, "sqlite3\nError: unexpected status code: 500\n", s)
   947  	})
   948  }
   949  
   950  func TestMigrate_ApplyCloudReportSet(t *testing.T) {
   951  	var (
   952  		dir    migrate.MemDir
   953  		status int
   954  		report cloudapi.ReportMigrationSetInput
   955  		srv    = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   956  			var m struct {
   957  				Query     string `json:"query"`
   958  				Variables struct {
   959  					Input json.RawMessage `json:"input"`
   960  				} `json:"variables"`
   961  			}
   962  			require.NoError(t, json.NewDecoder(r.Body).Decode(&m))
   963  			switch {
   964  			case strings.Contains(m.Query, "query"):
   965  				// Checksum before archiving.
   966  				hf, err := dir.Checksum()
   967  				require.NoError(t, err)
   968  				ht, err := hf.MarshalText()
   969  				require.NoError(t, err)
   970  				require.NoError(t, dir.WriteFile(migrate.HashFileName, ht))
   971  				// Archive and send.
   972  				arc, err := migrate.ArchiveDir(&dir)
   973  				require.NoError(t, err)
   974  				fmt.Fprintf(w, `{"data":{"dir":{"content":%q}}}`, base64.StdEncoding.EncodeToString(arc))
   975  			case strings.Contains(m.Query, "mutation"):
   976  				if status != 0 {
   977  					w.WriteHeader(status)
   978  				}
   979  				require.NoError(t, json.Unmarshal(m.Variables.Input, &report))
   980  			default:
   981  				t.Fatalf("unexpected query: %s", m.Query)
   982  			}
   983  		}))
   984  		h = `
   985  variable "cloud_url" {
   986    type = string
   987  }
   988  
   989  atlas {
   990    cloud {
   991      token   = "token"
   992      url     = var.cloud_url
   993      project = "example"
   994    }
   995  }
   996  
   997  data "remote_dir" "migration_set" {
   998    name = "migration_set"
   999  }
  1000  
  1001  variable "urls" {
  1002    type    = list(string)
  1003    default = []
  1004  }
  1005  
  1006  env {
  1007    name = atlas.env
  1008    for_each = toset(var.urls)
  1009    url = each.value
  1010    migration {
  1011      dir = data.remote_dir.migration_set.url
  1012    }
  1013  }
  1014  `
  1015  		p    = t.TempDir()
  1016  		path = filepath.Join(p, "atlas.hcl")
  1017  	)
  1018  	t.Cleanup(srv.Close)
  1019  	require.NoError(t, os.WriteFile(path, []byte(h), 0600))
  1020  
  1021  	t.Run("MultipleEnvs", func(t *testing.T) {
  1022  		cmd := migrateCmd()
  1023  		cmd.AddCommand(migrateApplyCmd())
  1024  		s, err := runCmd(
  1025  			cmd, "apply",
  1026  			"-c", "file://"+path,
  1027  			"--env", "local",
  1028  			"--var", fmt.Sprintf("urls=%s", openSQLite(t, "")),
  1029  			"--var", fmt.Sprintf("urls=%s", openSQLite(t, "")),
  1030  			"--var", "cloud_url="+srv.URL,
  1031  		)
  1032  		require.NoError(t, err)
  1033  		require.Equal(t, "No migration files to execute\nNo migration files to execute\n", s)
  1034  		require.NotEmpty(t, report.ID)
  1035  		_, err = uuid.Parse(report.ID)
  1036  		require.NoError(t, err, "set id is not a valid uuid")
  1037  		require.False(t, report.StartTime.IsZero())
  1038  		require.False(t, report.EndTime.IsZero())
  1039  		require.Equal(t, 2, report.Planned)
  1040  		require.Len(t, report.Completed, 2)
  1041  		require.Nil(t, report.Error)
  1042  
  1043  		// Verify summary log.
  1044  		require.Len(t, report.Log, 3)
  1045  		top := report.Log[0]
  1046  		require.Equal(t, top.Text, "Start migration for 2 targets")
  1047  		require.Contains(t, top.Log[0].Text, "sqlite://file")
  1048  		require.Contains(t, top.Log[1].Text, "sqlite://file")
  1049  		require.Equal(t, report.Log[1].Text, "Run migration: 1")
  1050  		require.Contains(t, report.Log[1].Log[0].Text, "Target URL: sqlite://file")
  1051  		require.Contains(t, report.Log[1].Log[1].Text, "Migration directory: mem://migration_set")
  1052  		require.Equal(t, report.Log[2].Text, "Run migration: 2")
  1053  		require.Contains(t, report.Log[2].Log[0].Text, "Target URL: sqlite://file")
  1054  		require.Contains(t, report.Log[2].Log[1].Text, "Migration directory: mem://migration_set")
  1055  	})
  1056  
  1057  	t.Run("Error", func(t *testing.T) {
  1058  		require.NoError(t, dir.WriteFile("1.sql", []byte("create table foo (id int)")))
  1059  		require.NoError(t, dir.WriteFile("2.sql", []byte("create table bar (id int)")))
  1060  
  1061  		cmd := migrateCmd()
  1062  		cmd.AddCommand(migrateApplyCmd())
  1063  		s, err := runCmd(
  1064  			cmd, "apply",
  1065  			"-c", "file://"+path,
  1066  			"--env", "local",
  1067  			"--var", fmt.Sprintf("urls=%s", openSQLite(t, "create table bar (id int)")),
  1068  			"--var", fmt.Sprintf("urls=%s", openSQLite(t, "create table bar (id int)")),
  1069  			"--var", "cloud_url="+srv.URL,
  1070  			"--allow-dirty",
  1071  		)
  1072  		require.EqualError(t, err, `sql/migrate: execute: executing statement "create table bar (id int)" from version "2": table bar already exists`)
  1073  		require.Contains(t, s, "Migrating to version 2 (2 migrations in total):")
  1074  		require.Contains(t, s, "Error: sql/migrate:")
  1075  		require.NotEmpty(t, report.ID)
  1076  		_, err = uuid.Parse(report.ID)
  1077  		require.NoError(t, err, "set id is not a valid uuid")
  1078  		require.False(t, report.StartTime.IsZero())
  1079  		require.False(t, report.EndTime.IsZero())
  1080  		require.Equal(t, 2, report.Planned)
  1081  		require.Len(t, report.Completed, 1)
  1082  		require.NotNil(t, report.Error)
  1083  		require.Equal(t, `Error: sql/migrate: execute: executing statement "create table bar (id int)" from version "2": table bar already exists`, *report.Error)
  1084  
  1085  		// Verify summary log.
  1086  		require.Len(t, report.Log, 2)
  1087  		top := report.Log[0]
  1088  		require.Equal(t, top.Text, "Start migration for 2 targets")
  1089  		require.Contains(t, top.Log[0].Text, "sqlite://file")
  1090  		require.Contains(t, top.Log[1].Text, "sqlite://file")
  1091  		require.Equal(t, report.Log[1].Text, "Run migration: 1")
  1092  		require.True(t, report.Log[1].Error)
  1093  		require.Contains(t, report.Log[1].Log[0].Text, "Target URL: sqlite://file")
  1094  		require.Contains(t, report.Log[1].Log[1].Text, "Migration directory: mem://migration_set")
  1095  		require.Equal(t, `Error: sql/migrate: execute: executing statement "create table bar (id int)" from version "2": table bar already exists`, report.Log[1].Log[2].Text)
  1096  	})
  1097  }
  1098  
  1099  func TestMigrate_Diff(t *testing.T) {
  1100  	p := t.TempDir()
  1101  	to := hclURL(t)
  1102  
  1103  	// Will create migration directory if not existing.
  1104  	_, err := runCmd(
  1105  		migrateDiffCmd(),
  1106  		"name",
  1107  		"--dir", "file://"+filepath.Join(p, "migrations"),
  1108  		"--dev-url", openSQLite(t, ""),
  1109  		"--to", to,
  1110  	)
  1111  	require.NoError(t, err)
  1112  	require.FileExists(t, filepath.Join(p, "migrations", fmt.Sprintf("%s_name.sql", time.Now().UTC().Format("20060102150405"))))
  1113  
  1114  	// Expect no clean dev error.
  1115  	p = t.TempDir()
  1116  	s, err := runCmd(
  1117  		migrateDiffCmd(),
  1118  		"name",
  1119  		"--dir", "file://"+p,
  1120  		"--dev-url", openSQLite(t, "create table t (c int);"),
  1121  		"--to", to,
  1122  	)
  1123  	require.ErrorAs(t, err, new(*migrate.NotCleanError))
  1124  	require.ErrorContains(t, err, "found table \"t\"")
  1125  
  1126  	// Works (on empty directory).
  1127  	s, err = runCmd(
  1128  		migrateDiffCmd(),
  1129  		"name",
  1130  		"--dir", "file://"+p,
  1131  		"--dev-url", openSQLite(t, ""),
  1132  		"--to", to,
  1133  	)
  1134  	require.NoError(t, err)
  1135  	require.Zero(t, s)
  1136  	require.FileExists(t, filepath.Join(p, fmt.Sprintf("%s_name.sql", time.Now().UTC().Format("20060102150405"))))
  1137  	require.FileExists(t, filepath.Join(p, "atlas.sum"))
  1138  
  1139  	// A lock will prevent diffing.
  1140  	sqlclient.Register("sqlitelockdiff", sqlclient.OpenerFunc(func(ctx context.Context, u *url.URL) (*sqlclient.Client, error) {
  1141  		u.Scheme = "sqlite"
  1142  		client, err := sqlclient.OpenURL(ctx, u)
  1143  		if err != nil {
  1144  			return nil, err
  1145  		}
  1146  		client.Driver = &sqliteLockerDriver{Driver: client.Driver}
  1147  		return client, nil
  1148  	}))
  1149  	f, err := os.Create(filepath.Join(p, "test.db"))
  1150  	require.NoError(t, err)
  1151  	require.NoError(t, f.Close())
  1152  	s, err = runCmd(
  1153  		migrateDiffCmd(),
  1154  		"name",
  1155  		"--dir", "file://"+t.TempDir(),
  1156  		"--dev-url", fmt.Sprintf("sqlitelockdiff://file:%s?cache=shared&_fk=1", filepath.Join(p, "test.db")),
  1157  		"--to", to,
  1158  	)
  1159  	require.True(t, strings.HasPrefix(s, "Error: acquiring database lock: "+errLock.Error()))
  1160  	require.ErrorIs(t, err, errLock)
  1161  
  1162  	t.Run("Edit", func(t *testing.T) {
  1163  		p := t.TempDir()
  1164  		require.NoError(t, os.Setenv("EDITOR", "echo '-- Comment' >>"))
  1165  		t.Cleanup(func() { require.NoError(t, os.Unsetenv("EDITOR")) })
  1166  		args := []string{
  1167  			"--edit",
  1168  			"--dir", "file://" + p,
  1169  			"--dev-url", openSQLite(t, ""),
  1170  			"--to", to,
  1171  		}
  1172  		_, err := runCmd(migrateDiffCmd(), args...)
  1173  		files, err := os.ReadDir(p)
  1174  		require.NoError(t, err)
  1175  		require.Len(t, files, 2)
  1176  		b, err := os.ReadFile(filepath.Join(p, files[0].Name()))
  1177  		require.NoError(t, err)
  1178  		require.Contains(t, string(b), "CREATE")
  1179  		require.True(t, strings.HasSuffix(string(b), "-- Comment\n"))
  1180  		require.Equal(t, "atlas.sum", files[1].Name())
  1181  
  1182  		// Second run will have no effect.
  1183  		_, err = runCmd(migrateDiffCmd(), args...)
  1184  		require.NoError(t, err)
  1185  		files, err = os.ReadDir(p)
  1186  		require.NoError(t, err)
  1187  		require.Len(t, files, 2)
  1188  	})
  1189  
  1190  	t.Run("Format", func(t *testing.T) {
  1191  		for f, out := range map[string]string{
  1192  			"{{sql .}}":            "CREATE TABLE `t` (`c` int NULL);",
  1193  			`{{- sql . "  " -}}`:   "CREATE TABLE `t` (\n  `c` int NULL\n);",
  1194  			"{{ sql . \"\t\" }}":   "CREATE TABLE `t` (\n\t`c` int NULL\n);",
  1195  			"{{sql $ \"  \t  \"}}": "CREATE TABLE `t` (\n  \t  `c` int NULL\n);",
  1196  		} {
  1197  			p := t.TempDir()
  1198  			d, err := migrate.NewLocalDir(p)
  1199  			require.NoError(t, err)
  1200  			// Works with indentation.
  1201  			s, err = runCmd(
  1202  				migrateDiffCmd(),
  1203  				"name",
  1204  				"--dir", "file://"+p,
  1205  				"--dev-url", openSQLite(t, ""),
  1206  				"--to", openSQLite(t, "create table t (c int);"),
  1207  				"--format", f,
  1208  			)
  1209  			require.NoError(t, err)
  1210  			require.Zero(t, s)
  1211  			files, err := d.Files()
  1212  			require.NoError(t, err)
  1213  			require.Len(t, files, 1)
  1214  			require.Equal(t, "-- Create \"t\" table\n"+out+"\n", string(files[0].Bytes()))
  1215  		}
  1216  
  1217  		// Invalid use of sql.
  1218  		s, err = runCmd(
  1219  			migrateDiffCmd(),
  1220  			"name",
  1221  			"--dir", "file://"+p,
  1222  			"--dev-url", openSQLite(t, ""),
  1223  			"--to", openSQLite(t, "create table t (c int);"),
  1224  			"--format", `{{ if . }}{{ sql . "  " }}{{ end }}`,
  1225  		)
  1226  		require.EqualError(t, err, `'sql' can only be used to indent statements. got: {{if .}}{{sql . "  "}}{{end}}`)
  1227  
  1228  		// Valid template.
  1229  		p := t.TempDir()
  1230  		d, err := migrate.NewLocalDir(p)
  1231  		require.NoError(t, err)
  1232  		s, err = runCmd(
  1233  			migrateDiffCmd(),
  1234  			"name",
  1235  			"--dir", "file://"+p,
  1236  			"--dev-url", openSQLite(t, ""),
  1237  			"--to", openSQLite(t, "create table t (c int);"),
  1238  			"--format", `{{ range .Changes }}{{ .Cmd }}{{ end }}`,
  1239  		)
  1240  		require.NoError(t, err)
  1241  		files, err := d.Files()
  1242  		require.NoError(t, err)
  1243  		require.Len(t, files, 1)
  1244  		require.Equal(t, "CREATE TABLE `t` (`c` int NULL)", string(files[0].Bytes()))
  1245  	})
  1246  
  1247  	t.Run("ProjectFile", func(t *testing.T) {
  1248  		p := t.TempDir()
  1249  		h := `
  1250  variable "schema" {
  1251    type = string
  1252  }
  1253  
  1254  variable "dir" {
  1255    type = string
  1256  }
  1257  
  1258  variable "destructive" {
  1259    type = bool
  1260    default = false
  1261  }
  1262  
  1263  env "local" {
  1264    src = "file://${var.schema}"
  1265    dev = "sqlite://ci?mode=memory&_fk=1"
  1266    migration {
  1267      dir = "file://${var.dir}"
  1268    }
  1269    diff {
  1270      skip {
  1271        drop_column = !var.destructive
  1272      }
  1273    }
  1274  }
  1275  `
  1276  		pathC := filepath.Join(p, "atlas.hcl")
  1277  		require.NoError(t, os.WriteFile(pathC, []byte(h), 0600))
  1278  		pathS := filepath.Join(p, "schema.sql")
  1279  		require.NoError(t, os.WriteFile(pathS, []byte(`CREATE TABLE t(c1 int, c2 int);`), 0600))
  1280  		pathD := t.TempDir()
  1281  		cmd := migrateCmd()
  1282  		cmd.AddCommand(migrateDiffCmd())
  1283  		s, err := runCmd(
  1284  			cmd, "diff", "initial",
  1285  			"-c", "file://"+pathC,
  1286  			"--env", "local",
  1287  			"--var", "schema="+pathS,
  1288  			"--var", "dir="+pathD,
  1289  		)
  1290  		require.NoError(t, err)
  1291  		require.Empty(t, s)
  1292  		d, err := migrate.NewLocalDir(pathD)
  1293  		require.NoError(t, err)
  1294  		files, err := d.Files()
  1295  		require.NoError(t, err)
  1296  		require.Len(t, files, 1)
  1297  		require.Equal(t, "-- Create \"t\" table\nCREATE TABLE `t` (`c1` int NULL, `c2` int NULL);\n", string(files[0].Bytes()))
  1298  
  1299  		// Drop column should be skipped.
  1300  		require.NoError(t, os.WriteFile(pathS, []byte(`CREATE TABLE t(c1 int);`), 0600))
  1301  		cmd = migrateCmd()
  1302  		cmd.AddCommand(migrateDiffCmd())
  1303  		s, err = runCmd(
  1304  			cmd, "diff", "no_change",
  1305  			"-c", "file://"+pathC,
  1306  			"--env", "local",
  1307  			"--var", "schema="+pathS,
  1308  			"--var", "dir="+pathD,
  1309  		)
  1310  		require.NoError(t, err)
  1311  		require.Equal(t, "The migration directory is synced with the desired state, no changes to be made\n", s)
  1312  		files, err = d.Files()
  1313  		require.NoError(t, err)
  1314  		require.Len(t, files, 1)
  1315  
  1316  		// Column is dropped when destructive is true.
  1317  		cmd = migrateCmd()
  1318  		cmd.AddCommand(migrateDiffCmd())
  1319  		s, err = runCmd(
  1320  			cmd, "diff", "second",
  1321  			"-c", "file://"+pathC,
  1322  			"--env", "local",
  1323  			"--var", "schema="+pathS,
  1324  			"--var", "dir="+pathD,
  1325  			"--var", "destructive=true",
  1326  		)
  1327  		require.NoError(t, err)
  1328  		require.Empty(t, s)
  1329  		files, err = d.Files()
  1330  		require.NoError(t, err)
  1331  		require.Len(t, files, 2)
  1332  	})
  1333  }
  1334  
  1335  func TestMigrate_StatusJSON(t *testing.T) {
  1336  	p := t.TempDir()
  1337  	s, err := runCmd(
  1338  		migrateStatusCmd(),
  1339  		"--dir", "file://"+p,
  1340  		"-u", openSQLite(t, ""),
  1341  		"--format", "{{ json .Env.Driver }}",
  1342  	)
  1343  	require.NoError(t, err)
  1344  	require.Equal(t, `"sqlite3"`, s)
  1345  }
  1346  
  1347  func TestMigrate_Set(t *testing.T) {
  1348  	u := fmt.Sprintf("sqlite://file:%s?_fk=1", filepath.Join(t.TempDir(), "test.db"))
  1349  	_, err := runCmd(
  1350  		migrateApplyCmd(),
  1351  		"--dir", "file://testdata/sqlite",
  1352  		"--url", u,
  1353  	)
  1354  	require.NoError(t, err)
  1355  
  1356  	s, err := runCmd(
  1357  		migrateSetCmd(),
  1358  		"--dir", "file://testdata/sqlite",
  1359  		"-u", u,
  1360  		"20220318104614",
  1361  	)
  1362  	require.NoError(t, err)
  1363  	require.Equal(t, `Current version is 20220318104614 (1 removed):
  1364  
  1365    - 20220318104615 (second)
  1366  
  1367  `, s)
  1368  	s, err = runCmd(
  1369  		migrateSetCmd(),
  1370  		"--dir", "file://testdata/sqlite",
  1371  		"-u", u,
  1372  		"20220318104615",
  1373  	)
  1374  	require.NoError(t, err)
  1375  	require.Equal(t, `Current version is 20220318104615 (1 set):
  1376  
  1377    + 20220318104615 (second)
  1378  
  1379  `, s)
  1380  
  1381  	s, err = runCmd(
  1382  		migrateSetCmd(),
  1383  		"--dir", "file://testdata/baseline1",
  1384  		"-u", u,
  1385  	)
  1386  	require.NoError(t, err)
  1387  	require.Equal(t, `Current version is 1 (1 set, 2 removed):
  1388  
  1389    + 1 (baseline)
  1390    - 20220318104614 (initial)
  1391    - 20220318104615 (second)
  1392  
  1393  `, s)
  1394  
  1395  	s, err = runCmd(
  1396  		migrateSetCmd(),
  1397  		"--dir", filepath.Join("file://", t.TempDir()), // empty dir.
  1398  		"-u", u,
  1399  	)
  1400  	require.NoError(t, err)
  1401  	require.Equal(t, `All revisions deleted (1 in total):
  1402  
  1403    - 1 (baseline)
  1404  
  1405  `, s)
  1406  
  1407  	// Empty database.
  1408  	u = fmt.Sprintf("sqlite://file:%s?_fk=1", filepath.Join(t.TempDir(), "test.db"))
  1409  	_, err = runCmd(
  1410  		migrateSetCmd(),
  1411  		"--dir", "file://testdata/sqlite",
  1412  		"-u", u,
  1413  	)
  1414  	require.EqualError(t, err, "accepts 1 arg(s), received 0")
  1415  
  1416  	s, err = runCmd(
  1417  		migrateSetCmd(),
  1418  		"--dir", "file://testdata/sqlite",
  1419  		"-u", u,
  1420  		"20220318104614",
  1421  	)
  1422  	require.NoError(t, err)
  1423  	require.Equal(t, `Current version is 20220318104614 (1 set):
  1424  
  1425    + 20220318104614 (initial)
  1426  
  1427  `, s)
  1428  }
  1429  
  1430  func TestMigrate_New(t *testing.T) {
  1431  	var (
  1432  		p = t.TempDir()
  1433  		v = time.Now().UTC().Format("20060102150405")
  1434  	)
  1435  
  1436  	s, err := runCmd(migrateNewCmd(), "--dir", "file://"+p)
  1437  	require.Zero(t, s)
  1438  	require.NoError(t, err)
  1439  	require.FileExists(t, filepath.Join(p, v+".sql"))
  1440  	require.FileExists(t, filepath.Join(p, "atlas.sum"))
  1441  	require.Equal(t, 2, countFiles(t, p))
  1442  
  1443  	s, err = runCmd(migrateNewCmd(), "my-migration-file", "--dir", "file://"+p)
  1444  	require.Zero(t, s)
  1445  	require.NoError(t, err)
  1446  	require.FileExists(t, filepath.Join(p, v+"_my-migration-file.sql"))
  1447  	require.FileExists(t, filepath.Join(p, "atlas.sum"))
  1448  	require.Equal(t, 3, countFiles(t, p))
  1449  
  1450  	p = t.TempDir()
  1451  	s, err = runCmd(migrateNewCmd(), "golang-migrate", "--dir", "file://"+p, "--dir-format", formatGolangMigrate)
  1452  	require.Zero(t, s)
  1453  	require.NoError(t, err)
  1454  	require.FileExists(t, filepath.Join(p, v+"_golang-migrate.up.sql"))
  1455  	require.FileExists(t, filepath.Join(p, v+"_golang-migrate.down.sql"))
  1456  	require.Equal(t, 3, countFiles(t, p))
  1457  
  1458  	p = t.TempDir()
  1459  	s, err = runCmd(migrateNewCmd(), "goose", "--dir", "file://"+p+"?format="+formatGoose)
  1460  	require.Zero(t, s)
  1461  	require.NoError(t, err)
  1462  	require.FileExists(t, filepath.Join(p, v+"_goose.sql"))
  1463  	require.Equal(t, 2, countFiles(t, p))
  1464  
  1465  	p = t.TempDir()
  1466  	s, err = runCmd(migrateNewCmd(), "flyway", "--dir", "file://"+p+"?format="+formatFlyway)
  1467  	require.Zero(t, s)
  1468  	require.NoError(t, err)
  1469  	require.FileExists(t, filepath.Join(p, fmt.Sprintf("V%s__%s.sql", v, formatFlyway)))
  1470  	require.FileExists(t, filepath.Join(p, fmt.Sprintf("U%s__%s.sql", v, formatFlyway)))
  1471  	require.Equal(t, 3, countFiles(t, p))
  1472  
  1473  	p = t.TempDir()
  1474  	s, err = runCmd(migrateNewCmd(), "liquibase", "--dir", "file://"+p+"?format="+formatLiquibase)
  1475  	require.Zero(t, s)
  1476  	require.NoError(t, err)
  1477  	require.FileExists(t, filepath.Join(p, v+"_liquibase.sql"))
  1478  	require.Equal(t, 2, countFiles(t, p))
  1479  
  1480  	p = t.TempDir()
  1481  	s, err = runCmd(migrateNewCmd(), "dbmate", "--dir", "file://"+p+"?format="+formatDBMate)
  1482  	require.Zero(t, s)
  1483  	require.NoError(t, err)
  1484  	require.FileExists(t, filepath.Join(p, v+"_dbmate.sql"))
  1485  	require.Equal(t, 2, countFiles(t, p))
  1486  
  1487  	f := filepath.Join("testdata", "mysql", "new.sql")
  1488  	require.NoError(t, os.WriteFile(f, []byte("contents"), 0600))
  1489  	t.Cleanup(func() { os.Remove(f) })
  1490  	s, err = runCmd(migrateNewCmd(), "--dir", "file://testdata/mysql")
  1491  	require.NotZero(t, s)
  1492  	require.Error(t, err)
  1493  
  1494  	t.Run("Edit", func(t *testing.T) {
  1495  		p := t.TempDir()
  1496  		require.NoError(t, os.Setenv("EDITOR", "echo 'contents' >"))
  1497  		t.Cleanup(func() { require.NoError(t, os.Unsetenv("EDITOR")) })
  1498  		s, err = runCmd(migrateNewCmd(), "--dir", "file://"+p, "--edit")
  1499  		files, err := os.ReadDir(p)
  1500  		require.NoError(t, err)
  1501  		require.Len(t, files, 2)
  1502  		b, err := os.ReadFile(filepath.Join(p, files[0].Name()))
  1503  		require.NoError(t, err)
  1504  		require.Equal(t, "contents\n", string(b))
  1505  		require.Equal(t, "atlas.sum", files[1].Name())
  1506  	})
  1507  }
  1508  
  1509  func TestMigrate_Validate(t *testing.T) {
  1510  	// Without re-playing.
  1511  	s, err := runCmd(migrateValidateCmd(), "--dir", "file://testdata/mysql")
  1512  	require.Zero(t, s)
  1513  	require.NoError(t, err)
  1514  
  1515  	f := filepath.Join("testdata", "mysql", "new.sql")
  1516  	require.NoError(t, os.WriteFile(f, []byte("contents"), 0600))
  1517  	t.Cleanup(func() { os.Remove(f) })
  1518  	s, err = runCmd(migrateValidateCmd(), "--dir", "file://testdata/mysql")
  1519  	require.NotZero(t, s)
  1520  	require.Error(t, err)
  1521  	require.NoError(t, os.Remove(f))
  1522  
  1523  	// Replay migration files if a dev-url is given.
  1524  	p := t.TempDir()
  1525  	require.NoError(t, os.WriteFile(filepath.Join(p, "1_initial.sql"), []byte("create table t1 (c1 int)"), 0644))
  1526  	require.NoError(t, os.WriteFile(filepath.Join(p, "2_second.sql"), []byte("create table t2 (c2 int)"), 0644))
  1527  	_, err = runCmd(migrateHashCmd(), "--dir", "file://"+p)
  1528  	require.NoError(t, err)
  1529  	s, err = runCmd(
  1530  		migrateValidateCmd(),
  1531  		"--dir", "file://"+p,
  1532  		"--dev-url", openSQLite(t, ""),
  1533  	)
  1534  	require.Zero(t, s)
  1535  	require.NoError(t, err)
  1536  
  1537  	// Should fail since the files are not compatible with SQLite.
  1538  	_, err = runCmd(migrateValidateCmd(), "--dir", "file://testdata/mysql", "--dev-url", openSQLite(t, ""))
  1539  	require.Error(t, err)
  1540  }
  1541  
  1542  func TestMigrate_Hash(t *testing.T) {
  1543  	s, err := runCmd(migrateHashCmd(), "--dir", "file://testdata/mysql")
  1544  	require.Zero(t, s)
  1545  	require.NoError(t, err)
  1546  
  1547  	// Prints a warning if --force flag is still used.
  1548  	s, err = runCmd(migrateHashCmd(), "--dir", "file://testdata/mysql", "--force")
  1549  	require.NoError(t, err)
  1550  	require.Equal(t, "Flag --force has been deprecated, you can safely omit it.\n", s)
  1551  
  1552  	p := t.TempDir()
  1553  	err = copyFile(filepath.Join("testdata", "mysql", "20220318104614_initial.sql"), filepath.Join(p, "20220318104614_initial.sql"))
  1554  	require.NoError(t, err)
  1555  
  1556  	s, err = runCmd(migrateHashCmd(), "--dir", "file://"+p)
  1557  	require.Zero(t, s)
  1558  	require.NoError(t, err)
  1559  	require.FileExists(t, filepath.Join(p, "atlas.sum"))
  1560  	d, err := os.ReadFile(filepath.Join(p, "atlas.sum"))
  1561  	require.NoError(t, err)
  1562  	dir, err := migrate.NewLocalDir(p)
  1563  	require.NoError(t, err)
  1564  	sum, err := dir.Checksum()
  1565  	require.NoError(t, err)
  1566  	b, err := sum.MarshalText()
  1567  	require.NoError(t, err)
  1568  	require.Equal(t, d, b)
  1569  
  1570  	p = t.TempDir()
  1571  	require.NoError(t, copyFile(
  1572  		filepath.Join("testdata", "mysql", "20220318104614_initial.sql"),
  1573  		filepath.Join(p, "20220318104614_initial.sql"),
  1574  	))
  1575  	s, err = runCmd(migrateHashCmd(), "--dir", "file://"+os.Getenv("MIGRATION_DIR"))
  1576  	require.NotZero(t, s)
  1577  	require.Error(t, err)
  1578  }
  1579  
  1580  func TestMigrate_Lint(t *testing.T) {
  1581  	p := t.TempDir()
  1582  	s, err := runCmd(
  1583  		migrateLintCmd(),
  1584  		"--dir", "file://"+p,
  1585  		"--dev-url", openSQLite(t, ""),
  1586  		"--latest", "1",
  1587  	)
  1588  	require.NoError(t, err)
  1589  	require.Empty(t, s)
  1590  
  1591  	err = os.WriteFile(filepath.Join(p, "1.sql"), []byte("CREATE TABLE t(c int);"), 0600)
  1592  	require.NoError(t, err)
  1593  	err = os.WriteFile(filepath.Join(p, "2.sql"), []byte("DROP TABLE t;"), 0600)
  1594  	require.NoError(t, err)
  1595  	s, err = runCmd(
  1596  		migrateLintCmd(),
  1597  		"--dir", "file://"+p,
  1598  		"--dev-url", openSQLite(t, ""),
  1599  		"--latest", "1",
  1600  	)
  1601  	require.Error(t, err)
  1602  	require.Equal(t, "2.sql: destructive changes detected:\n\n\tL1: Dropping table \"t\"\n\n", s)
  1603  	s, err = runCmd(
  1604  		migrateLintCmd(),
  1605  		"--dir", "file://"+p,
  1606  		"--dev-url", openSQLite(t, ""),
  1607  		"--latest", "1",
  1608  		"--log", "{{ range .Files }}{{ .Name }}{{ end }}", // Backward compatibility with old flag name.
  1609  	)
  1610  	require.Error(t, err)
  1611  	require.Equal(t, "2.sql", s)
  1612  
  1613  	t.Run("FromConfig", func(t *testing.T) {
  1614  		cfg := filepath.Join(p, "atlas.hcl")
  1615  		err := os.WriteFile(cfg, []byte(`
  1616  variable "error" {
  1617    type    = bool
  1618    default = false
  1619  }
  1620  
  1621  lint {
  1622    latest = 1
  1623    destructive {
  1624      error = var.error
  1625    }
  1626  }
  1627  `), 0600)
  1628  		require.NoError(t, err)
  1629  		cmd := migrateCmd()
  1630  		cmd.AddCommand(migrateLintCmd())
  1631  		s, err := runCmd(
  1632  			cmd, "lint",
  1633  			"--dir", "file://"+p,
  1634  			"--dev-url", openSQLite(t, ""),
  1635  			"-c", "file://"+cfg,
  1636  		)
  1637  		require.NoError(t, err)
  1638  		require.Equal(t, "2.sql: destructive changes detected:\n\n\tL1: Dropping table \"t\"\n\n", s)
  1639  
  1640  		cmd = migrateCmd()
  1641  		cmd.AddCommand(migrateLintCmd())
  1642  		s, err = runCmd(
  1643  			cmd, "lint",
  1644  			"--dir", "file://"+p,
  1645  			"--dev-url", openSQLite(t, ""),
  1646  			"-c", "file://"+cfg,
  1647  			"--var", "error=true",
  1648  		)
  1649  		require.Error(t, err)
  1650  		require.Equal(t, "2.sql: destructive changes detected:\n\n\tL1: Dropping table \"t\"\n\n", s)
  1651  	})
  1652  
  1653  	// Change files to golang-migrate format.
  1654  	require.NoError(t, os.Rename(filepath.Join(p, "1.sql"), filepath.Join(p, "1.up.sql")))
  1655  	require.NoError(t, os.Rename(filepath.Join(p, "2.sql"), filepath.Join(p, "1.down.sql")))
  1656  	s, err = runCmd(
  1657  		migrateLintCmd(),
  1658  		"--dir", "file://"+p+"?format="+formatGolangMigrate,
  1659  		"--dev-url", openSQLite(t, ""),
  1660  		"--latest", "2",
  1661  		"--format", "{{ range .Files }}{{ .Name }}:{{ len .Reports }}{{ end }}",
  1662  	)
  1663  	require.NoError(t, err)
  1664  	require.Equal(t, "1.up.sql:0", s)
  1665  	s, err = runCmd(
  1666  		migrateLintCmd(),
  1667  		"--dir", "file://"+p+"?format="+formatGolangMigrate,
  1668  		"--dev-url", openSQLite(t, ""),
  1669  		"--latest", "2",
  1670  		"--format", "{{ range .Files }}{{ .Name }}:{{ len .Reports }}{{ end }}",
  1671  		"--dir-format", formatGolangMigrate,
  1672  	)
  1673  	require.NoError(t, err)
  1674  	require.Equal(t, "1.up.sql:0", s)
  1675  
  1676  	// Invalid files.
  1677  	err = os.WriteFile(filepath.Join(p, "2.up.sql"), []byte("BORING"), 0600)
  1678  	require.NoError(t, err)
  1679  	s, err = runCmd(
  1680  		migrateLintCmd(),
  1681  		"--dir", "file://"+p+"?format="+formatGolangMigrate,
  1682  		"--dev-url", openSQLite(t, ""),
  1683  		"--latest", "1",
  1684  	)
  1685  	require.Error(t, err)
  1686  	require.Equal(t, "2.up.sql: executing statement: near \"BORING\": syntax error\n", s)
  1687  }
  1688  
  1689  const testSchema = `
  1690  schema "main" {
  1691  }
  1692  
  1693  table "table" {
  1694    schema = schema.main
  1695    column "col" {
  1696      type    = int
  1697      comment = "column comment"
  1698    }
  1699    column "age" {
  1700      type = int
  1701    }
  1702    column "price1" {
  1703      type = int
  1704    }
  1705    column "price2" {
  1706      type           = int
  1707    }
  1708    column "account_name" {
  1709      type = varchar(32)
  1710      null = true
  1711    }
  1712    column "created_at" {
  1713      type    = datetime
  1714      default = sql("current_timestamp")
  1715    }
  1716    primary_key {
  1717      columns = [table.table.column.col]
  1718    }
  1719    index "index" {
  1720      unique  = true
  1721      columns = [
  1722        table.table.column.col,
  1723        table.table.column.age,
  1724      ]
  1725      comment = "index comment"
  1726    }
  1727    foreign_key "accounts" {
  1728      columns = [
  1729        table.table.column.account_name,
  1730      ]
  1731      ref_columns = [
  1732        table.accounts.column.name,
  1733      ]
  1734      on_delete = SET_NULL
  1735      on_update = "NO_ACTION"
  1736    }
  1737    check "positive price" {
  1738      expr = "price1 > 0"
  1739    }
  1740    check {
  1741      expr     = "price1 <> price2"
  1742      enforced = true
  1743    }
  1744    check {
  1745      expr     = "price2 <> price1"
  1746      enforced = false
  1747    }
  1748    comment        = "table comment"
  1749  }
  1750  
  1751  table "accounts" {
  1752    schema = schema.main
  1753    column "name" {
  1754      type = varchar(32)
  1755    }
  1756    column "unsigned_float" {
  1757      type     = float(10)
  1758      unsigned = true
  1759    }
  1760    column "unsigned_decimal" {
  1761      type     = decimal(10, 2)
  1762      unsigned = true
  1763    }
  1764    primary_key {
  1765      columns = [table.accounts.column.name]
  1766    }
  1767  }`
  1768  
  1769  func hclURL(t *testing.T) string {
  1770  	p := t.TempDir()
  1771  	require.NoError(t, os.WriteFile(filepath.Join(p, "schema.hcl"), []byte(testSchema), 0600))
  1772  	return "file://" + filepath.Join(p, "schema.hcl")
  1773  }
  1774  
  1775  func copyFile(src, dst string) error {
  1776  	sf, err := os.Open(src)
  1777  	if err != nil {
  1778  		return err
  1779  	}
  1780  	defer sf.Close()
  1781  	df, err := os.Create(dst)
  1782  	if err != nil {
  1783  		return err
  1784  	}
  1785  	defer df.Close()
  1786  	_, err = io.Copy(df, sf)
  1787  	return err
  1788  }
  1789  
  1790  type sqliteLockerDriver struct{ migrate.Driver }
  1791  
  1792  var errLock = errors.New("lockErr")
  1793  
  1794  func (d *sqliteLockerDriver) Lock(context.Context, string, time.Duration) (schema.UnlockFunc, error) {
  1795  	return func() error { return nil }, errLock
  1796  }
  1797  
  1798  func (d *sqliteLockerDriver) Snapshot(ctx context.Context) (migrate.RestoreFunc, error) {
  1799  	return d.Driver.(migrate.Snapshoter).Snapshot(ctx)
  1800  }
  1801  
  1802  func (d *sqliteLockerDriver) CheckClean(ctx context.Context, revT *migrate.TableIdent) error {
  1803  	return d.Driver.(migrate.CleanChecker).CheckClean(ctx, revT)
  1804  }
  1805  
  1806  func countFiles(t *testing.T, p string) int {
  1807  	files, err := os.ReadDir(p)
  1808  	require.NoError(t, err)
  1809  	return len(files)
  1810  }
  1811  
  1812  func sed(t *testing.T, r, p string) {
  1813  	args := []string{"-i"}
  1814  	if runtime.GOOS == "darwin" {
  1815  		args = append(args, ".bk")
  1816  	}
  1817  	buf, err := exec.Command("sed", append(args, r, p)...).CombinedOutput()
  1818  	require.NoError(t, err, string(buf))
  1819  }