Repository: ariga/atlas
Pull request #3159 (2024-09-27 07:56:10)

cmdapi: added `fileset` functions


Patch:
diff --git a/cmd/atlas/go.mod b/cmd/atlas/go.mod
index a3b24f33..c0b84ab8 100644
--- a/cmd/atlas/go.mod
+++ b/cmd/atlas/go.mod
@@ -59,6 +59,7 @@ require (
 	github.com/aws/aws-sdk-go-v2/service/ssooidc v1.21.5 // indirect
 	github.com/aws/aws-sdk-go-v2/service/sts v1.26.5 // indirect
 	github.com/aws/smithy-go v1.19.0 // indirect
+	github.com/bmatcuk/doublestar v1.3.4 // indirect
 	github.com/davecgh/go-spew v1.1.1 // indirect
 	github.com/fsnotify/fsnotify v1.7.0 // indirect
 	github.com/go-openapi/inflect v0.19.0 // indirect
diff --git a/cmd/atlas/go.sum b/cmd/atlas/go.sum
index 79388666..3b672b20 100644
--- a/cmd/atlas/go.sum
+++ b/cmd/atlas/go.sum
@@ -68,6 +68,8 @@ github.com/aws/aws-sdk-go-v2/service/sts v1.26.5/go.mod h1:XX5gh4CB7wAs4KhcF46G6
 github.com/aws/smithy-go v1.14.1/go.mod h1:Tg+OJXh4MB2R/uN61Ko2f6hTZwB/ZYGOtib8J3gBHzA=
 github.com/aws/smithy-go v1.19.0 h1:KWFKQV80DpP3vJrrA9sVAHQ5gc2z8i4EzrLhLlWXcBM=
 github.com/aws/smithy-go v1.19.0/go.mod h1:NukqUGpCZIILqqiV0NIjeFh24kd/FAa4beRb6nbIUPE=
+github.com/bmatcuk/doublestar v1.3.4 h1:gPypJ5xD31uhX6Tf54sDPUOBXTqKH4c9aPY66CyQrS0=
+github.com/bmatcuk/doublestar v1.3.4/go.mod h1:wiQtGV+rzVYxB7WIlirSN++5HPtPlXEo9MEoZQC/PmE=
 github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
 github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
 github.com/chzyer/logex v1.2.1 h1:XHDu3E6q+gdHgsdTPH6ImJMIp436vR6MPtH8gP05QzM=
diff --git a/cmd/atlas/internal/cmdapi/project.go b/cmd/atlas/internal/cmdapi/project.go
index 32f292a5..6c3f8644 100644
--- a/cmd/atlas/internal/cmdapi/project.go
+++ b/cmd/atlas/internal/cmdapi/project.go
@@ -645,9 +645,10 @@ func parseConfig(ctx context.Context, path, env string, vars map[string]cty.Valu
 				}),
 			}),
 			schemahcl.WithFunctions(map[string]function.Function{
-				"file":   schemahcl.MakeFileFunc(base),
-				"glob":   schemahcl.MakeGlobFunc(base),
-				"getenv": getEnvFunc,
+				"file":    schemahcl.MakeFileFunc(base),
+				"glob":    schemahcl.MakeGlobFunc(base),
+				"fileset": schemahcl.MakeFileSetFunc(base),
+				"getenv":  getEnvFunc,
 			}),
 		)...,
 	)
