Repository: ariga/atlas
Pull request #2018 (2023-08-24 20:49:22)

sql/internal/sqlx: support type selectors on exclude globs
Closed: #2017


Patch:
diff --git a/doc/md/declarative/inspect.mdx b/doc/md/declarative/inspect.mdx
index b4117ebe..40532b7b 100644
--- a/doc/md/declarative/inspect.mdx
+++ b/doc/md/declarative/inspect.mdx
@@ -185,6 +185,8 @@ the table name:
 atlas schema inspect -u "mysql://localhost" --exclude "*.prefix_*"
 
 atlas schema inspect -u "mysql://localhost" --exclude "schema.table"
+
+atlas schema inspect -u "mysql://localhost" --exclude "schema.t*[type=table]"
 ```
 
 </TabItem>
@@ -196,6 +198,8 @@ When inspecting a specific schema, the first glob pattern matches the table name
 atlas schema inspect -u "mysql://localhost" --exclude "prefix_*"
 
 atlas schema inspect -u "mysql://localhost" --exclude "table"
+
+atlas schema inspect -u "mysql://localhost" --exclude "t*[type=table]"
 ```
 
 </TabItem>
@@ -216,6 +220,8 @@ the table name:
 atlas schema inspect -u "mysql://localhost" --exclude "*.*.prefix_*"
 
 atlas schema inspect -u "mysql://localhost" --exclude "public.*.c1"
+
+atlas schema inspect -u "mysql://localhost" --exclude "public.*.c*[type=column|index]"
 ```
 
 </TabItem>
@@ -227,6 +233,8 @@ When inspecting a specific schema, the first glob pattern matches the table name
 atlas schema inspect -u "mysql://localhost" --exclude "*.prefix_*"
 
 atlas schema inspect -u "mysql://localhost" --exclude "*.c1"
+
+atlas schema inspect -u "mysql://localhost" --exclude "*.c*[type=column|index]"
 ```
 
 </TabItem>
