Repository: ariga/atlas
Pull request #496 (2022-01-26 12:39:22)

cmd/action: multi path support for fmt


Patch:
diff --git a/cmd/action/fmt.go b/cmd/action/fmt.go
index 0ae3f2d6..9f5afe5f 100644
--- a/cmd/action/fmt.go
+++ b/cmd/action/fmt.go
@@ -28,8 +28,7 @@ Unless stated otherwise, the fmt command will use the current directory.
 After running, the command will print the names of the files it has formatted. If all
 files in the directory are formatted, no input will be printed out.
 `,
-		Run:  CmdFmtRun,
-		Args: cobra.MaximumNArgs(1),
+		Run: CmdFmtRun,
 	}
 )
 
@@ -40,10 +39,15 @@ func init() {
 // CmdFmtRun formats all HCL files in a given directory using canonical HCL formatting
 // rules.
 func CmdFmtRun(cmd *cobra.Command, args []string) {
-	path := "./"
-	if len(args) > 0 {
-		path = args[0]
+	if len(args) == 0 {
+		args = append(args, "./")
 	}
+	for _, path := range args {
+		handlePath(cmd, path)
+	}
+}
+
+func handlePath(cmd *cobra.Command, path string) {
 	tasks, err := tasks(path)
 	cobra.CheckErr(err)
 	for _, task := range tasks {
diff --git a/cmd/action/inspect.go b/cmd/action/inspect.go
index 64a3d1e4..2a133a3f 100644
--- a/cmd/action/inspect.go
+++ b/cmd/action/inspect.go
@@ -3,15 +3,17 @@ package action
 import (
 	"context"
 
+	"ariga.io/atlas/sql/schema"
 	"github.com/spf13/cobra"
 )
 
 var (
 	// InspectFlags are the flags used in Inspect command.
 	InspectFlags struct {
-		DSN  string
-		Web  bool
-		Addr string
+		DSN    string
+		Web    bool
+		Addr   string
+		Schema []string
 	}
 	// InspectCmd represents the inspect command.
 	InspectCmd = &cobra.Command{
@@ -29,7 +31,7 @@ and execute schema migrations against the given database.
 		Run: CmdInspectRun,
 		Example: `
 atlas schema inspect -d "mysql://user:pass@tcp(localhost:3306)/dbname"
-atlas schema inspect -d "mariadb://user:pass@tcp(localhost:3306)/dbname"
+atlas schema inspect -d "mariadb://user:pass@tcp(localhost:3306)/" --schema=schemaA,schemaB -s schemaC
 atlas schema inspect --dsn "postgres://user:pass@host:port/dbname"
 atlas schema inspect -d "sqlite://file:ex1.db?_fk=1"`,
 	}
@@ -45,7 +47,8 @@ func init() {
 		"[driver://username:password@protocol(address)/dbname?param=value] Select data source using the dsn format",
 	)
 	InspectCmd.Flags().BoolVarP(&InspectFlags.Web, "web", "w", false, "Open in a local Atlas UI")
-	InspectCmd.Flags().StringVarP(&InspectFlags.Addr, "addr", "", "127.0.0.1:5800", "used with -w, local address to bind the server to")
+	InspectCmd.Flags().StringVarP(&InspectFlags.Addr, "addr", "", "127.0.0.1:5800", "Used with -w, local address to bind the server to")
+	InspectCmd.Flags().StringSliceVarP(&InspectFlags.Schema, "schema", "s", nil, "Set schema name")
 	cobra.CheckErr(InspectCmd.MarkFlagRequired("dsn"))
 }
 
@@ -62,9 +65,14 @@ func CmdInspectRun(_ *cobra.Command, _ []string) {
 
 func inspectRun(d *Driver, dsn string) {
 	ctx := context.Background()
-	name, err := SchemaNameFromDSN(dsn)
-	cobra.CheckErr(err)
-	s, err := d.InspectSchema(ctx, name, nil)
+	schemas := InspectFlags.Schema
+	if n, err := SchemaNameFromDSN(dsn); n != "" {
+		cobra.CheckErr(err)
+		schemas = append(schemas, n)
+	}
+	s, err := d.InspectRealm(ctx, &schema.InspectRealmOption{
+		Schemas: schemas,
+	})
 	cobra.CheckErr(err)
 	ddl, err := d.MarshalSpec(s)
 	cobra.CheckErr(err)
diff --git a/cmd/action/mux.go b/cmd/action/mux.go
index 9de7d7d2..832e01b2 100644
--- a/cmd/action/mux.go
+++ b/cmd/action/mux.go
@@ -102,9 +102,7 @@ func SchemaNameFromDSN(url string) (string, error) {
 func postgresSchema(dsn string) (string, error) {
 	url, err := url.Parse(dsn)
 	if err != nil {
-		// For backwards compatibility, we default to "public" when failing to
-		// parse.
-		return "public", nil
+		return "", nil
 	}
 	// lib/pq supports setting default schemas via the `search_path` parameter
 	// in a dsn.
@@ -113,8 +111,7 @@ func postgresSchema(dsn string) (string, error) {
 	if schema := url.Query().Get("search_path"); schema != "" {
 		return schema, nil
 	}
-
-	return "public", nil
+	return "", nil
 }
 
 func schemaName(dsn string) (string, error) {
diff --git a/doc/md/cli.md b/doc/md/cli.md
index 2abe17a6..b0a9a032 100644
--- a/doc/md/cli.md
+++ b/doc/md/cli.md
@@ -161,15 +161,16 @@ and execute schema migrations against the given database.
 ```
 
 atlas schema inspect -d "mysql://user:pass@tcp(localhost:3306)/dbname"