diff --git a/doc/md/atlas-schema/projects.mdx b/doc/md/atlas-schema/projects.mdx
index 2d243fb1..ac08a4b1 100644
--- a/doc/md/atlas-schema/projects.mdx
+++ b/doc/md/atlas-schema/projects.mdx
@@ -330,14 +330,14 @@ variable "cloud_token" {
 }
 ```
 
-### `glob`
+### `fileset`
 
-The `glob` function returns the list of files that match the given pattern. The pattern is relative to the project
+The `fileset` function returns the list of files that match the given pattern. The pattern is relative to the project
 directory.
 
 ```hcl
 data "hcl_schema" "app" {
-  paths = glob("schema/*.pg.hcl")
+  paths = fileset("schema/*.pg.hcl")
 }
 ```
 
diff --git a/doc/md/cloud/getting-started.mdx b/doc/md/cloud/getting-started.mdx
index 6985ca83..2f7ac6b8 100644
--- a/doc/md/cloud/getting-started.mdx
+++ b/doc/md/cloud/getting-started.mdx
@@ -58,6 +58,7 @@ Yes, head over to our [live demo account](https://gh.atlasgo.cloud) to see Atlas
 
 ## Can I use Atlas Cloud for free?
 
-Atlas Cloud comes with a free "Community" plan which should be more than enough for most teams.
+We provide a 30 day free trial for new users. After the trial period, a license is required to continue using
+Atlas Cloud.
 
-To learn more see the [pricing page](https://atlasgo.cloud/pricing).
+To learn more see the [pricing page](https://atlasgo.io/pricing).
diff --git a/doc/md/cloud/pricing.mdx b/doc/md/cloud/pricing.mdx
index 281603fa..6fde9efd 100644
--- a/doc/md/cloud/pricing.mdx
+++ b/doc/md/cloud/pricing.mdx
@@ -6,15 +6,14 @@ slug: /cloud/pricing
 
 Atlas Cloud offers two pricing plans:
 
-1. **Pro Plan** - for individuals and small teams that want to unlock the full potential of Atlas. This plan provides 3
-free seats per company and full access to all the capabilities of Atlas as a CLI. The free version of the plan offers access to enough Atlas Cloud
-quota to successfully manage a single project.
+1. **Pro Plan** - for individuals and small teams that want to unlock the full potential of Atlas. The Pro plan
+   provides full access to advanced CLI capabilities, the Cloud UI, and other features. A free, 30-day trial is available
+   for new users. After the trial period, a license is required to continue using Atlas Pro.
 
-* For teams who need additional seats or quotas (projects, databases, runs, etc.), it is possible to pay as you go. View
-the pricing breakdown [here](https://atlasgo.cloud/pricing).
+  To learn more about Atlas Pro pricing, visit our [pricing page](https://atlasgo.io/pricing).
 
 2. **Enterprise Plan** - for larger organizations looking to solve schema management at scale. This plan includes a
-dedicated support channel, solution engineering, and other features required for adoption by enterprises.
+   dedicated support channel, solution engineering, and other features required for adoption by enterprises.
 
 For a full list of the features in each plan, click [here](https://atlasgo.cloud/pricing).
 
diff --git a/doc/md/concepts/dev.mdx b/doc/md/concepts/dev.mdx
index 17157165..2c42f660 100644
--- a/doc/md/concepts/dev.mdx
+++ b/doc/md/concepts/dev.mdx
@@ -230,6 +230,26 @@ docker "postgres" "dev" {
 }
 ```
 
+When needed, the `build` block can be used to build the docker image with custom configurations:
+
+```hcl
+docker "postgres" "dev" {
+  image = "postgres:16-aws"
+  // highlight-start
+  // The built image will be tagged as "postgres:16-aws".
+  build {
+    context    = "path/to/context"
+    dockerfile = "Dockerfile"
+    target     = "build-target"
+    args       = {
+      key = "value"
+    }
+  }
+  // highlight-end
+}
+```
+
+
 MySQL and MariaDB users can use the `docker "mysql"` / `docker "mariadb"` to configure such block.
 
 
diff --git a/doc/md/features.mdx b/doc/md/features.mdx
index db2e34dd..e418f586 100644
--- a/doc/md/features.mdx
+++ b/doc/md/features.mdx
@@ -17,8 +17,10 @@ an Atlas account.
 
 ### Pro Plan {#pro}
 
-Users can upgrade to the Pro Plan for **free**, granting them up to 3 seats **per company**. After that limit, users can
-purchase additional seats. To learn more about our plans and pricing, visit the [Atlas Pricing Page](/pricing).
+Users can upgrade to the Pro Plan for **$9/seat per month**. We provide a free, 30-day trial for new users. After the
+trial period, a license is required to continue using Atlas Pro.
+
+To learn more about our plans and pricing, visit the [Atlas Pricing Page](/pricing).
 
 Create your Atlas Pro account by running the command below and following the instructions on the screen:
 
