Repository: ariga/atlas
Pull request #229 (2021-11-15 16:30:22)

cmd/atlas: connect postgres
- for now there isn't a schema flag.
- the deafult schema in postgres is public.
- fixed a small bug when parsing dsn.



Patch:
diff --git a/cmd/atlas/mux.go b/cmd/atlas/mux.go
index 604c92d4..978e2e1c 100644
--- a/cmd/atlas/mux.go
+++ b/cmd/atlas/mux.go
@@ -9,7 +9,6 @@ import (
 	"ariga.io/atlas/schema/schemaspec"
 	"ariga.io/atlas/sql/schema"
 	"github.com/go-sql-driver/mysql"
-	"github.com/jackc/pgconn"
 )
 
 type (
@@ -98,11 +97,7 @@ func schemaNameFromDSN(url string) (string, error) {
 		}
 		return cfg.DBName, err
 	case "postgres":
-		cfg, err := pgconn.ParseConfig(dsn)
-		if err != nil {
-			return "", err
-		}
-		return cfg.Database, err
+		return "public", nil
 	default:
 		return "", fmt.Errorf("unknown database type: %q", key)
 	}
diff --git a/cmd/atlas/provider.go b/cmd/atlas/provider.go
index 3f4b14fe..39aa4b9f 100644
--- a/cmd/atlas/provider.go
+++ b/cmd/atlas/provider.go
@@ -32,7 +32,8 @@ func mysqlProvider(dsn string) (*Driver, error) {
 	}, nil
 }
 func postgresProvider(dsn string) (*Driver, error) {
-	db, err := sql.Open("postgres", dsn)
+	url := "postgres://" + dsn
+	db, err := sql.Open("postgres", url)
 	if err != nil {
 		return nil, err
 	}
diff --git a/schema/schemaspec/extension.go b/schema/schemaspec/extension.go
index faa0b4e5..700c6c4f 100644
--- a/schema/schemaspec/extension.go
+++ b/schema/schemaspec/extension.go
@@ -204,20 +204,41 @@ func setField(field reflect.Value, attr *Attr) error {
 }
 
 // setSliceAttr sets the value of attr to the slice field. This function expects both the target field
-// and the source attr to be slices. Currently, only slices of type *Ref are supported.
-// TODO(rotemtam): support also strings, booleans and numeric values.
+// and the source attr to be slices.
 func setSliceAttr(field reflect.Value, attr *Attr) error {
 	lst, ok := attr.V.(*ListValue)
 	if !ok {
 		return fmt.Errorf("schemaspec: field is of type slice but attr %q does not contain a ListValue", attr.K)
 	}
 	typ := field.Type().Elem()
-	if typ != reflect.TypeOf(&Ref{}) {
-		return fmt.Errorf("schemaspec: currently on ref slice values supported, got %s", typ)
-	}
+
 	slc := reflect.MakeSlice(reflect.SliceOf(typ), 0, len(lst.V))
-	for _, c := range lst.V {
-		slc = reflect.Append(slc, reflect.ValueOf(c))
+	switch typ.Kind() {
+	case reflect.String:
+		s, err := attr.Strings()
+		if err != nil {
+			return fmt.Errorf("cannot read attribute %q as string list: %w", attr.K, err)
+		}
+		for _, item := range s {
+			slc = reflect.Append(slc, reflect.ValueOf(item))
+		}
+	case reflect.Bool:
+		bools, err := attr.Bools()
+		if err != nil {
+			return fmt.Errorf("cannot read attribute %q as bool list: %w", attr.K, err)
+		}
+		for _, item := range bools {
+			slc = reflect.Append(slc, reflect.ValueOf(item))
+		}
+	case reflect.Ptr:
+		if typ != reflect.TypeOf(&Ref{}) {
+			return fmt.Errorf("only pointers to refs supported, got %s", typ)
+		}
+		for _, c := range lst.V {
+			slc = reflect.Append(slc, reflect.ValueOf(c))
+		}
+	default:
+		return fmt.Errorf("slice of unsupported kind: %q", typ.Kind())
 	}
 	field.Set(slc)
 	return nil
@@ -320,17 +341,32 @@ func scanAttr(key string, r *Resource, field reflect.Value) error {
 }
 
 // scanSliceAttr sets an Attr named "key" into the Resource r, by converting
-// the value stored in "field" into a *ListValue. Currently, only slices of *Ref
-// values are supported. TODO(rotemtam): support strings, booleans and numeric values.
+// the value stored in "field" into a *ListValue.
 func scanSliceAttr(key string, r *Resource, field reflect.Value) error {
 	typ := field.Type()
-	if typ.Kind() != reflect.Slice || typ.Elem() != reflect.TypeOf(&Ref{}) {
-		return fmt.Errorf("schemaspec: currently on ref slice values supported, got %s", typ)
-	}
 	lst := &ListValue{}
-	for i := 0; i < field.Len(); i++ {
-		item := field.Index(i).Interface().(*Ref)
-		lst.V = append(lst.V, item)
+
+	switch typ.Elem().Kind() {
+	case reflect.String:
+		for i := 0; i < field.Len(); i++ {
+			item := field.Index(i).Interface().(string)
+			lst.V = append(lst.V, &LiteralValue{V: strconv.Quote(item)})
+		}
+	case reflect.Bool:
+		for i := 0; i < field.Len(); i++ {
+			item := field.Index(i).Interface().(bool)
+			lst.V = append(lst.V, &LiteralValue{V: strconv.FormatBool(item)})
+		}
+	case reflect.Ptr:
+		if typ.Elem() != reflect.TypeOf(&Ref{}) {
+			return fmt.Errorf("schemaspec: currently on ref slice values supported, got %s", typ)
+		}
+		for i := 0; i < field.Len(); i++ {
+			item := field.Index(i).Interface().(*Ref)
+			lst.V = append(lst.V, item)
+		}
+	default:
+		return fmt.Errorf("unsupported kind %q for %q", typ.Kind(), key)
 	}
 	r.SetAttr(&Attr{
 		K: key,
diff --git a/sql/postgres/diff.go b/sql/postgres/diff.go
index 1c1cd2a8..fa5a4936 100644
--- a/sql/postgres/diff.go
+++ b/sql/postgres/diff.go
@@ -155,37 +155,19 @@ func (d *diff) typeChanged(from, to *schema.Column) (bool, error) {
 	}
 	var changed bool
 	switch fromT := fromT.(type) {
-	case *schema.BinaryType:
-		toT := toT.(*schema.BinaryType)
-		changed = fromT.T != toT.T
-	case *schema.DecimalType:
-		toT := toT.(*schema.DecimalType)
-		changed = fromT.T != toT.T || fromT.Scale != toT.Scale || fromT.Precision != toT.Precision
+	case *schema.BinaryType, *schema.BoolType, *schema.DecimalType, *schema.FloatType,
+		*schema.IntegerType, *schema.JSONType, *schema.SpatialType, *schema.StringType,
+		*schema.TimeType, *BitType, *SerialType:
+		changed = d.mustFormat(toT) != d.mustFormat(fromT)
 	case *EnumType:
 		toT := toT.(*schema.EnumType)
 		changed = fromT.T != toT.T || !sqlx.ValuesEqual(fromT.Values, toT.Values)
 	case *schema.EnumType:
 		toT := toT.(*schema.EnumType)
 		changed = fromT.T != toT.T || !sqlx.ValuesEqual(fromT.Values, toT.Values)
-	case *schema.FloatType:
-		toT := toT.(*schema.FloatType)
-		changed = fromT.T != toT.T || fromT.Precision != toT.Precision
-	case *schema.IntegerType:
-		toT := toT.(*schema.IntegerType)
-		// Unsigned integers are not supported.
-		changed = fromT.T != toT.T
 	case *NetworkType:
 		toT := toT.(*NetworkType)
 		changed = fromT.T != toT.T
-	case *SerialType:
-		toT := toT.(*SerialType)
-		changed = fromT.T != toT.T || fromT.Precision != toT.Precision
-	case *schema.StringType:
-		toT := toT.(*schema.StringType)
-		changed = fromT.T != toT.T || fromT.Size != toT.Size
-	case *BitType:
-		toT := toT.(*BitType)
-		changed = fromT.T != toT.T || fromT.Len != toT.Len
 	case *CurrencyType:
 		toT := toT.(*CurrencyType)
 		changed = fromT.T != toT.T
@@ -207,18 +189,6 @@ func (d *diff) typeChanged(from, to *schema.Column) (bool, error) {
 	case *UserDefinedType:
 		toT := toT.(*UserDefinedType)
 		changed = fromT.T != toT.T
-	case *schema.BoolType:
-		toT := toT.(*schema.BoolType)
-		changed = fromT.T != toT.T
-	case *schema.JSONType:
-		toT := toT.(*schema.JSONType)
-		changed = fromT.T != toT.T
-	case *schema.SpatialType:
-		toT := toT.(*schema.SpatialType)
-		changed = fromT.T != toT.T
-	case *schema.TimeType:
-		toT := toT.(*schema.TimeType)
-		changed = fromT.T != toT.T
 	default:
 		return false, &sqlx.UnsupportedTypeError{Type: fromT}
 	}
diff --git a/sql/postgres/migrate.go b/sql/postgres/migrate.go
index 6573e9a3..81f3f947 100644
--- a/sql/postgres/migrate.go
+++ b/sql/postgres/migrate.go
@@ -436,27 +436,27 @@ func skipAutoChanges(changes []schema.Change) []schema.Change {
 			dropC[c.C.Name] = true
 		}
 	}
-search:
 	for i, c := range changes {
 		switch c := c.(type) {
-		// Simple case for skipping key dropping, if its columns are dropped.
+		// Indexes involving the column are automatically dropped
+		// with it. This true for multi-columns indexes as well.
 		// See https://www.postgresql.org/docs/current/sql-altertable.html
 		case *schema.DropIndex:
 			for _, p := range c.I.Parts {
-				if p.C == nil || !dropC[p.C.Name] {
-					continue search
+				if p.C == nil && dropC[p.C.Name] {
+					changes = append(changes[:i], changes[i+1:]...)
+					break
 				}
 			}
-			changes = append(changes[:i], changes[i+1:]...)
 		// Simple case for skipping constraint dropping,
 		// if the child table columns were dropped.
 		case *schema.DropForeignKey:
 			for _, c := range c.F.Columns {
-				if !dropC[c.Name] {
-					continue search
+				if dropC[c.Name] {
+					changes = append(changes[:i], changes[i+1:]...)
+					break
 				}
 			}
-			changes = append(changes[:i], changes[i+1:]...)
 		}
 	}
 	return changes


Test patch:
diff --git a/internal/integration/postgres_test.go b/internal/integration/postgres_test.go
index 75597258..d035f49c 100644
--- a/internal/integration/postgres_test.go
+++ b/internal/integration/postgres_test.go
@@ -88,7 +88,10 @@ func TestPostgres_Relation(t *testing.T) {
 func TestPostgres_AddIndexedColumns(t *testing.T) {
 	pgRun(t, func(t *pgTest) {
 		ctx := context.Background()
-		usersT := t.users()
+		usersT := &schema.Table{
+			Name:    "users",
+			Columns: []*schema.Column{{Name: "id", Type: &schema.ColumnType{Type: &schema.IntegerType{T: "int"}}}},
+		}
 		err := t.drv.Migrate().Exec(ctx, []schema.Change{
 			&schema.AddTable{T: usersT},
 		})
@@ -102,20 +105,35 @@ func TestPostgres_AddIndexedColumns(t *testing.T) {
 			Name:    "b",
 			Type:    &schema.ColumnType{Raw: "bigint", Type: &schema.IntegerType{T: "bigint"}, Null: true},
 			Default: &schema.RawExpr{X: "10"},
+		}, &schema.Column{
+			Name:    "c",
+			Type:    &schema.ColumnType{Raw: "bigint", Type: &schema.IntegerType{T: "bigint"}, Null: true},
+			Default: &schema.RawExpr{X: "10"},
 		})
+		parts := usersT.Columns[len(usersT.Columns)-3:]
 		usersT.Indexes = append(usersT.Indexes, &schema.Index{
 			Unique: true,
-			Name:   "a_b_unique",
-			Parts:  []*schema.IndexPart{{C: usersT.Columns[1]}, {C: usersT.Columns[2]}},
+			Name:   "a_b_c_unique",
+			Parts:  []*schema.IndexPart{{C: parts[0]}, {C: parts[1]}, {C: parts[2]}},
 		})
-		realm := t.loadRealm()
-		changes, err := t.drv.Diff().TableDiff(realm.Schemas[0].Tables[0], usersT)
+		changes, err := t.drv.Diff().TableDiff(t.loadUsers(), usersT)
 		require.NoError(t, err)
-		require.NotEmpty(t, changes, "usersT contains 2 new columns and 1 new index")
-
+		require.NotEmpty(t, changes, "usersT contains 3 new columns and 1 new index")
 		err = t.drv.Migrate().Exec(ctx, []schema.Change{&schema.ModifyTable{T: usersT, Changes: changes}})
 		require.NoError(t, err)
 		t.ensureNoChange(usersT)
+
+		// Dropping a column involves in a multi-column
+		// index causes the index to be dropped as well.
+		usersT.Columns = usersT.Columns[:len(usersT.Columns)-1]
+		changes, err = t.drv.Diff().TableDiff(t.loadUsers(), usersT)
+		require.NoError(t, err)
+		err = t.drv.Migrate().Exec(ctx, []schema.Change{&schema.ModifyTable{T: usersT, Changes: changes}})
+		require.NoError(t, err)
+		t.ensureNoChange(t.loadUsers())
+		usersT = t.loadUsers()
+		_, ok := usersT.Index("a_b_c_unique")
+		require.False(t, ok)
 	})
 }
 
diff --git a/schema/schemaspec/schemahcl/hcl_test.go b/schema/schemaspec/schemahcl/hcl_test.go
index 41342c99..a7cd75bf 100644
--- a/schema/schemaspec/schemahcl/hcl_test.go
+++ b/schema/schemaspec/schemahcl/hcl_test.go
@@ -9,20 +9,26 @@ import (
 )
 
 func TestAttributes(t *testing.T) {
-	f := `i = 1
-b = true
-s = "hello, world"
+	f := `i  = 1
+b  = true
+s  = "hello, world"
+sl = ["hello", "world", ]
+bl = [true, false, ]
 `
 	var test struct {
-		Int  int    `spec:"i"`
-		Bool bool   `spec:"b"`
-		Str  string `spec:"s"`
+		Int        int      `spec:"i"`
+		Bool       bool     `spec:"b"`
+		Str        string   `spec:"s"`
+		StringList []string `spec:"sl"`
+		BoolList   []bool   `spec:"bl"`
 	}
 	err := Unmarshal([]byte(f), &test)
 	require.NoError(t, err)
 	require.EqualValues(t, 1, test.Int)
 	require.EqualValues(t, true, test.Bool)
 	require.EqualValues(t, "hello, world", test.Str)
+	require.EqualValues(t, []string{"hello", "world"}, test.StringList)
+	require.EqualValues(t, []bool{true, false}, test.BoolList)
 	marshal, err := Marshal(&test)
 	require.NoError(t, err)
 	require.EqualValues(t, f, string(marshal))