Repository: ariga/atlas
Pull request #521 (2022-01-27 22:58:41)

schema/schemaspec: add support for omitempty


Patch:
diff --git a/schema/schemaspec/extension.go b/schema/schemaspec/extension.go
index c247a434..f63b8904 100644
--- a/schema/schemaspec/extension.go
+++ b/schema/schemaspec/extension.go
@@ -364,6 +364,7 @@ func (r *Resource) Scan(ext interface{}) error {
 	for _, ft := range specFields(ext) {
 		field := v.FieldByName(ft.Name)
 		switch {
+		case ft.omitempty() && isEmpty(field):
 		case ft.isName():
 			if field.Kind() != reflect.String {
 				return errors.New("schemaspec: extension name field must be string")
@@ -512,29 +513,54 @@ func specFields(ext interface{}) []fieldDesc {
 	var fields []fieldDesc
 	for i := 0; i < t.Elem().NumField(); i++ {
 		f := t.Elem().Field(i)
-		lookup, ok := f.Tag.Lookup("spec")
+		tag, ok := f.Tag.Lookup("spec")
 		if !ok {
 			continue
 		}
-		fields = append(fields, fieldDesc{
-			tag:         lookup,
-			StructField: f,
-		})
+		d := fieldDesc{tag: tag, StructField: f}
+		if idx := strings.IndexByte(tag, ','); idx != -1 {
+			d.tag, d.options = tag[:idx], tag[idx+1:]
+		}
+		fields = append(fields, d)
 	}
 	return fields
 }
 
+func isEmpty(v reflect.Value) bool {
+	switch v.Kind() {
+	case reflect.Array, reflect.Map, reflect.Slice, reflect.String:
+		return v.Len() == 0
+	case reflect.Bool:
+		return !v.Bool()
+	case reflect.Interface, reflect.Ptr:
+		return v.IsNil()
+	case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
+		return v.Int() == 0
+	case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
+		return v.Uint() == 0
+	case reflect.Float32, reflect.Float64:
+		return v.Float() == 0
+	}
+	return false
+}
+
 type fieldDesc struct {
-	tag string
+	tag     string // tag name.
+	options string // rest of the options.
 	reflect.StructField
 }
 
-func (f fieldDesc) isName() bool {
-	if f.tag == "" {
-		return false
+func (f fieldDesc) isName() bool { return f.is("name") }
+
+func (f fieldDesc) omitempty() bool { return f.is("omitempty") }
+
+func (f fieldDesc) is(t string) bool {
+	for _, opt := range strings.Split(f.options, ",") {
+		if opt == t {
+			return true
+		}
 	}
-	parts := strings.Split(f.tag, ",")
-	return len(parts) > 1 && parts[1] == "name"
+	return false
 }
 
 func (f fieldDesc) isInterfaceSlice() bool {
diff --git a/sql/internal/specutil/convert.go b/sql/internal/specutil/convert.go
index 83ad0e3a..727e3cb0 100644
--- a/sql/internal/specutil/convert.go
+++ b/sql/internal/specutil/convert.go
@@ -172,17 +172,14 @@ func Index(spec *sqlspec.Index, parent *schema.Table) (*schema.Index, error) {
 		}
 	case m > 0:
 		for i, p := range spec.Parts {
-			part := &schema.IndexPart{SeqNo: i}
-			if p.Desc != nil {
-				part.Desc = *p.Desc
-			}
+			part := &schema.IndexPart{SeqNo: i, Desc: p.Desc}
 			switch {
-			case p.Column == nil && p.Expr == nil:
+			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 != nil:
+			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 != nil:
-				part.X = &schema.RawExpr{X: *p.Expr}
+			case p.Expr != "":
+				part.X = &schema.RawExpr{X: p.Expr}
 			case p.Column != nil:
 				c, err := column(parent, p.Column)
 				if err != nil {
@@ -442,7 +439,7 @@ func FromIndex(idx *schema.Index) (*sqlspec.Index, error) {
 	}
 	spec.Parts = make([]*sqlspec.IndexPart, len(idx.Parts))
 	for i, p := range idx.Parts {
-		part := &sqlspec.IndexPart{Desc: &p.Desc}
+		part := &sqlspec.IndexPart{Desc: p.Desc}
 		switch {
 		case p.C == nil && p.X == nil:
 			return nil, fmt.Errorf("missing column or expression for key part of index %q", idx.Name)
@@ -455,7 +452,7 @@ func FromIndex(idx *schema.Index) (*sqlspec.Index, error) {
 			if !ok {
 				return nil, fmt.Errorf("unexpected expression %T for index %q", p.X, idx.Name)
 			}
-			part.Expr = &x.X
+			part.Expr = x.X
 		}
 		spec.Parts[i] = part
 	}
diff --git a/sql/sqlspec/sqlspec.go b/sql/sqlspec/sqlspec.go
index 3298ef24..a3bd7eda 100644
--- a/sql/sqlspec/sqlspec.go
+++ b/sql/sqlspec/sqlspec.go
@@ -43,7 +43,7 @@ type (
 	// Index holds a specification for the index key of a table.
 	Index struct {
 		Name    string            `spec:",name"`
-		Unique  bool              `spec:"unique"`
+		Unique  bool              `spec:"unique,omitempty"`
 		Parts   []*IndexPart      `spec:"on"`
 		Columns []*schemaspec.Ref `spec:"columns"`
 		schemaspec.DefaultExtension
@@ -51,9 +51,9 @@ type (
 
 	// IndexPart holds a specification for the index key part.
 	IndexPart struct {
-		Desc   *bool           `spec:"desc"`
+		Desc   bool            `spec:"desc,omitempty"`
 		Column *schemaspec.Ref `spec:"column"`
-		Expr   *string         `spec:"expr"`
+		Expr   string          `spec:"expr,omitempty"`
 		schemaspec.DefaultExtension
 	}
 


Test patch:
diff --git a/schema/schemaspec/extension_test.go b/schema/schemaspec/extension_test.go
index 6d7664f5..48385648 100644
--- a/schema/schemaspec/extension_test.go
+++ b/schema/schemaspec/extension_test.go
@@ -16,6 +16,8 @@ type OwnerBlock struct {
 	Born      int                      `spec:"born"`
 	Active    bool                     `spec:"active"`
 	BoolPtr   *bool                    `spec:"bool_ptr"`
+	OmitBool1 bool                     `spec:"omit_bool1,omitempty"`
+	OmitBool2 bool                     `spec:"omit_bool2,omitempty"`
 	Lit       *schemaspec.LiteralValue `spec:"lit"`
 }
 
@@ -47,6 +49,7 @@ func TestExtension(t *testing.T) {
 			schemautil.LitAttr("born", "2019"),
 			schemautil.LitAttr("active", "true"),
 			schemautil.LitAttr("bool_ptr", "true"),
+			schemautil.LitAttr("omit_bool1", "true"),
 			schemautil.LitAttr("lit", "1000"),
 			schemautil.LitAttr("extra", "true"),
 		},
diff --git a/sql/mysql/sqlspec_test.go b/sql/mysql/sqlspec_test.go
index 2a04eefd..951d7072 100644
--- a/sql/mysql/sqlspec_test.go
+++ b/sql/mysql/sqlspec_test.go
@@ -535,13 +535,11 @@ func TestMarshalSpec_IndexParts(t *testing.T) {
     type = text
   }
   index "idx" {
-    unique = false
     on {
       desc   = true
       column = table.users.column.name
     }
     on {
-      desc = false
       expr = "lower(name)"
     }
   }