Repository: ariga/atlas
Pull request #1272 (2022-11-07 19:30:06)

cmd/atlas: support reading config files in 'schema diff' command
Fixed https://github.com/ariga/atlas/issues/1252


Patch:
diff --git a/cmd/atlas/internal/cmdapi/schema.go b/cmd/atlas/internal/cmdapi/schema.go
index d8d5a671..137ca562 100644
--- a/cmd/atlas/internal/cmdapi/schema.go
+++ b/cmd/atlas/internal/cmdapi/schema.go
@@ -272,6 +272,7 @@ The database states can be read from a connected database, an HCL project or a m
 			Example: `  atlas schema diff --from mysql://user:pass@localhost:3306/test --to file://schema.hcl
   atlas schema diff --from mysql://user:pass@localhost:3306 --to file://schema_1.hcl --to file://schema_2.hcl
   atlas schema diff --from mysql://user:pass@localhost:3306 --to file://migrations`,
+			PreRunE: schemaFlagsFromEnv,
 			RunE: func(cmd *cobra.Command, args []string) error {
 				return schemaDiffRun(cmd, args, flags)
 			},
diff --git a/sql/postgres/inspect.go b/sql/postgres/inspect.go
index 8ece5326..59536d53 100644
--- a/sql/postgres/inspect.go
+++ b/sql/postgres/inspect.go
@@ -5,6 +5,7 @@
 package postgres
 
 import (
+	"bytes"
 	"context"
 	"database/sql"
 	"fmt"
@@ -413,16 +414,8 @@ func mayAppendOps(part *schema.IndexPart, name string, params string, defaults b
 		return nil
 	}
 	op := &IndexOpClass{Name: name, Default: defaults}
-	switch {
-	case len(params) == 0:
-	case len(params) < 2, params[0] != '{', params[len(params)-1] != '}':
-		return fmt.Errorf("postgres: unexpected operator class parameters format: %q", params)
-	default:
-		for _, e := range strings.Split(params[1:len(params)-1], ",") {
-			if kv := strings.Split(strings.TrimSpace(e), "="); len(kv) == 2 {
-				op.Params = append(op.Params, struct{ N, V string }{N: kv[0], V: kv[1]})
-			}
-		}
+	if err := op.parseParams(params); err != nil {
+		return err
 	}
 	part.Attrs = append(part.Attrs, op)
 	return nil
@@ -975,6 +968,34 @@ func (o *IndexOpClass) String() string {
 	return b.String()
 }
 
+// UnmarshalText parses the operator class from its string representation.
+func (o *IndexOpClass) UnmarshalText(text []byte) error {
+	i := bytes.IndexByte(text, '(')
+	if i == -1 {
+		o.Name = string(text)
+		return nil
+	}
+	o.Name = string(text[:i])
+	return o.parseParams(string(text[i:]))
+}
+
+// parseParams parses index class parameters defined in HCL or returned
+// from the database. For example: '{k=v}', '(k1=v1,k2=v2)'.
+func (o *IndexOpClass) parseParams(kv string) error {
+	switch {
+	case kv == "":
+	case strings.HasPrefix(kv, "(") && strings.HasSuffix(kv, ")"), strings.HasPrefix(kv, "{") && strings.HasSuffix(kv, "}"):
+		for _, e := range strings.Split(kv[1:len(kv)-1], ",") {
+			if kv := strings.Split(strings.TrimSpace(e), "="); len(kv) == 2 {
+				o.Params = append(o.Params, struct{ N, V string }{N: kv[0], V: kv[1]})
+			}
+		}
+	default:
+		return fmt.Errorf("postgres: unexpected operator class parameters format: %q", kv)
+	}
+	return nil
+}
+
 // newIndexStorage parses and returns the index storage parameters.
 func newIndexStorage(opts string) (*IndexStorageParams, error) {
 	params := &IndexStorageParams{}
diff --git a/sql/postgres/sqlspec.go b/sql/postgres/sqlspec.go
index 0b27f0e6..9ad89ac0 100644
--- a/sql/postgres/sqlspec.go
+++ b/sql/postgres/sqlspec.go
@@ -354,13 +354,12 @@ func convertPart(spec *sqlspec.IndexPart, part *schema.IndexPart) error {
 		if err != nil {
 			return err
 		}
-		switch parts := strings.Split(expr.X, "("); {
-		case len(parts) == 1 && parts[0] != "":
-			part.Attrs = append(part.Attrs, &IndexOpClass{Name: parts[0]})
-		case len(parts) == 2 && strings.HasSuffix(parts[1], ")"):
-			return mayAppendOps(part, parts[0], fmt.Sprintf("{%s}", parts[1][:len(parts[1])-1]), false)
-		default:
-			return fmt.Errorf("unexpected index.on.ops expression %q", expr.X)
+		var op IndexOpClass
+		if err := op.UnmarshalText([]byte(expr.X)); err != nil {
+			return fmt.Errorf("unexpected index.on.ops expression %q: %w", expr.X, err)
+		}
+		if op.Name != "" {
+			part.Attrs = append(part.Attrs, &op)
 		}
 	case opc.IsRef():
 		name, err := opc.Ref()


Test patch:
diff --git a/cmd/atlas/internal/cmdapi/schema_test.go b/cmd/atlas/internal/cmdapi/schema_test.go
index 74618491..e44e2bad 100644
--- a/cmd/atlas/internal/cmdapi/schema_test.go
+++ b/cmd/atlas/internal/cmdapi/schema_test.go
@@ -192,6 +192,37 @@ table "tbl" {
 		"-- Add column \"col_3\" to table: \"tbl\"\nALTER TABLE `tbl` ADD COLUMN `col_3` text NOT NULL\n",
 		s,
 	)
+
+	t.Run("FromConfig", func(t *testing.T) {
+		var (
+			p   = t.TempDir()
+			cp  = filepath.Join(p, "atlas.hcl")
+			sp  = filepath.Join(p, "schema.hcl")
+			cfg = fmt.Sprintf(`env "local" { dev = "%s" }`, openSQLite(t, ""))
+		)
+		require.NoError(t, os.WriteFile(cp, []byte(cfg), 0600))
+		require.NoError(t, os.WriteFile(sp, []byte(`
+schema "main" {}
+table "users" {
+  schema = schema.main
+  column "id" {
+    type = int
+  }
+}
+`), 0600))
+
+		cmd := schemaCmd()
+		cmd.AddCommand(schemaDiffCmd())
+		s, err := runCmd(
+			cmd, "diff",
+			"-c", "file://"+cp,
+			"--env", "local",
+			"--to", "file://"+sp,
+			"--from", openSQLite(t, ""),
+		)
+		require.NoError(t, err)
+		require.Equal(t, "-- Create \"users\" table\nCREATE TABLE `users` (`id` int NOT NULL)\n", s)
+	})
 }
 
 func TestCmdSchemaApply(t *testing.T) {
diff --git a/sql/postgres/inspect_test.go b/sql/postgres/inspect_test.go
index 0bedd59f..94da2c39 100644
--- a/sql/postgres/inspect_test.go
+++ b/sql/postgres/inspect_test.go
@@ -674,6 +674,19 @@ public
 	}(), realm)
 }
 
+func TestIndexOpClass_UnmarshalText(t *testing.T) {
+	var op IndexOpClass
+	require.NoError(t, op.UnmarshalText([]byte("int4_ops")))
+	require.EqualValues(t, IndexOpClass{Name: "int4_ops"}, op)
+
+	op = IndexOpClass{}
+	require.NoError(t, op.UnmarshalText([]byte("")))
+	require.EqualValues(t, IndexOpClass{}, op)
+
+	require.NoError(t, op.UnmarshalText([]byte("int4_ops(siglen=1)")))
+	require.EqualValues(t, IndexOpClass{Name: "int4_ops", Params: []struct{ N, V string }{{"siglen", "1"}}}, op)
+}
+
 type mock struct {
 	sqlmock.Sqlmock
 }