-atlas schema inspect -d "mariadb://user:pass@tcp(localhost:3306)/dbname"
+atlas schema inspect -d "mariadb://user:pass@tcp(localhost:3306)/" --schema=schemaA,schemaB -s schemaC
 atlas schema inspect --dsn "postgres://user:pass@host:port/dbname"
 atlas schema inspect -d "sqlite://file:ex1.db?_fk=1"
 ```
 #### Flags
 ```
-      --addr string   used with -w, local address to bind the server to (default "127.0.0.1:5800")
-  -d, --dsn string    [driver://username:password@protocol(address)/dbname?param=value] Select data source using the dsn format
-  -w, --web           Open in a local Atlas UI
+      --addr string      Used with -w, local address to bind the server to (default "127.0.0.1:5800")
+  -d, --dsn string       [driver://username:password@protocol(address)/dbname?param=value] Select data source using the dsn format
+  -s, --schema strings   Set schema name
+  -w, --web              Open in a local Atlas UI
 
 ```
 
diff --git a/sql/internal/specutil/convert.go b/sql/internal/specutil/convert.go
index 15016d2d..67b14206 100644
--- a/sql/internal/specutil/convert.go
+++ b/sql/internal/specutil/convert.go
@@ -153,20 +153,42 @@ func Column(spec *sqlspec.Column, conv ConvertTypeFunc) (*schema.Column, error)
 
 // Index converts a sqlspec.Index to a schema.Index.
 func Index(spec *sqlspec.Index, parent *schema.Table) (*schema.Index, error) {
-	parts := make([]*schema.IndexPart, 0, len(spec.Columns))
-	for seqno, c := range spec.Columns {
-		cn, err := columnName(c)
-		if err != nil {
-			return nil, fmt.Errorf("specutil: failed converting column to index: %w", err)
+	parts := make([]*schema.IndexPart, 0, len(spec.Columns)+len(spec.Parts))
+	switch n, m := len(spec.Columns), len(spec.Parts); {
+	case n == 0 && m == 0:
+		return nil, fmt.Errorf("missing definition for index %q", spec.Name)
+	case n > 0 && m > 0:
+		return nil, fmt.Errorf(`multiple definitions for index %q, use "columns" or "on"`, spec.Name)
+	case n > 0:
+		for i, c := range spec.Columns {
+			c, err := column(parent, c)
+			if err != nil {
+				return nil, err
+			}
+			parts = append(parts, &schema.IndexPart{
+				SeqNo: i,
+				C:     c,
+			})
 		}
-		col, ok := parent.Column(cn)
-		if !ok {
-			return nil, fmt.Errorf("specutil: unknown column %q in table %q", cn, parent.Name)
+	case m > 0:
+		for i, p := range spec.Parts {
+			part := &schema.IndexPart{SeqNo: i, Desc: p.Desc}
+			switch {
+			case p.Column == nil && p.Expr == "":
+				return nil, fmt.Errorf(`"column" or "expr" are required for index %q at position %d`, spec.Name, i)
+			case p.Column != nil && p.Expr != "":
+				return nil, fmt.Errorf(`cannot use both "column" and "expr" in index %q at position %d`, spec.Name, i)
+			case p.Expr != "":
+				part.X = &schema.RawExpr{X: p.Expr}
+			case p.Column != nil:
+				c, err := column(parent, p.Column)
+				if err != nil {
+					return nil, err
+				}
+				part.C = c
+			}
+			parts = append(parts, part)
 		}
-		parts = append(parts, &schema.IndexPart{
-			SeqNo: seqno,
-			C:     col,
-		})
 	}
 	i := &schema.Index{
 		Name:   spec.Name,
@@ -192,17 +214,13 @@ func Check(spec *sqlspec.Check) (*schema.Check, error) {
 func PrimaryKey(spec *sqlspec.PrimaryKey, parent *schema.Table) (*schema.Index, error) {
 	parts := make([]*schema.IndexPart, 0, len(spec.Columns))
 	for seqno, c := range spec.Columns {
-		n, err := columnName(c)
+		c, err := column(parent, c)
 		if err != nil {
-			return nil, fmt.Errorf("sqlspec: cannot get column name %q as primary key for table %q", c.V, parent.Name)
-		}
-		pkc, ok := parent.Column(n)
-		if !ok {
-			return nil, fmt.Errorf("sqlspec: cannot set column %q as primary key for table %q", n, parent.Name)
+			return nil, nil
 		}
 		parts = append(parts, &schema.IndexPart{
 			SeqNo: seqno,
-			C:     pkc,
+			C:     c,
 		})
 	}
 	return &schema.Index{
@@ -262,15 +280,7 @@ func resolveCol(ref *schemaspec.Ref, sch *schema.Schema) (*schema.Column, error)
 	if !ok {
 		return nil, fmt.Errorf("sqlspec: table %q not found", t)
 	}
-	c, err := columnName(ref)
-	if err != nil {
-		return nil, fmt.Errorf("sqlspec: column %q not found", ref.V)
-	}
-	col, ok := tbl.Column(c)
-	if !ok {
-		return nil, fmt.Errorf("sqlspec: column %q not found in table %q", c, t)
-	}
-	return col, nil
+	return column(tbl, ref)
 }
 
 // FromRealm converts a schema.Realm into []sqlspec.Schema and []sqlspec.Table.
@@ -476,12 +486,16 @@ func SchemaName(ref *schemaspec.Ref) (string, error) {
 	return parts[1], nil
 }
 
-func columnName(ref *schemaspec.Ref) (string, error) {
+func column(t *schema.Table, ref *schemaspec.Ref) (*schema.Column, error) {
 	s := strings.Split(ref.V, "$column.")
 	if len(s) != 2 {
-		return "", fmt.Errorf("sqlspec: failed to extract column name from %q", ref)
+		return nil, fmt.Errorf("specutil: failed to extract column name from %q", ref)
 	}
-	return s[1], nil
+	c, ok := t.Column(s[1])
+	if !ok {
+		return nil, fmt.Errorf("specutil: unknown column %q in table %q", s[1], t.Name)
+	}
+	return c, nil
 }
 
 func tableName(ref *schemaspec.Ref) (string, error) {
diff --git a/sql/postgres/inspect.go b/sql/postgres/inspect.go
index 5bbeb3c5..63c6742a 100644
--- a/sql/postgres/inspect.go
+++ b/sql/postgres/inspect.go
@@ -706,7 +706,7 @@ const (
 	paramsQuery = `SELECT setting FROM pg_settings WHERE name IN ('lc_collate', 'lc_ctype', 'server_version_num') ORDER BY name`
 
 	// Query to list database schemas.
-	schemasQuery = "SELECT schema_name FROM information_schema.schemata WHERE schema_name NOT IN ('information_schema', 'pg_catalog', 'pg_toast') ORDER BY schema_name"
+	schemasQuery = "SELECT schema_name FROM information_schema.schemata WHERE schema_name NOT IN ('information_schema', 'pg_catalog', 'pg_toast') AND schema_name NOT LIKE 'pg_%temp_%' ORDER BY schema_name"
 
 	// Query to list specific database schemas.
 	schemasQueryArgs = "SELECT schema_name FROM information_schema.schemata WHERE schema_name %s ORDER BY schema_name"
diff --git a/sql/schema/dsl.go b/sql/schema/dsl.go
index ba9acb7c..e89e4870 100644
--- a/sql/schema/dsl.go
+++ b/sql/schema/dsl.go
@@ -571,14 +571,25 @@ func (i *Index) AddParts(parts ...*IndexPart) *Index {
 		if p.C != nil && !p.C.hasIndex(i) {
 			p.C.Indexes = append(p.C.Indexes, i)
 		}
+		p.SeqNo = len(i.Parts)
 		i.Parts = append(i.Parts, p)
 	}
 	return i
 }
 
 // NewIndexPart creates a new index part.
-func NewIndexPart(seqno int) *IndexPart {
-	return &IndexPart{SeqNo: seqno}
+func NewIndexPart() *IndexPart { return &IndexPart{} }
+
+// NewColumnPart creates a new index part with the given column.
+func NewColumnPart(c *Column) *IndexPart { return &IndexPart{C: c} }
+
+// NewExprPart creates a new index part with the given expression.
+func NewExprPart(x Expr) *IndexPart { return &IndexPart{X: x} }
+
+// SetDesc configures the "DESC" attribute of the key part.
+func (p *IndexPart) SetDesc(b bool) *IndexPart {
+	p.Desc = b
+	return p
 }
 
 // AddAttrs adds and additional attributes to the index-part.
diff --git a/sql/sqlspec/sqlspec.go b/sql/sqlspec/sqlspec.go
index 73ac81d3..b004b00a 100644
--- a/sql/sqlspec/sqlspec.go
+++ b/sql/sqlspec/sqlspec.go
@@ -44,10 +44,19 @@ type (
 	Index struct {
 		Name    string            `spec:",name"`
 		Unique  bool              `spec:"unique"`
+		Parts   []*IndexPart      `spec:"on"`
 		Columns []*schemaspec.Ref `spec:"columns"`
 		schemaspec.DefaultExtension
 	}
 
+	// IndexPart holds a specification for the index key part.
+	IndexPart struct {
+		Desc   bool            `spec:"desc"`
+		Column *schemaspec.Ref `spec:"column"`
+		Expr   string          `spec:"expr"`
+		schemaspec.DefaultExtension
+	}
+
 	// Check holds a specification for a check constraint on a table.
 	Check struct {
 		Name string `spec:",name"`
@@ -69,26 +78,6 @@ type (
 	Type string
 )
 
-// Type<X> are Types that represent database agnostic column types to be used
-// in Atlas DDL documents.
-const (
-	TypeInt     Type = "int"
-	TypeInt8    Type = "int8"
-	TypeInt16   Type = "int16"
-	TypeInt64   Type = "int64"
-	TypeUint    Type = "uint"
-	TypeUint8   Type = "uint8"
-	TypeUint16  Type = "uint16"
-	TypeUint64  Type = "uint64"
-	TypeString  Type = "string"
-	TypeBinary  Type = "binary"
-	TypeEnum    Type = "enum"
-	TypeBoolean Type = "boolean"
-	TypeDecimal Type = "decimal"
-	TypeFloat   Type = "float"
-	TypeTime    Type = "time"
-)
-
 func init() {
 	schemaspec.Register("table", &Table{})
 	schemaspec.Register("schema", &Schema{})


Test patch:
diff --git a/cmd/action/fmt_test.go b/cmd/action/fmt_test.go
index 90a5d6de..92d1acbd 100644
--- a/cmd/action/fmt_test.go
+++ b/cmd/action/fmt_test.go
@@ -6,6 +6,7 @@ package action
 
 import (
 	"bytes"
+	"io/ioutil"
 	"os"
 	"path"
 	"path/filepath"
@@ -30,49 +31,118 @@ const (
 func TestFmt(t *testing.T) {
 	for _, tt := range []struct {
 		name          string
-		input         string
+		inputDir      map[string]string
+		expectedDir   map[string]string
 		expectedFile  string
+		expectedOut   string
+		args          []string
 		expectedPrint bool
 	}{
 		{
-			name:          "unformatted",
-			input:         unformatted,
-			expectedFile:  formatted,
-			expectedPrint: true,
+			name: "specific file",
+			inputDir: map[string]string{
+				"test.hcl": unformatted,
+			},
+			expectedDir: map[string]string{
+				"test.hcl": formatted,
+			},
+			args:        []string{"test.hcl"},
+			expectedOut: "test.hcl\n",
 		},
 		{
-			name:          "formatted",
-			input:         formatted,
-			expectedFile:  formatted,
-			expectedPrint: false,
+			name: "current dir",
+			inputDir: map[string]string{
+				"test.hcl": unformatted,
+			},
+			expectedDir: map[string]string{
+				"test.hcl": formatted,
+			},
+			expectedOut: "test.hcl\n",
+		},
+		{
+			name: "multi path implicit",
+			inputDir: map[string]string{
+				"test.hcl":  unformatted,
+				"test2.hcl": unformatted,
+			},
+			expectedDir: map[string]string{
+				"test.hcl":  formatted,
+				"test2.hcl": formatted,
+			},
+			expectedOut: "test.hcl\ntest2.hcl\n",
+		},
+		{
+			name: "multi path explicit",
+			inputDir: map[string]string{
+				"test.hcl":  unformatted,
+				"test2.hcl": unformatted,
+			},
+			expectedDir: map[string]string{
+				"test.hcl":  formatted,
+				"test2.hcl": formatted,
+			},
+			args:        []string{"test.hcl", "test2.hcl"},
+			expectedOut: "test.hcl\ntest2.hcl\n",
+		},
+		{
+			name: "formatted",
+			inputDir: map[string]string{
+				"test.hcl": formatted,
+			},
+			expectedDir: map[string]string{
+				"test.hcl": formatted,
+			},
 		},
 	} {
 		t.Run(tt.name, func(t *testing.T) {
-			var out bytes.Buffer
-			filename := "test.hcl"
-			dir := setupFmtTest(t, filename, tt.input)
-			FmtCmd.ResetCommands() // Detach from sub-commands and parents, needed to skip input validation done by them.
-			FmtCmd.SetOut(&out)
-			FmtCmd.SetArgs([]string{dir})
-			err := FmtCmd.Execute()
-			require.NoError(t, err)
-			require.Equal(t, tt.expectedPrint, out.Len() > 0)
-			rf, err := os.ReadFile(filepath.Join(dir, filename))
-			require.NoError(t, err)
-			require.Equal(t, tt.expectedFile, string(rf))
+			dir := setupFmtTest(t, tt.inputDir)
+			out := runFmt(t, tt.args)
+			assertDir(t, dir, tt.expectedDir)
+			require.EqualValues(t, tt.expectedOut, out)
 		})
 	}
 }
 
-func setupFmtTest(t *testing.T, filename, contents string) string {
+func runFmt(t *testing.T, args []string) string {
+	var out bytes.Buffer
+	FmtCmd.ResetCommands() // Detach from sub-commands and parents, needed to skip input validation done by them.
+	FmtCmd.SetOut(&out)
+	FmtCmd.SetArgs(args)
+	err := FmtCmd.Execute()
+	require.NoError(t, err)
+	return out.String()
+}
+
+func assertDir(t *testing.T, dir string, expected map[string]string) {
+	act := make(map[string]string)
+	files, err := ioutil.ReadDir(dir)
+	require.NoError(t, err)
+	for _, f := range files {
+		if f.IsDir() {
+			continue
+		}
+		contents, err := os.ReadFile(filepath.Join(dir, f.Name()))
+		require.NoError(t, err)
+		act[f.Name()] = string(contents)
+	}
+	require.EqualValues(t, expected, act)
+}
+
+func setupFmtTest(t *testing.T, inputDir map[string]string) string {
+	wd, err := os.Getwd()
+	require.NoError(t, err)
 	dir, err := os.MkdirTemp(os.TempDir(), "fmt-test-")
 	require.NoError(t, err)
+	err = os.Chdir(dir)
+	require.NoError(t, err)
 	t.Cleanup(func() {
 		os.RemoveAll(dir)
+		os.Chdir(wd) //nolint:errcheck
 	})
-
-	file := path.Join(dir, filename)
-	err = os.WriteFile(file, []byte(contents), 0644)
+	for name, contents := range inputDir {
+		file := path.Join(dir, name)
+		err = os.WriteFile(file, []byte(contents), 0644)
+	}
 	require.NoError(t, err)
 	return dir
 }
diff --git a/internal/integration/integration_test.go b/internal/integration/integration_test.go
index 816e904d..9e1f0434 100644
--- a/internal/integration/integration_test.go
+++ b/internal/integration/integration_test.go
@@ -8,6 +8,7 @@ import (
 	"io/ioutil"
 	"os"
 	"os/exec"
+	"strings"
 	"testing"
 	"time"
 
@@ -33,6 +34,7 @@ type T interface {
 	migrate(...schema.Change)
 	diff(*schema.Table, *schema.Table) []schema.Change
 	applyHcl(spec string)
+	applyRealmHcl(spec string)
 }
 
 func testAddDrop(t T) {
@@ -169,6 +171,33 @@ func testCLISchemaInspect(t T, h string, dsn string, unmarshaler schemaspec.Unma
 	require.Equal(t, expected, actual)
 }
 
+func testCLIMultiSchemaInspect(t T, h string, dsn string, schemas []string, unmarshaler schemaspec.Unmarshaler) {
+	// Required to have a clean "stderr" while running first time.
+	err := exec.Command("go", "run", "-mod=mod", "ariga.io/atlas/cmd/atlas").Run()
+	require.NoError(t, err)
+	var expected schema.Realm
+	err = unmarshaler.UnmarshalSpec([]byte(h), &expected)
+	require.NoError(t, err)
+	t.applyRealmHcl(h)
+	cmd := exec.Command("go", "run", "ariga.io/atlas/cmd/atlas",
+		"schema",
+		"inspect",
+		"-d",
+		dsn,
+		"-s",
+		strings.Join(schemas, ","),
+	)
+	stdout, stderr := bytes.NewBuffer(nil), bytes.NewBuffer(nil)
+	cmd.Stderr = stderr
+	cmd.Stdout = stdout
+	require.NoError(t, cmd.Run(), stderr.String())
+	var actual schema.Realm
+	err = unmarshaler.UnmarshalSpec(stdout.Bytes(), &actual)
+	require.NoError(t, err)
+	require.Empty(t, stderr.String())
+	require.Equal(t, expected, actual)
+}
+
 func testCLISchemaApply(t T, h string, dsn string) {
 	// Required to have a clean "stderr" while running first time.
 	err := exec.Command("go", "run", "-mod=mod", "ariga.io/atlas/cmd/atlas").Run()
diff --git a/internal/integration/mysql_test.go b/internal/integration/mysql_test.go
index 35539b42..1e1420de 100644
--- a/internal/integration/mysql_test.go
+++ b/internal/integration/mysql_test.go
@@ -527,26 +527,65 @@ func TestMySQL_CLI(t *testing.T) {
 		myRun(t, func(t *myTest) {
 			attrs := t.defaultAttrs()
 			charset, collate := attrs[0].(*schema.Charset), attrs[1].(*schema.Collation)
-			testCLISchemaInspect(t, fmt.Sprintf(h, charset.V, collate.V), t.dsn(), mysql.UnmarshalHCL)
+			testCLISchemaInspect(t, fmt.Sprintf(h, charset.V, collate.V), t.dsn("test"), mysql.UnmarshalHCL)
 		})
 	})
 	t.Run("SchemaApply", func(t *testing.T) {
 		myRun(t, func(t *myTest) {
 			attrs := t.defaultAttrs()
 			charset, collate := attrs[0].(*schema.Charset), attrs[1].(*schema.Collation)
-			testCLISchemaApply(t, fmt.Sprintf(h, charset.V, collate.V), t.dsn())
+			testCLISchemaApply(t, fmt.Sprintf(h, charset.V, collate.V), t.dsn("test"))
 		})
 	})
 	t.Run("SchemaApplyDryRun", func(t *testing.T) {
 		myRun(t, func(t *myTest) {
 			attrs := t.defaultAttrs()
 			charset, collate := attrs[0].(*schema.Charset), attrs[1].(*schema.Collation)
-			testCLISchemaApplyDry(t, fmt.Sprintf(h, charset.V, collate.V), t.dsn())
+			testCLISchemaApplyDry(t, fmt.Sprintf(h, charset.V, collate.V), t.dsn("test"))
 		})
 	})
 	t.Run("SchemaDiffRun", func(t *testing.T) {
 		myRun(t, func(t *myTest) {
-			testCLISchemaDiff(t, t.dsn())
+			testCLISchemaDiff(t, t.dsn("test"))
+		})
+	})
+}
+
+func TestMySQL_CLI_MultiSchema(t *testing.T) {
+	t.Run("SchemaInspect", func(t *testing.T) {
+		h := `	
+			schema "test" {
+				charset   = "%s"
+				collation = "%s"
+			}
+			table "users" {
+				schema = schema.test
+				column "id" {
+					type = int
+				}
+				primary_key {
+					columns = [table.users.column.id]
+				}
+			}
+			schema "test2" {
+				charset   = "%s"
+				collation = "%s"
+			}
+			table "users" {
+				schema = schema.test2
+				column "id" {
+					type = int
+				}
+				primary_key {
+					columns = [table.users.column.id]
+				}
+			}`
+		myRun(t, func(t *myTest) {
+			t.dropDB("test2")
+			t.dropTables("users")
+			attrs := t.defaultAttrs()
+			charset, collate := attrs[0].(*schema.Charset), attrs[1].(*schema.Collation)
+			testCLIMultiSchemaInspect(t, fmt.Sprintf(h, charset.V, collate.V, charset.V, collate.V), t.dsn(""), []string{"test", "test2"}, mysql.UnmarshalHCL)
 		})
 	})
 }
@@ -1040,12 +1079,12 @@ create table atlas_types_sanity
 	})
 }
 
-func (t *myTest) dsn() string {
+func (t *myTest) dsn(dbname string) string {
 	d := "mysql"
 	if t.mariadb() {
 		d = "mariadb"
 	}
-	return fmt.Sprintf("%s://root:pass@tcp(localhost:%d)/test", d, t.port)
+	return fmt.Sprintf("%s://root:pass@tcp(localhost:%d)/%s", d, t.port, dbname)
 }
 
 func (t *myTest) applyHcl(spec string) {
@@ -1092,8 +1131,10 @@ func (t *myTest) dropTables(names ...string) {
 
 func (t *myTest) dropDB(names ...string) {
 	t.Cleanup(func() {
-		_, err := t.db.Exec("DROP DATABASE IF EXISTS " + strings.Join(names, ", "))
-		require.NoError(t.T, err, "drop db %q", names)
+		for _, n := range names {
+			_, err := t.db.Exec("DROP DATABASE IF EXISTS " + n)
+			require.NoError(t.T, err, "drop db %q", names)
+		}
 	})
 }
 
diff --git a/internal/integration/postgres_test.go b/internal/integration/postgres_test.go
index ea776bf0..9227c9dc 100644
--- a/internal/integration/postgres_test.go
+++ b/internal/integration/postgres_test.go
@@ -437,7 +437,7 @@ schema "second" {
 func (t *pgTest) applyRealmHcl(spec string) {
 	realm := t.loadRealm()
 	var desired schema.Realm
-	err := mysql.UnmarshalHCL([]byte(spec), &desired)
+	err := postgres.UnmarshalHCL([]byte(spec), &desired)
 	require.NoError(t, err)
 	diff, err := t.drv.RealmDiff(realm, &desired)
 	require.NoError(t, err)
@@ -480,6 +480,39 @@ func TestPostgres_CLI(t *testing.T) {
 	})
 }
 
+func TestPostgres_CLI_MultiSchema(t *testing.T) {
+	t.Run("SchemaInspect", func(t *testing.T) {
+		h := `
+			schema "public" {	
+			}
+			table "users" {
+				schema = schema.public
+				column "id" {
+					type = integer
+				}
+				primary_key {
+					columns = [table.users.column.id]
+				}
+			}
+			schema "test2" {	
+			}
+			table "users" {
+				schema = schema.test2
+				column "id" {
+					type = integer
+				}
+				primary_key {
+					columns = [table.users.column.id]
+				}
+			}`
+		pgRun(t, func(t *pgTest) {
+			t.dropSchemas("test2")
+			t.dropTables("users")
+			testCLIMultiSchemaInspect(t, h, t.dsn(), []string{"public", "test2"}, postgres.UnmarshalHCL)
+		})
+	})
+}
+
 func TestPostgres_DefaultsHCL(t *testing.T) {
 	n := "atlas_defaults"
 	pgRun(t, func(t *pgTest) {
@@ -1042,7 +1075,7 @@ func (t *pgTest) dropTables(names ...string) {
 
 func (t *pgTest) dropSchemas(names ...string) {
 	t.Cleanup(func() {
-		_, err := t.db.Exec("DROP SCHEMA IF EXISTS " + strings.Join(names, ", "))
+		_, err := t.db.Exec("DROP SCHEMA IF EXISTS " + strings.Join(names, ", ") + " CASCADE")
 		require.NoError(t.T, err, "drop schema %q", names)
 	})
 }
diff --git a/internal/integration/sqlite_test.go b/internal/integration/sqlite_test.go
index fbfc48b4..9668951e 100644
--- a/internal/integration/sqlite_test.go
+++ b/internal/integration/sqlite_test.go
@@ -839,3 +839,7 @@ func (t *liteTest) dropTables(names ...string) {
 func (t *liteTest) dsn() string {
 	return fmt.Sprintf("sqlite://file:%s?cache=shared&_fk=1", t.file)
 }
+
+func (t *liteTest) applyRealmHcl(spec string) {
+	t.applyHcl(spec)
+}
diff --git a/sql/internal/specutil/convert_test.go b/sql/internal/specutil/convert_test.go
index 80c78602..ec29043a 100644
--- a/sql/internal/specutil/convert_test.go
+++ b/sql/internal/specutil/convert_test.go
@@ -10,13 +10,6 @@ import (
 	"github.com/stretchr/testify/require"
 )
 
-func TestRef_ColumnName(t *testing.T) {
-	ref := &schemaspec.Ref{V: "$table.accounts.$column.user_active"}
-	c, err := columnName(ref)
-	require.NoError(t, err)
-	require.Equal(t, "user_active", c)
-}
-
 func TestRef_TableName(t *testing.T) {
 	ref := &schemaspec.Ref{V: "$table.accounts.$column.user_active"}
 	c, err := tableName(ref)
diff --git a/sql/mysql/migrate_test.go b/sql/mysql/migrate_test.go
index 12edf0cf..94dc2e72 100644
--- a/sql/mysql/migrate_test.go
+++ b/sql/mysql/migrate_test.go
@@ -188,9 +188,7 @@ func TestPlanChanges(t *testing.T) {
 								Type: &schema.ColumnType{Type: &schema.StringType{T: "varchar(255)"}},
 								Indexes: []*schema.Index{
 									schema.NewIndex("name_index").
-										AddParts(schema.NewIndexPart(0).SetColumn(
-											schema.NewColumn("name"),
-										)),
+										AddParts(schema.NewColumnPart(schema.NewColumn("name"))),
 								},
 							}},
 					}
@@ -199,9 +197,7 @@ func TestPlanChanges(t *testing.T) {
 						Changes: []schema.Change{
 							&schema.DropIndex{
 								I: schema.NewIndex("name_index").
-									AddParts(schema.NewIndexPart(0).SetColumn(
-										schema.NewColumn("name"),
-									)),
+									AddParts(schema.NewColumnPart(schema.NewColumn("name"))),
 							},
 						},
 					}
diff --git a/sql/mysql/sqlspec_test.go b/sql/mysql/sqlspec_test.go
index 7b70d2e2..2d57265d 100644
--- a/sql/mysql/sqlspec_test.go
+++ b/sql/mysql/sqlspec_test.go
@@ -471,6 +471,47 @@ schema "test" {
 	require.EqualValues(t, expected, string(buf))
 }
 
+func TestUnmarshalSpec_IndexParts(t *testing.T) {
+	var (
+		s schema.Schema
+		f = `
+schema "test" {}
+table "users" {
+	schema = schema.test
+	column "name" {
+		type = text
+	}
+	index "idx" {
+		on {
+			column = table.users.column.name
+			desc = true
+		}
+		on {
+			expr = "lower(name)"
+		}
+	}
+}
+`
+	)
+	err := UnmarshalHCL([]byte(f), &s)
+	require.NoError(t, err)
+	c := schema.NewStringColumn("name", "text")
+	exp := schema.New("test").
+		AddTables(
+			schema.NewTable("users").
+				AddColumns(c).
+				AddIndexes(
+					schema.NewIndex("idx").
+						AddParts(
+							schema.NewColumnPart(c).SetDesc(true),
+							schema.NewExprPart(&schema.RawExpr{X: "lower(name)"}),
+						),
+				),
+		)
+	exp.Tables[0].Columns[0].Indexes = nil
+	require.EqualValues(t, exp, &s)
+}
+
 func TestMarshalSpec_TimePrecision(t *testing.T) {
 	s := schema.New("test").
 		AddTables(