github.com/iasthc/atlas/cmd/atlas@v0.0.0-20230523071841-73246df3f88d/internal/cmdapi/schema_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/json"
    11  	"fmt"
    12  	"net/url"
    13  	"os"
    14  	"path"
    15  	"path/filepath"
    16  	"strings"
    17  	"testing"
    18  
    19  	"github.com/iasthc/atlas/cmd/atlas/internal/cmdlog"
    20  	"github.com/iasthc/atlas/sql/migrate"
    21  	"github.com/iasthc/atlas/sql/schema"
    22  	"github.com/iasthc/atlas/sql/sqlclient"
    23  	"github.com/stretchr/testify/require"
    24  )
    25  
    26  const (
    27  	unformatted = `block  "x"  {
    28   x = 1
    29      y     = 2
    30  }
    31  `
    32  	formatted = `block "x" {
    33    x = 1
    34    y = 2
    35  }
    36  `
    37  )
    38  
    39  func TestSchema_Diff(t *testing.T) {
    40  	// Creates the missing table.
    41  	s, err := runCmd(
    42  		schemaDiffCmd(),
    43  		"--from", openSQLite(t, ""),
    44  		"--to", openSQLite(t, "create table t1 (id int);"),
    45  	)
    46  	require.NoError(t, err)
    47  	require.EqualValues(t, "-- Create \"t1\" table\nCREATE TABLE `t1` (`id` int NULL);\n", s)
    48  
    49  	// Format indentation one table.
    50  	s, err = runCmd(
    51  		schemaDiffCmd(),
    52  		"--from", openSQLite(t, ""),
    53  		"--to", openSQLite(t, "create table t1 (id int);"),
    54  		"--format", `{{ sql . "  " }}`,
    55  	)
    56  	require.NoError(t, err)
    57  	require.EqualValues(t, "-- Create \"t1\" table\nCREATE TABLE `t1` (\n  `id` int NULL\n);\n", s)
    58  
    59  	// No changes.
    60  	s, err = runCmd(
    61  		schemaDiffCmd(),
    62  		"--from", openSQLite(t, ""),
    63  		"--to", openSQLite(t, ""),
    64  	)
    65  	require.NoError(t, err)
    66  	require.EqualValues(t, "Schemas are synced, no changes to be made.\n", s)
    67  
    68  	// Format no changes.
    69  	s, err = runCmd(
    70  		schemaDiffCmd(),
    71  		"--from", openSQLite(t, ""),
    72  		"--to", openSQLite(t, ""),
    73  		"--format", `{{ sql . " " }}`,
    74  	)
    75  	require.NoError(t, err)
    76  	require.Empty(t, s)
    77  
    78  	// Desired state from migration directory requires dev database.
    79  	_, err = runCmd(
    80  		schemaDiffCmd(),
    81  		"--from", "file://testdata/sqlite",
    82  		"--to", openSQLite(t, ""),
    83  	)
    84  	require.EqualError(t, err, "--dev-url cannot be empty")
    85  
    86  	// Desired state from migration directory.
    87  	s, err = runCmd(
    88  		schemaDiffCmd(),
    89  		"--from", openSQLite(t, ""),
    90  		"--to", "file://testdata/sqlite",
    91  		"--dev-url", openSQLite(t, ""),
    92  	)
    93  	require.NoError(t, err)
    94  	require.EqualValues(t, "-- Create \"tbl\" table\nCREATE TABLE `tbl` (`col` int NOT NULL, `col_2` bigint NULL);\n", s)
    95  
    96  	// Desired state from migration directory.
    97  	s, err = runCmd(
    98  		schemaDiffCmd(),
    99  		"--from", openSQLite(t, ""),
   100  		"--to", "file://testdata/sqlite",
   101  		"--dev-url", openSQLite(t, ""),
   102  	)
   103  	require.NoError(t, err)
   104  	require.EqualValues(t, "-- Create \"tbl\" table\nCREATE TABLE `tbl` (`col` int NOT NULL, `col_2` bigint NULL);\n", s)
   105  
   106  	// Current state from migration directory, desired state from HCL - synced.
   107  	p := filepath.Join(t.TempDir(), "schema.hcl")
   108  	require.NoError(t, os.WriteFile(p, []byte(`schema "main" {}
   109  table "tbl" {
   110    schema = schema.main
   111    column "col" {
   112      type = int
   113    }
   114    column "col_2" {
   115      type = bigint
   116      null = true
   117    }
   118  }`), 0644))
   119  	s, err = runCmd(
   120  		schemaDiffCmd(),
   121  		"--from", "file://testdata/sqlite",
   122  		"--to", "file://"+p,
   123  		"--dev-url", openSQLite(t, ""),
   124  	)
   125  	require.NoError(t, err)
   126  	require.EqualValues(t, "Schemas are synced, no changes to be made.\n", s)
   127  
   128  	// Current state from migration directory, desired state from HCL - missing column.
   129  	p = filepath.Join(t.TempDir(), "schema.hcl")
   130  	require.NoError(t, os.WriteFile(p, []byte(`schema "main" {}
   131  table "tbl" {
   132    schema = schema.main
   133    column "col" {
   134      type = int
   135    }
   136    column "col_2" {
   137      type = bigint
   138      null = true
   139    }
   140    column "col_3" {
   141      type = text
   142    }
   143  }`), 0644))
   144  	s, err = runCmd(
   145  		schemaDiffCmd(),
   146  		"--from", "file://testdata/sqlite",
   147  		"--to", "file://"+p,
   148  		"--dev-url", openSQLite(t, ""),
   149  	)
   150  	require.NoError(t, err)
   151  	require.EqualValues(
   152  		t,
   153  		"-- Add column \"col_3\" to table: \"tbl\"\nALTER TABLE `tbl` ADD COLUMN `col_3` text NOT NULL;\n",
   154  		s,
   155  	)
   156  
   157  	// Current state from migration directory with version, desired state from HCL - two missing columns.
   158  	s, err = runCmd(
   159  		schemaDiffCmd(),
   160  		"--from", "file://testdata/sqlite?version=20220318104614",
   161  		"--to", "file://"+p,
   162  		"--dev-url", openSQLite(t, ""),
   163  	)
   164  	require.NoError(t, err)
   165  	require.EqualValues(
   166  		t,
   167  		"-- Add column \"col_2\" to table: \"tbl\"\n"+
   168  			"ALTER TABLE `tbl` ADD COLUMN `col_2` bigint NULL;\n"+
   169  			"-- Add column \"col_3\" to table: \"tbl\"\n"+
   170  			"ALTER TABLE `tbl` ADD COLUMN `col_3` text NOT NULL;\n",
   171  		s,
   172  	)
   173  
   174  	// Current state from migration directory, desired state from multi file HCL - missing column.
   175  	p = t.TempDir()
   176  	var (
   177  		one = filepath.Join(p, "one.hcl")
   178  		two = filepath.Join(p, "two.hcl")
   179  	)
   180  	require.NoError(t, os.WriteFile(one, []byte(`table "tbl" {
   181    schema = schema.main
   182    column "col" {
   183      type = int
   184    }
   185    column "col_2" {
   186      type = bigint
   187      null = true
   188    }
   189    column "col_3" {
   190      type = text
   191    }
   192  }`), 0644))
   193  	require.NoError(t, os.WriteFile(two, []byte(`schema "main" {}`), 0644))
   194  	s, err = runCmd(
   195  		schemaDiffCmd(),
   196  		"--from", "file://testdata/sqlite",
   197  		"--to", "file://"+p,
   198  		"--dev-url", openSQLite(t, ""),
   199  	)
   200  	require.NoError(t, err)
   201  	require.EqualValues(
   202  		t,
   203  		"-- Add column \"col_3\" to table: \"tbl\"\nALTER TABLE `tbl` ADD COLUMN `col_3` text NOT NULL;\n",
   204  		s,
   205  	)
   206  	s, err = runCmd(
   207  		schemaDiffCmd(),
   208  		"--from", "file://testdata/sqlite",
   209  		"--to", "file://"+one,
   210  		"--to", "file://"+two,
   211  		"--dev-url", openSQLite(t, ""),
   212  	)
   213  	require.NoError(t, err)
   214  	require.EqualValues(
   215  		t,
   216  		"-- Add column \"col_3\" to table: \"tbl\"\nALTER TABLE `tbl` ADD COLUMN `col_3` text NOT NULL;\n",
   217  		s,
   218  	)
   219  
   220  	t.Run("FromConfig", func(t *testing.T) {
   221  		var (
   222  			p   = t.TempDir()
   223  			cp  = filepath.Join(p, "atlas.hcl")
   224  			sp  = filepath.Join(p, "schema.hcl")
   225  			cfg = fmt.Sprintf(`
   226  env "local" {
   227    dev = "%s"
   228    format {
   229      schema {
   230        diff = "{{ sql . \"\t\" }}"
   231      }
   232    }
   233  }`, openSQLite(t, ""))
   234  		)
   235  		require.NoError(t, os.WriteFile(cp, []byte(cfg), 0600))
   236  		require.NoError(t, os.WriteFile(sp, []byte(`
   237  schema "main" {}
   238  table "users" {
   239    schema = schema.main
   240    column "id" {
   241      type = int
   242    }
   243  }
   244  `), 0600))
   245  
   246  		cmd := schemaCmd()
   247  		cmd.AddCommand(schemaDiffCmd())
   248  		s, err := runCmd(
   249  			cmd, "diff",
   250  			"-c", "file://"+cp,
   251  			"--env", "local",
   252  			"--to", "file://"+sp,
   253  			"--from", openSQLite(t, ""),
   254  		)
   255  		require.NoError(t, err)
   256  		require.Equal(t, "-- Create \"users\" table\nCREATE TABLE `users` (\n\t`id` int NOT NULL\n);\n", s)
   257  	})
   258  
   259  	t.Run("SkipChanges", func(t *testing.T) {
   260  		var (
   261  			p   = t.TempDir()
   262  			cfg = filepath.Join(p, "atlas.hcl")
   263  		)
   264  		err = os.WriteFile(cfg, []byte(`
   265  variable "destructive" {
   266    type = bool
   267    default = false
   268  }
   269  
   270  env "local" {
   271    diff {
   272      skip {
   273        drop_table = !var.destructive
   274      }
   275    }
   276  }
   277  `), 0600)
   278  		require.NoError(t, err)
   279  
   280  		// Skip destructive changes.
   281  		cmd := schemaCmd()
   282  		cmd.AddCommand(schemaDiffCmd())
   283  		s, err := runCmd(
   284  			cmd, "diff",
   285  			"-c", "file://"+cfg,
   286  			"--from", openSQLite(t, "create table users (id int);"),
   287  			"--to", openSQLite(t, ""),
   288  			"--env", "local",
   289  		)
   290  		require.NoError(t, err)
   291  		require.Equal(t, "Schemas are synced, no changes to be made.\n", s)
   292  
   293  		// Apply destructive changes.
   294  		cmd = schemaCmd()
   295  		cmd.AddCommand(schemaDiffCmd())
   296  		s, err = runCmd(
   297  			cmd, "diff",
   298  			"-c", "file://"+cfg,
   299  			"--from", openSQLite(t, "create table users (id int);"),
   300  			"--to", openSQLite(t, ""),
   301  			"--env", "local",
   302  			"--var", "destructive=true",
   303  		)
   304  		require.NoError(t, err)
   305  		lines := strings.Split(strings.TrimSpace(s), "\n")
   306  		require.Equal(t, []string{
   307  			"-- Disable the enforcement of foreign-keys constraints",
   308  			"PRAGMA foreign_keys = off;",
   309  			`-- Drop "users" table`,
   310  			"DROP TABLE `users`;",
   311  			"-- Enable back the enforcement of foreign-keys constraints",
   312  			"PRAGMA foreign_keys = on;",
   313  		}, lines)
   314  	})
   315  }
   316  
   317  func TestSchema_Apply(t *testing.T) {
   318  	const drvName = "checknormalizer"
   319  	// If no dev-database is given, there must not be a call to Driver.Normalize.
   320  	sqlclient.Register(
   321  		drvName,
   322  		sqlclient.OpenerFunc(func(ctx context.Context, url *url.URL) (*sqlclient.Client, error) {
   323  			url.Scheme = "sqlite"
   324  			c, err := sqlclient.OpenURL(ctx, url)
   325  			if err != nil {
   326  				return nil, err
   327  			}
   328  			c.Driver = &assertNormalizerDriver{t: t, Driver: c.Driver}
   329  			return c, nil
   330  		}),
   331  	)
   332  
   333  	p := filepath.Join(t.TempDir(), "schema.hcl")
   334  	require.NoError(t, os.WriteFile(p, []byte(`schema "my_schema" {}`), 0644))
   335  	_, _ = runCmd(
   336  		schemaApplyCmd(),
   337  		"--url", drvName+"://?mode=memory",
   338  		"-f", p,
   339  	)
   340  }
   341  
   342  func TestSchema_ApplyMultiEnv(t *testing.T) {
   343  	p := t.TempDir()
   344  	cfg := filepath.Join(p, "atlas.hcl")
   345  	src := filepath.Join(p, "schema.hcl")
   346  	err := os.WriteFile(cfg, []byte(`
   347  variable "urls" {
   348    type = list(string)
   349  }
   350  
   351  variable "src" {
   352    type = string
   353  }
   354  
   355  env "local" {
   356    for_each = toset(var.urls)
   357    url      = each.value
   358    src 	   = var.src
   359  }`), 0600)
   360  	require.NoError(t, err)
   361  	err = os.WriteFile(src, []byte(`
   362  schema "main" {}
   363  
   364  table "users" {
   365    schema = schema.main
   366    column "id" {
   367      type = int
   368    }
   369  }
   370  `), 0600)
   371  	require.NoError(t, err)
   372  	db1, db2 := filepath.Join(p, "test1.db"), filepath.Join(p, "test2.db")
   373  	cmd := schemaCmd()
   374  	cmd.AddCommand(schemaApplyCmd())
   375  	s, err := runCmd(
   376  		cmd, "apply",
   377  		"-c", "file://"+cfg,
   378  		"--env", "local",
   379  		"--var", fmt.Sprintf("src=file://%s", src),
   380  		"--var", fmt.Sprintf("urls=sqlite://file:%s?cache=shared&_fk=1", db1),
   381  		"--var", fmt.Sprintf("urls=sqlite://file:%s?cache=shared&_fk=1", db2),
   382  		"--auto-approve",
   383  	)
   384  	require.NoError(t, err)
   385  	require.Equal(t, 2, strings.Count(s, "CREATE TABLE `users` (`id` int NOT NULL)"))
   386  	_, err = os.Stat(db1)
   387  	require.NoError(t, err)
   388  	_, err = os.Stat(db2)
   389  	require.NoError(t, err)
   390  
   391  	cmd = schemaCmd()
   392  	cmd.AddCommand(schemaApplyCmd())
   393  	s, err = runCmd(
   394  		cmd, "apply",
   395  		"-c", "file://"+cfg,
   396  		"--env", "local",
   397  		"--var", fmt.Sprintf("src=file://%s", src),
   398  		"--var", fmt.Sprintf("urls=sqlite://file:%s?cache=shared&_fk=1", db1),
   399  		"--var", fmt.Sprintf("urls=sqlite://file:%s?cache=shared&_fk=1", db2),
   400  		"--auto-approve",
   401  	)
   402  	require.NoError(t, err)
   403  	require.Equal(t, 2, strings.Count(s, "Schema is synced, no changes to be made"))
   404  }
   405  
   406  func TestSchema_ApplyLog(t *testing.T) {
   407  	t.Run("DryRun", func(t *testing.T) {
   408  		db := openSQLite(t, "")
   409  		cmd := schemaCmd()
   410  		cmd.AddCommand(schemaApplyCmd())
   411  		s, err := runCmd(
   412  			cmd, "apply",
   413  			"-u", db,
   414  			"--to", openSQLite(t, ""),
   415  			"--dry-run",
   416  			"--format", "{{ json .Changes }}",
   417  		)
   418  		require.NoError(t, err)
   419  		require.Equal(t, "{}", s)
   420  
   421  		cmd = schemaCmd()
   422  		cmd.AddCommand(schemaApplyCmd())
   423  		s, err = runCmd(
   424  			cmd, "apply",
   425  			"-u", db,
   426  			"--to", openSQLite(t, "create table t1 (id int);"),
   427  			"--dry-run",
   428  			"--format", "{{ json .Changes }}",
   429  		)
   430  		require.NoError(t, err)
   431  		require.Equal(t, "{\"Pending\":[\"CREATE TABLE `t1` (`id` int NULL)\"]}", s)
   432  	})
   433  
   434  	t.Run("AutoApprove", func(t *testing.T) {
   435  		db := openSQLite(t, "")
   436  		cmd := schemaCmd()
   437  		cmd.AddCommand(schemaApplyCmd())
   438  		s, err := runCmd(
   439  			cmd, "apply",
   440  			"-u", db,
   441  			"--to", openSQLite(t, "create table t1 (id int);"),
   442  			"--auto-approve",
   443  			"--format", "{{ json .Changes }}",
   444  		)
   445  		require.NoError(t, err)
   446  		require.Equal(t, "{\"Applied\":[\"CREATE TABLE `t1` (`id` int NULL)\"]}", s)
   447  
   448  		cmd = schemaCmd()
   449  		cmd.AddCommand(schemaApplyCmd())
   450  		s, err = runCmd(
   451  			cmd, "apply",
   452  			"-u", db,
   453  			"--to", openSQLite(t, "create table t1 (id int);"),
   454  			"--auto-approve",
   455  			"--format", "{{ json .Changes }}",
   456  		)
   457  		require.NoError(t, err)
   458  		require.Equal(t, "{}", s)
   459  
   460  		cmd = schemaCmd()
   461  		cmd.AddCommand(schemaApplyCmd())
   462  		s, err = runCmd(
   463  			cmd, "apply",
   464  			"-u", db,
   465  			"--to", openSQLite(t, "create table t2 (id int);"),
   466  			"--auto-approve",
   467  			"--format", "{{ json .Changes }}",
   468  		)
   469  		require.NoError(t, err)
   470  		require.Equal(t, "{\"Applied\":[\"PRAGMA foreign_keys = off\",\"DROP TABLE `t1`\",\"CREATE TABLE `t2` (`id` int NULL)\",\"PRAGMA foreign_keys = on\"]}", s)
   471  
   472  		// Simulate a failed execution.
   473  		conn, err := sql.Open("sqlite3", strings.TrimPrefix(db, "sqlite://"))
   474  		require.NoError(t, err)
   475  		_, err = conn.Exec("INSERT INTO t2 (`id`) VALUES (1), (1)")
   476  		require.NoError(t, err)
   477  
   478  		cmd = schemaCmd()
   479  		cmd.AddCommand(schemaApplyCmd())
   480  		s, err = runCmd(
   481  			cmd, "apply",
   482  			"-u", db,
   483  			"--to", openSQLite(t, "create table t2 (id int, c int);create unique index t2_id on t2 (id);"),
   484  			"--auto-approve",
   485  			"--format", "{{ json .Changes }}\n",
   486  		)
   487  		require.EqualError(t, err, `create index "t2_id" to table: "t2": UNIQUE constraint failed: t2.id`)
   488  		var out struct {
   489  			Applied []string
   490  			Pending []string
   491  			Error   cmdlog.StmtError
   492  		}
   493  		require.NoError(t, json.NewDecoder(strings.NewReader(s)).Decode(&out))
   494  		require.Equal(t, []string{"ALTER TABLE `t2` ADD COLUMN `c` int NULL"}, out.Applied)
   495  		require.Equal(t, []string{"CREATE UNIQUE INDEX `t2_id` ON `t2` (`id`)"}, out.Pending)
   496  		require.Equal(t, out.Pending[0], out.Error.Stmt)
   497  		require.Equal(t, `create index "t2_id" to table: "t2": UNIQUE constraint failed: t2.id`, out.Error.Text)
   498  	})
   499  }
   500  
   501  func TestSchema_ApplySchemaMismatch(t *testing.T) {
   502  	var (
   503  		p   = t.TempDir()
   504  		src = filepath.Join(p, "schema.hcl")
   505  	)
   506  	// SQLite always has a schema called "main".
   507  	err := os.WriteFile(src, []byte(`
   508  schema "hello" {}
   509  `), 0600)
   510  	require.NoError(t, err)
   511  	cmd := schemaCmd()
   512  	cmd.AddCommand(schemaApplyCmd())
   513  	_, err = runCmd(
   514  		cmd, "apply",
   515  		"-u", openSQLite(t, ""),
   516  		"-f", src,
   517  	)
   518  	require.EqualError(t, err, `mismatched HCL and database schemas: "main" <> "hello"`)
   519  }
   520  
   521  func TestSchema_ApplySkip(t *testing.T) {
   522  	var (
   523  		p   = t.TempDir()
   524  		cfg = filepath.Join(p, "atlas.hcl")
   525  		src = filepath.Join(p, "schema.hcl")
   526  	)
   527  	err := os.WriteFile(src, []byte(`
   528  schema "main" {}
   529  
   530  table "users" {
   531    schema = schema.main
   532    column "id" {
   533      type = int
   534    }
   535  }
   536  `), 0600)
   537  	require.NoError(t, err)
   538  	err = os.WriteFile(cfg, []byte(`
   539  variable "schema" {
   540    type = string
   541    default = "dev"
   542  }
   543  
   544  variable "destructive" {
   545    type = bool
   546    default = false
   547  }
   548  
   549  env "local" {
   550    src = var.schema
   551    dev_url = "sqlite://dev?mode=memory&_fk=1"
   552  }
   553  
   554  diff {
   555    skip {
   556      drop_table = !var.destructive
   557    }
   558  }
   559  `), 0600)
   560  	require.NoError(t, err)
   561  
   562  	// Skip destructive changes.
   563  	cmd := schemaCmd()
   564  	cmd.AddCommand(schemaApplyCmd())
   565  	s, err := runCmd(
   566  		cmd, "apply",
   567  		"-u", openSQLite(t, "create table pets (id int);"),
   568  		"-c", "file://"+cfg,
   569  		"--var", "schema=file://"+src,
   570  		"--env", "local",
   571  		"--auto-approve",
   572  	)
   573  	require.NoError(t, err)
   574  	lines := strings.Split(strings.TrimSpace(s), "\n")
   575  	require.Equal(t, []string{
   576  		"-- Planned Changes:",
   577  		`-- Create "users" table`,
   578  		"CREATE TABLE `users` (`id` int NOT NULL);",
   579  	}, lines)
   580  
   581  	// Skip destructive changes by using project-level policy (no --env was passed).
   582  	cmd = schemaCmd()
   583  	cmd.AddCommand(schemaApplyCmd())
   584  	s, err = runCmd(
   585  		cmd, "apply",
   586  		"-u", openSQLite(t, "create table pets (id int);"),
   587  		"-c", "file://"+cfg, // Using the project-level policy.
   588  		"--to", "file://"+src,
   589  		"--dev-url", "sqlite://dev?mode=memory&_fk=1",
   590  		"--auto-approve",
   591  	)
   592  	require.NoError(t, err)
   593  	lines = strings.Split(strings.TrimSpace(s), "\n")
   594  	require.Equal(t, []string{
   595  		"-- Planned Changes:",
   596  		`-- Create "users" table`,
   597  		"CREATE TABLE `users` (`id` int NOT NULL);",
   598  	}, lines)
   599  
   600  	// Apply destructive changes.
   601  	cmd = schemaCmd()
   602  	cmd.AddCommand(schemaApplyCmd())
   603  	s, err = runCmd(
   604  		cmd, "apply",
   605  		"-u", openSQLite(t, "create table pets (id int);"),
   606  		"-c", "file://"+cfg,
   607  		"--var", "schema=file://"+src,
   608  		"--var", "destructive=true",
   609  		"--env", "local",
   610  		"--auto-approve",
   611  	)
   612  	require.NoError(t, err)
   613  	lines = strings.Split(strings.TrimSpace(s), "\n")
   614  	require.Equal(t, []string{
   615  		"-- Planned Changes:",
   616  		"-- Disable the enforcement of foreign-keys constraints",
   617  		"PRAGMA foreign_keys = off;",
   618  		`-- Drop "pets" table`,
   619  		"DROP TABLE `pets`;",
   620  		`-- Create "users" table`,
   621  		"CREATE TABLE `users` (`id` int NOT NULL);",
   622  		"-- Enable back the enforcement of foreign-keys constraints",
   623  		"PRAGMA foreign_keys = on;",
   624  	}, lines)
   625  }
   626  
   627  func TestSchema_ApplySources(t *testing.T) {
   628  	var (
   629  		p   = t.TempDir()
   630  		cfg = filepath.Join(p, "atlas.hcl")
   631  		src = []string{filepath.Join(p, "one.hcl"), filepath.Join(p, "two.hcl")}
   632  	)
   633  	err := os.WriteFile(src[0], []byte(`
   634  schema "main" {}
   635  
   636  table "one" {
   637    schema = schema.main
   638    column "id" {
   639      type = int
   640    }
   641  }
   642  `), 0600)
   643  	require.NoError(t, err)
   644  	err = os.WriteFile(src[1], []byte(`
   645  table "two" {
   646    schema = schema.main
   647    column "id" {
   648      type = int
   649    }
   650  }
   651  `), 0600)
   652  	require.NoError(t, err)
   653  	err = os.WriteFile(cfg, []byte(fmt.Sprintf(`
   654  env "local" {
   655    src = [%q, %q]
   656  }`, src[0], src[1])), 0600)
   657  	require.NoError(t, err)
   658  
   659  	cmd := schemaCmd()
   660  	cmd.AddCommand(schemaApplyCmd())
   661  	s, err := runCmd(
   662  		cmd, "apply",
   663  		"-u", openSQLite(t, ""),
   664  		"-c", "file://"+cfg,
   665  		"--env", "local",
   666  		"--auto-approve",
   667  	)
   668  	require.NoError(t, err)
   669  	lines := strings.Split(strings.TrimSpace(s), "\n")
   670  	require.Equal(t, []string{
   671  		"-- Planned Changes:",
   672  		`-- Create "one" table`,
   673  		"CREATE TABLE `one` (`id` int NOT NULL);",
   674  		`-- Create "two" table`,
   675  		"CREATE TABLE `two` (`id` int NOT NULL);",
   676  	}, lines)
   677  }
   678  
   679  func TestSchema_ApplySQL(t *testing.T) {
   680  	t.Run("File", func(t *testing.T) {
   681  		db := openSQLite(t, "")
   682  		p := filepath.Join(t.TempDir(), "schema.sql")
   683  		require.NoError(t, os.WriteFile(p, []byte(`create table t1 (id int NOT NULL);`), 0600))
   684  		s, err := runCmd(
   685  			schemaApplyCmd(),
   686  			"-u", db,
   687  			"--dev-url", openSQLite(t, ""),
   688  			"--to", "file://"+p,
   689  			"--auto-approve",
   690  		)
   691  		require.NoError(t, err)
   692  		require.Equal(t, "-- Planned Changes:\n-- Create \"t1\" table\nCREATE TABLE `t1` (`id` int NOT NULL);\n", s)
   693  
   694  		s, err = runCmd(
   695  			schemaApplyCmd(),
   696  			"-u", db,
   697  			"--dev-url", openSQLite(t, ""),
   698  			"--to", "file://"+p,
   699  			"--auto-approve",
   700  		)
   701  		require.NoError(t, err)
   702  		require.Equal(t, "Schema is synced, no changes to be made.\n", s)
   703  	})
   704  	t.Run("Dir", func(t *testing.T) {
   705  		db := openSQLite(t, "")
   706  		s, err := runCmd(
   707  			schemaApplyCmd(),
   708  			"-u", db,
   709  			"--dev-url", openSQLite(t, ""),
   710  			"--to", "file://testdata/sqlite",
   711  			"--auto-approve",
   712  		)
   713  		require.NoError(t, err)
   714  		require.Equal(t, "-- Planned Changes:\n-- Create \"tbl\" table\nCREATE TABLE `tbl` (`col` int NOT NULL, `col_2` bigint NULL);\n", s)
   715  
   716  		s, err = runCmd(
   717  			schemaApplyCmd(),
   718  			"-u", db,
   719  			"--dev-url", openSQLite(t, ""),
   720  			"--to", "file://testdata/sqlite",
   721  			"--auto-approve",
   722  		)
   723  		require.NoError(t, err)
   724  		require.Equal(t, "Schema is synced, no changes to be made.\n", s)
   725  	})
   726  	t.Run("Error", func(t *testing.T) {
   727  		_, err := runCmd(
   728  			schemaApplyCmd(),
   729  			"-u", openSQLite(t, ""),
   730  			"--dev-url", openSQLite(t, ""),
   731  			"--to", "file://testdata/sqlite",
   732  			"--to", "file://testdata/sqlite2",
   733  		)
   734  		require.EqualError(t, err, `the provided SQL state must be either a single schema file or a migration directory, but 2 paths were found`)
   735  
   736  		_, err = runCmd(
   737  			schemaApplyCmd(),
   738  			"-u", openSQLite(t, ""),
   739  			"--dev-url", openSQLite(t, ""),
   740  			"--to", "file://"+t.TempDir(),
   741  		)
   742  		require.ErrorContains(t, err, `contains neither SQL nor HCL files`)
   743  
   744  		p := t.TempDir()
   745  		require.NoError(t, os.WriteFile(filepath.Join(p, "schema.sql"), []byte(`create table t1 (id int NOT NULL);`), 0600))
   746  		require.NoError(t, os.WriteFile(filepath.Join(p, "schema.hcl"), []byte(`schema "main" {}`), 0600))
   747  		_, err = runCmd(
   748  			schemaApplyCmd(),
   749  			"-u", openSQLite(t, ""),
   750  			"--dev-url", openSQLite(t, ""),
   751  			"--to", "file://"+p,
   752  		)
   753  		require.EqualError(t, err, `ambiguous schema: both SQL and HCL files found: "schema.hcl", "schema.sql"`)
   754  
   755  		_, err = runCmd(
   756  			schemaApplyCmd(),
   757  			"-u", openSQLite(t, ""),
   758  			"--dev-url", openSQLite(t, ""),
   759  			"--to", "testdata/sqlite",
   760  		)
   761  		require.EqualError(t, err, "missing scheme. Did you mean file://testdata/sqlite?")
   762  	})
   763  }
   764  
   765  func TestSchema_InspectLog(t *testing.T) {
   766  	db := openSQLite(t, "create table t1 (id integer primary key);create table t2 (name text);")
   767  	cmd := schemaCmd()
   768  	cmd.AddCommand(schemaInspectCmd())
   769  	s, err := runCmd(
   770  		cmd, "inspect",
   771  		"-u", db,
   772  		"--format", "{{ json . }}",
   773  	)
   774  	require.NoError(t, err)
   775  	require.Equal(t, `{"schemas":[{"name":"main","tables":[{"name":"t1","columns":[{"name":"id","type":"INTEGER","null":true}],"primary_key":{"parts":[{"column":"id"}]}},{"name":"t2","columns":[{"name":"name","type":"TEXT","null":true}]}]}]}`, s)
   776  }
   777  
   778  func TestFmt(t *testing.T) {
   779  	for _, tt := range []struct {
   780  		name          string
   781  		inputDir      map[string]string
   782  		expectedDir   map[string]string
   783  		expectedFile  string
   784  		expectedOut   string
   785  		args          []string
   786  		expectedPrint bool
   787  	}{
   788  		{
   789  			name: "specific file",
   790  			inputDir: map[string]string{
   791  				"test.hcl": unformatted,
   792  			},
   793  			expectedDir: map[string]string{
   794  				"test.hcl": formatted,
   795  			},
   796  			args:        []string{"test.hcl"},
   797  			expectedOut: "test.hcl\n",
   798  		},
   799  		{
   800  			name: "current dir",
   801  			inputDir: map[string]string{
   802  				"test.hcl": unformatted,
   803  			},
   804  			expectedDir: map[string]string{
   805  				"test.hcl": formatted,
   806  			},
   807  			expectedOut: "test.hcl\n",
   808  		},
   809  		{
   810  			name: "multi path implicit",
   811  			inputDir: map[string]string{
   812  				"test.hcl":  unformatted,
   813  				"test2.hcl": unformatted,
   814  			},
   815  			expectedDir: map[string]string{
   816  				"test.hcl":  formatted,
   817  				"test2.hcl": formatted,
   818  			},
   819  			expectedOut: "test.hcl\ntest2.hcl\n",
   820  		},
   821  		{
   822  			name: "multi path explicit",
   823  			inputDir: map[string]string{
   824  				"test.hcl":  unformatted,
   825  				"test2.hcl": unformatted,
   826  			},
   827  			expectedDir: map[string]string{
   828  				"test.hcl":  formatted,
   829  				"test2.hcl": formatted,
   830  			},
   831  			args:        []string{"test.hcl", "test2.hcl"},
   832  			expectedOut: "test.hcl\ntest2.hcl\n",
   833  		},
   834  		{
   835  			name: "formatted",
   836  			inputDir: map[string]string{
   837  				"test.hcl": formatted,
   838  			},
   839  			expectedDir: map[string]string{
   840  				"test.hcl": formatted,
   841  			},
   842  		},
   843  	} {
   844  		t.Run(tt.name, func(t *testing.T) {
   845  			dir := setupFmtTest(t, tt.inputDir)
   846  			out, err := runCmd(schemaFmtCmd(), tt.args...)
   847  			require.NoError(t, err)
   848  			assertDir(t, dir, tt.expectedDir)
   849  			require.EqualValues(t, tt.expectedOut, out)
   850  		})
   851  	}
   852  }
   853  
   854  func TestSchema_Clean(t *testing.T) {
   855  	var (
   856  		u      = fmt.Sprintf("sqlite://file:%s?cache=shared&_fk=1", filepath.Join(t.TempDir(), "test.db"))
   857  		c, err = sqlclient.Open(context.Background(), u)
   858  	)
   859  	require.NoError(t, err)
   860  
   861  	// Apply migrations onto database.
   862  	_, err = runCmd(migrateApplyCmd(), "--dir", "file://testdata/sqlite", "--url", u)
   863  	require.NoError(t, err)
   864  
   865  	// Run clean and expect to be clean.
   866  	_, err = runCmd(migrateApplyCmd(), "--dir", "file://testdata/sqlite", "--url", u)
   867  	require.NoError(t, err)
   868  	s, err := runCmd(schemaCleanCmd(), "--url", u, "--auto-approve")
   869  	require.NoError(t, err)
   870  	require.NotZero(t, s)
   871  	require.NoError(t, c.Driver.(migrate.CleanChecker).CheckClean(context.Background(), nil))
   872  }
   873  
   874  func assertDir(t *testing.T, dir string, expected map[string]string) {
   875  	act := make(map[string]string)
   876  	files, err := os.ReadDir(dir)
   877  	require.NoError(t, err)
   878  	for _, f := range files {
   879  		if f.IsDir() {
   880  			continue
   881  		}
   882  		contents, err := os.ReadFile(filepath.Join(dir, f.Name()))
   883  		require.NoError(t, err)
   884  		act[f.Name()] = string(contents)
   885  	}
   886  	require.EqualValues(t, expected, act)
   887  }
   888  
   889  func setupFmtTest(t *testing.T, inputDir map[string]string) string {
   890  	wd, err := os.Getwd()
   891  	require.NoError(t, err)
   892  	dir, err := os.MkdirTemp(os.TempDir(), "fmt-test-")
   893  	require.NoError(t, err)
   894  	err = os.Chdir(dir)
   895  	require.NoError(t, err)
   896  	t.Cleanup(func() {
   897  		os.RemoveAll(dir)
   898  		os.Chdir(wd) //nolint:errcheck
   899  	})
   900  	for name, contents := range inputDir {
   901  		file := path.Join(dir, name)
   902  		err = os.WriteFile(file, []byte(contents), 0600)
   903  	}
   904  	require.NoError(t, err)
   905  	return dir
   906  }
   907  
   908  type assertNormalizerDriver struct {
   909  	migrate.Driver
   910  	t *testing.T
   911  }
   912  
   913  // NormalizeSchema returns the normal representation of a schema.
   914  func (d *assertNormalizerDriver) NormalizeSchema(context.Context, *schema.Schema) (*schema.Schema, error) {
   915  	d.t.Fatal("did not expect a call to NormalizeSchema")
   916  	return nil, nil
   917  }
   918  
   919  // NormalizeRealm returns the normal representation of a database.
   920  func (d *assertNormalizerDriver) NormalizeRealm(context.Context, *schema.Realm) (*schema.Realm, error) {
   921  	d.t.Fatal("did not expect a call to NormalizeRealm")
   922  	return nil, nil
   923  }