Repository: ariga/atlas
Pull request #926 (2022-07-09 17:46:33)

sql/postgres: finish support for array types


Patch:
diff --git a/cmd/atlas/internal/cmdapi/migrate.go b/cmd/atlas/internal/cmdapi/migrate.go
index 820fb4f6..469d4717 100644
--- a/cmd/atlas/internal/cmdapi/migrate.go
+++ b/cmd/atlas/internal/cmdapi/migrate.go
@@ -9,7 +9,7 @@ import (
 	"errors"
 	"fmt"
 	"io"
-	"io/ioutil"
+	"os"
 	"path/filepath"
 	"strconv"
 	"strings"
@@ -25,6 +25,8 @@ import (
 	"ariga.io/atlas/sql/sqlcheck/destructive"
 	"ariga.io/atlas/sql/sqlclient"
 	"ariga.io/atlas/sql/sqltool"
+	"github.com/hashicorp/hcl/v2"
+	"github.com/hashicorp/hcl/v2/hclsyntax"
 
 	"github.com/fatih/color"
 	"github.com/hashicorp/hcl/v2/hclparse"
@@ -478,12 +480,8 @@ func to(ctx context.Context, client *sqlclient.Client) (migrate.StateReader, err
 	schemas := MigrateFlags.Schemas
 	switch parts[0] {
 	case "file": // hcl file
-		f, err := ioutil.ReadFile(parts[1])
-		if err != nil {
-			return nil, err
-		}
 		realm := &schema.Realm{}
-		parsed, err := parseHCLBytes(f)
+		parsed, err := parseHCLPaths(parts[1])
 		if err != nil {
 			return nil, err
 		}
@@ -527,14 +525,66 @@ func to(ctx context.Context, client *sqlclient.Client) (migrate.StateReader, err
 	}
 }
 
-func parseHCLBytes(b []byte) (*hclparse.Parser, error) {
+// parseHCL paths parses the HCL files in the given paths. If a path represents a directory,
+// its direct descendants will be considered, skipping any subdirectories. If a project file
+// is present in the input paths, an error is returned.
+func parseHCLPaths(paths ...string) (*hclparse.Parser, error) {
 	p := hclparse.NewParser()
-	if _, diag := p.ParseHCL(b, ""); diag.HasErrors() {
-		return nil, diag
+	for _, path := range paths {
+		switch stat, err := os.Stat(path); {
+		case err != nil:
+			return nil, err
+		case stat.IsDir():
+			dir, err := os.ReadDir(path)
+			if err != nil {
+				return nil, err
+			}
+			for _, f := range dir {
+				// Skip nested dirs.
+				if f.IsDir() {
+					continue
+				}
+				if err := mayParse(p, filepath.Join(path, f.Name())); err != nil {
+					return nil, err
+				}
+			}
+		default:
+			if err := mayParse(p, path); err != nil {
+				return nil, err
+			}
+		}
+	}
+	if len(p.Files()) == 0 {
+		return nil, fmt.Errorf("no schema files found in: %s", paths)
 	}
 	return p, nil
 }
 
+// mayParse will parse the file in path if it is an HCL file. If the file is an Atlas
+// project file an error is returned.
+func mayParse(p *hclparse.Parser, path string) error {
+	if n := filepath.Base(path); filepath.Ext(n) != ".hcl" {
+		return nil
+	}
+	switch f, diag := p.ParseHCLFile(path); {
+	case diag.HasErrors():
+		return diag
+	case isProjectFile(f):
+		return fmt.Errorf("cannot parse project file %q as a schema file", path)
+	default:
+		return nil
+	}
+}
+
+func isProjectFile(f *hcl.File) bool {
+	for _, blk := range f.Body.(*hclsyntax.Body).Blocks {
+		if blk.Type == "env" {
+			return true
+		}
+	}
+	return false
+}
+
 const (
 	formatAtlas         = "atlas"
 	formatGolangMigrate = "golang-migrate"
diff --git a/cmd/atlas/internal/cmdapi/schema.go b/cmd/atlas/internal/cmdapi/schema.go
index beff2cee..1f7e1ca6 100644
--- a/cmd/atlas/internal/cmdapi/schema.go
+++ b/cmd/atlas/internal/cmdapi/schema.go
@@ -51,7 +51,7 @@ var (
 	// ApplyFlags are the flags used in SchemaApply command.
 	ApplyFlags struct {
 		DevURL      string
-		File        string
+		Paths       []string
 		Web         bool
 		Addr        string
 		DryRun      bool
@@ -64,19 +64,26 @@ var (
 		Short: "Apply an atlas schema to a target database.",
 		// Use 80-columns as max width.
 		Long: `'atlas schema apply' plans and executes a database migration to bring a given
-database to the state described in the Atlas schema file. Before running the
+database to the state described in the provided Atlas schema. Before running the
 migration, Atlas will print the migration plan and prompt the user for approval.
 
+The schema is provided by one or more paths (to a file or directory) using the "-f" flag:
+  atlas schema apply -u URL -f file1.hcl -f file2.hcl
+  atlas schema apply -u URL -f schema/ -f override.hcl
+
+As a convenience, schemas may also be provided via an environment definition in
+the project file (see: https://atlasgo.io/cli/projects).
+
 If run with the "--dry-run" flag, atlas will exit after printing out the planned
 migration.`,
 		PreRunE: schemaFlagsFromEnv,
 		RunE:    CmdApplyRun,
 		Example: `  atlas schema apply -u "mysql://user:pass@localhost/dbname" -f atlas.hcl
-  atlas schema apply -u "mysql://localhost" -f atlas.hcl --schema prod --schema staging
-  atlas schema apply -u "mysql://user:pass@localhost:3306/dbname" -f atlas.hcl --dry-run
-  atlas schema apply -u "mariadb://user:pass@localhost:3306/dbname" -f atlas.hcl
-  atlas schema apply --url "postgres://user:pass@host:port/dbname?sslmode=disable" -f atlas.hcl
-  atlas schema apply -u "sqlite://file:ex1.db?_fk=1" -f atlas.hcl`,
+  atlas schema apply -u "mysql://localhost" -f schema.hcl --schema prod --schema staging
+  atlas schema apply -u "mysql://user:pass@localhost:3306/dbname" -f schema.hcl --dry-run
+  atlas schema apply -u "mariadb://user:pass@localhost:3306/dbname" -f schema.hcl
+  atlas schema apply --url "postgres://user:pass@host:port/dbname?sslmode=disable" -f schema.hcl
+  atlas schema apply -u "sqlite://file:ex1.db?_fk=1" -f schema.hcl`,
 	}
 
 	// InspectFlags are the flags used in SchemaInspect command.
@@ -92,7 +99,7 @@ migration.`,
 It then prints to the screen the schema of that database in Atlas DDL syntax. This output can be
 saved to a file, commonly by redirecting the output to a file named with a ".hcl" suffix:
 
-  atlas schema inspect -u "mysql://user:pass@localhost:3306/dbname" > atlas.hcl
+  atlas schema inspect -u "mysql://user:pass@localhost:3306/dbname" > schema.hcl
 
 This file can then be edited and used with the` + " `atlas schema apply` " + `command to plan
 and execute schema migrations against the given database. In cases where users wish to inspect
@@ -136,7 +143,7 @@ func init() {
 	// Schema apply flags.
 	schemaCmd.AddCommand(SchemaApply)
 	SchemaApply.Flags().SortFlags = false
-	SchemaApply.Flags().StringVarP(&ApplyFlags.File, fileFlag, "f", "", "[/path/to/file] file containing the HCL schema.")
+	SchemaApply.Flags().StringSliceVarP(&ApplyFlags.Paths, fileFlag, "f", nil, "[paths...] file or directory containing the HCL files")
 	SchemaApply.Flags().StringVarP(&SchemaFlags.URL, urlFlag, "u", "", "URL to the database using the format:\n[driver://username:password@address/dbname?param=value]")
 	SchemaApply.Flags().StringSliceVarP(&SchemaFlags.Schemas, schemaFlag, "s", nil, "Set schema names.")
 	SchemaApply.Flags().StringVarP(&ApplyFlags.DevURL, devURLFlag, "", "", "URL for the dev database. Used to validate schemas and calculate diffs\nbefore running migration.")
@@ -275,7 +282,7 @@ func CmdApplyRun(cmd *cobra.Command, _ []string) error {
 		return err
 	}
 	defer c.Close()
-	return applyRun(cmd.Context(), c, ApplyFlags.DevURL, ApplyFlags.File, ApplyFlags.DryRun, ApplyFlags.AutoApprove, GlobalFlags.Vars)
+	return applyRun(cmd.Context(), c, ApplyFlags.DevURL, ApplyFlags.Paths, ApplyFlags.DryRun, ApplyFlags.AutoApprove, GlobalFlags.Vars)
 }
 
 // CmdFmtRun formats all HCL files in a given directory using canonical HCL formatting
@@ -289,7 +296,7 @@ func CmdFmtRun(cmd *cobra.Command, args []string) {
 	}
 }
 
-func applyRun(ctx context.Context, client *sqlclient.Client, devURL string, file string, dryRun, autoApprove bool, input map[string]string) error {
+func applyRun(ctx context.Context, client *sqlclient.Client, devURL string, paths []string, dryRun, autoApprove bool, input map[string]string) error {
 	schemas := SchemaFlags.Schemas
 	if client.URL.Schema != "" {
 		schemas = append(schemas, client.URL.Schema)
@@ -300,12 +307,8 @@ func applyRun(ctx context.Context, client *sqlclient.Client, devURL string, file
 	if err != nil {
 		return err
 	}
-	f, err := ioutil.ReadFile(file)
-	if err != nil {
-		return err
-	}
 	desired := &schema.Realm{}
-	parsed, err := parseHCLBytes(f)
+	parsed, err := parseHCLPaths(paths...)
 	if err != nil {
 		return err
 	}
@@ -320,7 +323,7 @@ func applyRun(ctx context.Context, client *sqlclient.Client, devURL string, file
 		}
 		for _, s := range desired.Schemas {
 			if !sm[s.Name] {
-				return fmt.Errorf("schema %q from file %q was not selected %q, all schemas defined in file must be selected", s.Name, file, schemas)
+				return fmt.Errorf("schema %q was not selected %q, all schemas defined in file must be selected", s.Name, schemas)
 			}
 		}
 	}
diff --git a/doc/md/cli/reference.md b/doc/md/cli/reference.md
index cdf98e8b..38f7eea6 100644
--- a/doc/md/cli/reference.md
+++ b/doc/md/cli/reference.md
@@ -44,6 +44,16 @@ List of supported environment parameters:
   variable "ATLAS_NO_UPDATE_NOTIFIER".
 
 
+## atlas license
+
+Display license information
+
+#### Usage
+```
+atlas license
+```
+
+
 ## atlas migrate
 
 Manage versioned migration files
@@ -74,7 +84,7 @@ Applies pending migration files on the connected database.
 
 #### Usage
 ```
-atlas migrate apply [flags]
+atlas migrate apply [flags] [count]
 ```
 
 #### Details
@@ -88,42 +98,45 @@ As a safety measure 'atlas migrate apply' will abort with an error, if:
 #### Example
 
 ```
-  atlas migrate apply --to mysql://user:pass@localhost:3306/dbname
-  atlas migrate apply 1 --dir file:///path/to/migration/directory --to mysql://user:pass@localhost:3306/dbname
+  atlas migrate apply -u mysql://user:pass@localhost:3306/dbname
+  atlas migrate apply --dir file:///path/to/migration/directory --url mysql://user:pass@localhost:3306/dbname 1
+  atlas migrate apply --env dev 1
 ```
 #### Flags
 ```
       --log string                log format to use (default "tty")
       --revisions-schema string   schema name where the revisions table is to be created
-      --to string                 [driver://username:password@address/dbname?param=value] select a data source using the URL format
+  -u, --url string                [driver://username:password@address/dbname?param=value] select a database using the URL format
 
 ```
 
 
 ### atlas migrate diff
 
-Compute the diff between the migration directory and a connected database and create a new migration file.
+Compute the diff between the migration directory and a desired state and create a new migration file.
 
 #### Usage
 ```
-atlas migrate diff [flags]
+atlas migrate diff [flags] [name]
 ```
 
 #### Details
-'atlas migrate diff' uses the dev-database to re-run all migration files in the migration
-directory and compares it to a given desired state and create a new migration file containing SQL statements to migrate 
-the migration directory state to the desired schema. The desired state can be another connected database or an HCL file.
+'atlas migrate diff' uses the dev-database to re-run all migration files in the migration directory, compares
+it to a given desired state and create a new migration file containing SQL statements to migrate the migration
+directory state to the desired schema. The desired state can be another connected database or an HCL file.
 
 #### Example
 
 ```
-  atlas migrate diff --dev-url mysql://user:pass@localhost:3306/dev --to mysql://user:pass@localhost:3306/dbname
   atlas migrate diff --dev-url mysql://user:pass@localhost:3306/dev --to file://atlas.hcl
+  atlas migrate diff --dev-url mysql://user:pass@localhost:3306/dev --to file://atlas.hcl add_users_table
+  atlas migrate diff --dev-url mysql://user:pass@localhost:3306/dev --to mysql://user:pass@localhost:3306/dbname
+  atlas migrate diff --env dev
 ```
 #### Flags
 ```
-      --dev-url string   [driver://username:password@address/dbname?param=value] select a data source using the URL format
-      --to string        [driver://username:password@address/dbname?param=value] select a data source using the URL format
+      --dev-url string   [driver://username:password@address/dbname?param=value] select a database using the URL format
+      --to string        [driver://username:password@address/dbname?param=value] select a database using the URL format
       --verbose          enable verbose logging
 
 ```
@@ -148,13 +161,41 @@ This command should be used whenever a manual change in the migration directory
   atlas migrate hash --force
 ```
 
+### atlas migrate lint
+
+Run analysis on the migration directory
+
+#### Usage
+```
+atlas migrate lint [flags]
+```
+
+#### Example
+
+```
+  atlas migrate lint --env dev
+  atlas migrate lint --dir file:///path/to/migration/directory --dev-url mysql://root:pass@localhost:3306 --latest 1
+  atlas migrate lint --dir file:///path/to/migration/directory --dev-url mysql://root:pass@localhost:3306 --git-base master
+  atlas migrate lint --dir file:///path/to/migration/directory --dev-url mysql://root:pass@localhost:3306 --log '{{ json .Files }}'
+```
+#### Flags
+```
+      --dev-url string    [driver://username:password@address/dbname?param=value] select a database using the URL format
+      --git-base string   run analysis against the base Git branch
+      --git-dir string    path to the repository working directory (default ".")
+      --latest uint       run analysis on the latest N migration files
+      --log string        custom logging using a Go template
+
+```
+
+
 ### atlas migrate new
 
 Creates a new empty migration file in the migration directory.
 
 #### Usage
 ```
-atlas migrate new
+atlas migrate new [name]
 ```
 
 #### Details
@@ -176,9 +217,9 @@ atlas migrate validate [flags]
 ```
 
 #### Details
-'atlas migrate validate' computes the integrity hash sum of the migration directory and compares it to 
-the atlas.sum file. If there is a mismatch it will be reported. If the --dev-url flag is given, the migration files are 
-executed on the connected database in order to validate SQL semantics.
+'atlas migrate validate' computes the integrity hash sum of the migration directory and compares it to the
+atlas.sum file. If there is a mismatch it will be reported. If the --dev-url flag is given, the migration
+files are executed on the connected database in order to validate SQL semantics.
 
 #### Example
 
@@ -186,10 +227,11 @@ executed on the connected database in order to validate SQL semantics.
   atlas migrate validate
   atlas migrate validate --dir file:///path/to/migration/directory
   atlas migrate validate --dir file:///path/to/migration/directory --dev-url mysql://user:pass@localhost:3306/dev
+  atlas migrate validate --env dev
 ```
 #### Flags
 ```
-      --dev-url string   [driver://username:password@address/dbname?param=value] select a data source using the URL format
+      --dev-url string   [driver://username:password@address/dbname?param=value] select a database using the URL format
 
 ```
 
@@ -225,9 +267,16 @@ atlas schema apply [flags]
 
 #### Details
 'atlas schema apply' plans and executes a database migration to bring a given
-database to the state described in the Atlas schema file. Before running the
+database to the state described in the provided Atlas schema. Before running the
 migration, Atlas will print the migration plan and prompt the user for approval.
 
+The schema is provided by one or more paths (to a file or directory) using the "-f" flag:
+  atlas schema apply -u URL -f file1.hcl -f file2.hcl
+  atlas schema apply -u URL -f schema/ -f override.hcl
+
+As a convenience, schemas may also be provided via an environment definition in
+the project file (see: https://atlasgo.io/cli/projects).
+
 If run with the "--dry-run" flag, atlas will exit after printing out the planned
 migration.
 
@@ -235,15 +284,15 @@ migration.
 
 ```
   atlas schema apply -u "mysql://user:pass@localhost/dbname" -f atlas.hcl
-  atlas schema apply -u "mysql://localhost" -f atlas.hcl --schema prod --schema staging
-  atlas schema apply -u "mysql://user:pass@localhost:3306/dbname" -f atlas.hcl --dry-run
-  atlas schema apply -u "mariadb://user:pass@localhost:3306/dbname" -f atlas.hcl
-  atlas schema apply --url "postgres://user:pass@host:port/dbname?sslmode=disable" -f atlas.hcl
-  atlas schema apply -u "sqlite://file:ex1.db?_fk=1" -f atlas.hcl
+  atlas schema apply -u "mysql://localhost" -f schema.hcl --schema prod --schema staging
+  atlas schema apply -u "mysql://user:pass@localhost:3306/dbname" -f schema.hcl --dry-run
+  atlas schema apply -u "mariadb://user:pass@localhost:3306/dbname" -f schema.hcl
+  atlas schema apply --url "postgres://user:pass@host:port/dbname?sslmode=disable" -f schema.hcl
+  atlas schema apply -u "sqlite://file:ex1.db?_fk=1" -f schema.hcl
 ```
 #### Flags
 ```
-  -f, --file string      [/path/to/file] file containing the HCL schema.
+  -f, --file strings     [paths...] file or directory containing the HCL files
   -u, --url string       URL to the database using the format:
                          [driver://username:password@address/dbname?param=value]
   -s, --schema strings   Set schema names.
@@ -274,8 +323,8 @@ SQL statements to migrate the "from" database to the schema of the "to" database
 
 #### Flags
 ```
-      --from string   [driver://username:password@protocol(address)/dbname?param=value] Select data source using the url format
-      --to string     [driver://username:password@protocol(address)/dbname?param=value] Select data source using the url format
+      --from string   [driver://username:password@protocol(address)/dbname?param=value] select a database using the URL format
+      --to string     [driver://username:password@protocol(address)/dbname?param=value] select a database using the URL format
 
 ```
 
@@ -313,7 +362,7 @@ atlas schema inspect [flags]
 It then prints to the screen the schema of that database in Atlas DDL syntax. This output can be
 saved to a file, commonly by redirecting the output to a file named with a ".hcl" suffix:
 
-  atlas schema inspect -u "mysql://user:pass@localhost:3306/dbname" > atlas.hcl
+  atlas schema inspect -u "mysql://user:pass@localhost:3306/dbname" > schema.hcl
 
 This file can then be edited and used with the `atlas schema apply` command to plan
 and execute schema migrations against the given database. In cases where users wish to inspect
@@ -335,7 +384,7 @@ flag.
 ```
       --addr string      Used with -w, local address to bind the server to (default ":5800")
   -s, --schema strings   Set schema name
-  -u, --url string       [driver://username:password@protocol(address)/dbname?param=value] Select data source using the url format
+  -u, --url string       [driver://username:password@protocol(address)/dbname?param=value] select a database using the URL format
   -w, --web              Open in a local Atlas UI
 
 ```
diff --git a/sql/postgres/convert.go b/sql/postgres/convert.go
index aaa63088..8aaf93dd 100644
--- a/sql/postgres/convert.go
+++ b/sql/postgres/convert.go
@@ -146,16 +146,21 @@ func FormatType(t schema.Type) (string, error) {
 // The raw value is expected to follow the format in PostgreSQL information schema
 // or as an input for the CREATE TABLE statement.
 func ParseType(typ string) (schema.Type, error) {
-	d, err := parseColumn(typ)
-	if err != nil {
-		return nil, err
-	}
+	var (
+		err error
+		d   *columnDesc
+	)
 	// Normalize PostgreSQL array data types from "CREATE TABLE" format to
 	// "INFORMATION_SCHEMA" format (i.e. as it is inspected from the database).
 	if t, ok := arrayType(typ); ok {
-		d = &columnDesc{typ: TypeArray, udt: t}
+		d = &columnDesc{typ: TypeArray, fmtype: t + "[]"}
+	} else if d, err = parseColumn(typ); err != nil {
+		return nil, err
+	}
+	t, err := columnType(d)
+	if err != nil {
+		return nil, err
 	}
-	t := columnType(d)
 	// If the type is unknown (to us), we fall back to user-defined but expect
 	// to improve this in future versions by ensuring this against the database.
 	if ut, ok := t.(*schema.UnsupportedType); ok {
@@ -165,16 +170,16 @@ func ParseType(typ string) (schema.Type, error) {
 }
 
 // reArray parses array declaration. See: https://postgresql.org/docs/current/arrays.html.
-var reArray = regexp.MustCompile(`(?i)(\w+)\s*(?:(?:\[\d*])+|\s+ARRAY\s*(?:\[\d*])*)`)
+var reArray = regexp.MustCompile(`(?i)(.+?)(( +ARRAY( *\[[ \d]*] *)*)+|( *\[[ \d]*] *)+)$`)
 
 // arrayType reports if the given string is an array type (e.g. int[], text[2]),
 // and returns its "udt_name" as it was inspected from the database.
 func arrayType(t string) (string, bool) {
 	matches := reArray.FindStringSubmatch(t)
-	if len(matches) != 2 {
+	if len(matches) < 2 {
 		return "", false
 	}
-	return matches[1], true
+	return strings.TrimSpace(matches[1]), true
 }
 
 // reInterval parses declaration of interval fields. See: https://www.postgresql.org/docs/current/datatype-datetime.html.
@@ -192,14 +197,14 @@ func intervalField(t string) (string, bool) {
 
 // columnDesc represents a column descriptor.
 type columnDesc struct {
-	typ           string
-	size          int64
-	udt           string
+	typ           string // data_type
+	fmtype        string // pg_catalog.format_type
+	size          int64  // character_maximum_length
+	typtype       string // pg_type.typtype
+	typid         int64  // pg_type.oid
 	precision     int64
 	timePrecision *int64
 	scale         int64
-	typtype       string
-	typid         int64
 	parts         []string
 	interval      string
 }
@@ -291,7 +296,7 @@ func parseCharParts(parts []string, c *columnDesc) error {
 	}
 	size, err := strconv.ParseInt(parts[0], 10, 64)
 	if err != nil {
-		return fmt.Errorf("postgres: parse size %q: %w", parts[1], err)
+		return fmt.Errorf("postgres: parse size %q: %w", parts[0], err)
 	}
 	c.size = size
 	return nil
diff --git a/sql/postgres/crdb.go b/sql/postgres/crdb.go
index 4bac89b2..61e76059 100644
--- a/sql/postgres/crdb.go
+++ b/sql/postgres/crdb.go
@@ -82,7 +82,7 @@ func (cd *crdbDiff) Normalize(from, to *schema.Table) error {
 }
 
 func (cd *crdbDiff) ColumnChange(fromT *schema.Table, from, to *schema.Column) (schema.ChangeKind, error) {
-	// All serial types in crdb become bigints under the hood,
+	// All serial types in Cockroach are implemented as bigint.
 	// See: https://www.cockroachlabs.com/docs/stable/serial.html#generated-values-for-mode-sql_sequence-and-sql_sequence_cached.
 	if s, ok := to.Type.Type.(*SerialType); ok {
 		to.Type.Type = &schema.IntegerType{
@@ -306,6 +306,7 @@ SELECT
 	t1.table_name,
 	t1.column_name,
 	t1.data_type,
+	pg_catalog.format_type(a.atttypid, a.atttypmod) AS format_type,
 	t1.is_nullable,
 	t1.column_default,
 	t1.character_maximum_length,
@@ -315,7 +316,6 @@ SELECT
 	t1.interval_type,
 	t1.character_set_name,
 	t1.collation_name,
-	t1.udt_name,
 	t1.is_identity,
 	t5.start_value as identity_start,
 	t5.increment_by as identity_increment,
@@ -329,6 +329,7 @@ FROM
 	"information_schema"."columns" AS t1
 	JOIN pg_catalog.pg_namespace AS t2 ON t2.nspname = t1.table_schema
 	JOIN pg_catalog.pg_class AS t3 ON t3.relnamespace = t2.oid AND t3.relname = t1.table_name
+	JOIN pg_catalog.pg_attribute AS a ON a.attrelid = t3.oid AND a.attname = t1.column_name
 	LEFT JOIN pg_catalog.pg_type AS t4
 	ON t1.udt_name = t4.typname
 	LEFT JOIN pg_sequences AS t5
diff --git a/sql/postgres/diff.go b/sql/postgres/diff.go
index ee98f593..f600c399 100644
--- a/sql/postgres/diff.go
+++ b/sql/postgres/diff.go
@@ -248,12 +248,24 @@ func (d *diff) typeChanged(from, to *schema.Column) (bool, error) {
 		changed = fromT.T != toT.T
 	case *ArrayType:
 		toT := toT.(*ArrayType)
-		changed = fromT.T != toT.T
-		// Array types can be defined differently, but they may represent the same type.
-		// Therefore, in case of mismatch, we verify it using the database engine.
-		if changed {
-			equals, err := d.typesEqual(fromT.T, toT.T)
-			return !equals, err
+		// Same type.
+		if changed = fromT.T != toT.T; !changed {
+			break
+		}
+		// In case the desired schema is not normalized, the string type can look different even
+		// if the two strings represent the same array type (varchar(1), character varying (1)).
+		// Therefore, we try by comparing the underlying types if they were defined.
+		if fromT.Type != nil && toT.Type != nil {
+			t1, err := FormatType(fromT.Type)
+			if err != nil {
+				return false, err
+			}
+			t2, err := FormatType(toT.Type)
+			if err != nil {
+				return false, err
+			}
+			// Same underlying type.
+			changed = t1 != t2
 		}
 	default:
 		return false, &sqlx.UnsupportedTypeError{Type: fromT}
@@ -333,22 +345,6 @@ func (d *diff) valuesEqual(x, y string) (bool, error) {
 	return b, nil
 }
 
-// typesEqual reports if the data types x and y
-// equal according to the database engine.
-func (d *diff) typesEqual(x, y string) (bool, error) {
-	var b bool
-	// The datatype are safe to be inlined in the SELECT statement
-	// same as we inline them in the CREATE TABLE statement.
-	rows, err := d.QueryContext(context.Background(), fmt.Sprintf("SELECT '%s'::regtype = '%s'::regtype", x, y))
-	if err != nil {
-		return false, err
-	}
-	if err := sqlx.ScanOne(rows, &b); err != nil {
-		return false, err
-	}
-	return b, nil
-}
-
 // Default IDENTITY attributes.
 const (
 	defaultIdentityGen  = "BY DEFAULT"
diff --git a/sql/postgres/inspect.go b/sql/postgres/inspect.go
index 3eaa43d7..42edb454 100644
--- a/sql/postgres/inspect.go
+++ b/sql/postgres/inspect.go
@@ -163,14 +163,14 @@ func (i *inspect) columns(ctx context.Context, s *schema.Schema) error {
 }
 
 // addColumn scans the current row and adds a new column from it to the table.
-func (i *inspect) addColumn(s *schema.Schema, rows *sql.Rows) error {
+func (i *inspect) addColumn(s *schema.Schema, rows *sql.Rows) (err error) {
 	var (
-		typid, maxlen, precision, timeprecision, scale, seqstart, seqinc, seqlast                                               sql.NullInt64
-		table, name, typ, nullable, defaults, udt, identity, genidentity, genexpr, charset, collate, comment, typtype, interval sql.NullString
+		typid, maxlen, precision, timeprecision, scale, seqstart, seqinc, seqlast                                                  sql.NullInt64
+		table, name, typ, fmtype, nullable, defaults, identity, genidentity, genexpr, charset, collate, comment, typtype, interval sql.NullString
 	)
-	if err := rows.Scan(
-		&table, &name, &typ, &nullable, &defaults, &maxlen, &precision, &timeprecision, &scale, &interval, &charset,
-		&collate, &udt, &identity, &seqstart, &seqinc, &seqlast, &genidentity, &genexpr, &comment, &typtype, &typid,
+	if err = rows.Scan(
+		&table, &name, &typ, &fmtype, &nullable, &defaults, &maxlen, &precision, &timeprecision, &scale, &interval,
+		&charset, &collate, &identity, &seqstart, &seqinc, &seqlast, &genidentity, &genexpr, &comment, &typtype, &typid,
 	); err != nil {
 		return err
 	}
@@ -185,10 +185,10 @@ func (i *inspect) addColumn(s *schema.Schema, rows *sql.Rows) error {
 			Null: nullable.String == "YES",
 		},
 	}
-	c.Type.Type = columnType(&columnDesc{
+	c.Type.Type, err = columnType(&columnDesc{
 		typ:           typ.String,
+		fmtype:        fmtype.String,
 		size:          maxlen.Int64,
-		udt:           udt.String,
 		scale:         scale.Int64,
 		typtype:       typtype.String,
 		typid:         typid.Int64,
@@ -227,7 +227,7 @@ func (i *inspect) addColumn(s *schema.Schema, rows *sql.Rows) error {
 	return nil
 }
 
-func columnType(c *columnDesc) schema.Type {
+func columnType(c *columnDesc) (schema.Type, error) {
 	var typ schema.Type
 	switch t := c.typ; strings.ToLower(t) {
 	case TypeBigInt, TypeInt8, TypeInt, TypeInteger, TypeInt4, TypeSmallInt, TypeInt2, TypeInt64:
@@ -264,7 +264,7 @@ func columnType(c *columnDesc) schema.Type {
 		if c.interval != "" {
 			f, ok := intervalField(c.interval)
 			if !ok {
-				return &schema.UnsupportedType{T: c.interval}
+				return &schema.UnsupportedType{T: c.interval}, nil
 			}
 			typ.(*IntervalType).F = f
 		}
@@ -283,23 +283,28 @@ func columnType(c *columnDesc) schema.Type {
 	case TypeXML:
 		typ = &XMLType{T: t}
 	case TypeArray:
-		// Note that for ARRAY types, the 'udt_name' column holds the array type
-		// prefixed with '_'. For example, for 'integer[]' the result is '_int',
-		// and for 'text[N][M]' the result is also '_text'. That's because, the
-		// database ignores any size or multi-dimensions constraints.
-		typ = &ArrayType{T: strings.TrimPrefix(c.udt, "_") + "[]"}
+		// Ignore multi-dimensions or size constraints
+		// as they are ignored by the database.
+		typ = &ArrayType{T: c.fmtype}
+		if t, ok := arrayType(c.fmtype); ok {
+			tt, err := ParseType(t)
+			if err != nil {
+				return nil, err
+			}
+			typ.(*ArrayType).Type = tt
+		}
 	case TypeUserDefined:
-		typ = &UserDefinedType{T: c.udt}
+		typ = &UserDefinedType{T: c.fmtype}
 		// The `typtype` column is set to 'e' for enum types, and the
 		// values are filled in batch after the rows above is closed.
 		// https://www.postgresql.org/docs/current/catalog-pg-type.html
 		if c.typtype == "e" {
-			typ = &enumType{T: c.udt, ID: c.typid}
+			typ = &enumType{T: c.fmtype, ID: c.typid}
 		}
 	default:
 		typ = &schema.UnsupportedType{T: t}
 	}
-	return typ
+	return typ, nil
 }
 
 // enumValues fills enum columns with their values from the database.
@@ -673,8 +678,8 @@ type (
 	// ArrayType defines an array type.
 	// https://www.postgresql.org/docs/current/arrays.html
 	ArrayType struct {
-		schema.Type
-		T string
+		schema.Type        // Underlying items type (e.g. varchar(255)).
+		T           string // Formatted type (e.g. int[]).
 	}
 
 	// BitType defines a bit type.
@@ -909,6 +914,7 @@ SELECT
 	t1.table_name,
 	t1.column_name,
 	t1.data_type,
+	pg_catalog.format_type(a.atttypid, a.atttypmod) AS format_type,
 	t1.is_nullable,
 	t1.column_default,
 	t1.character_maximum_length,
@@ -918,7 +924,6 @@ SELECT
 	t1.interval_type,
 	t1.character_set_name,
 	t1.collation_name,
-	t1.udt_name,
 	t1.is_identity,
 	t1.identity_start,
 	t1.identity_increment,
@@ -932,8 +937,8 @@ FROM
 	"information_schema"."columns" AS t1
 	JOIN pg_catalog.pg_namespace AS t2 ON t2.nspname = t1.table_schema
 	JOIN pg_catalog.pg_class AS t3 ON t3.relnamespace = t2.oid AND t3.relname = t1.table_name
-	LEFT JOIN pg_catalog.pg_type AS t4
-	ON t1.udt_name = t4.typname
+	JOIN pg_catalog.pg_attribute AS a ON a.attrelid = t3.oid AND a.attname = t1.column_name
+	LEFT JOIN pg_catalog.pg_type AS t4 ON t1.udt_name = t4.typname
 WHERE
 	t1.table_schema = $1 AND t1.table_name IN (%s)
 ORDER BY


Test patch:
diff --git a/cmd/atlas/internal/cmdapi/migrate_test.go b/cmd/atlas/internal/cmdapi/migrate_test.go
index 0ecb1fb3..f5a91382 100644
--- a/cmd/atlas/internal/cmdapi/migrate_test.go
+++ b/cmd/atlas/internal/cmdapi/migrate_test.go
@@ -312,7 +312,7 @@ func TestMigrate_Lint(t *testing.T) {
 	require.Equal(t, "new.sql", s)
 }
 
-const hcl = `
+const testSchema = `
 schema "main" {
 }
 
@@ -394,8 +394,8 @@ table "accounts" {
 
 func hclURL(t *testing.T) string {
 	p := t.TempDir()
-	require.NoError(t, os.WriteFile(filepath.Join(p, "atlas.hcl"), []byte(hcl), 0600))
-	return "file://" + filepath.Join(p, "atlas.hcl")
+	require.NoError(t, os.WriteFile(filepath.Join(p, "schema.hcl"), []byte(testSchema), 0600))
+	return "file://" + filepath.Join(p, "schema.hcl")
 }
 
 func copyFile(src, dst string) error {
diff --git a/internal/integration/cockroach_test.go b/internal/integration/cockroach_test.go
index d635aee4..aec5d11e 100644
--- a/internal/integration/cockroach_test.go
+++ b/internal/integration/cockroach_test.go
@@ -165,7 +165,7 @@ func TestCockroach_AddColumns(t *testing.T) {
 			&schema.Column{Name: "m", Type: &schema.ColumnType{Type: &schema.BoolType{T: "boolean"}, Null: true}, Default: &schema.Literal{V: "false"}},
 			&schema.Column{Name: "n", Type: &schema.ColumnType{Type: &schema.SpatialType{T: "geometry"}, Null: true}, Default: &schema.Literal{V: "'POINT(1 2)'"}},
 			&schema.Column{Name: "o", Type: &schema.ColumnType{Type: &schema.SpatialType{T: "geometry"}, Null: true}, Default: &schema.Literal{V: "'LINESTRING(0 0, 1440 900)'"}},
-			&schema.Column{Name: "q", Type: &schema.ColumnType{Type: &postgres.ArrayType{T: "text[]"}, Null: true}, Default: &schema.Literal{V: "'{}'"}},
+			&schema.Column{Name: "q", Type: &schema.ColumnType{Type: &postgres.ArrayType{Type: &schema.StringType{T: "text"}, T: "text[]"}, Null: true}, Default: &schema.Literal{V: "'{}'"}},
 		)
 		changes := t.diff(t.loadUsers(), usersT)
 		require.Len(t, changes, 14)
@@ -215,7 +215,7 @@ func TestCockroach_ColumnArray(t *testing.T) {
 		// Add column.
 		usersT.Columns = append(
 			usersT.Columns,
-			&schema.Column{Name: "a", Type: &schema.ColumnType{Raw: "int[]", Type: &postgres.ArrayType{T: "int[]"}}, Default: &schema.Literal{V: "'{1}'"}},
+			&schema.Column{Name: "a", Type: &schema.ColumnType{Raw: "bigint[]", Type: &postgres.ArrayType{Type: &schema.IntegerType{T: "bigint"}, T: "bigint[]"}}, Default: &schema.Literal{V: "'{1}'"}},
 		)
 		changes := t.diff(t.loadUsers(), usersT)
 		require.Len(t, changes, 1)
@@ -774,7 +774,7 @@ create table atlas_types_sanity
 				},
 				{
 					Name: "tArray",
-					Type: &schema.ColumnType{Type: &postgres.ArrayType{T: "text[]"}, Raw: "ARRAY", Null: true},
+					Type: &schema.ColumnType{Type: &postgres.ArrayType{Type: &schema.StringType{T: "text"}, T: "text[]"}, Raw: "ARRAY", Null: true},
 					Default: &schema.RawExpr{
 						X: "ARRAY[]:::STRING[]",
 					},
diff --git a/internal/integration/integration_test.go b/internal/integration/integration_test.go
index 7485fc7e..e0276eea 100644
--- a/internal/integration/integration_test.go
+++ b/internal/integration/integration_test.go
@@ -244,7 +244,7 @@ func initCLI() error {
 
 func testCLIMultiSchemaApply(t T, h string, dsn string, schemas []string, eval schemahcl.Evaluator) {
 	err := initCLI()
-	f := filepath.Join(t.TempDir(), "atlas.hcl")
+	f := filepath.Join(t.TempDir(), "schema.hcl")
 	err = ioutil.WriteFile(f, []byte(h), 0644)
 	require.NoError(t, err)
 	require.NoError(t, err)
@@ -302,7 +302,7 @@ func testCLISchemaApply(t T, h string, dsn string, args ...string) {
 	err := initCLI()
 	require.NoError(t, err)
 	t.dropTables("users")
-	f := filepath.Join(t.TempDir(), "atlas.hcl")
+	f := filepath.Join(t.TempDir(), "schema.hcl")
 	err = ioutil.WriteFile(f, []byte(h), 0644)
 	require.NoError(t, err)
 	runArgs := []string{
@@ -337,7 +337,7 @@ func testCLISchemaApplyDry(t T, h string, dsn string) {
 	err := initCLI()
 	require.NoError(t, err)
 	t.dropTables("users")
-	f := filepath.Join(t.TempDir(), "atlas.hcl")
+	f := filepath.Join(t.TempDir(), "schema.hcl")
 	err = ioutil.WriteFile(f, []byte(h), 0644)
 	require.NoError(t, err)
 	cmd := exec.Command("go", "run", "ariga.io/atlas/cmd/atlas",
@@ -370,7 +370,7 @@ func testCLISchemaApplyAutoApprove(t T, h string, dsn string, args ...string) {
 	err := initCLI()
 	require.NoError(t, err)
 	t.dropTables("users")
-	f := filepath.Join(t.TempDir(), "atlas.hcl")
+	f := filepath.Join(t.TempDir(), "schema.hcl")
 	err = ioutil.WriteFile(f, []byte(h), 0644)
 	require.NoError(t, err)
 	runArgs := []string{
diff --git a/internal/integration/postgres_test.go b/internal/integration/postgres_test.go
index 027a61c1..0e082f30 100644
--- a/internal/integration/postgres_test.go
+++ b/internal/integration/postgres_test.go
@@ -196,7 +196,7 @@ func TestPostgres_AddColumns(t *testing.T) {
 			&schema.Column{Name: "n", Type: &schema.ColumnType{Type: &schema.SpatialType{T: "point"}, Null: true}, Default: &schema.Literal{V: "'(1,2)'"}},
 			&schema.Column{Name: "o", Type: &schema.ColumnType{Type: &schema.SpatialType{T: "line"}, Null: true}, Default: &schema.Literal{V: "'{1,2,3}'"}},
 			&schema.Column{Name: "p", Type: &schema.ColumnType{Type: &postgres.UserDefinedType{T: "hstore"}, Null: true}, Default: &schema.RawExpr{X: "'a => 1'"}},
-			&schema.Column{Name: "q", Type: &schema.ColumnType{Type: &postgres.ArrayType{T: "text[]"}, Null: true}, Default: &schema.Literal{V: "'{}'"}},
+			&schema.Column{Name: "q", Type: &schema.ColumnType{Type: &postgres.ArrayType{Type: &schema.StringType{T: "text"}, T: "text[]"}, Null: true}, Default: &schema.Literal{V: "'{}'"}},
 		)
 		changes := t.diff(t.loadUsers(), usersT)
 		require.Len(t, changes, 17)
@@ -252,7 +252,7 @@ func TestPostgres_ColumnArray(t *testing.T) {
 		// Add column.
 		usersT.Columns = append(
 			usersT.Columns,
-			&schema.Column{Name: "a", Type: &schema.ColumnType{Raw: "int[]", Type: &postgres.ArrayType{T: "int[]"}}, Default: &schema.Literal{V: "'{1}'"}},
+			&schema.Column{Name: "a", Type: &schema.ColumnType{Raw: "int[]", Type: &postgres.ArrayType{Type: &schema.IntegerType{T: "int"}, T: "int[]"}}, Default: &schema.Literal{V: "'{1}'"}},
 		)
 		changes := t.diff(t.loadUsers(), usersT)
 		require.Len(t, changes, 1)
@@ -1011,7 +1011,7 @@ create table atlas_types_sanity
 				},
 				{
 					Name: "tArray",
-					Type: &schema.ColumnType{Type: &postgres.ArrayType{T: "text[]"}, Raw: "ARRAY", Null: true},
+					Type: &schema.ColumnType{Type: &postgres.ArrayType{Type: &schema.StringType{T: "text"}, T: "text[]"}, Raw: "ARRAY", Null: true},
 					Default: &schema.Literal{
 						V: "'{}'",
 					},
diff --git a/internal/integration/testdata/postgres/column-array.txt b/internal/integration/testdata/postgres/column-array.txt
new file mode 100644
index 00000000..c7fdcc84
--- /dev/null
+++ b/internal/integration/testdata/postgres/column-array.txt
@@ -0,0 +1,103 @@
+apply 1.hcl
+cmpshow logs 1.sql
+
+# Change size of the underlying type.
+apply 2.change-size.hcl
+cmpshow logs 2.sql
+
+synced 3.nochange.hcl
+synced 4.nochange.hcl
+
+apply 5.hcl
+cmpshow logs 5.sql
+
+
+-- 1.hcl --
+schema "$db" {}
+
+table "logs" {
+  schema    = schema.$db
+  column "records" {
+    null      = false
+    type      = sql("varchar(255)[]")
+  }
+}
+
+-- 1.sql --
+                  Table "script_column_array.logs"
+ Column  |           Type           | Collation | Nullable | Default
+---------+--------------------------+-----------+----------+---------
+ records | character varying(255)[] |           | not null |
+
+
+-- 2.change-size.hcl --
+schema "$db" {}
+
+table "logs" {
+  schema    = schema.$db
+  column "records" {
+    null      = false
+    type      = sql("varchar(100) ARRAY")
+  }
+}
+
+-- 2.sql --
+                  Table "script_column_array.logs"
+ Column  |           Type           | Collation | Nullable | Default
+---------+--------------------------+-----------+----------+---------
+ records | character varying(100)[] |           | not null |
+
+-- 3.nochange.hcl --
+schema "$db" {}
+
+table "logs" {
+  schema    = schema.$db
+  column "records" {
+    null      = false
+    type      = sql("varchar(100) ARRAY")
+  }
+}
+
+-- 4.nochange.hcl --
+schema "$db" {}
+
+table "logs" {
+  schema    = schema.$db
+  column "records" {
+    null      = false
+    type      = sql("varchar(100) [10][]")
+  }
+}
+
+-- 5.hcl --
+schema "$db" {}
+
+table "logs" {
+  schema    = schema.$db
+  column "a" {
+    null = false
+    type = sql("int[1]")
+  }
+  column "b" {
+    null = false
+    type = sql("int ARRAY[1]")
+  }
+  column "c" {
+    null = false
+    type = sql("character varying(100) ARRAY[1]")
+  }
+  column "d" {
+    null = false
+    type = sql("point [][1]")
+  }
+}
+
+-- 5.sql --
+       Table "script_column_array.logs"
+ Column |           Type           | Collation | Nullable | Default
+--------+--------------------------+-----------+----------+---------
+ a      | integer[]                |           | not null |
+ b      | integer[]                |           | not null |
+ c      | character varying(100)[] |           | not null |
+ d      | point[]                  |           | not null |
+
diff --git a/internal/integration/testdata/sqlite/cli-apply-directory.txt b/internal/integration/testdata/sqlite/cli-apply-directory.txt
new file mode 100644
index 00000000..797504c8
--- /dev/null
+++ b/internal/integration/testdata/sqlite/cli-apply-directory.txt
@@ -0,0 +1,26 @@
+! atlas schema apply -f empty/ -u URL --auto-approve
+stderr 'no schema files found'
+
+atlas schema apply -f schema/ -u URL --auto-approve
+cmpshow users expected.sql
+
+-- schema/users.hcl --
+table "users" {
+  schema = schema.main
+  column "id" {
+    null = false
+    type = int
+  }
+  column "status" {
+    null = true
+    type = text
+    default = "hello"
+  }
+}
+-- schema/schema.hcl --
+schema "main" {
+}
+-- empty/hello.txt --
+hello, world!
+-- expected.sql --
+CREATE TABLE `users` (`id` int NOT NULL, `status` text NULL DEFAULT 'hello')
\ No newline at end of file
diff --git a/internal/integration/testdata/sqlite/cli-apply-multifile.txt b/internal/integration/testdata/sqlite/cli-apply-multifile.txt
new file mode 100644
index 00000000..45d8cecd
--- /dev/null
+++ b/internal/integration/testdata/sqlite/cli-apply-multifile.txt
@@ -0,0 +1,21 @@
+atlas schema apply -f users.hcl -f schema.hcl -u URL --auto-approve
+cmpshow users expected.sql
+
+-- users.hcl --
+table "users" {
+  schema = schema.main
+  column "id" {
+    null = false
+    type = int
+  }
+  column "status" {
+    null = true
+    type = text
+    default = "hello"
+  }
+}
+-- schema.hcl --
+schema "main" {
+}
+-- expected.sql --
+CREATE TABLE `users` (`id` int NOT NULL, `status` text NULL DEFAULT 'hello')
\ No newline at end of file
diff --git a/internal/integration/testdata/sqlite/cli-apply-project-multifile.txt b/internal/integration/testdata/sqlite/cli-apply-project-multifile.txt
new file mode 100644
index 00000000..f4a9de80
--- /dev/null
+++ b/internal/integration/testdata/sqlite/cli-apply-project-multifile.txt
@@ -0,0 +1,31 @@
+atlas schema apply --env local --auto-approve
+cmpshow users expected.sql
+
+-- atlas.hcl --
+env "local" {
+    url = "URL"
+    src = "./schema"
+    def_val = "hello"
+}
+-- schema/vars.hcl --
+variable "def_val" {
+    type = string
+}
+-- schema/table.hcl --
+table "users" {
+  schema = schema.main
+  column "id" {
+    null = false
+    type = int
+  }
+  column "status" {
+    null = true
+    type = text
+    default = var.def_val
+  }
+}
+-- schema/schema.hcl --
+schema "main" {
+}
+-- expected.sql --
+CREATE TABLE `users` (`id` int NOT NULL, `status` text NULL DEFAULT 'hello')
\ No newline at end of file
diff --git a/internal/integration/testdata/sqlite/cli-migrate-project-multifile.txt b/internal/integration/testdata/sqlite/cli-migrate-project-multifile.txt
new file mode 100644
index 00000000..a08634e9
--- /dev/null
+++ b/internal/integration/testdata/sqlite/cli-migrate-project-multifile.txt
@@ -0,0 +1,41 @@
+exec mkdir migrations
+atlas migrate diff --env local
+cmpmig 0 diff.sql
+
+atlas migrate validate --env local
+
+atlas migrate new 1 --env local
+cmpmig 1 empty.sql
+
+exec touch migrations/2.sql
+! atlas migrate validate --env local
+stderr 'Error: checksum mismatch'
+
+atlas migrate hash --force --env local
+atlas migrate validate --env local
+
+-- atlas.hcl --
+env "local" {
+    url = "URL"
+    dev = "sqlite://test?mode=memory&_fk=1"
+    src = "./schema"
+    migration_dir {
+        url = "file://migrations"
+        format = atlas
+    }
+}
+-- schema/1.hcl --
+table "users" {
+  schema = schema.main
+  column "id" {
+    null = false
+    type = int
+  }
+}
+-- schema/2.hcl --
+schema "main" {
+}
+-- diff.sql --
+-- create "users" table
+CREATE TABLE `users` (`id` int NOT NULL);
+-- empty.sql --
diff --git a/internal/integration/testdata/sqlite/cli-schema-project-file.txt b/internal/integration/testdata/sqlite/cli-schema-project-file.txt
index 2feff2da..ff9a0029 100644
--- a/internal/integration/testdata/sqlite/cli-schema-project-file.txt
+++ b/internal/integration/testdata/sqlite/cli-schema-project-file.txt
@@ -7,6 +7,9 @@ stderr '"url" not set'
 ! atlas schema apply --url URL
 stderr '"file" not set'
 
+! atlas schema apply -f atlas.hcl -u URL
+stderr 'cannot parse project file'
+
 # Verify "url" and "src" attributes of the env are used.
 atlas schema apply --env local --auto-approve
 atlas schema inspect --env local > inspected.hcl
diff --git a/schemahcl/hcl_test.go b/schemahcl/hcl_test.go
index a417f329..8686a51b 100644
--- a/schemahcl/hcl_test.go
+++ b/schemahcl/hcl_test.go
@@ -312,8 +312,9 @@ person "rotem" {
 
 func TestMultiFile(t *testing.T) {
 	type Person struct {
-		Name  string `spec:",name"`
-		Hobby string `spec:"hobby"`
+		Name   string `spec:",name"`
+		Hobby  string `spec:"hobby"`
+		Parent *Ref   `spec:"parent"`
 	}
 	var test struct {
 		People []*Person `spec:"person"`
@@ -334,5 +335,9 @@ func TestMultiFile(t *testing.T) {
 	require.NoError(t, err)
 	require.Len(t, test.People, 2)
 	require.EqualValues(t, &Person{Name: "rotemtam", Hobby: "coding"}, test.People[0])
-	require.EqualValues(t, &Person{Name: "tzuri", Hobby: "ice-cream"}, test.People[1])
+	require.EqualValues(t, &Person{
+		Name:   "tzuri",
+		Hobby:  "ice-cream",
+		Parent: &Ref{V: "$person.rotemtam"},
+	}, test.People[1])
 }
diff --git a/schemahcl/testdata/b.hcl b/schemahcl/testdata/b.hcl
index 847c7daf..7ee0cee3 100644
--- a/schemahcl/testdata/b.hcl
+++ b/schemahcl/testdata/b.hcl
@@ -1,3 +1,4 @@
 person "tzuri" {
   hobby = "ice-cream"
+  parent = person.rotemtam
 }
\ No newline at end of file
diff --git a/sql/postgres/driver_test.go b/sql/postgres/driver_test.go
index 3b60b80e..0c3a34bf 100644
--- a/sql/postgres/driver_test.go
+++ b/sql/postgres/driver_test.go
@@ -50,7 +50,7 @@ func TestDriver_LockError(t *testing.T) {
 			WithArgs(hash).
 			WillReturnError(context.DeadlineExceeded).
 			RowsWillBeClosed()
-		unlock, err := d.Lock(context.Background(), name, time.Millisecond)
+		unlock, err := d.Lock(context.Background(), name, time.Minute)
 		require.Equal(t, schema.ErrLocked, err)
 		require.Nil(t, unlock)
 	})
diff --git a/sql/postgres/inspect_test.go b/sql/postgres/inspect_test.go
index ab10857b..cd7f2a97 100644
--- a/sql/postgres/inspect_test.go
+++ b/sql/postgres/inspect_test.go
@@ -41,41 +41,41 @@ func TestDriver_InspectTable(t *testing.T) {
 				m.ExpectQuery(queryColumns).
 					WithArgs("public", "users").
 					WillReturnRows(sqltest.Rows(`
- table_name  |  column_name |          data_type          | is_nullable |         column_default          | character_maximum_length | numeric_precision | datetime_precision | numeric_scale |    interval_type    | character_set_name | collation_name |  udt_name   | is_identity | identity_start | identity_increment |   identity_last  | identity_generation | generation_expression | comment | typtype |  oid
--------------+--------------+-----------------------------+-------------+---------------------------------+--------------------------+-------------------+--------------------+---------------+---------------------+--------------------+----------------+-------------+-------------+----------------+--------------------+------------------+---------------------+-----------------------+---------+---------+-------
- users       |  id          | bigint                      | NO          |                                 |                          |                64 |                    |             0 |                     |                    |                | int8        | YES         |      100       |          1         |          1       |    BY DEFAULT       |                       |         | b       |    20
- users       |  rank        | integer                     | YES         |                                 |                          |                32 |                    |             0 |                     |                    |                | int4        | NO          |                |                    |                  |                     |                       | rank    | b       |    23
- users       |  c1          | smallint                    | NO          |           1000                  |                          |                16 |                    |             0 |                     |                    |                | int2        | NO          |                |                    |                  |                     |                       |         | b       |    21
- users       |  c2          | bit                         | NO          |                                 |                        1 |                   |                    |               |                     |                    |                | bit         | NO          |                |                    |                  |                     |                       |         | b       |  1560
- users       |  c3          | bit varying                 | NO          |                                 |                       10 |                   |                    |               |                     |                    |                | varbit      | NO          |                |                    |                  |                     |                       |         | b       |  1562
- users       |  c4          | boolean                     | NO          |                                 |                          |                   |                    |               |                     |                    |                | bool        | NO          |                |                    |                  |                     |                       |         | b       |    16
- users       |  c5          | bytea                       | NO          |                                 |                          |                   |                    |               |                     |                    |                | bytea       | NO          |                |                    |                  |                     |                       |         | b       |    17
- users       |  c6          | character                   | NO          |                                 |                      100 |                   |                    |               |                     |                    |                | bpchar      | NO          |                |                    |                  |                     |                       |         | b       |  1042
- users       |  c7          | character varying           | NO          | 'logged_in'::character varying  |                          |                   |                    |               |                     |                    |                | varchar     | NO          |                |                    |                  |                     |                       |         | b       |  1043
- users       |  c8          | cidr                        | NO          |                                 |                          |                   |                    |               |                     |                    |                | cidr        | NO          |                |                    |                  |                     |                       |         | b       |   650
- users       |  c9          | circle                      | NO          |                                 |                          |                   |                    |               |                     |                    |                | circle      | NO          |                |                    |                  |                     |                       |         | b       |   718
- users       |  c10         | date                        | NO          |                                 |                          |                   |                    |               |                     |                    |                | date        | NO          |                |                    |                  |                     |                       |         | b       |  1082
- users       |  c11         | time with time zone         | NO          |                                 |                          |                   |                    |               |                     |                    |                | timetz      | NO          |                |                    |                  |                     |                       |         | b       |  1266
- users       |  c12         | double precision            | NO          |                                 |                          |                53 |                    |               |                     |                    |                | float8      | NO          |                |                    |                  |                     |                       |         | b       |   701
- users       |  c13         | real                        | NO          |           random()              |                          |                24 |                    |               |                     |                    |                | float4      | NO          |                |                    |                  |                     |                       |         | b       |   700
- users       |  c14         | json                        | NO          |           '{}'::json            |                          |                   |                    |               |                     |                    |                | json        | NO          |                |                    |                  |                     |                       |         | b       |   114
- users       |  c15         | jsonb                       | NO          |           '{}'::jsonb           |                          |                   |                    |               |                     |                    |                | jsonb       | NO          |                |                    |                  |                     |                       |         | b       |  3802
- users       |  c16         | money                       | NO          |                                 |                          |                   |                    |               |                     |                    |                | money       | NO          |                |                    |                  |                     |                       |         | b       |   790
- users       |  c17         | numeric                     | NO          |                                 |                          |                   |                    |               |                     |                    |                | numeric     | NO          |                |                    |                  |                     |                       |         | b       |  1700
- users       |  c18         | numeric                     | NO          |                                 |                          |                 4 |                    |             4 |                     |                    |                | numeric     | NO          |                |                    |                  |                     |                       |         | b       |  1700
- users       |  c19         | integer                     | NO          | nextval('t1_c19_seq'::regclass) |                          |                32 |                    |             0 |                     |                    |                | int4        | NO          |                |                    |                  |                     |                       |         | b       |    23
- users       |  c20         | uuid                        | NO          |                                 |                          |                   |                    |               |                     |                    |                | uuid        | NO          |                |                    |                  |                     |                       |         | b       |  2950
- users       |  c21         | xml                         | NO          |                                 |                          |                   |                    |               |                     |                    |                | xml         | NO          |                |                    |                  |                     |                       |         | b       |   142
- users       |  c22         | ARRAY                       | YES         |                                 |                          |                   |                    |               |                     |                    |                | _int4       | NO          |                |                    |                  |                     |                       |         | b       |  1007
- users       |  c23         | USER-DEFINED                | YES         |                                 |                          |                   |                    |               |                     |                    |                | ltree       | NO          |                |                    |                  |                     |                       |         | b       | 16535
- users       |  c24         | USER-DEFINED                | NO          |                                 |                          |                   |                    |               |                     |                    |                | state       | NO          |                |                    |                  |                     |                       |         | e       | 16774
- users       |  c25         | timestamp without time zone | NO          |            now()                |                          |                   |                  4 |               |                     |                    |                | timestamp   | NO          |                |                    |                  |                     |                       |         | b       |  1114
- users       |  c26         | timestamp with time zone    | NO          |                                 |                          |                   |                  6 |               |                     |                    |                | timestamptz | NO          |                |                    |                  |                     |                       |         | b       |  1184
- users       |  c27         | time without time zone      | NO          |                                 |                          |                   |                  6 |               |                     |                    |                | time        | NO          |                |                    |                  |                     |                       |         | b       |  1266
- users       |  c28         | int                         | NO          |                                 |                          |                   |                  6 |               |                     |                    |                | int8        | NO          |                |                    |                  |                     |        (c1 + c2)      |         | b       |  1267
- users       |  c29         | interval                    | NO          |                                 |                          |                   |                  6 |               |                     |                    |                | interval    | NO          |                |                    |                  |                     |                       |         | b       |  1268
- users       |  c30         | interval                    | NO          |                                 |                          |                   |                  6 |               |        MONTH        |                    |                | interval    | NO          |                |                    |                  |                     |                       |         | b       |  1269
- users       |  c31         | interval                    | NO          |                                 |                          |                   |                  6 |               | MINUTE TO SECOND(6) |                    |                | interval    | NO          |                |                    |                  |                     |                       |         | b       |  1233
+ table_name  |  column_name |          data_type          |  formatted  | is_nullable |         column_default          | character_maximum_length | numeric_precision | datetime_precision | numeric_scale |    interval_type    | character_set_name | collation_name | is_identity | identity_start | identity_increment |   identity_last  | identity_generation | generation_expression | comment | typtype |  oid
+-------------+--------------+-----------------------------+-------------|-------------+---------------------------------+--------------------------+-------------------+--------------------+---------------+---------------------+--------------------+----------------+-------------+----------------+--------------------+------------------+---------------------+-----------------------+---------+---------+-------
+ users       |  id          | bigint                      | int8        | NO          |                                 |                          |                64 |                    |             0 |                     |                    |                | YES         |      100       |          1         |          1       |    BY DEFAULT       |                       |         | b       |    20
+ users       |  rank        | integer                     | int4        | YES         |                                 |                          |                32 |                    |             0 |                     |                    |                | NO          |                |                    |                  |                     |                       | rank    | b       |    23
+ users       |  c1          | smallint                    | int2        | NO          |           1000                  |                          |                16 |                    |             0 |                     |                    |                | NO          |                |                    |                  |                     |                       |         | b       |    21
+ users       |  c2          | bit                         | bit         | NO          |                                 |                        1 |                   |                    |               |                     |                    |                | NO          |                |                    |                  |                     |                       |         | b       |  1560
+ users       |  c3          | bit varying                 | varbit      | NO          |                                 |                       10 |                   |                    |               |                     |                    |                | NO          |                |                    |                  |                     |                       |         | b       |  1562
+ users       |  c4          | boolean                     | bool        | NO          |                                 |                          |                   |                    |               |                     |                    |                | NO          |                |                    |                  |                     |                       |         | b       |    16
+ users       |  c5          | bytea                       | bytea       | NO          |                                 |                          |                   |                    |               |                     |                    |                | NO          |                |                    |                  |                     |                       |         | b       |    17
+ users       |  c6          | character                   | bpchar      | NO          |                                 |                      100 |                   |                    |               |                     |                    |                | NO          |                |                    |                  |                     |                       |         | b       |  1042
+ users       |  c7          | character varying           | varchar     | NO          | 'logged_in'::character varying  |                          |                   |                    |               |                     |                    |                | NO          |                |                    |                  |                     |                       |         | b       |  1043
+ users       |  c8          | cidr                        | cidr        | NO          |                                 |                          |                   |                    |               |                     |                    |                | NO          |                |                    |                  |                     |                       |         | b       |   650
+ users       |  c9          | circle                      | circle      | NO          |                                 |                          |                   |                    |               |                     |                    |                | NO          |                |                    |                  |                     |                       |         | b       |   718
+ users       |  c10         | date                        | date        | NO          |                                 |                          |                   |                    |               |                     |                    |                | NO          |                |                    |                  |                     |                       |         | b       |  1082
+ users       |  c11         | time with time zone         | timetz      | NO          |                                 |                          |                   |                    |               |                     |                    |                | NO          |                |                    |                  |                     |                       |         | b       |  1266
+ users       |  c12         | double precision            | float8      | NO          |                                 |                          |                53 |                    |               |                     |                    |                | NO          |                |                    |                  |                     |                       |         | b       |   701
+ users       |  c13         | real                        | float4      | NO          |           random()              |                          |                24 |                    |               |                     |                    |                | NO          |                |                    |                  |                     |                       |         | b       |   700
+ users       |  c14         | json                        | json        | NO          |           '{}'::json            |                          |                   |                    |               |                     |                    |                | NO          |                |                    |                  |                     |                       |         | b       |   114
+ users       |  c15         | jsonb                       | jsonb       | NO          |           '{}'::jsonb           |                          |                   |                    |               |                     |                    |                | NO          |                |                    |                  |                     |                       |         | b       |  3802
+ users       |  c16         | money                       | money       | NO          |                                 |                          |                   |                    |               |                     |                    |                | NO          |                |                    |                  |                     |                       |         | b       |   790
+ users       |  c17         | numeric                     | numeric     | NO          |                                 |                          |                   |                    |               |                     |                    |                | NO          |                |                    |                  |                     |                       |         | b       |  1700
+ users       |  c18         | numeric                     | numeric     | NO          |                                 |                          |                 4 |                    |             4 |                     |                    |                | NO          |                |                    |                  |                     |                       |         | b       |  1700
+ users       |  c19         | integer                     | int4        | NO          | nextval('t1_c19_seq'::regclass) |                          |                32 |                    |             0 |                     |                    |                | NO          |                |                    |                  |                     |                       |         | b       |    23
+ users       |  c20         | uuid                        | uuid        | NO          |                                 |                          |                   |                    |               |                     |                    |                | NO          |                |                    |                  |                     |                       |         | b       |  2950
+ users       |  c21         | xml                         | xml         | NO          |                                 |                          |                   |                    |               |                     |                    |                | NO          |                |                    |                  |                     |                       |         | b       |   142
+ users       |  c22         | ARRAY                       | integer[]   | YES         |                                 |                          |                   |                    |               |                     |                    |                | NO          |                |                    |                  |                     |                       |         | b       |  1007
+ users       |  c23         | USER-DEFINED                | ltree       | YES         |                                 |                          |                   |                    |               |                     |                    |                | NO          |                |                    |                  |                     |                       |         | b       | 16535
+ users       |  c24         | USER-DEFINED                | state       | NO          |                                 |                          |                   |                    |               |                     |                    |                | NO          |                |                    |                  |                     |                       |         | e       | 16774
+ users       |  c25         | timestamp without time zone | timestamp   | NO          |            now()                |                          |                   |                  4 |               |                     |                    |                | NO          |                |                    |                  |                     |                       |         | b       |  1114
+ users       |  c26         | timestamp with time zone    | timestamptz | NO          |                                 |                          |                   |                  6 |               |                     |                    |                | NO          |                |                    |                  |                     |                       |         | b       |  1184
+ users       |  c27         | time without time zone      | time        | NO          |                                 |                          |                   |                  6 |               |                     |                    |                | NO          |                |                    |                  |                     |                       |         | b       |  1266
+ users       |  c28         | int                         | int8        | NO          |                                 |                          |                   |                  6 |               |                     |                    |                | NO          |                |                    |                  |                     |        (c1 + c2)      |         | b       |  1267
+ users       |  c29         | interval                    | interval    | NO          |                                 |                          |                   |                  6 |               |                     |                    |                | NO          |                |                    |                  |                     |                       |         | b       |  1268
+ users       |  c30         | interval                    | interval    | NO          |                                 |                          |                   |                  6 |               |        MONTH        |                    |                | NO          |                |                    |                  |                     |                       |         | b       |  1269
+ users       |  c31         | interval                    | interval    | NO          |                                 |                          |                   |                  6 |               | MINUTE TO SECOND(6) |                    |                | NO          |                |                    |                  |                     |                       |         | b       |  1233
 `))
 				m.ExpectQuery(sqltest.Escape(`SELECT enumtypid, enumlabel FROM pg_enum WHERE enumtypid IN ($1)`)).
 					WithArgs(16774).
@@ -117,7 +117,7 @@ func TestDriver_InspectTable(t *testing.T) {
 					{Name: "c19", Type: &schema.ColumnType{Raw: "integer", Type: &schema.IntegerType{T: "integer"}}, Default: &schema.RawExpr{X: "nextval('t1_c19_seq'::regclass)"}},
 					{Name: "c20", Type: &schema.ColumnType{Raw: "uuid", Type: &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{T: "int4[]"}}},
+					{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: "c24", Type: &schema.ColumnType{Raw: "state", Type: &schema.EnumType{T: "state", Values: []string{"on", "off"}}}},
 					{Name: "c25", Type: &schema.ColumnType{Raw: "timestamp without time zone", Type: &schema.TimeType{T: "timestamp without time zone", Precision: p(4)}}, Default: &schema.RawExpr{X: "now()"}},
@@ -137,11 +137,11 @@ func TestDriver_InspectTable(t *testing.T) {
 				m.ExpectQuery(queryColumns).
 					WithArgs("public", "users").
 					WillReturnRows(sqltest.Rows(`
-table_name | column_name |      data_type      | is_nullable |         column_default          | character_maximum_length | numeric_precision | datetime_precision | numeric_scale | interval_type | character_set_name | collation_name | udt_name | is_identity | identity_start | identity_increment |   identity_last  | identity_generation | generation_expression | comment | typtype |  oid
------------+-------------+---------------------+-------------+---------------------------------+--------------------------+-------------------+--------------------+---------------+---------------+--------------------+----------------+----------+-------------+----------------+--------------------+------------------+---------------------+-----------------------+---------+---------+-------
-users      | id          | bigint              | NO          |                                 |                          |                64 |                    |             0 |               |                    |                | int8     | NO          |                |                    |                  |                     |                       |         | b       |    20
-users      | c1          | smallint            | NO          |                                 |                          |                16 |                    |             0 |               |                    |                | int2     | NO          |                |                    |                  |                     |                       |         | b       |    21
-users      | parent_id   | bigint              | YES         |                                 |                          |                64 |                    |             0 |               |                    |                | int8     | NO          |                |                    |                  |                     |                       |         | b       |    22
+table_name | column_name |      data_type      | formatted |  is_nullable |         column_default          | character_maximum_length | numeric_precision | datetime_precision | numeric_scale | interval_type | character_set_name | collation_name | is_identity | identity_start | identity_increment |   identity_last  | identity_generation | generation_expression | comment | typtype |  oid
+-----------+-------------+---------------------+-----------+--------------+---------------------------------+--------------------------+-------------------+--------------------+---------------+---------------+--------------------+----------------+-------------+----------------+--------------------+------------------+---------------------+-----------------------+---------+---------+-------
+users      | id          | bigint              | int8      |  NO          |                                 |                          |                64 |                    |             0 |               |                    |                | NO          |                |                    |                  |                     |                       |         | b       |    20
+users      | c1          | smallint            | int2      |  NO          |                                 |                          |                16 |                    |             0 |               |                    |                | NO          |                |                    |                  |                     |                       |         | b       |    21
+users      | parent_id   | bigint              | int8      |  YES         |                                 |                          |                64 |                    |             0 |               |                    |                | NO          |                |                    |                  |                     |                       |         | b       |    22
 `))
 				m.ExpectQuery(queryIndexes).
 					WithArgs("public", "users").
@@ -198,11 +198,11 @@ users           | idx6            | brin        | c1          | f       | t
 				m.ExpectQuery(queryColumns).
 					WithArgs("public", "users").
 					WillReturnRows(sqltest.Rows(`
-table_name | column_name |      data_type      | is_nullable |         column_default          | character_maximum_length | numeric_precision | datetime_precision | numeric_scale | interval_type | character_set_name | collation_name | udt_name | is_identity | identity_start | identity_increment |   identity_last  | identity_generation | generation_expression | comment | typtype |  oid
------------+-------------+---------------------+-------------+---------------------------------+--------------------------+-------------------+--------------------+---------------+---------------+--------------------+----------------+----------+-------------+----------------+--------------------+------------------+---------------------+-----------------------+---------+---------+-------
-users      | id          | integer             | NO          |                                 |                          |                32 |                    |             0 |               |                    |                | int      | NO          |                |                    |                  |                     |                       |         | b       |    20
-users      | oid         | integer             | NO          |                                 |                          |                32 |                    |             0 |               |                    |                | int      | NO          |                |                    |                  |                     |                       |         | b       |    21
-users      | uid         | integer             | NO          |                                 |                          |                32 |                    |             0 |               |                    |                | int      | NO          |                |                    |                  |                     |                       |         | b       |    21
+table_name | column_name |      data_type      | formatted | is_nullable |         column_default          | character_maximum_length | numeric_precision | datetime_precision | numeric_scale | interval_type | character_set_name | collation_name | is_identity | identity_start | identity_increment |   identity_last  | identity_generation | generation_expression | comment | typtype |  oid
+-----------+-------------+---------------------+-----------+-------------+---------------------------------+--------------------------+-------------------+--------------------+---------------+---------------+--------------------+----------------+-------------+----------------+--------------------+------------------+---------------------+-----------------------+---------+---------+-------
+users      | id          | integer             | int       | NO          |                                 |                          |                32 |                    |             0 |               |                    |                | NO          |                |                    |                  |                     |                       |         | b       |    20
+users      | oid         | integer             | int       | NO          |                                 |                          |                32 |                    |             0 |               |                    |                | NO          |                |                    |                  |                     |                       |         | b       |    21
+users      | uid         | integer             | int       | NO          |                                 |                          |                32 |                    |             0 |               |                    |                | NO          |                |                    |                  |                     |                       |         | b       |    21
 `))
 				m.noIndexes()
 				m.ExpectQuery(queryFKs).
@@ -245,11 +245,11 @@ self_reference  | users      | uid         | public       | users
 				m.ExpectQuery(queryColumns).
 					WithArgs("public", "users").
 					WillReturnRows(sqltest.Rows(`
-table_name |column_name | data_type | is_nullable | column_default | character_maximum_length | numeric_precision | datetime_precision | numeric_scale | interval_type | character_set_name | collation_name | udt_name | is_identity | identity_start | identity_increment |   identity_last  | identity_generation | generation_expression | comment | typtype | oid
------------+------------+-----------+-------------+----------------+--------------------------+-------------------+--------------------+---------------+---------------+--------------------+----------------+----------+-------------+----------------+--------------------+------------------+---------------------+-----------------------+---------+---------+-----
-users      | c1         | integer   | NO          |                |                          |                32 |                    |             0 |               |                    |                | int4     | NO          |                |                    |                  |                     |                       |         | b       |  23
-users      | c2         | integer   | NO          |                |                          |                32 |                    |             0 |               |                    |                | int4     | NO          |                |                    |                  |                     |                       |         | b       |  23
-users      | c3         | integer   | NO          |                |                          |                32 |                    |             0 |               |                    |                | int4     | NO          |                |                    |                  |                     |                       |         | b       |  23
+table_name |column_name | data_type | formatted | is_nullable | column_default | character_maximum_length | numeric_precision | datetime_precision | numeric_scale | interval_type | character_set_name | collation_name | is_identity | identity_start | identity_increment |   identity_last  | identity_generation | generation_expression | comment | typtype | oid
+-----------+------------+-----------+-----------+-------------+----------------+--------------------------+-------------------+--------------------+---------------+---------------+--------------------+----------------+-------------+----------------+--------------------+------------------+---------------------+-----------------------+---------+---------+-----
+users      | c1         | integer   | int4      | NO          |                |                          |                32 |                    |             0 |               |                    |                | NO          |                |                    |                  |                     |                       |         | b       |  23
+users      | c2         | integer   | int4      | NO          |                |                          |                32 |                    |             0 |               |                    |                | NO          |                |                    |                  |                     |                       |         | b       |  23
+users      | c3         | integer   | int4      | NO          |                |                          |                32 |                    |             0 |               |                    |                | NO          |                |                    |                  |                     |                       |         | b       |  23
 `))
 				m.noIndexes()
 				m.noFKs()
@@ -338,13 +338,13 @@ public
 	m.ExpectQuery(sqltest.Escape(fmt.Sprintf(columnsQuery, "$2, $3, $4"))).
 		WithArgs("public", "logs1", "logs2", "logs3").
 		WillReturnRows(sqltest.Rows(`
-table_name |column_name | data_type | is_nullable | column_default | character_maximum_length | numeric_precision | datetime_precision | numeric_scale | interval_type | character_set_name | collation_name | udt_name | is_identity | identity_start | identity_increment |   identity_last  | identity_generation | generation_expression | comment | typtype | oid
------------+------------+-----------+-------------+----------------+--------------------------+-------------------+--------------------+---------------+---------------+--------------------+----------------+----------+-------------+----------------+--------------------+------------------+---------------------+-----------------------+---------+---------+-----
-logs1      | c1         | integer   | NO          |                |                          |                32 |                    |             0 |               |                    |                | int4     | NO          |                |                    |                  |                     |                       |         | b       |  23
-logs2      | c2         | integer   | NO          |                |                          |                32 |                    |             0 |               |                    |                | int4     | NO          |                |                    |                  |                     |                       |         | b       |  23
-logs2      | c3         | integer   | NO          |                |                          |                32 |                    |             0 |               |                    |                | int4     | NO          |                |                    |                  |                     |                       |         | b       |  23
-logs3      | c4         | integer   | NO          |                |                          |                32 |                    |             0 |               |                    |                | int4     | NO          |                |                    |                  |                     |                       |         | b       |  23
-logs3      | c5         | integer   | NO          |                |                          |                32 |                    |             0 |               |                    |                | int4     | NO          |                |                    |                  |                     |                       |         | b       |  23
+table_name |column_name | data_type | formatted | is_nullable | column_default | character_maximum_length | numeric_precision | datetime_precision | numeric_scale | interval_type | character_set_name | collation_name | is_identity | identity_start | identity_increment |   identity_last  | identity_generation | generation_expression | comment | typtype | oid
+-----------+------------+-----------+-----------+-------------+----------------+--------------------------+-------------------+--------------------+---------------+---------------+--------------------+----------------+-------------+----------------+--------------------+------------------+---------------------+-----------------------+---------+---------+-----
+logs1      | c1         | integer   | integer   | NO          |                |                          |                32 |                    |             0 |               |                    |                | NO          |                |                    |                  |                     |                       |         | b       |  23
+logs2      | c2         | integer   | integer   | NO          |                |                          |                32 |                    |             0 |               |                    |                | NO          |                |                    |                  |                     |                       |         | b       |  23
+logs2      | c3         | integer   | integer   | NO          |                |                          |                32 |                    |             0 |               |                    |                | NO          |                |                    |                  |                     |                       |         | b       |  23
+logs3      | c4         | integer   | integer   | NO          |                |                          |                32 |                    |             0 |               |                    |                | NO          |                |                    |                  |                     |                       |         | b       |  23
+logs3      | c5         | integer   | integer   | NO          |                |                          |                32 |                    |             0 |               |                    |                | NO          |                |                    |                  |                     |                       |         | b       |  23
 `))
 	m.ExpectQuery(sqltest.Escape(fmt.Sprintf(indexesQuery, "$2, $3, $4"))).
 		WillReturnRows(sqlmock.NewRows([]string{"table_name", "index_name", "column_name", "primary", "unique", "constraint_type", "predicate", "expression"}))
@@ -406,12 +406,12 @@ public
 	mk.ExpectQuery(queryCrdbColumns).
 		WithArgs("public", "users").
 		WillReturnRows(sqltest.Rows(`
-table_name  | column_name | data_type | is_nullable |              column_default               | character_maximum_length | numeric_precision | datetime_precision | numeric_scale | interval_type | character_set_name | collation_name | udt_name | is_identity | identity_start | identity_increment |   identity_last  |  identity_generation  | generation_expression | comment | typtype | oid 
-------------+-------------+-----------+-------------+-------------------------------------------+--------------------------+-------------------+--------------------+---------------+---------------+--------------------+----------------+----------+-------------+----------------+--------------------+------------------+-----------------------+-----------------------+---------+---------+-----
-users       | a           | bigint    | NO          |                                           |                          |                64 |                    |             0 |               |                    |                | int8     | NO          |                |                    |                  |                       |                       |         | b       | 20 
-users       | b           | bigint    | NO          |                                           |                          |                64 |                    |             0 |               |                    |                | int8     | NO          |                |                    |                  |                       |                       |         | b       | 20 
-users       | c           | bigint    | NO          |                                           |                          |                64 |                    |             0 |               |                    |                | int8     | NO          |                |                    |                  |                       |                       |         | b       | 20 
-users       | d           | bigint    | NO          |                                           |                          |                64 |                    |             0 |               |                    |                | int8     | NO          |                |                    |                  |                       |                       |         | b       | 20 
+table_name  | column_name | data_type | formatted | is_nullable |              column_default               | character_maximum_length | numeric_precision | datetime_precision | numeric_scale | interval_type | character_set_name | collation_name | is_identity | identity_start | identity_increment |   identity_last  |  identity_generation  | generation_expression | comment | typtype | oid 
+------------+-------------+-----------+-----------+-------------+-------------------------------------------+--------------------------+-------------------+--------------------+---------------+---------------+--------------------+----------------|-------------+----------------+--------------------+------------------+-----------------------+-----------------------+---------+---------+-----
+users       | a           | bigint    | bigint    | NO          |                                           |                          |                64 |                    |             0 |               |                    |                | NO          |                |                    |                  |                       |                       |         | b       | 20 
+users       | b           | bigint    | bigint    | NO          |                                           |                          |                64 |                    |             0 |               |                    |                | NO          |                |                    |                  |                       |                       |         | b       | 20 
+users       | c           | bigint    | bigint    | NO          |                                           |                          |                64 |                    |             0 |               |                    |                | NO          |                |                    |                  |                       |                       |         | b       | 20 
+users       | d           | bigint    | bigint    | NO          |                                           |                          |                64 |                    |             0 |               |                    |                | NO          |                |                    |                  |                       |                       |         | b       | 20 
 `))
 	mk.ExpectQuery(queryCrdbIndexes).
 		WithArgs("public", "users").
diff --git a/sql/postgres/sqlspec_test.go b/sql/postgres/sqlspec_test.go
index 98209096..cdc91c37 100644
--- a/sql/postgres/sqlspec_test.go
+++ b/sql/postgres/sqlspec_test.go
@@ -1011,27 +1011,39 @@ func TestTypes(t *testing.T) {
 		},
 		{
 			typeExpr: `sql("int[]")`,
-			expected: &ArrayType{T: "int[]"},
+			expected: &ArrayType{Type: &schema.IntegerType{T: "int"}, T: "int[]"},
 		},
 		{
 			typeExpr: `sql("int[2]")`,
-			expected: &ArrayType{T: "int[]"},
+			expected: &ArrayType{Type: &schema.IntegerType{T: "int"}, T: "int[]"},
 		},
 		{
 			typeExpr: `sql("text[][]")`,
-			expected: &ArrayType{T: "text[]"},
+			expected: &ArrayType{Type: &schema.StringType{T: "text"}, T: "text[]"},
 		},
 		{
 			typeExpr: `sql("integer [3][3]")`,
-			expected: &ArrayType{T: "integer[]"},
+			expected: &ArrayType{Type: &schema.IntegerType{T: "integer"}, T: "integer[]"},
 		},
 		{
-			typeExpr: `sql("integer ARRAY[4]")`,
-			expected: &ArrayType{T: "integer[]"},
+			typeExpr: `sql("integer  ARRAY[4]")`,
+			expected: &ArrayType{Type: &schema.IntegerType{T: "integer"}, T: "integer[]"},
 		},
 		{
 			typeExpr: `sql("integer ARRAY")`,
-			expected: &ArrayType{T: "integer[]"},
+			expected: &ArrayType{Type: &schema.IntegerType{T: "integer"}, T: "integer[]"},
+		},
+		{
+			typeExpr: `sql("character varying(255) [1][2]")`,
+			expected: &ArrayType{Type: &schema.StringType{T: "character varying", Size: 255}, T: "character varying(255)[]"},
+		},
+		{
+			typeExpr: `sql("character varying ARRAY[2]")`,
+			expected: &ArrayType{Type: &schema.StringType{T: "character varying"}, T: "character varying[]"},
+		},
+		{
+			typeExpr: `sql("varchar(2) [ 2 ] [  ]")`,
+			expected: &ArrayType{Type: &schema.StringType{T: "varchar", Size: 2}, T: "varchar(2)[]"},
 		},
 	} {
 		t.Run(tt.typeExpr, func(t *testing.T) {