diff --git a/doc/website/package-lock.json b/doc/website/package-lock.json
index 694ea349..85b611e9 100644
--- a/doc/website/package-lock.json
+++ b/doc/website/package-lock.json
@@ -8,7 +8,7 @@
       "name": "website",
       "version": "0.0.0",
       "dependencies": {
-        "@ariga/atlas-website": "^0.0.66",
+        "@ariga/atlas-website": "^0.0.67",
         "@docusaurus/core": "^2.0.0-rc.1",
         "@docusaurus/plugin-client-redirects": "^2.4.1",
         "@docusaurus/plugin-ideal-image": "^2.4.1",
@@ -435,9 +435,9 @@
       }
     },
     "node_modules/@ariga/atlas-website": {
-      "version": "0.0.66",
-      "resolved": "https://registry.npmjs.org/@ariga/atlas-website/-/atlas-website-0.0.66.tgz",
-      "integrity": "sha512-74jfV29d3L1W9PW4eQfpxRIKdQZnZuatCXkweeR9D4tzsS1kQ8Vuuh/RXKcDk8TaH7UK5seQFOpI+fQQaNTIgw==",
+      "version": "0.0.67",
+      "resolved": "https://registry.npmjs.org/@ariga/atlas-website/-/atlas-website-0.0.67.tgz",
+      "integrity": "sha512-a0reFrNS2Nd1XtPHAmGsw+/1+onnWf8JDPtK/5qDLrpHlLDROCfeN3ASTNmzONRs3ofGppSdmnAr5yMEU9Sc/A==",
       "dependencies": {
         "@amplitude/analytics-browser": "^1.9.4",
         "@types/amplitude-js": "^8.16.2",
diff --git a/doc/website/package.json b/doc/website/package.json
index 63ac7763..8e53f59f 100644
--- a/doc/website/package.json
+++ b/doc/website/package.json
@@ -16,7 +16,7 @@
     "invalidate-cdn": "aws cloudfront create-invalidation --distribution-id E3VOJYSV9YO33D --paths \"/*\""
   },
   "dependencies": {
-    "@ariga/atlas-website": "^0.0.66",
+    "@ariga/atlas-website": "^0.0.67",
     "@docusaurus/core": "^2.0.0-rc.1",
     "@docusaurus/plugin-client-redirects": "^2.4.1",
     "@docusaurus/plugin-ideal-image": "^2.4.1",
diff --git a/go.mod b/go.mod
index d1715b85..e557d690 100644
--- a/go.mod
+++ b/go.mod
@@ -4,6 +4,7 @@ go 1.22.7
 
 require (
 	github.com/DATA-DOG/go-sqlmock v1.5.0
+	github.com/bmatcuk/doublestar v1.3.4
 	github.com/go-openapi/inflect v0.19.0
 	github.com/hashicorp/hcl/v2 v2.13.0
 	github.com/stretchr/testify v1.8.2
diff --git a/go.sum b/go.sum
index d0360059..8859ba8e 100644
--- a/go.sum
+++ b/go.sum
@@ -6,6 +6,8 @@ github.com/apparentlymart/go-textseg/v13 v13.0.0 h1:Y+KvPE1NYz0xl601PVImeQfFyEy6
 github.com/apparentlymart/go-textseg/v13 v13.0.0/go.mod h1:ZK2fH7c4NqDTLtiYLvIkEghdlcqw7yxLeM89kiTRPUo=
 github.com/apparentlymart/go-textseg/v15 v15.0.0 h1:uYvfpb3DyLSCGWnctWKGj857c6ew1u1fNQOlOtuGxQY=
 github.com/apparentlymart/go-textseg/v15 v15.0.0/go.mod h1:K8XmNZdhEBkdlyDdvbmmsvpAG721bKi0joRfFdHIWJ4=
+github.com/bmatcuk/doublestar v1.3.4 h1:gPypJ5xD31uhX6Tf54sDPUOBXTqKH4c9aPY66CyQrS0=
+github.com/bmatcuk/doublestar v1.3.4/go.mod h1:wiQtGV+rzVYxB7WIlirSN++5HPtPlXEo9MEoZQC/PmE=
 github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
diff --git a/internal/integration/go.mod b/internal/integration/go.mod
index e13ea8cf..996d6892 100644
--- a/internal/integration/go.mod
+++ b/internal/integration/go.mod
@@ -49,6 +49,7 @@ require (
 	github.com/aws/aws-sdk-go-v2/service/ssooidc v1.21.5 // indirect
 	github.com/aws/aws-sdk-go-v2/service/sts v1.26.5 // indirect
 	github.com/aws/smithy-go v1.19.0 // indirect
+	github.com/bmatcuk/doublestar v1.3.4 // indirect
 	github.com/chzyer/readline v1.5.1 // indirect
 	github.com/davecgh/go-spew v1.1.1 // indirect
 	github.com/fatih/color v1.13.0 // indirect
diff --git a/internal/integration/go.sum b/internal/integration/go.sum
index f22c00bb..3e0de7ab 100644
--- a/internal/integration/go.sum
+++ b/internal/integration/go.sum
@@ -68,6 +68,8 @@ github.com/aws/aws-sdk-go-v2/service/sts v1.26.5/go.mod h1:XX5gh4CB7wAs4KhcF46G6
 github.com/aws/smithy-go v1.14.1/go.mod h1:Tg+OJXh4MB2R/uN61Ko2f6hTZwB/ZYGOtib8J3gBHzA=
 github.com/aws/smithy-go v1.19.0 h1:KWFKQV80DpP3vJrrA9sVAHQ5gc2z8i4EzrLhLlWXcBM=
 github.com/aws/smithy-go v1.19.0/go.mod h1:NukqUGpCZIILqqiV0NIjeFh24kd/FAa4beRb6nbIUPE=
+github.com/bmatcuk/doublestar v1.3.4 h1:gPypJ5xD31uhX6Tf54sDPUOBXTqKH4c9aPY66CyQrS0=
+github.com/bmatcuk/doublestar v1.3.4/go.mod h1:wiQtGV+rzVYxB7WIlirSN++5HPtPlXEo9MEoZQC/PmE=
 github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
 github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
 github.com/chzyer/logex v1.2.1 h1:XHDu3E6q+gdHgsdTPH6ImJMIp436vR6MPtH8gP05QzM=
diff --git a/schemahcl/stdlib.go b/schemahcl/stdlib.go
index 261be6a2..21df999d 100644
--- a/schemahcl/stdlib.go
+++ b/schemahcl/stdlib.go
@@ -14,6 +14,7 @@ import (
 	"strconv"
 	"strings"
 
+	"github.com/bmatcuk/doublestar"
 	"github.com/hashicorp/hcl/v2"
 	"github.com/hashicorp/hcl/v2/ext/tryfunc"
 	"github.com/zclconf/go-cty/cty"
@@ -490,6 +491,7 @@ func MakeFileFunc(base string) function.Function {
 
 // MakeGlobFunc returns a function that returns the names of all files
 // matching pattern or nil if there is no matching file
+// Deprecated: use fileset function instead.
 func MakeGlobFunc(base string) function.Function {
 	return function.New(&function.Spec{
 		Params: []function.Parameter{
@@ -523,3 +525,41 @@ func MakeGlobFunc(base string) function.Function {
 		},
 	})
 }
+
+// MakeFileSetFunc returns a function that returns the names of all files
+// matching pattern or nil if there is no matching file
+func MakeFileSetFunc(base string) function.Function {
+	return function.New(&function.Spec{
+		Params: []function.Parameter{
+			{
+				Name:        "pattern",
+				Type:        cty.String,
+				Description: "A file glob pattern to match against files in the base directory.",
+			},
+		},
+		Type:        function.StaticReturnType(cty.List(cty.String)),
+		Description: "fileset returns a list of file paths matching the given glob pattern in the base directory.",
+		Impl: func(args []cty.Value, _ cty.Type) (cty.Value, error) {
+			if !filepath.IsAbs(base) {
+				return cty.NilVal, fmt.Errorf("base directory must be an absolute path. got: %s", base)
+			}
+			path := args[0].AsString()
+			if !filepath.IsAbs(path) {
+				path = filepath.Clean(filepath.Join(base, path))
+			}
+			matches, err := doublestar.Glob(path)
+			switch {
+			case err != nil:
+				return cty.NilVal, err
+			case len(matches) == 0:
+				return cty.ListValEmpty(cty.String), nil
+			default:
+				vals := make([]cty.Value, len(matches))
+				for i, match := range matches {
+					vals[i] = cty.StringVal(match)
+				}
+				return cty.ListVal(vals), nil
+			}
+		},
+	})
+}
diff --git a/schemahcl/testdata/nested/c.hcl b/schemahcl/testdata/nested/c.hcl
new file mode 100644
index 00000000..e69de29b
diff --git a/sql/internal/specutil/spec.go b/sql/internal/specutil/spec.go
index 82d7eb09..6ebfd410 100644
--- a/sql/internal/specutil/spec.go
+++ b/sql/internal/specutil/spec.go
@@ -231,6 +231,21 @@ func ObjectRef(s *schema.Schema, o SpecTypeNamer) *schemahcl.Ref {
 	return schemahcl.BuildRef([]schemahcl.PathIndex{idx})
 }
 
+// TableSpecRef returns a reference to the table in the spec. In case there is more than
+// one table with the same name, the reference will be qualified with the schema name.
+func TableSpecRef(t *schema.Table) *schemahcl.Ref {
+	typ, name := typeTable, t.Name
+	idx := schemahcl.PathIndex{T: typ, V: []string{name}}
+	if s := t.Schema; s != nil && s.Realm != nil && len(s.Realm.Schemas) > 1 && slices.ContainsFunc(s.Realm.Schemas, func(s1 *schema.Schema) bool {
+		return s1 != s && slices.ContainsFunc(s1.Tables, func(t1 *schema.Table) bool {
+			return t1.Name == t.Name
+		})
+	}) {
+		idx.V = append([]string{s.Name}, idx.V...)
+	}
+	return schemahcl.BuildRef([]schemahcl.PathIndex{idx})
+}
+
 // HCLBytesFunc returns a helper that evaluates an HCL document from a byte slice instead
 // of from an hclparse.Parser instance.
 func HCLBytesFunc(ev schemahcl.Evaluator) func(b []byte, v any, inp map[string]cty.Value) error {
diff --git a/sql/internal/sqlx/plan.go b/sql/internal/sqlx/plan.go
index 7928d950..b687c3ba 100644
--- a/sql/internal/sqlx/plan.go
+++ b/sql/internal/sqlx/plan.go
@@ -447,14 +447,21 @@ func SortChanges(changes []schema.Change, opts *SortOptions) []schema.Change {
 	return planned
 }
 
-// Depender can be implemented by an object to determine if a change to it
-// depends on other change, or if other change depends on it. For example:
-// A table creation depends on type creation, and a type deletion depends on
-// table deletion.
-type Depender interface {
-	DependsOn(change, other schema.Change) bool
-	DependencyOf(change, other schema.Change) bool
-}
+type (
+	// Depender can be implemented by an object to determine if a change to it
+	// depends on other change, or if other change depends on it. For example:
+	// A table creation depends on type creation, and a type deletion depends on
+	// table deletion.
+	Depender interface {
+		DependsOn(change, other schema.Change) bool
+		DependencyOf(change, other schema.Change) bool
+	}
+	// RowTyper can be implemented by a type to determine if its source
+	// is a regular table (e.g., row types).
+	RowTyper interface {
+		RowType() *schema.Table
+	}
+)
 
 // dependsOn reports if the given change depends on the other change.
 func dependsOn(c1, c2 schema.Change, opts SortOptions) bool {
@@ -492,6 +499,11 @@ func dependsOn(c1, c2 schema.Change, opts SortOptions) bool {
 			if refTo(c1.T.ForeignKeys, c2.T) {
 				return true
 			}
+			if slices.ContainsFunc(c1.T.Columns, func(c *schema.Column) bool {
+				return c.Type != nil && typeDependsOnT(c.Type.Type, c2.T)
+			}) {
+				return true
+			}
 		case *schema.ModifyTable:
 			if (c1.T.Name != c2.T.Name || !SameSchema(c1.T.Schema, c2.T.Schema)) && refTo(c1.T.ForeignKeys, c2.T) {
 				return true
@@ -512,18 +524,43 @@ func dependsOn(c1, c2 schema.Change, opts SortOptions) bool {
 		// after all resources that rely on it will be dropped.
 		switch c2 := c2.(type) {
 		case *schema.DropTable:
+			// References to this table, must be dropped first.
 			if refTo(c2.T.ForeignKeys, c1.T) {
 				return true
 			}
+			if slices.ContainsFunc(c2.T.Columns, func(c *schema.Column) bool {
+				return c.Type != nil && typeDependsOnT(c.Type.Type, c1.T)
+			}) {
+				return true
+			}
 		case *schema.ModifyTable:
-			return slices.ContainsFunc(c2.Changes, func(c schema.Change) bool {
-				fk, ok := c.(*schema.DropForeignKey)
-				return ok && refTo([]*schema.ForeignKey{fk.F}, c1.T)
-			})
+			if slices.ContainsFunc(c2.Changes, func(c schema.Change) bool {
+				switch c := c.(type) {
+				case *schema.DropForeignKey:
+					return refTo([]*schema.ForeignKey{c.F}, c1.T)
+				case *schema.DropColumn:
+					return c.C.Type != nil && typeDependsOnT(c.C.Type.Type, c1.T)
+				}
+				return false
+			}) {
+				return true
+			}
 		case *schema.DropTrigger:
 			if SameTable(c2.T.Table, c1.T) {
 				return true
 			}
+		case *schema.DropFunc:
+			if c2.F.Ret != nil && typeDependsOnT(c2.F.Ret, c1.T) || slices.ContainsFunc(c2.F.Args, func(f *schema.FuncArg) bool {
+				return typeDependsOnT(f.Type, c1.T)
+			}) {
+				return true
+			}
+		case *schema.DropProc:
+			if slices.ContainsFunc(c2.P.Args, func(f *schema.FuncArg) bool {
+				return typeDependsOnT(f.Type, c1.T)
+			}) {
+				return true
+			}
 		}
 		return depOfDrop(c1.T, c2)
 	case *schema.ModifyTable:
@@ -534,10 +571,19 @@ func dependsOn(c1, c2 schema.Change, opts SortOptions) bool {
 				return true
 			}
 			// Tables need to be created before referencing them.
-			return slices.ContainsFunc(c1.Changes, func(c schema.Change) bool {
-				fk, ok := c.(*schema.AddForeignKey)
-				return ok && refTo([]*schema.ForeignKey{fk.F}, c2.T)
-			})
+			if slices.ContainsFunc(c1.Changes, func(c schema.Change) bool {
+				switch c := c.(type) {
+				case *schema.AddForeignKey:
+					return refTo([]*schema.ForeignKey{c.F}, c2.T)
+				case *schema.AddColumn:
+					return c.C.Type != nil && typeDependsOnT(c.C.Type.Type, c2.T)
+				case *schema.ModifyColumn:
+					return c.To.Type != nil && typeDependsOnT(c.To.Type.Type, c2.T)
+				}
+				return false
+			}) {
+				return true
+			}
 		case *schema.ModifyTable:
 			if c1.T != c2.T {
 				addC := make(map[*schema.Column]bool)
@@ -631,6 +677,11 @@ func dependsOn(c1, c2 schema.Change, opts SortOptions) bool {
 			if opts.FuncDepT != nil && opts.FuncDepT(c1.F, c2.T) {
 				return true
 			}
+			if c1.F.Ret != nil && typeDependsOnT(c1.F.Ret, c2.T) || slices.ContainsFunc(c1.F.Args, func(f *schema.FuncArg) bool {
+				return typeDependsOnT(f.Type, c2.T)
+			}) {
+				return true
+			}
 		case *schema.AddView:
 			if opts.FuncDepV != nil && opts.FuncDepV(c1.F, c2.V) {
 				return true
@@ -679,6 +730,12 @@ func dependsOn(c1, c2 schema.Change, opts SortOptions) bool {
 		switch c2 := c2.(type) {
 		case *schema.AddSchema:
 			return c1.P.Schema.Name == c2.S.Name
+		case *schema.AddTable:
+			if slices.ContainsFunc(c1.P.Args, func(f *schema.FuncArg) bool {
+				return typeDependsOnT(f.Type, c2.T)
+			}) {
+				return true
+			}
 		case *schema.DropProc:
 			return c1.P.Name == c2.P.Name && SameSchema(c1.P.Schema, c2.P.Schema) // Proc recreation.
 		case *schema.AddProc:
@@ -734,16 +791,14 @@ func dependsOn(c1, c2 schema.Change, opts SortOptions) bool {
 		// Dropping a type must occur after all its usage were dropped.
 		switch c2 := c2.(type) {
 		case *schema.DropView:
-			// Dropping a view also drops its triggers
-			// and they might depend on the type.
+			// Dropping a view also drops its triggers and might depend on the type.
 			if slices.ContainsFunc(c2.V.Triggers, func(tg *schema.Trigger) bool {
 				return slices.Contains(tg.Deps, c1.O)
 			}) {
 				return true
 			}
 		case *schema.DropTable:
-			// Dropping a table also drops its triggers
-			// and they might depend on the type.
+			// Dropping a table also drops its triggers and might depend on the type.
 			if slices.ContainsFunc(c2.T.Triggers, func(tg *schema.Trigger) bool {
 				return slices.Contains(tg.Deps, c1.O)
 			}) {
@@ -912,6 +967,15 @@ func refTo(fks []*schema.ForeignKey, to *schema.Table) bool {
 	})
 }
 
+// typeDependsOnT reports if the declaration of type t1 depends on the given change.
+func typeDependsOnT(t schema.Type, tt *schema.Table) bool {
+	rt, ok := schema.UnderlyingType(t).(RowTyper)
+	if !ok {
+		return false
+	}
+	return SameTable(rt.RowType(), tt)
+}
+
 // dependsOnT reports if t1 depends on t2.
 func dependsOnT(t1, t2 schema.Type) bool {
 	// Comparing might panic due to mismatch types.
diff --git a/sql/postgres/convert.go b/sql/postgres/convert.go
index 5eb89e7c..6e2d59b5 100644
--- a/sql/postgres/convert.go
+++ b/sql/postgres/convert.go
@@ -42,6 +42,11 @@ func FormatType(t schema.Type) (string, error) {
 			return "", errors.New("postgres: missing composite type name")
 		}
 		f = t.T
+	case *RowType:
+		if t.T == nil || t.T.Name == "" {
+			return "", errors.New("postgres: missing table for composite (row) type")
+		}
+		f = t.T.Name
 	case *DomainType:
 		if t.T == "" {
 			return "", errors.New("postgres: missing domain type name")
@@ -298,12 +303,12 @@ func columnType(c *columnDesc) (schema.Type, error) {
 		if ft == "" {
 			ft = t
 		}
-		typ = &UserDefinedType{T: ft}
+		typ = &UserDefinedType{T: ft, C: c.typtype}
 	}
 	switch c.typtype {
 	case "d", "e":
 		// User-defined types supported by Atlas.
-		typ = &UserDefinedType{T: c.fmtype}
+		typ = &UserDefinedType{T: c.fmtype, C: c.typtype}
 	}
 	return typ, nil
 }
diff --git a/sql/postgres/inspect_oss.go b/sql/postgres/inspect_oss.go
index 3ed4c4c6..ed7834c1 100644
--- a/sql/postgres/inspect_oss.go
+++ b/sql/postgres/inspect_oss.go
@@ -906,6 +906,14 @@ type (
 	UserDefinedType struct {
 		schema.Type
 		T string
+		C string // Optional type class.
+
+	}
+
+	// RowType defines a composite type that represents a table row.
+	RowType struct {
+		schema.Type
+		T *schema.Table // Table that this row type represents.
 	}
 
 	// PseudoType defines a non-column pseudo-type, such as function arguments and return types.


Test patch:
diff --git a/internal/integration/postgres_test.go b/internal/integration/postgres_test.go
index e6340e59..da7299fa 100644
--- a/internal/integration/postgres_test.go
+++ b/internal/integration/postgres_test.go
@@ -1420,7 +1420,7 @@ create table atlas_types_sanity
 				},
 				{
 					Name: "tUserDefined",
-					Type: &schema.ColumnType{Type: &postgres.UserDefinedType{T: "public.address"}, Raw: "USER-DEFINED", Null: true},
+					Type: &schema.ColumnType{Type: &postgres.UserDefinedType{T: "public.address", C: "c"}, Raw: "USER-DEFINED", Null: true},
 					Default: &schema.RawExpr{
 						X: "'(ab,cd)'::public.address",
 					},
diff --git a/schemahcl/stdlib_test.go b/schemahcl/stdlib_test.go
index 70b813f6..47a2ab62 100644
--- a/schemahcl/stdlib_test.go
+++ b/schemahcl/stdlib_test.go
@@ -197,6 +197,69 @@ func TestMakeGlobFunc(t *testing.T) {
 	require.Equal(t, []string{"a.hcl", "b.hcl", "variables.hcl"}, result)
 }
 
+func TestMakeFilesetFunc(t *testing.T) {
+	base, err := filepath.Abs("testdata")
+	require.NoError(t, err)
+
+	fn := MakeFileSetFunc(base)
+
+	tests := []struct {
+		name     string
+		pattern  string
+		expected []string
+	}{
+		{
+			name:     "Simple HCL files",
+			pattern:  "*.hcl",
+			expected: []string{"a.hcl", "b.hcl", "variables.hcl"},
+		},
+		{
+			name:     "Non-existent files",
+			pattern:  "*.txt",
+			expected: []string{},
+		},
+		{
+			name:     "Nested directories",
+			pattern:  "**/*.hcl",
+			expected: []string{"a.hcl", "b.hcl", "nested/c.hcl", "variables.hcl"},
+		},
+		{
+			name:     "Single file",
+			pattern:  "a.hcl",
+			expected: []string{"a.hcl"},
+		},
+		{
+			name:     "Files with specific prefix",
+			pattern:  "a*.hcl",
+			expected: []string{"a.hcl"},
+		},
+		{
+			name:     "Files in specific directory",
+			pattern:  "nested/*.hcl",
+			expected: []string{"nested/c.hcl"},
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			v, err := fn.Call([]cty.Value{cty.StringVal(tt.pattern)})
+			require.NoError(t, err)
+			var result []string
+			for _, f := range v.AsValueSlice() {
+				p, err := filepath.Rel(base, f.AsString())
+				require.NoError(t, err)
+				result = append(result, filepath.ToSlash(p))
+			}
+			require.ElementsMatch(t, tt.expected, result)
+		})
+	}
+
+	// Test with relative base path
+	relativeFn := MakeFileSetFunc("testdata")
+	_, err = relativeFn.Call([]cty.Value{cty.StringVal("*.hcl")})
+	require.EqualError(t, err, "base directory must be an absolute path. got: testdata")
+}
+
 func Example_RegexpEscapeFunc() {
 	for _, f := range []string{
 		`v  = regexpescape("a|b|c")`,
diff --git a/sql/internal/sqlx/plan_test.go b/sql/internal/sqlx/plan_test.go
index 638cc23b..7374a1eb 100644
--- a/sql/internal/sqlx/plan_test.go
+++ b/sql/internal/sqlx/plan_test.go
@@ -296,6 +296,55 @@ func TestSortChanges(t *testing.T) {
 	require.IsType(t, &schema.DropTrigger{}, planned[1])
 }
 
+// RowType is a mocked RowTyper implementation.
+type RowType struct {
+	schema.Type
+	*schema.Table
+}
+
+func (r *RowType) RowType() *schema.Table { return r.Table }
+
+func TestSortChanges_RowTyper(t *testing.T) {
+	t1, t2 := schema.NewTable("t1"), schema.NewTable("t2")
+	t2.AddColumns(schema.NewColumn("t1").SetType(&RowType{Table: t1}))
+	// Tables created before other tables that depend on them.
+	changes := []schema.Change{&schema.AddTable{T: t2}, &schema.AddTable{T: t1}}
+	planned := SortChanges(changes, nil)
+	require.Equal(t, []schema.Change{changes[1], changes[0]}, planned)
+
+	// Tables created before other columns that depend on them.
+	changes = []schema.Change{
+		&schema.ModifyTable{T: t2, Changes: schema.Changes{&schema.AddColumn{C: t2.Columns[0]}}},
+		&schema.AddTable{T: t1},
+	}
+	planned = SortChanges(changes, nil)
+	require.Equal(t, []schema.Change{changes[1], changes[0]}, planned)
+
+	// Tables dropped after other tables that depend on them.
+	changes = []schema.Change{&schema.DropTable{T: t1}, &schema.DropTable{T: t2}}
+	planned = SortChanges(changes, nil)
+	require.Equal(t, []schema.Change{changes[1], changes[0]}, planned)
+
+	// Tables dropped after other columns that depend on them.
+	changes = []schema.Change{
+		&schema.DropTable{T: t1},
+		&schema.ModifyTable{T: t2, Changes: schema.Changes{&schema.DropColumn{C: t2.Columns[0]}}},
+	}
+	planned = SortChanges(changes, nil)
+	require.Equal(t, []schema.Change{changes[1], changes[0]}, planned)
+
+	// Functions created after the tables they depend on.
+	f1 := &schema.Func{Name: "f1", Ret: &RowType{Table: t1}}
+	changes = []schema.Change{&schema.AddFunc{F: f1}, &schema.AddTable{T: t1}}
+	planned = SortChanges(changes, nil)
+	require.Equal(t, []schema.Change{changes[1], changes[0]}, planned)
+
+	// Functions dropped before the tables they depend on.
+	changes = []schema.Change{&schema.DropTable{T: t1}, &schema.DropFunc{F: f1}}
+	planned = SortChanges(changes, nil)
+	require.Equal(t, []schema.Change{changes[1], changes[0]}, planned)
+}
+
 func TestSameTable(t *testing.T) {
 	t1 := schema.NewTable("t1")
 	require.True(t, SameTable(t1, t1))
diff --git a/sql/postgres/inspect_oss_test.go b/sql/postgres/inspect_oss_test.go
index 49c35a09..8ccaf014 100644
--- a/sql/postgres/inspect_oss_test.go
+++ b/sql/postgres/inspect_oss_test.go
@@ -135,7 +135,7 @@ func TestDriver_InspectTable(t *testing.T) {
 					{Name: "c20", Type: &schema.ColumnType{Raw: "uuid", Type: &schema.UUIDType{T: "uuid"}}},
 					{Name: "c21", Type: &schema.ColumnType{Raw: "xml", Type: &XMLType{T: "xml"}}},
 					{Name: "c22", Type: &schema.ColumnType{Raw: "ARRAY", Null: true, Type: &ArrayType{Type: &schema.IntegerType{T: "integer"}, T: "integer[]"}}},
-					{Name: "c23", Type: &schema.ColumnType{Raw: "USER-DEFINED", Null: true, Type: &UserDefinedType{T: "ltree"}}},
+					{Name: "c23", Type: &schema.ColumnType{Raw: "USER-DEFINED", Null: true, Type: &UserDefinedType{T: "ltree", C: "b"}}},
 					{Name: "c24", Type: &schema.ColumnType{Raw: "state", Type: stateE}},
 					{Name: "c25", Type: &schema.ColumnType{Raw: "timestamp without time zone", Type: &schema.TimeType{T: "timestamp without time zone", Precision: sqlx.P(4)}}, Default: &schema.RawExpr{X: "now()"}},
 					{Name: "c26", Type: &schema.ColumnType{Raw: "timestamp with time zone", Type: &schema.TimeType{T: "timestamp with time zone", Precision: sqlx.P(6)}}},
@@ -147,7 +147,7 @@ func TestDriver_InspectTable(t *testing.T) {
 					{Name: "c32", Type: &schema.ColumnType{Raw: "bigserial", Type: &SerialType{T: "bigserial", SequenceName: "t1_c32_seq"}}},
 					{Name: "c33", Type: &schema.ColumnType{Raw: `status`, Type: statusE}, Default: &schema.Literal{V: "'unknown'"}},
 					{Name: "c34", Type: &schema.ColumnType{Raw: "ARRAY", Type: &ArrayType{T: "state[]", Type: stateE}}},
-					{Name: "c35", Type: &schema.ColumnType{Raw: "domain_char", Type: &UserDefinedType{T: "domain_char"}}},
+					{Name: "c35", Type: &schema.ColumnType{Raw: "domain_char", Type: &UserDefinedType{T: "domain_char", C: "d"}}},
 					{Name: "c36", Type: &schema.ColumnType{Raw: "tsvector", Type: &TextSearchType{T: "tsvector"}}},
 					{Name: "c37", Type: &schema.ColumnType{Raw: "tsquery", Type: &TextSearchType{T: "tsquery"}}},
 					{Name: "c38", Type: &schema.ColumnType{Raw: "datemultirange", Type: &RangeType{T: "datemultirange"}}},