diff --git a/sql/internal/sqlx/exclude.go b/sql/internal/sqlx/exclude.go
index d3c07547..ea24e845 100644
--- a/sql/internal/sqlx/exclude.go
+++ b/sql/internal/sqlx/exclude.go
@@ -8,6 +8,7 @@ import (
 	"encoding/csv"
 	"fmt"
 	"path/filepath"
+	"regexp"
 	"strings"
 
 	"ariga.io/atlas/sql/schema"
@@ -89,71 +90,140 @@ func split(patterns []string) ([][]string, error) {
 }
 
 func excludeS(s *schema.Schema, glob []string) error {
-	var tables []*schema.Table
-	for _, t := range s.Tables {
-		match, err := filepath.Match(glob[0], t.Name)
-		if err != nil {
-			return err
-		}
-		if match {
-			// In case there is a match, and it is
-			// a single glob we exclude this table.
-			if len(glob) == 1 {
-				continue
+	if globT, exclude := excludeType(typeT, glob[0]); exclude {
+		var tables []*schema.Table
+		for _, t := range s.Tables {
+			match, err := filepath.Match(globT, t.Name)
+			if err != nil {
+				return err
+			}
+			if match {
+				// In case there is a match, and it is
+				// a single glob we exclude this table.
+				if len(glob) == 1 {
+					continue
+				}
+				if err := excludeT(t, glob[1]); err != nil {
+					return err
+				}
 			}
-			if err := excludeT(t, glob[1]); err != nil {
+			// No match or glob has more than one pattern.
+			tables = append(tables, t)
+		}
+		s.Tables = tables
+	}
+	if globV, exclude := excludeType(typeV, glob[0]); exclude {
+		var views []*schema.View
+		for _, v := range s.Views {
+			match, err := filepath.Match(globV, v.Name)
+			if err != nil {
 				return err
 			}
+			if match {
+				if len(glob) == 1 {
+					continue
+				}
+				if err := excludeV(v, glob[1]); err != nil {
+					return err
+				}
+			}
+			views = append(views, v)
 		}
-		// No match or glob has more than one pattern.
-		tables = append(tables, t)
+		s.Views = views
 	}
-	s.Tables = tables
 	return nil
 }
 
 func excludeT(t *schema.Table, pattern string) (err error) {
 	ex := make(map[*schema.Index]struct{})
 	ef := make(map[*schema.ForeignKey]struct{})
-	t.Columns, err = filter(t.Columns, func(c *schema.Column) (bool, error) {
-		match, err := filepath.Match(pattern, c.Name)
-		if !match || err != nil {
-			return false, err
-		}
-		for _, idx := range c.Indexes {
-			ex[idx] = struct{}{}
-		}
-		for _, fk := range c.ForeignKeys {
-			ef[fk] = struct{}{}
-		}
-		return true, nil
-	})
-	t.Indexes, err = filter(t.Indexes, func(idx *schema.Index) (bool, error) {
-		if _, ok := ex[idx]; ok {
+	if p, exclude := excludeType(typeC, pattern); exclude {
+		t.Columns, err = filter(t.Columns, func(c *schema.Column) (bool, error) {
+			match, err := filepath.Match(p, c.Name)
+			if !match || err != nil {
+				return false, err
+			}
+			for _, idx := range c.Indexes {
+				ex[idx] = struct{}{}
+			}
+			for _, fk := range c.ForeignKeys {
+				ef[fk] = struct{}{}
+			}
 			return true, nil
-		}
-		return filepath.Match(pattern, idx.Name)
-	})
-	t.ForeignKeys, err = filter(t.ForeignKeys, func(fk *schema.ForeignKey) (bool, error) {
-		if _, ok := ef[fk]; ok {
+		})
+	}
+	if p, exclude := excludeType(typeI, pattern); exclude {
+		t.Indexes, err = filter(t.Indexes, func(idx *schema.Index) (bool, error) {
+			if _, ok := ex[idx]; ok {
+				return true, nil
+			}
+			return filepath.Match(p, idx.Name)
+		})
+	}
+	if p, exclude := excludeType(typeF, pattern); exclude {
+		t.ForeignKeys, err = filter(t.ForeignKeys, func(fk *schema.ForeignKey) (bool, error) {
+			if _, ok := ef[fk]; ok {
+				return true, nil
+			}
+			return filepath.Match(p, fk.Symbol)
+		})
+	}
+	if p, exclude := excludeType(typeK, pattern); exclude {
+		t.Attrs, err = filter(t.Attrs, func(a schema.Attr) (bool, error) {
+			c, ok := a.(*schema.Check)
+			if !ok {
+				return false, nil
+			}
+			match, err := filepath.Match(p, c.Name)
+			if !match || err != nil {
+				return false, err
+			}
 			return true, nil
-		}
-		return filepath.Match(pattern, fk.Symbol)
-	})
-	t.Attrs, err = filter(t.Attrs, func(a schema.Attr) (bool, error) {
-		c, ok := a.(*schema.Check)
-		if !ok {
-			return false, nil
-		}
-		match, err := filepath.Match(pattern, c.Name)
-		if !match || err != nil {
-			return false, err
-		}
-		return true, nil
-	})
+		})
+	}
+	return
+}
+
+func excludeV(t *schema.View, pattern string) (err error) {
+	if p, exclude := excludeType(typeC, pattern); exclude {
+		t.Columns, err = filter(t.Columns, func(c *schema.Column) (bool, error) {
+			match, err := filepath.Match(p, c.Name)
+			if !match || err != nil {
+				return false, err
+			}
+			return true, nil
+		})
+	}
 	return
 }
 
+const (
+	typeV = "view"
+	typeT = "table"
+	typeC = "column"
+	typeI = "index"
+	typeF = "fk"
+	typeK = "check"
+)
+
+var reTypeVT = regexp.MustCompile(`\[type=([a-z|]+)+\]$`)
+
+func excludeType(t, v string) (string, bool) {
+	matches := reTypeVT.FindStringSubmatch(v)
+	if len(matches) != 2 {
+		return v, true
+	}
+	v = strings.TrimSuffix(v, matches[0])
+	for _, m := range strings.Split(matches[1], "|") {
+		if m == t {
+			// Selector matches.
+			return v, true
+		}
+	}
+	// There is a selector with no match.
+	return v, false
+}
+
 func filter[T any](s []T, f func(T) (bool, error)) ([]T, error) {
 	r := make([]T, 0, len(s))
 	for i := range s {


Test patch:
diff --git a/sql/internal/sqlx/exclude_test.go b/sql/internal/sqlx/exclude_test.go
index 825674c8..99cd2f0a 100644
--- a/sql/internal/sqlx/exclude_test.go
+++ b/sql/internal/sqlx/exclude_test.go
@@ -94,6 +94,63 @@ func TestExcludeRealm_Tables(t *testing.T) {
 	require.Empty(t, r.Schemas[2].Tables)
 }
 
+func TestExcludeRealm_Selector(t *testing.T) {
+	r := schema.NewRealm(
+		schema.New("s1").
+			AddTables(
+				schema.NewTable("t1"),
+			).
+			AddViews(
+				schema.NewView("v1", "SELECT * FROM t1"),
+			),
+	)
+	r, err := ExcludeRealm(r, []string{"*.*[type=table]"})
+	require.NoError(t, err)
+	require.Empty(t, r.Schemas[0].Tables)
+	require.Len(t, r.Schemas[0].Views, 1)
+
+	r.Schemas[0].AddTables(
+		schema.NewTable("t1"),
+		schema.NewTable("t2"),
+		schema.NewTable("t3"),
+	)
+	r, err = ExcludeRealm(r, []string{"*.*[type=view]"})
+	require.NoError(t, err)
+	require.Empty(t, r.Schemas[0].Views)
+	require.Len(t, r.Schemas[0].Tables, 3)
+
+	r, err = ExcludeRealm(r, []string{"*.t[12][type=table]"})
+	require.Empty(t, r.Schemas[0].Views)
+	require.Len(t, r.Schemas[0].Tables, 1)
+
+	r.Schemas[0].AddViews(
+		schema.NewView("v1", "SELECT * FROM t1"),
+	)
+	r, err = ExcludeRealm(r, []string{"*.*[type=view|table]"})
+	require.NoError(t, err)
+	require.Empty(t, r.Schemas[0].Views)
+	require.Empty(t, r.Schemas[0].Tables)
+
+	r.Schemas[0].
+		AddTables(
+			schema.NewTable("t1").AddColumns(schema.NewColumn("c1")).AddIndexes(schema.NewIndex("i1")).AddChecks(schema.NewCheck().SetName("k1")),
+		).
+		AddViews(
+			schema.NewView("v1", "SELECT * FROM t1").AddColumns(schema.NewColumn("c1")),
+		)
+	r, err = ExcludeRealm(r, []string{"*.*[type=table].*1[type=column|check]"})
+	require.NoError(t, err)
+	require.Empty(t, r.Schemas[0].Tables[0].Attrs)
+	require.Empty(t, r.Schemas[0].Tables[0].Columns)
+	require.Len(t, r.Schemas[0].Tables[0].Indexes, 1)
+	require.Len(t, r.Schemas[0].Views[0].Columns, 1)
+
+	r, err = ExcludeRealm(r, []string{"*.*[type=table|view].*1[type=column|check|index]"})
+	require.NoError(t, err)
+	require.Empty(t, r.Schemas[0].Tables[0].Indexes)
+	require.Empty(t, r.Schemas[0].Views[0].Columns)
+}
+
 func TestExcludeRealm_Checks(t *testing.T) {
 	r := schema.NewRealm(
 		schema.New("s1").AddTables(