Repository: ariga/atlas
Pull request #2901 (2024-06-27 06:43:11)

doc/md/integrations: sync circleci-orbs doc

https://github.com/ariga/atlas/assets/16095902/fa2afce6-dd95-4dc1-a732-6c1ec637e523




Patch:
diff --git a/cmd/atlas/go.mod b/cmd/atlas/go.mod
index 60dc28f7..909f8123 100644
--- a/cmd/atlas/go.mod
+++ b/cmd/atlas/go.mod
@@ -1,8 +1,6 @@
 module ariga.io/atlas/cmd/atlas
 
-go 1.22.3
-
-toolchain go1.22.4
+go 1.22.5
 
 replace ariga.io/atlas => ../..
 
@@ -19,13 +17,14 @@ require (
 	github.com/hashicorp/hcl/v2 v2.13.0
 	github.com/lib/pq v1.10.9
 	github.com/libsql/libsql-client-go v0.0.0-20230602133133-5905f0c4f8a5
+	github.com/mattn/go-isatty v0.0.18
 	github.com/mattn/go-sqlite3 v1.14.16
 	github.com/mitchellh/go-homedir v1.1.0
 	github.com/pganalyze/pg_query_go/v5 v5.1.0
 	github.com/spf13/cobra v1.8.0
 	github.com/spf13/pflag v1.0.5
-	github.com/stretchr/testify v1.8.4
-	github.com/vektah/gqlparser/v2 v2.5.1
+	github.com/stretchr/testify v1.9.0
+	github.com/vektah/gqlparser/v2 v2.5.16
 	github.com/zclconf/go-cty v1.14.4
 	gocloud.dev v0.36.0
 	golang.org/x/exp v0.0.0-20230515195305-f3d0a9c9a5cc
@@ -76,7 +75,6 @@ require (
 	github.com/kr/pretty v0.3.0 // indirect
 	github.com/libsql/sqlite-antlr4-parser v0.0.0-20230512205400-b2348f0d1196 // indirect
 	github.com/mattn/go-colorable v0.1.12 // indirect
-	github.com/mattn/go-isatty v0.0.18 // indirect
 	github.com/mitchellh/go-wordwrap v1.0.0 // indirect
 	github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect
 	github.com/pmezard/go-difflib v1.0.0 // indirect
diff --git a/cmd/atlas/go.sum b/cmd/atlas/go.sum
index 6f5d4318..7f23cda5 100644
--- a/cmd/atlas/go.sum
+++ b/cmd/atlas/go.sum
@@ -22,7 +22,6 @@ github.com/DATA-DOG/go-sqlmock v1.5.0 h1:Shsta01QNfFxHCfpW6YH2STWB0MudeXXEWMr20O
 github.com/DATA-DOG/go-sqlmock v1.5.0/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM=
 github.com/agext/levenshtein v1.2.1 h1:QmvMAjj2aEICytGiWzmxoE0x2KZvE0fvmqMOfy2tjT8=
 github.com/agext/levenshtein v1.2.1/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558=
-github.com/agnivade/levenshtein v1.0.1/go.mod h1:CURSv5d9Uaml+FovSIICkLbAUZ9S4RqaHDIsdSBg7lM=
 github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883 h1:bvNMNQO63//z+xNgfBlViaCIJKLlCJ6/fmUseuG0wVQ=
 github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8=
 github.com/antlr/antlr4/runtime/Go/antlr/v4 v4.0.0-20230512164433-5d1fd1a340c9 h1:goHVqTbFX3AIo0tzGr14pgfAW2ZfPChKO21Z9MGf/gk=
@@ -233,8 +232,8 @@ github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:
 github.com/rogpeppe/go-internal v1.6.1 h1:/FiVV8dS/e+YqF2JvO3yXRFbBLTIuSDkuC7aBOAvL+k=
 github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
 github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
-github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0=
-github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
+github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8=
+github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I=
 github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0=
 github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho=
 github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
@@ -247,14 +246,14 @@ github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81P
 github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
 github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
 github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
-github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
-github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
+github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
+github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
 github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo=
 github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
 github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs=
 github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
-github.com/vektah/gqlparser/v2 v2.5.1 h1:ZGu+bquAY23jsxDRcYpWjttRZrUz07LbiY77gUOHcr4=
-github.com/vektah/gqlparser/v2 v2.5.1/go.mod h1:mPgqFBu/woKTVYWyNk8cO3kh4S/f4aRFZrvOnp3hmCs=
+github.com/vektah/gqlparser/v2 v2.5.16 h1:1gcmLTvs3JLKXckwCwlUagVn/IlV2bwqle0vJ0vy5p8=
+github.com/vektah/gqlparser/v2 v2.5.16/go.mod h1:1lz1OeCqgQbQepsGxPVywrjdBHW2T08PUS3pJqepRww=
 github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
 github.com/zclconf/go-cty v1.14.4 h1:uXXczd9QDGsgu0i/QFR/hzI5NYCHLf6NQw/atrbnhq8=
 github.com/zclconf/go-cty v1.14.4/go.mod h1:VvMs5i0vgZdhYawQNq5kePSpLAoz8u1xvZgrPIxfnZE=
@@ -383,12 +382,10 @@ google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGm
 google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
-gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU=
 gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
 gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
-gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
 gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
diff --git a/cmd/atlas/internal/cloudapi/client.go b/cmd/atlas/internal/cloudapi/client.go
index 212ffebe..8a3c3be6 100644
--- a/cmd/atlas/internal/cloudapi/client.go
+++ b/cmd/atlas/internal/cloudapi/client.go
@@ -246,6 +246,9 @@ func (c *Client) ReportMigration(ctx context.Context, input ReportMigrationInput
 	return payload.ReportMigration.URL, nil
 }
 
+// ErrUnauthorized is returned when the server returns a 401 status code.
+var ErrUnauthorized = errors.New(http.StatusText(http.StatusUnauthorized))
+
 func (c *Client) post(ctx context.Context, query string, vars, data any) error {
 	body, err := json.Marshal(struct {
 		Query     string `json:"query"`
@@ -267,7 +270,10 @@ func (c *Client) post(ctx context.Context, query string, vars, data any) error {
 		return err
 	}
 	defer req.Body.Close()
-	if res.StatusCode != http.StatusOK {
+	switch {
+	case res.StatusCode == http.StatusUnauthorized:
+		return ErrUnauthorized
+	case res.StatusCode != http.StatusOK:
 		var v struct {
 			Errors errlist `json:"errors,omitempty"`
 		}
diff --git a/cmd/atlas/internal/cmdapi/cmdapi.go b/cmd/atlas/internal/cmdapi/cmdapi.go
index 92d0ea8c..0c85d327 100644
--- a/cmd/atlas/internal/cmdapi/cmdapi.go
+++ b/cmd/atlas/internal/cmdapi/cmdapi.go
@@ -578,7 +578,7 @@ func stateReader(ctx context.Context, env *Env, config *stateReaderConfig) (*cmd
 			return rc, nil
 		}
 		// All other schemes are database (or docker) connections.
-		c, err := sqlclient.Open(ctx, config.urls[0]) // call to selectScheme already checks for len > 0
+		c, err := env.openClient(ctx, config.urls[0]) // call to selectScheme already checks for len > 0
 		if err != nil {
 			return nil, err
 		}
diff --git a/cmd/atlas/internal/cmdapi/cmdapi_oss.go b/cmd/atlas/internal/cmdapi/cmdapi_oss.go
index 70275dde..af19cf8e 100644
--- a/cmd/atlas/internal/cmdapi/cmdapi_oss.go
+++ b/cmd/atlas/internal/cmdapi/cmdapi_oss.go
@@ -10,6 +10,7 @@ import (
 	"context"
 	"errors"
 	"fmt"
+	"net/url"
 	"text/template"
 
 	"ariga.io/atlas/cmd/atlas/internal/cloudapi"
@@ -33,6 +34,7 @@ func init() {
 		schemaDiffCmd(),
 		schemaFmtCmd(),
 		schemaInspectCmd(),
+		unsupportedCommand("schema", "test"),
 	)
 	Root.AddCommand(schemaCmd)
 	migrateCmd := migrateCmd()
@@ -46,10 +48,46 @@ func init() {
 		migrateSetCmd(),
 		migrateStatusCmd(),
 		migrateValidateCmd(),
+		unsupportedCommand("migrate", "checkpoint"),
+		unsupportedCommand("migrate", "down"),
+		unsupportedCommand("migrate", "rebase"),
+		unsupportedCommand("migrate", "rm"),
+		unsupportedCommand("migrate", "edit"),
+		unsupportedCommand("migrate", "push"),
+		unsupportedCommand("migrate", "test"),
 	)
 	Root.AddCommand(migrateCmd)
 }
 
+// unsupportedCommand create a stub command that reports
+// the command is not supported by this build.
+func unsupportedCommand(cmd, sub string) *cobra.Command {
+	s := fmt.Sprintf(
+		`'atlas %s %s' is not supported by the community version.
+
+To install the non-community version of Atlas, use the following command:
+
+	curl -sSf https://atlasgo.sh | sh
+
+Or, visit the website to see all installation options:
+
+	https://atlasgo.io/docs#installation
+`,
+		cmd, sub,
+	)
+	c := &cobra.Command{
+		Hidden: true,
+		Use:    fmt.Sprintf("%s is not supported by this build", sub),
+		Short:  s,
+		Long:   s,
+		RunE: RunE(func(*cobra.Command, []string) error {
+			return AbortErrorf(s)
+		}),
+	}
+	c.SetHelpTemplate(s + "\n")
+	return c
+}
+
 // Project represents an atlas.hcl project config file.
 type Project struct {
 	Envs  []*Env `spec:"env"`  // List of environments
@@ -120,6 +158,99 @@ func migrateLintRun(cmd *cobra.Command, _ []string, flags migrateLintFlags, env
 	return err
 }
 
+func migrateDiffRun(cmd *cobra.Command, args []string, flags migrateDiffFlags, env *Env) error {
+	ctx := cmd.Context()
+	dev, err := sqlclient.Open(ctx, flags.devURL)
+	if err != nil {
+		return err
+	}
+	defer dev.Close()
+	// Acquire a lock.
+	if l, ok := dev.Driver.(schema.Locker); ok {
+		unlock, err := l.Lock(ctx, "atlas_migrate_diff", flags.lockTimeout)
+		if err != nil {
+			return fmt.Errorf("acquiring database lock: %w", err)
+		}
+		// If unlocking fails notify the user about it.
+		defer func() { cobra.CheckErr(unlock()) }()
+	}
+	// Open the migration directory.
+	u, err := url.Parse(flags.dirURL)
+	if err != nil {
+		return err
+	}
+	dir, err := cmdmigrate.DirURL(ctx, u, false)
+	if err != nil {
+		return err
+	}
+	if flags.edit {
+		dir = &editDir{dir}
+	}
+	var name, indent string
+	if len(args) > 0 {
+		name = args[0]
+	}
+	f, err := cmdmigrate.Formatter(u)
+	if err != nil {
+		return err
+	}
+	if f, indent, err = mayIndent(u, f, flags.format); err != nil {
+		return err
+	}
+	diffOpts := append(diffOptions(cmd, env), schema.DiffNormalized())
+	// If there is a state-loader that requires a custom
+	// 'migrate diff' handling, offload it the work.
+	if d, ok := cmdext.States.Differ(flags.desiredURLs); ok {
+		err := d.MigrateDiff(ctx, &cmdext.MigrateDiffOptions{
+			To:      flags.desiredURLs,
+			Name:    name,
+			Indent:  indent,
+			Dir:     dir,
+			Dev:     dev,
+			Options: diffOpts,
+		})
+		return maskNoPlan(cmd, err)
+	}
+	// Get a state reader for the desired state.
+	desired, err := stateReader(ctx, env, &stateReaderConfig{
+		urls:    flags.desiredURLs,
+		dev:     dev,
+		client:  dev,
+		schemas: flags.schemas,
+		vars:    GlobalFlags.Vars,
+	})
+	if err != nil {
+		return err
+	}
+	defer desired.Close()
+	opts := []migrate.PlannerOption{
+		migrate.PlanFormat(f),
+		migrate.PlanWithIndent(indent),
+		migrate.PlanWithDiffOptions(diffOpts...),
+	}
+	if dev.URL.Schema != "" {
+		// Disable tables qualifier in schema-mode.
+		opts = append(opts, migrate.PlanWithSchemaQualifier(flags.qualifier))
+	}
+	// Plan the changes and create a new migration file.
+	pl := migrate.NewPlanner(dev.Driver, dir, opts...)
+	plan, err := func() (*migrate.Plan, error) {
+		if dev.URL.Schema != "" {
+			return pl.PlanSchema(ctx, name, desired.StateReader)
+		}
+		return pl.Plan(ctx, name, desired.StateReader)
+	}()
+	var cerr *migrate.NotCleanError
+	switch {
+	case errors.As(err, &cerr) && dev.URL.Schema == "" && desired.Schema != "":
+		return fmt.Errorf("dev database is not clean (%s). Add a schema to the URL to limit the scope of the connection", cerr.Reason)
+	case err != nil:
+		return maskNoPlan(cmd, err)
+	default:
+		return pl.WritePlan(plan)
+	}
+}
+
 func promptApply(cmd *cobra.Command, flags schemaApplyFlags, diff *diff, client, _ *sqlclient.Client) error {
 	if !flags.dryRun && (flags.autoApprove || promptUser(cmd)) {
 		return applyChanges(cmd.Context(), client, diff.changes, flags.txMode)
@@ -132,12 +263,6 @@ func withTokenContext(ctx context.Context, _ string, _ *cloudapi.Client) (contex
 	return ctx, nil // unimplemented.
 }
 
-// checkDirRebased checks that the local directory is up-to-date with the latest version of the directory.
-// For example, Atlas Cloud or Git.
-func checkDirRebased(context.Context, *cobra.Command, migrate.Dir) error {
-	return nil // unimplemented.
-}
-
 func setEnvs(context.Context, []*Env) {}
 
 // specOptions are the options for the schema spec.
diff --git a/cmd/atlas/internal/cmdapi/migrate.go b/cmd/atlas/internal/cmdapi/migrate.go
index e0a4faf3..54e6beda 100644
--- a/cmd/atlas/internal/cmdapi/migrate.go
+++ b/cmd/atlas/internal/cmdapi/migrate.go
@@ -643,102 +643,6 @@ an HCL, SQL, or ORM schema. See: https://atlasgo.io/versioned/diff`,
 	return cmd
 }
 
-func migrateDiffRun(cmd *cobra.Command, args []string, flags migrateDiffFlags, env *Env) error {
-	ctx := cmd.Context()
-	dev, err := sqlclient.Open(ctx, flags.devURL)
-	if err != nil {
-		return err
-	}
-	defer dev.Close()
-	// Acquire a lock.
-	if l, ok := dev.Driver.(schema.Locker); ok {
-		unlock, err := l.Lock(ctx, "atlas_migrate_diff", flags.lockTimeout)
-		if err != nil {
-			return fmt.Errorf("acquiring database lock: %w", err)
-		}
-		// If unlocking fails notify the user about it.
-		defer func() { cobra.CheckErr(unlock()) }()
-	}
-	// Open the migration directory.
-	u, err := url.Parse(flags.dirURL)
-	if err != nil {
-		return err
-	}
-	dir, err := cmdmigrate.DirURL(ctx, u, false)
-	if err != nil {
-		return err
-	}
-	if err := checkDirRebased(ctx, cmd, dir); err != nil {
-		return err
-	}
-	if flags.edit {
-		dir = &editDir{dir}
-	}
-	var name, indent string
-	if len(args) > 0 {
-		name = args[0]
-	}
-	f, err := cmdmigrate.Formatter(u)
-	if err != nil {
-		return err
-	}
-	if f, indent, err = mayIndent(u, f, flags.format); err != nil {
-		return err
-	}
-	// If there is a state-loader that requires a custom
-	// 'migrate diff' handling, offload it the work.
-	if d, ok := cmdext.States.Differ(flags.desiredURLs); ok {
-		err := d.MigrateDiff(ctx, &cmdext.MigrateDiffOptions{
-			To:      flags.desiredURLs,
-			Name:    name,
-			Indent:  indent,
-			Dir:     dir,
-			Dev:     dev,
-			Options: diffOptions(cmd, env),
-		})
-		return maskNoPlan(cmd, err)
-	}
-	// Get a state reader for the desired state.
-	desired, err := stateReader(ctx, env, &stateReaderConfig{
-		urls:    flags.desiredURLs,
-		dev:     dev,
-		client:  dev,
-		schemas: flags.schemas,
-		vars:    GlobalFlags.Vars,
-	})
-	if err != nil {
-		return err
-	}
-	defer desired.Close()
-	opts := []migrate.PlannerOption{
-		migrate.PlanFormat(f),
-		migrate.PlanWithIndent(indent),
-		migrate.PlanWithDiffOptions(diffOptions(cmd, env)...),
-	}
-	if dev.URL.Schema != "" {
-		// Disable tables qualifier in schema-mode.
-		opts = append(opts, migrate.PlanWithSchemaQualifier(flags.qualifier))
-	}
-	// Plan the changes and create a new migration file.
-	pl := migrate.NewPlanner(dev.Driver, dir, opts...)
-	plan, err := func() (*migrate.Plan, error) {
-		if dev.URL.Schema != "" {
-			return pl.PlanSchema(ctx, name, desired.StateReader)
-		}
-		return pl.Plan(ctx, name, desired.StateReader)
-	}()
-	var cerr *migrate.NotCleanError
-	switch {
-	case errors.As(err, &cerr) && dev.URL.Schema == "" && desired.Schema != "":
-		return fmt.Errorf("dev database is not clean (%s). Add a schema to the URL to limit the scope of the connection", cerr.Reason)
-	case err != nil:
-		return maskNoPlan(cmd, err)
-	default:
-		// Write the plan to a new file.
-		return pl.WritePlan(plan)
-	}
-}
-
 func mayIndent(dir *url.URL, f migrate.Formatter, format string) (migrate.Formatter, string, error) {
 	if format == "" {
 		return f, "", nil
diff --git a/cmd/atlas/internal/cmdapi/schema.go b/cmd/atlas/internal/cmdapi/schema.go
index 74209abc..957d6859 100644
--- a/cmd/atlas/internal/cmdapi/schema.go
+++ b/cmd/atlas/internal/cmdapi/schema.go
@@ -630,9 +630,6 @@ func setSchemaEnvFlags(cmd *cobra.Command, env *Env) error {
 	if err := inputValuesFromEnv(cmd, env); err != nil {
 		return err
 	}
-	if err := maySetFlag(cmd, flagURL, env.URL); err != nil {
-		return err
-	}
 	if err := maySetFlag(cmd, flagDevURL, env.DevURL); err != nil {
 		return err
 	}
@@ -655,11 +652,21 @@ func setSchemaEnvFlags(cmd *cobra.Command, env *Env) error {
 		return err
 	}
 	switch cmd.Name() {
+	case "clean":
+		if err := maySetFlag(cmd, flagURL, env.URL); err != nil {
+			return err
+		}
 	case "inspect":
+		if err := maySetFlag(cmd, flagURL, env.URL); err != nil {
+			return err
+		}
 		if err := maySetFlag(cmd, flagFormat, env.Format.Schema.Inspect); err != nil {
 			return err
 		}
 	case "apply":
+		if err := maySetFlag(cmd, flagURL, env.URL); err != nil {
+			return err
+		}
 		if err := maySetFlag(cmd, flagFormat, env.Format.Schema.Apply); err != nil {
 			return err
 		}
@@ -672,7 +679,12 @@ func setSchemaEnvFlags(cmd *cobra.Command, env *Env) error {
 			return err
 		}
 	case "test":
-		if err := maySetFlag(cmd, flagURL, strings.Join(srcs, ",")); err != nil {
+		// Give the "src" precedence over the "url" argument.
+		if len(srcs) > 0 {
+			if err := maySetFlag(cmd, flagURL, strings.Join(srcs, ",")); err != nil {
+				return err
+			}
+		} else if err := maySetFlag(cmd, flagURL, env.URL); err != nil {
 			return err
 		}
 	}
diff --git a/cmd/atlas/main.go b/cmd/atlas/main.go
index d11357c2..98f32ece 100644
--- a/cmd/atlas/main.go
+++ b/cmd/atlas/main.go
@@ -8,6 +8,7 @@ import (
 	"bytes"
 	"context"
 	"fmt"
+
 	"os"
 	"os/signal"
 	"syscall"
@@ -22,12 +23,13 @@ import (
 	_ "ariga.io/atlas/sql/postgres/postgrescheck"
 	_ "ariga.io/atlas/sql/sqlite"
 	_ "ariga.io/atlas/sql/sqlite/sqlitecheck"
-	"golang.org/x/mod/semver"
 
 	_ "github.com/go-sql-driver/mysql"
 	_ "github.com/lib/pq"
 	_ "github.com/libsql/libsql-client-go/libsql"
+	"github.com/mattn/go-isatty"
 	_ "github.com/mattn/go-sqlite3"
+	"golang.org/x/mod/semver"
 )
 
 func main() {
@@ -47,9 +49,14 @@ func main() {
 		<-stop // will not block if no signal received due to main routine exiting
 		os.Exit(1)
 	}()
-	ctx, done := initialize(extendContext(ctx))
+	ctx, err := extendContext(ctx)
+	if err != nil {
+		fmt.Fprintln(os.Stderr, err)
+		os.Exit(1)
+	}
+	ctx, done := initialize(ctx)
 	update := checkForUpdate(ctx)
-	err := cmdapi.Root.ExecuteContext(ctx)
+	err = cmdapi.Root.ExecuteContext(ctx)
 	if u := update(); u != "" {
 		fmt.Fprintln(os.Stderr, u)
 	}
@@ -67,9 +74,7 @@ const (
 
 func noText() string { return "" }
 
-// checkForUpdate checks for version updates and security advisories for Atlas.
 func checkForUpdate(ctx context.Context) func() string {
-	done := make(chan struct{})
 	version := cmdapi.Version()
 	// Users may skip update checking behavior.
 	if v := os.Getenv(envNoUpdate); v != "" {
@@ -79,20 +84,28 @@ func checkForUpdate(ctx context.Context) func() string {
 	if !semver.IsValid(version) {
 		return noText
 	}
+	endpoint := vercheckEndpoint(ctx)
+	vc := vercheck.New(endpoint)
+	if isatty.IsTerminal(os.Stdout.Fd()) {
+		return bgCheck(ctx, version, vc)
+	}
+	return func() string {
+		msg, _ := runCheck(ctx, vc, version)
+		return msg
+	}
+}
+
+// bgCheck checks for version updates and security advisories for Atlas in the background.
+func bgCheck(ctx context.Context, version string, vc *vercheck.VerChecker) func() string {
+	done := make(chan struct{})
 	var message string
 	go func() {
 		defer close(done)
-		endpoint := vercheckEndpoint(ctx)
-		vc := vercheck.New(endpoint)
-		payload, err := vc.Check(ctx, version)
+		msg, err := runCheck(ctx, vc, version)
 		if err != nil {
 			return
 		}
-		var b bytes.Buffer
-		if err := vercheck.Notify.Execute(&b, payload); err != nil {
-			return
-		}
-		message = b.String()
+		message = msg
 	}()
 	return func() string {
 		select {
@@ -102,3 +115,15 @@ func checkForUpdate(ctx context.Context) func() string {
 		return message
 	}
 }
+
+func runCheck(ctx context.Context, vc *vercheck.VerChecker, version string) (string, error) {
+	payload, err := vc.Check(ctx, version)
+	if err != nil {
+		return "", err
+	}
+	var b bytes.Buffer
+	if err := vercheck.Notify.Execute(&b, payload); err != nil {
+		return "", err
+	}
+	return b.String(), nil
+}
diff --git a/cmd/atlas/main_oss.go b/cmd/atlas/main_oss.go
index db33b0b4..7ad1f543 100644
--- a/cmd/atlas/main_oss.go
+++ b/cmd/atlas/main_oss.go
@@ -10,7 +10,9 @@ import (
 	"context"
 )
 
-func extendContext(ctx context.Context) context.Context { return ctx }
+func extendContext(ctx context.Context) (context.Context, error) {
+	return ctx, nil
+}
 
 func vercheckEndpoint(context.Context) string {
 	return vercheckURL
diff --git a/doc/md/atlas-schema/hcl.mdx b/doc/md/atlas-schema/hcl.mdx
index e25297d5..094849b4 100644
--- a/doc/md/atlas-schema/hcl.mdx
+++ b/doc/md/atlas-schema/hcl.mdx
@@ -155,11 +155,8 @@ table "products" {
 
 ### Partitions
 
-Table partitioning refers to splitting logical large tables into smaller physical ones.
-
-:::note
-Partitions are currently supported only by the PostgreSQL driver. Support for the remaining drivers will be added in future versions.
-:::
+The `partition` option is a PostgreSQL-specific option that allows defining table partitioning. Table partitioning refers
+to splitting logical large tables into smaller physical ones.
 
 ```hcl
 table "logs" {
@@ -170,10 +167,12 @@ table "logs" {
   column "text" {
     type = integer
   }
+  // highlight-start
   partition {
     type = RANGE
     columns = [column.date]
   }
+  // highlight-end
 }
 
 table "metrics" {
@@ -184,6 +183,7 @@ table "metrics" {
   column "y" {
     type = integer
   }
+  // highlight-start
   partition {
     type = RANGE
     by {
@@ -193,33 +193,33 @@ table "metrics" {
       expr = "floor(y)"
     }
   }
+  // highlight-end
 }
 ```
 
-### Storage Engine
+### Row Level Security
 
-The `engine` attribute allows for overriding the default storage engine of the table. Supported by MySQL and MariaDB.
+The `row_security` option is a PostgreSQL-specific option that allows enabling row-level security policies for a table.
 
 ```hcl
 table "users" {
   schema = schema.public
-  // highlight-next-line
-  engine = MyISAM
-}
-
-table "posts" {
-  schema = schema.public
-  // highlight-next-line
-  engine = InnoDB
-}
-
-table "orders" {
-  schema = schema.public
-  // highlight-next-line
-  engine = "MyRocks"
+  column "id" {
+    type = int
+  }
+  // highlight-start
+  row_security {
+    enabled  = true // ENABLE ROW LEVEL SECURITY
+    enforced = true // FORCE ROW LEVEL SECURITY
+  }
+  // highlight-end
 }
 ```
 
+:::note Defining Policies
+To define row-level security policies for a table, refer to the [policy](#row-level-security-policy) example.
+:::
+
 ### Table Qualification
 
 In some cases, an Atlas DDL document may contain multiple tables of the same name. This usually happens
@@ -241,70 +241,101 @@ table "b" "users" {
 }
 ```
 
-### Distribution
+#### Storage Engine
 
-The `distribution` attribute allows for specifying the distribution method of the table.
+The `engine` attribute allows for overriding the default storage engine of the table. Supported by MySQL and MariaDB.
+
+```hcl
+table "users" {
+  schema = schema.public
+  // highlight-next-line
+  engine = MyISAM
+}
 
-:::note
-Distribution is currently supported only by the Redshift driver. Support for the remaining drivers will be added in future versions.
+table "posts" {
+  schema = schema.public
+  // highlight-next-line
+  engine = InnoDB
+}
+
+table "orders" {
+  schema = schema.public
+  // highlight-next-line
+  engine = "MyRocks"
+}
+```
+
+#### System-Versioned Tables
+
+The `system_versioned` attribute allows marking a table as a [system-versioned table](https://mariadb.com/kb/en/system-versioned-tables/).
+Supported by MariaDB.
+
+:::info [Atlas Pro Feature](/features#pro-plan)
+System-Versioned tables are currently available only to [Atlas Pro users](/features#pro-plan). To use this feature, run:
+```
+atlas login
+```
 :::
 
-<Tabs>
-<TabItem label="Redshift" value="redshift">
+```hcl
+table "f" {
+  schema = schema.public
+  column "id" {
+    type = int
+  }
+  // ...
+  //highlight-next-line
+  system_versioned = true
+}
+```
+
+#### Distribution
+
+The `distribution` block is a Redshift-specific option that allows specifying the distribution method of the table.
 
 ```hcl
 table "users" {
   schema = schema.public
-  // highlight-next-line
   column "id" {
     type = int
   }
+  // highlight-start
   distribution {
-    style = KEY // EVEN | ALL | AUTO
+    style = KEY     // EVEN | ALL | AUTO
     key = column.id // only for KEY style
   }
+  // highlight-end
 }
 ```
 
-</TabItem>
-</Tabs>
-
-### Sorting
 
-The `sort` attribute allows for specifying the sorting method of the table.
-
-:::note
-Sorting is currently supported only by the Redshift driver. Support for the remaining drivers will be added in future versions.
-:::
+#### Sorting
 
-<Tabs>
-<TabItem label="Redshift" value="redshift">
+The `sort` block is a Redshift-specific option that allows specifying the sorting method of the table.
 
 ```hcl
 table "users" {
   schema = schema.public
-  // highlight-next-line
   column "id" {
     type = int
   }
+  // highlight-start
   sort {
     style = COMPOUND // INTERLEAVED | COMPOUND | AUTO
     columns = [column.id]
   }
+  // highlight-end
 }
 
 ```
 
-</TabItem>
-</Tabs>
-
 ## View
 
 A `view` is a virtual table in the database, defined by a statement that queries rows from one or more existing
 tables or views.
 
-:::info LOGIN REQUIRED
-Views are currently available to logged-in users only. To use this feature, run:
+:::info [Atlas Pro Feature](/features#pro-plan)
+Views are currently available only to [Atlas Pro users](/features#pro-plan). To use this feature, run:
 ```
 atlas login
 ```
@@ -342,13 +373,89 @@ view "comedies" {
 }
 ```
 
+
+:::info <a class="sticky-anchor" id={"#testing-views"} href={"#testing-views"}>Testing Views</a>
+
+Atlas testing framework allows you to write unit tests for your views. The following example demonstrates how
+to write tests for the `clean_users` view defined above. For more detail, read the [schema testing docs](/testing/schema).
+
+<Tabs>
+<TabItem label="Simple Test" value="simple">
+
+```hcl title="schema.test.hcl"
+test "schema" "view" {
+  # Seeding to test view.
+  exec {
+    sql = "INSERT INTO users (id, name) VALUES (1, 'Alice'), (2, 'Bob'), (3, 'Charlie');"
+  }
+  log {
+    message = "Seeded the database"
+  }
+  # Expected exec to pass.
+  exec {
+    sql = <<SQL
+    SELECT id, name
+    FROM clean_users;
+    SQL
+  }
+  log {
+    message = "Tested the view"
+  }
+  # Validates data.
+  exec {
+    sql = "SELECT id, name FROM clean_users;"
+    format = table
+    output = <<TAB
+ id |  name
+----+---------
+ 1  | Alice
+ 2  | Bob
+ 3  | Charlie
+TAB
+  }
+  log {
+    message = "Table is as expected"
+  }
+}
+```
+
+</TabItem>
+<TabItem label="Table-driven Test" value="table-driven">
+
+```hcl title="schema.test.hcl"
+test "schema" "view" {
+  for_each = [
+    {id = 1, name = "Alice"},
+    {id = 2, name = "Bob"},
+    {id = 3, name = "Charlie"}
+  ]
+  # Seed the `users` table.
+  exec {
+    sql = "INSERT INTO users (id, name) VALUES (1, 'Alice'), (2, 'Bob'), (3, 'Charlie');"
+  }
+  # Query the `clean_users` view.
+  exec {
+    sql = "SELECT id, name FROM clean_users WHERE id IN (1, 2, 3);"
+  }
+  # Check each ID returns the right user.
+  log {
+    message = "Testing ${each.value.id} -> ${each.value.name}"
+  }
+}
+```
+
+</TabItem>
+</Tabs>
+
+:::
+
 ## Materialized View
 
 A `materialized` view is a table-like structure that holds the results of a query. Unlike a regular view, the results of
 a materialized view are stored in the database and can be refreshed periodically to reflect changes in the underlying data.
 
-:::info LOGIN REQUIRED
-Materialized views are currently available to logged-in users only. To use this feature, run:
+:::info [Atlas Pro Feature](/features#pro-plan)
+Materialized views are currently available only to [Atlas Pro users](/features#pro-plan). To use this feature, run:
 ```
 atlas login
 ```
@@ -386,7 +493,7 @@ materialized "mat_view" {
 </TabItem>
 <TabItem label="Clickhouse" value="clickhouse">
 
-When creating materialized views with `TO [db.]table`, 
+When creating materialized views with `TO [db.]table`,
 the view will be created with the same structure as the table or view specified in the `TO` clause.
 ```hcl
 materialized "mat_view" {
@@ -420,7 +527,7 @@ materialized "mat_view" {
 ```
 
 :::info
-Note that modifying the materialized view structure 
+Note that modifying the materialized view structure
 after the initial creation is not supported by Atlas currently.
 :::
 
@@ -642,7 +749,7 @@ primary_key {
 
 :::info
 Note, primary key expressions are supported by ClickHouse.
-::: 
+:::
 
 ```hcl
 primary_key {
@@ -953,8 +1060,8 @@ exclude "excl_speaker_during" {
 
 ## Trigger
 
-:::info LOGIN REQUIRED
-Triggers are currently available to logged-in users only. To use this feature, run:
+:::info [Atlas Pro Feature](/features#pro-plan)
+Triggers are currently available only to [Atlas Pro users](/features#pro-plan). To use this feature, run:
 ```
 atlas login
 ```
@@ -1112,11 +1219,10 @@ trigger "audit_log_trigger" {
 }
 ```
 
-
 ## Event Trigger
 
-:::info LOGIN REQUIRED
-Event Triggers are currently available to logged-in users only. To use this feature, run:
+:::info [Atlas Pro Feature](/features#pro-plan)
+Event Triggers are currently available only to [Atlas Pro users](/features#pro-plan). To use this feature, run:
 ```
 atlas login
 ```
@@ -1142,8 +1248,8 @@ event_trigger "record_table_creation" {
 
 ## Function
 
-:::info LOGIN REQUIRED
-Functions are currently available to logged-in users only. To use this feature, run:
+:::info [Atlas Pro Feature](/features#pro-plan)
+Functions are currently available only to [Atlas Pro users](/features#pro-plan). To use this feature, run:
 ```
 atlas login
 ```
@@ -1341,7 +1447,7 @@ function "fn_return_table" {
 </TabItem>
 </Tabs>
 
-:::info <a class="sticky-anchor" id={"scope"} href={"#testing-functions"}>Testing Functions</a>
+:::info <a class="sticky-anchor" id={"#testing-functions"} href={"#testing-functions"}>Testing Functions</a>
 
 Atlas testing framework allows you to write unit tests for your functions. The following example demonstrates how
 to write tests for the `positive` function defined above. For more detail, read the [schema testing docs](/testing/schema).
@@ -1426,8 +1532,8 @@ function "sum_squares_sfunc" {
 
 ## Procedure
 
-:::info LOGIN REQUIRED
-Procedures are currently available to logged-in users only. To use this feature, run:
+:::info [Atlas Pro Feature](/features#pro-plan)
+Procedures are currently available only to [Atlas Pro users](/features#pro-plan). To use this feature, run:
 ```
 atlas login
 ```
@@ -1594,8 +1700,8 @@ procedure "p8" {
 
 ## Domain
 
-:::info LOGIN REQUIRED
-Domains are currently available to logged-in users only. To use this feature, run:
+:::info [Atlas Pro Feature](/features#pro-plan)
+Domains are currently available only to [Atlas Pro users](/features#pro-plan). To use this feature, run:
 ```
 atlas login
 ```
@@ -1642,8 +1748,8 @@ schema "public" {
 
 ## Composite Type
 
-:::info LOGIN REQUIRED
-Composite types are currently available to logged-in users only. To use this feature, run:
+:::info [Atlas Pro Feature](/features#pro-plan)
+Composite types are currently available only to [Atlas Pro users](/features#pro-plan). To use this feature, run:
 ```
 atlas login
 ```
@@ -1674,10 +1780,58 @@ schema "public" {
 }
 ```
 
+## Policies {#row-level-security-policy}
+
+:::info [Atlas Pro Feature](/features#pro-plan)
+Policies are currently available only to [Atlas Pro users](/features#pro-plan). To use this feature, run:
+```
+atlas login
+```
+:::
+
+The `policy` block allows defining [row-level security policies](https://www.postgresql.org/docs/current/ddl-rowsecurity.html). Supported by PostgreSQL.
+
+```hcl title="schema.pg.hcl"
+policy "sales_rep_access" {
+  on    = table.orders
+  for   = SELECT
+  to    = [PUBLIC]
+  using = "(sales_rep_id = (CURRENT_USER)::integer)"
+}
+
+policy "restrict_sales_rep_updates" {
+  on      = table.orders
+  as      = RESTRICTIVE
+  for     = UPDATE
+  to      = ["custom_role"]
+  check   = "(sales_rep_id = (CURRENT_USER)::integer)"
+  comment = "This is a restrictive policy"
+}
+```
+
+:::note Enabling Row-Level Security
+To enable and force row-level security on a table, refer to the [table row-level security](#row-level-security) example.
+:::
+
+### Computed Policies
+
+To configure the same policy for multiple tables, users can utilize the `for_each` meta-argument. By setting it
+up, a `policy` block will be computed for each item in the provided value. Note that `for_each` accepts either a `map`
+or a `set` of references.
+
+```hcl title="schema.pg.hcl" {2-3}
+policy "tenant_access_policy" {
+  for_each = [table.users, table.orders, table.payments]
+  on       = each.value
+  as       = RESTRICTIVE
+  using    = "tenant_isolation_policy()"
+}
+```
+
 ## Sequence
 
-:::info LOGIN REQUIRED
-Sequences are currently available to logged-in users only. To use this feature, run:
+:::info [Atlas Pro Feature](/features#pro-plan)
+Sequences are currently available only to [Atlas Pro users](/features#pro-plan). To use this feature, run:
 ```
 atlas login
 ```
@@ -1816,8 +1970,8 @@ table "t2" {
 
 ## Extension
 
-:::info LOGIN REQUIRED
-Extensions are currently available to logged-in users only. To use this feature, run:
+:::info [Atlas Pro Feature](/features#pro-plan)
+Extensions are currently available only to [Atlas Pro users](/features#pro-plan). To use this feature, run:
 ```
 atlas login
 ```
diff --git a/doc/md/cloud/features/monitoring.mdx b/doc/md/cloud/features/monitoring.mdx
new file mode 100644
index 00000000..86656c07
--- /dev/null
+++ b/doc/md/cloud/features/monitoring.mdx
@@ -0,0 +1,33 @@
+---
+title: Schema Monitoring
+slug: /cloud/features/schema-monitoring
+---
+
+## Drift Detection
+
+Schema migrations on production databases should ideally be automated within CI/CD pipelines, keeping developers from
+needing root access. However, this setup isn't always the case, leading to **Schema Drift**, where database schemas
+drift from their intended state, causing inconsistencies and issues.
+
+### Schema Drift
+Schema drift is dangerous because it can lead to unexpected behavior in applications, data corruption, and even system outages.
+When the actual database schema deviates from the expected schema, queries may fail, data integrity constraints may be violated,
+and application logic may break. Additionally, schema migrations are planned based on the assumption the target
+database is in a specific state and may fail and cause unknown side effects. This can result in hard-to-debug issues and
+degraded system performance, ultimately affecting the reliability and stability of your application.
+
+#### Detecting Schema Drifts
+Atlas enables to automatically monitor schema drift by periodically checking if your deployed database schemas align with their desired state.
+Since most databases are not openly accessible, Atlas allows the use of Atlas Agents within your database's network.
+These agents register themselves with your Atlas Cloud account and continuously poll for tasks, ensuring your
+database schemas remain consistent and up-to-date.
+
+![drift-detection](https://atlasgo.io/uploads/cloud/images/drift-detection-mockup.png)
+
+You can instruct Atlas to notify you if there is a drift. Atlas supports various channels, such as email, Slack, Workplace
+or by a plain webhook.
+
+![drift-webhook](https://atlasgo.io/uploads/cloud/images/drift-webhook.png)
+
+These notifications are particularly useful because they provide real-time alerts, ensuring that any drifts are promptly addressed.
+
diff --git a/doc/md/cloud/pricing.mdx b/doc/md/cloud/pricing.mdx
index 979a285d..76a459a1 100644
--- a/doc/md/cloud/pricing.mdx
+++ b/doc/md/cloud/pricing.mdx
@@ -4,37 +4,35 @@ id: pricing
 slug: /cloud/pricing
 ---
 
-Atlas Cloud offers three pricing plans:
+Atlas Cloud offers two pricing plans:
 
-1. **Free Plan** - for individuals and small teams that want to unlock the full potential of Atlas. This plan will remain
-free forever and provides full access to all the capabilities of Atlas as a CLI as well as access to enough Atlas Cloud
-quota to successfully manage a single project. Support is provided via public [community support](https://atlasgo.io/support)
-channels.
+1. **Pro Plan** - for individuals and small teams that want to unlock the full potential of Atlas. This plan provides 3
+free seats per company and full access to all the capabilities of Atlas as a CLI. The free version of the plan offers access to enough Atlas Cloud
+quota to successfully manage a single project.
 
-2. **Business Plan** - for teams professionally using Atlas beyond a single project. This plan has the same features and
-capabilities as the Free Plan but allows teams to purchase additional quotas if required. In addition, teams subscribed
-to this plan will get access to priority email support and in-app support via Intercom.
+* For those who need additional seats or quotas (projects, databases, runs, etc.), it is possible to pay as you go. View
+the pricing breakdown [here](https://atlasgo.cloud/pricing).
 
-3. **Enterprise Plan** - for larger organizations looking to solve schema management at scale. This plan includes a
+2. **Enterprise Plan** - for larger organizations looking to solve schema management at scale. This plan includes a
 dedicated support channel, solution engineering, and other features required for adoption by enterprises.
 
 For a full list of the features in each plan, click [here](https://atlasgo.cloud/pricing).
 
 ## Upgrading your Pricing Plan
 
-### Free to Business
-To upgrade your plan from Free to Business, log in to your [Atlas Cloud](https://auth.atlasgo.cloud/login) account and click on the 'Settings' tab.
+### Adding Quota to Pro
+To purchase extra seats, databases or projects, log in to your [Atlas Cloud](https://auth.atlasgo.cloud/login) account and click on the 'Settings' tab.
 
-![](https://atlasgo.io/uploads/cloud/images/pricing-settings.png)
+![](https://atlasgo.io/uploads/cloud/images/pro-pricing.png)
 
 Under the 'Plan' tab, click the 'Upgrade' button. Note, to access the Plan tab you must be an admin of the organization.
 
-![](https://atlasgo.io/uploads/cloud/images/pricing-modal.png)
+![](https://atlasgo.io/uploads/cloud/images/pro-quotas.png)
 
 Add the desired resources and proceed to checkout.
 
 That's it!
 
-### Business to Enterprise
-To upgrade your plan from Business to Enterprise, send an email to <hello@ariga.io>.
+### Pro to Enterprise
+To upgrade your plan from Pro to Enterprise, send an email to <hello@ariga.io>.
 
diff --git a/doc/md/components/login-required.mdx b/doc/md/components/login-required.mdx
index 43890e83..5c33425c 100644
--- a/doc/md/components/login-required.mdx
+++ b/doc/md/components/login-required.mdx
@@ -1,3 +1,3 @@
 <span className="badge login badge--info" style={{fontSize: "12px"}}>
-    <a style={{color: "white", textDecoration: "none"}} href="/cloud/getting-started">Login Required</a>
+    <a style={{color: "white", textDecoration: "none"}} href="/features#pro-plan">Atlas Pro</a>
 </span>
\ No newline at end of file
diff --git a/doc/md/concepts/dev.mdx b/doc/md/concepts/dev.mdx
index 0f5993cb..f78d6a0a 100644
--- a/doc/md/concepts/dev.mdx
+++ b/doc/md/concepts/dev.mdx
@@ -181,9 +181,9 @@ we recommend using a real Redshift cluster with separate database.
 
 ## Baseline Schema
 
-:::info LOGIN REQUIRED
+:::info [Atlas Pro Feature](/features#pro-plan)
 
-The `docker` block is available for logged-in users only. To use this feature, run:
+The `docker` block is available for [Atlas Pro users](/features#pro-plan). To use this feature, run:
 
 ```
 atlas login
diff --git a/doc/md/declarative/apply.mdx b/doc/md/declarative/apply.mdx
index 1e823114..c4deb7dd 100644
--- a/doc/md/declarative/apply.mdx
+++ b/doc/md/declarative/apply.mdx
@@ -20,7 +20,7 @@ By default, running `atlas schema apply` applies only **schemas**, **tables**, a
 constraints such as **foreign keys** and **checks**.
 
 **Views**, **materialized views**, **functions**, **procedures**, **triggers**, **sequences**, **domains**, **extensions**,
-and additional beta features are available to logged-in users only. To include these resources in schema migrations, use
+and additional beta features are available to [Atlas Pro users](/features#pro-plan). To include these resources in schema migrations, use
 the following command:
 
 ```shell
diff --git a/doc/md/declarative/diff.mdx b/doc/md/declarative/diff.mdx
index 0909a2e9..b7ca0824 100644
--- a/doc/md/declarative/diff.mdx
+++ b/doc/md/declarative/diff.mdx
@@ -24,7 +24,7 @@ By default, running `atlas schema diff` diffs only **schemas**, **tables**, and
 constraints such as **foreign keys** and **checks**.
 
 **Views**, **materialized views**, **functions**, **procedures**, **triggers**, **sequences**, **domains**, **extensions**,
-and additional features are available to logged-in users only. To include these resources in the schema diffing, use the
+and additional features are available to [Atlas Pro users](/features#pro-plan). To include these resources in the schema diffing, use the
 following command:
 
 ```shell
diff --git a/doc/md/declarative/inspect.mdx b/doc/md/declarative/inspect.mdx
index a204c521..e3de7dc8 100644
--- a/doc/md/declarative/inspect.mdx
+++ b/doc/md/declarative/inspect.mdx
@@ -22,7 +22,7 @@ By default, running `atlas schema inspect` inspects only **schemas**, **tables**
 constraints such as **foreign keys** and **checks**.
 
 **Views**, **materialized views**, **functions**, **procedures**, **triggers**, **sequences**, **domains**, **extensions**,
-and additional features are available to logged-in users only. To include these resources in the inspection, use the
+and additional features are available to [Atlas Pro users](/features#pro-plan). To include these resources in the inspection, use the
  following command:
 
 ```shell
diff --git a/doc/md/features.mdx b/doc/md/features.mdx
index 3ed4828e..482f283d 100644
--- a/doc/md/features.mdx
+++ b/doc/md/features.mdx
@@ -12,14 +12,15 @@ diffing, migration planning and execution, and more. For the basic and common us
 Atlas is more than enough.
 
 In addition, Atlas also offers a number of advanced features that are only available in proprietary versions. The
-vast majority of these features are available in the Free Plan of Atlas, which you can use for free by creating
-an Atlas account. The Free Plan has some limitations, but those pertain mostly to the number of projects you can
-use in [Atlas Cloud](https://atlasgo.cloud).
+vast majority of these features are available in the Pro Plan of Atlas, which you can use for free by creating
+an Atlas account. 
 
-### Free Plan
+### Pro Plan
 
-The Free Plan is available to all Atlas users. Create your Atlas Free Plan account by running the following command
-and following the instructions on the screen:
+Users can upgrade to the Pro Plan for **free**, granting them up to 3 seats **per company**. After that limit, users can
+purchase additional seats. To learn more about our plans and pricing, visit the [Atlas Pricing Page](/pricing).
+
+Create your Atlas Pro account by running the command below and following the instructions on the screen:
 
 ```bash
 atlas login
@@ -38,15 +39,19 @@ This document compares the CLI features available to users who are logged in to
 | Versioned Migrations   | Open           |
 | Declarative Migrations | Open           |
 | Migration Linting      | Open           |
-| Migration Checkpoints  | Requires Login |
-| Schema Visualization   | Requires Login |
-| Interactive Migrations | Requires Login |
+| Pre-migration checks   | Pro            |
+| Migration Checkpoints  | Pro            |
+| Schema Visualization   | Pro            |
+| Interactive Migrations | Pro            |
+| Testing Framework      | Pro            |
 
-Schema Visualization as well as Interactive Migrations are features that require connectivity to the cloud and hence are not available in the open-source version of Atlas.
+Schema Visualization as well as Interactive Migrations are features that require connectivity to the cloud and hence
+are not available in the open-source version of Atlas.
 
 ### Database Support
 
-All common open-source RDBMS are supported in all versions of Atlas. In addition, Atlas also supports SQL Server and ClickHouse in the Free Plan of Atlas Cloud.
+All common open-source RDBMS are supported in all versions of Atlas. In addition, Atlas also supports SQL Server, ClickHouse
+and Redshift in the Pro Plan.
 
 | Driver     | Availability          |
 |------------|-----------------------|
@@ -55,29 +60,32 @@ All common open-source RDBMS are supported in all versions of Atlas. In addition
 | MariaDB    | Open                  |
 | SQLite     | Open                  |
 | LibSQL     | Open                  |
-| SQL Server | Requires Login        |
-| ClickHouse | Requires Login (Beta) |
+| SQL Server | Pro                   |
+| ClickHouse | Pro                   |
+| Redshift   | Pro                   |
 
 ### Database Features
 
-Most common database features are supported in all versions of Atlas. More advanced features are available by running `atlas login`.
-
-| Feature                    | Availability   |
-|----------------------------|----------------|
-| Tables and Columns         | Open           |
-| Indexes                    | Open           |
-| Foreign Keys               | Open           |
-| Constraints                | Open           |
-| Partitions                 | Open           |
-| Views                      | Requires Login |
-| Materialized Views         | Requires Login |
-| Stored Procedures          | Requires Login |
-| Functions                  | Requires Login |
-| Triggers                   | Requires Login |
-| Sequences (Postgres)       | Requires Login |
-| Composite Types (Postgres) | Requires Login |
-| Domain Types (Postgres)    | Requires Login |
-| Extensions (Postgres)      | Requires Login |
+Most common database features are supported in all versions of Atlas. More advanced features are available
+to users of the Pro Plan.
+
+| Feature                       | Availability   |
+|-------------------------------|----------------|
+| Tables and Columns            | Open           |
+| Indexes                       | Open           |
+| Foreign Keys                  | Open           |
+| Constraints                   | Open           |
+| Partitions                    | Open           |
+| Views                         | Pro            |
+| Materialized Views            | Pro            |
+| Stored Procedures             | Pro            |
+| Functions                     | Pro            |
+| Triggers                      | Pro            |
+| Sequences (Postgres)          | Pro            |
+| Composite Types (Postgres)    | Pro            |
+| Domain Types (Postgres)       | Pro            |
+| Extensions (Postgres)         | Pro            |
+| Row-level Security (Postgres) | Pro            |
 
 ### Data Sources
 
@@ -94,5 +102,4 @@ Most common database features are supported in all versions of Atlas. More advan
 | Template Dir       | Open           |
 | AWS RDS Token      | Open           |
 | GCP CloudSQL Token | Open           |
-| Composite Schema   | Requires Login |
-
+| Composite Schema   | Pro            |
\ No newline at end of file
diff --git a/doc/md/guides/docker.mdx b/doc/md/guides/docker.mdx
new file mode 100644
index 00000000..50e05af7
--- /dev/null
+++ b/doc/md/guides/docker.mdx
@@ -0,0 +1,109 @@
+---
+title: Running Atlas in Docker
+id: atlas-in-docker
+slug: atlas-in-docker
+---
+Atlas ships as a set of official [Docker Images](https://hub.docker.com/r/arigaio/atlas) for you to use.
+
+To run Atlas in Docker, execute:
+
+```shell
+docker run --rm -it arigaio/atlas:latest-alpine
+```
+
+Depending on your use case, you may want to use a different image type:
+
+| Base Image | Image Tags                    | Purpose                                        |
+|------------|-------------------------------|------------------------------------------------|
+| Distroless | `latest`, `latest-distroless` | Bare bone image containing only Atlas          |
+| Alpine     | `latest-alpine`               | Alpine based image, with basic shell (/bin/sh) |
+
+## Common Issues
+
+### `Use 'atlas login' to access this feature`
+
+Atlas is an open-core project, with some features available only to signed-in users.  To use these features, you
+must sign in to Atlas.  To sign in:
+
+1. Run:
+
+  ```shell
+  docker run --rm -it \
+ //highlight-next-line
+    -v ~/.atlas:/root/.atlas \
+    arigaio/atlas:latest login
+  ```
+
+2. Atlas will provide you with a URL to visit in your browser:
+
+  ```
+  Please visit:
+
+  https://auth.atlasgo.cloud/login?cli=ade66529-e6c0-4c56-8311-e23d0efe9ee9&port=33281
+
+  Follow the instructions on screen. (Hit <ENTER> to manually provide the code.)
+  ```
+
+3. Visit the URL in your browser and follow the on-screen instructions.
+
+4. Copy the code provided by Atlas Cloud:
+   ![](https://atlasgo.io/uploads/docker-guide/copy-this-code.png)
+
+5. Paste the code back into the terminal where you ran `atlas login` and hit `<ENTER>`:
+   ```
+   Please enter the auth code:
+   ```
+
+6. Atlas will verify your code and provide you with a success message:
+   ```
+   You are now connected to acme-corp-1337-ltd on Atlas Cloud.
+   ```
+
+7. You can now use Atlas features that require authentication. Use the `-v ~/.atlas:/root/.atlas` flag to persist your
+   login credentials across Docker runs. For example:
+
+   ```shell
+    docker run --rm -it \
+   //highlight-next-line
+      -v ~/.atlas:/root/.atlas \
+      arigaio/atlas:latest-alpine schema inspect --url "<url>"
+    ```
+
+### `"docker": executable file not found in $PATH`
+
+Atlas heavily relies on the presence of a [Dev Database](/concepts/dev-database) for various calculations
+and schema normalization.  To use a Dev Database, users provide Atlas with the URL to connect to an empty
+database of the type they wish to operate on.
+
+To streamline work with Dev Databases, Atlas provides a convenience driver named `docker://`, in which Atlas
+depends on the Docker CLI command `docker` to be present in the runtime environment.  Running Docker-in-Docker
+is a notoriously nuanced topic and so we do not ship `docker` in the distributed Atlas images.
+
+For this reason, Atlas users who wish to run Atlas in Docker, cannot, by default use the `docker://` driver.
+
+#### Workaround: Spin up a local database container and use it
+
+A common workaround is to spin up a local, empty database container and connect to it.
+
+1. Create a Docker Network to establish network connectivity between your local DB and Atlas:
+  ```
+  docker network create db-network
+  ```
+2. Run the database:
+  ```
+  docker run --name pg-dev-db --network db-network -e POSTGRES_PASSWORD=mysecretpassword -d postgres:16
+  ```
+3. Use the new dev db:
+  ```
+  docker run --rm --network db-network \
+    -v $PWD:/data \
+    arigaio/atlas migrate diff \
+    --to file:///data/schema.hcl \
+    --dir file:///data/migrations \
+    --dev-url "postgres://postgres:mysecretpassword@pg-dev:5432/postgres?sslmode=disable"
+  ```
+  Note a few things about this command:
+  * We use the `--network` flag to use the network we created for our dev database on step 1.
+  * We mount our local dir as `/data`
+  * We use the URL for our dev database as the `--dev-url` flag, note that the hostname `pg-dev` was specified in step
+    2 as the container name.
diff --git a/doc/md/guides/guides.mdx b/doc/md/guides/guides.mdx
index 34f08c29..f007b2bc 100644
--- a/doc/md/guides/guides.mdx
+++ b/doc/md/guides/guides.mdx
@@ -321,7 +321,7 @@ import { Card, CardImage } from "../components/card";
   />
 </div>
 
-## Tests
+## Testing
 
 <div className="row">
   <Card
@@ -336,4 +336,10 @@ import { Card, CardImage } from "../components/card";
     url="/guides/testing/github-actions"
     image={CardImage.Testing}
   />
+  <Card
+      name="Testing Views"
+      description="This guide is for writing and executing tests for database views in."
+      url="/guides/testing/views"
+      image={CardImage.Testing}
+  />
 </div>
diff --git a/doc/md/guides/orms/gorm.md b/doc/md/guides/orms/gorm.md
index a8bd3528..58e1525b 100644
--- a/doc/md/guides/orms/gorm.md
+++ b/doc/md/guides/orms/gorm.md
@@ -300,7 +300,7 @@ From a querying perspective, views and tables are identical. For this reason, GO
 However, defining and managing views has previously had [partial support](https://github.com/go-gorm/gorm/issues/4966).
 The Atlas GORM Provider provides an API that allows you to define database views in the form of GORM models and with the help of [Atlas](https://atlasgo.io/getting-started), migration files can be automatically generated for them.
 
-> The view feature is only available for logged-in users; run `atlas login` if you haven't already.
+> The view feature is only available for [Atlas Pro users](/features#pro-plan); run `atlas login` if you haven't already.
 
 To define a Go struct as a database `VIEW`, implement the [`ViewDefiner`](https://pkg.go.dev/ariga.io/atlas-provider-gorm/gormschema#ViewDefiner) interface. 
 The `ViewDef()` method receives the dialect argument to determine the SQL dialect to generate the view. It is helpful for generating the view definition for different SQL dialects. The `gormschema` package provide two styles for defining a view's base query:
diff --git a/doc/md/home/docs.mdx b/doc/md/home/docs.mdx
index 03c7b004..3bdc51d1 100644
--- a/doc/md/home/docs.mdx
+++ b/doc/md/home/docs.mdx
@@ -26,7 +26,7 @@ Atlas is a language-independent tool for managing and migrating database schemas
 
 <QuickStart />
 
-### Installation <div className="order-3 justify-self-end col-end-3 lg:col-end-12"/>
+### Installation<div className="order-3 justify-self-end col-end-3 lg:col-end-12"/>
 
 <Tabs
     defaultValue="apple-intel-plain+linux"
diff --git a/doc/md/integrations/circleci-orbs.mdx b/doc/md/integrations/circleci-orbs.mdx
index f0b63dc6..d73c7115 100644
--- a/doc/md/integrations/circleci-orbs.mdx
+++ b/doc/md/integrations/circleci-orbs.mdx
@@ -19,6 +19,8 @@ Atlas provides a number of Orbs to help you integrate Atlas into your CircleCI w
 | [atlas-orb/migrate_lint](https://github.com/ariga/atlas-orb/blob/master/src/commands/migrate_lint.yml)        | Lint migration changes with Atlas (Required `atlas login`)  |
 | [atlas-orb/migrate_apply](https://github.com/ariga/atlas-orb/blob/master/src/commands/migrate_apply.yml)   | Apply migrations to a database |
 | [atlas-orb/migrate_down](https://github.com/ariga/atlas-orb/blob/master/src/commands/migrate_down.yml)   | Revert migrations to a database. |
+| [atlas-orb/migrate_test](https://github.com/ariga/atlas-orb/blob/master/src/commands/migrate_test.yml)   | Test migrations to a database. |
+| [atlas-orb/schema_test](https://github.com/ariga/atlas-orb/blob/master/src/commands/schema_test.yml)   | Test schema to a database. |
 
 ## `atlas-orb/setup`
 
@@ -1027,4 +1029,483 @@ Learn more about [Atlas configuration files](https://atlasgo.io/atlas-schema/pro
 * `wait_timeout` -  Time after which no other retry attempt is made and the action exits. If not set, only one attempt is made.
 * `wait-interval` - Time in seconds between different migrate down attempts, useful when waiting for plan approval, defaults to 1s.
 * `vars` - Extra variables to pass to the Atlas configuration file. For example, `key=value key2=values`.
+* `working_directory` - The working directory to run from. Defaults to project root.
+
+## `atlas-orb/migrate_test`
+
+Test migrations on a database.
+
+### Usage
+
+<Tabs
+defaultValue="mysql"
+values={[
+    {label: 'MySQL', value: 'mysql'},
+    {label: 'MariaDB', value: 'maria'},
+    {label: 'PostgreSQL', value: 'postgres'},
+    {label: 'SQLite', value: 'sqlite'},
+    {label: 'SQL Server', value: 'sqlserver'},
+    {label: 'Clickhouse', value: 'clickhouse'},
+]}>
+<TabItem value="mysql">
+
+```yaml
+version: 2.1
+orbs:
+  atlas-orb: ariga/atlas-orb@0.0.6
+jobs:
+  push-dir:
+    docker:
+      - image: cimg/base:current
+      - image: cimg/mysql:8
+        environment:
+          MYSQL_DATABASE: dev
+          MYSQL_ROOT_PASSWORD: pass
+    environment:
+      # Define those envs in CircleCI context to keep them secure
+      ATLAS_TOKEN: "your-atlas-token"
+    steps:
+      - checkout
+      - run:
+          name: Wait for MySQL
+          command: dockerize -wait tcp://127.0.0.1:3306 -timeout 60s
+      - atlas-orb/setup:
+          version: "latest"
+      - atlas-orb/migrate_test:
+          dir: atlas://my-project
+          dev_url: "mysql://root:pass@localhost:3306/dev"
+          run: "example"
+workflows:
+  use-atlas:
+    jobs:
+      - push-dir
+```
+
+</TabItem>
+<TabItem value="maria">
+
+```yaml
+version: 2.1
+orbs:
+  atlas-orb: ariga/atlas-orb@0.0.6
+jobs:
+  push-dir:
+    docker:
+      - image: cimg/base:current
+      - image: cimg/mariadb:10.6
+        environment:
+          MYSQL_DATABASE: dev
+          MYSQL_ROOT_PASSWORD: pass
+    environment:
+      # Define those envs in CircleCI context to keep them secure
+      ATLAS_TOKEN: "your-atlas-token"
+    steps:
+      - checkout
+      - run:
+          name: Wait for MariaDB
+          command: dockerize -wait tcp://127.0.0.1:3306 -timeout 60s
+      - atlas-orb/setup:
+          version: "latest"
+      - atlas-orb/migrate_test:
+          dir: atlas://my-project
+          dev_url: "maria://root:pass@localhost:3306/dev"
+          run: "example"
+workflows:
+  use-atlas:
+    jobs:
+      - push-dir
+```
+
+</TabItem>
+<TabItem value="postgres">
+
+```yaml
+version: 2.1
+orbs:
+  atlas-orb: ariga/atlas-orb@0.0.6
+jobs:
+  push-dir:
+    docker:
+      - image: cimg/base:current
+      - image: cimg/postgres:15.0
+        environment:
+          POSTGRES_DB: dev
+          POSTGRES_PASSWORD: pass
+    environment:
+      # Define those envs in CircleCI context to keep them secure
+      ATLAS_TOKEN: "your-atlas-token"
+    steps:
+      - checkout
+      - run:
+          name: Wait for Postgres
+          command: dockerize -wait tcp://127.0.0.1:5432 -timeout 60s
+      - atlas-orb/setup:
+          version: "latest"
+      - atlas-orb/migrate_test:
+          dir: atlas://my-project
+          dev_url: "postgres://postgres:pass@localhost:5432/dev"
+          run: "example"
+workflows:
+  use-atlas:
+    jobs:
+      - push-dir
+```
+
+</TabItem>
+<TabItem value="sqlite">
+
+```yaml
+version: 2.1
+orbs:
+  atlas-orb: ariga/atlas-orb@0.0.6
+jobs:
+  push-dir:
+    docker:
+      - image: cimg/base:current
+    environment:
+      ATLAS_TOKEN: "your-atlas-token"
+    steps:
+      - checkout
+      - atlas-orb/setup:
+          version: "latest"
+      - atlas-orb/migrate_test:
+          dir: atlas://my-project
+          dev_url: "sqlite://file2?mode=memory&_fk=1"
+          run: "example"
+workflows:
+  use-atlas:
+    jobs:
+      - push-dir
+```
+</TabItem>
+<TabItem value="sqlserver">
+
+```yaml
+version: 2.1
+orbs:
+  atlas-orb: ariga/atlas-orb@0.0.6
+jobs:
+  push-dir:
+    docker:
+      - image: cimg/base:current
+      - image: mcr.microsoft.com/mssql/server:2022-latest
+        environment:
+          ACCEPT_EULA: Y
+          MSSQL_PID: Developer
+          MSSQL_SA_PASSWORD: P@ssw0rd0995
+    environment:
+      # Define those envs in CircleCI context to keep them secure
+      ATLAS_TOKEN: "your-atlas-token"
+    steps:
+      - checkout
+      - run:
+          name: Wait for SQL Server
+          command: dockerize -wait tcp://127.0.0.1:1433 -timeout 60s
+      - atlas-orb/setup:
+          version: "latest"
+      - atlas-orb/migrate_test:
+          dir: atlas://my-project
+          dev_url: "sqlserver://sa:P@ssw0rd0995@localhost:1433?database=dev"
+          run: "example"
+workflows:
+  use-atlas:
+    jobs:
+      - push-dir
+```
+
+</TabItem>
+<TabItem value="clickhouse">
+
+```yaml
+version: 2.1
+orbs:
+  atlas-orb: ariga/atlas-orb@0.0.6
+jobs:
+  push-dir:
+    docker:
+      - image: cimg/base:current
+      - image: clickhouse/clickhouse-server:23.10
+        environment:
+          CLICKHOUSE_DB: test
+          CLICKHOUSE_DEFAULT_ACCESS_MANAGEMENT: 1
+          CLICKHOUSE_PASSWORD: pass
+          CLICKHOUSE_USER: root
+    environment:
+      # Define those envs in CircleCI context to keep them secure
+      ATLAS_TOKEN: "your-atlas-token"
+    steps:
+      - checkout
+      - run:
+          name: Wait for Clickhouse
+          command: dockerize -wait tcp://127.0.0.1:9000 -timeout 60s
+      - atlas-orb/setup:
+          version: "latest"
+      - atlas-orb/migrate_test:
+          dir: atlas://my-project
+          dev_url: "clickhouse://root:pass@localhost:9000/dev"
+          run: "example"
+workflows:
+  use-atlas:
+    jobs:
+      - push-dir
+```
+
+</TabItem>
+</Tabs>
+
+### Inputs
+
+All inputs are optional as they may be specified in the Atlas configuration file.
+
+* `dev_url` - The URL of the dev-database to use for analysis. For example: `mysql://root:pass@localhost:3306/dev`.
+  Read more about [dev-databases](https://atlasgo.io/concepts/dev-database).
+* `dir` - The URL of the migration directory to apply.  For example: `atlas://dir-name` for cloud
+   based directories or `file://migrations` for local ones.
+* `run` - Filter tests to run by regexp. For example, `^test_.*` will only run tests that start with `test_`.
+  Default is to run all tests.
+* `config` - The URL of the Atlas configuration file.  By default, Atlas will look for a file
+  named `atlas.hcl` in the current directory. For example, `file://config/atlas.hcl`.
+  Learn more about [Atlas configuration files](https://atlasgo.io/atlas-schema/projects). 
+* `env` - The environment to use from the Atlas configuration file.  For example, `dev`.
+* `vars` - Stringify JSON object containing variables to be used inside the Atlas configuration file.
+   For example: `'{"var1": "value1", "var2": "value2"}'`.
+* `working_directory` - The working directory to run from. Defaults to project root.
+
+## `atlas-orb/schema_test`
+
+Test schema on a database.
+
+### Usage
+
+<Tabs
+defaultValue="mysql"
+values={[
+    {label: 'MySQL', value: 'mysql'},
+    {label: 'MariaDB', value: 'maria'},
+    {label: 'PostgreSQL', value: 'postgres'},
+    {label: 'SQLite', value: 'sqlite'},
+    {label: 'SQL Server', value: 'sqlserver'},
+    {label: 'Clickhouse', value: 'clickhouse'},
+]}>
+<TabItem value="mysql">
+
+```yaml
+version: 2.1
+orbs:
+  atlas-orb: ariga/atlas-orb@0.0.6
+jobs:
+  push-dir:
+    docker:
+      - image: cimg/base:current
+      - image: cimg/mysql:8
+        environment:
+          MYSQL_DATABASE: dev
+          MYSQL_ROOT_PASSWORD: pass
+    environment:
+      # Define those envs in CircleCI context to keep them secure
+      ATLAS_TOKEN: "your-atlas-token"
+    steps:
+      - checkout
+      - run:
+          name: Wait for MySQL
+          command: dockerize -wait tcp://127.0.0.1:3306 -timeout 60s
+      - atlas-orb/setup:
+          version: "latest"
+      - atlas-orb/schema_test:
+          url: file://schema.hcl
+          dev_url: "mysql://root:pass@localhost:3306/dev"
+          run: "example"
+workflows:
+  use-atlas:
+    jobs:
+      - push-dir
+```
+
+</TabItem>
+<TabItem value="maria">
+
+```yaml
+version: 2.1
+orbs:
+  atlas-orb: ariga/atlas-orb@0.0.6
+jobs:
+  push-dir:
+    docker:
+      - image: cimg/base:current
+      - image: cimg/mariadb:10.6
+        environment:
+          MYSQL_DATABASE: dev
+          MYSQL_ROOT_PASSWORD: pass
+    environment:
+      # Define those envs in CircleCI context to keep them secure
+      ATLAS_TOKEN: "your-atlas-token"
+    steps:
+      - checkout
+      - run:
+          name: Wait for MariaDB
+          command: dockerize -wait tcp://127.0.0.1:3306 -timeout 60s
+      - atlas-orb/setup:
+          version: "latest"
+      - atlas-orb/schema_test:
+          url: file://schema.hcl
+          dev_url: "maria://root:pass@localhost:3306/dev"
+          run: "example"
+workflows:
+  use-atlas:
+    jobs:
+      - push-dir
+```
+
+</TabItem>
+<TabItem value="postgres">
+
+```yaml
+version: 2.1
+orbs:
+  atlas-orb: ariga/atlas-orb@0.0.6
+jobs:
+  push-dir:
+    docker:
+      - image: cimg/base:current
+      - image: cimg/postgres:15.0
+        environment:
+          POSTGRES_DB: dev
+          POSTGRES_PASSWORD: pass
+    environment:
+      # Define those envs in CircleCI context to keep them secure
+      ATLAS_TOKEN: "your-atlas-token"
+    steps:
+      - checkout
+      - run:
+          name: Wait for Postgres
+          command: dockerize -wait tcp://127.0.0.1:5432 -timeout 60s
+      - atlas-orb/setup:
+          version: "latest"
+      - atlas-orb/schema_test:
+          url: file://schema.hcl
+          dev_url: "postgres://postgres:pass@localhost:5432/dev"
+          run: "example"
+workflows:
+  use-atlas:
+    jobs:
+      - push-dir
+```
+
+</TabItem>
+<TabItem value="sqlite">
+
+```yaml
+version: 2.1
+orbs:
+  atlas-orb: ariga/atlas-orb@0.0.6
+jobs:
+  push-dir:
+    docker:
+      - image: cimg/base:current
+    environment:
+      ATLAS_TOKEN: "your-atlas-token"
+    steps:
+      - checkout
+      - atlas-orb/setup:
+          version: "latest"
+      - atlas-orb/schema_test:
+          url: file://schema.hcl
+          dev_url: "sqlite://file2?mode=memory&_fk=1"
+          run: "example"
+workflows:
+  use-atlas:
+    jobs:
+      - push-dir
+```
+</TabItem>
+<TabItem value="sqlserver">
+
+```yaml
+version: 2.1
+orbs:
+  atlas-orb: ariga/atlas-orb@0.0.6
+jobs:
+  push-dir:
+    docker:
+      - image: cimg/base:current
+      - image: mcr.microsoft.com/mssql/server:2022-latest
+        environment:
+          ACCEPT_EULA: Y
+          MSSQL_PID: Developer
+          MSSQL_SA_PASSWORD: P@ssw0rd0995
+    environment:
+      # Define those envs in CircleCI context to keep them secure
+      ATLAS_TOKEN: "your-atlas-token"
+    steps:
+      - checkout
+      - run:
+          name: Wait for SQL Server
+          command: dockerize -wait tcp://127.0.0.1:1433 -timeout 60s
+      - atlas-orb/setup:
+          version: "latest"
+      - atlas-orb/schema_test:
+          url: file://schema.hcl
+          dev_url: "sqlserver://sa:P@ssw0rd0995@localhost:1433?database=dev"
+          run: "example"
+workflows:
+  use-atlas:
+    jobs:
+      - push-dir
+```
+
+</TabItem>
+<TabItem value="clickhouse">
+
+```yaml
+version: 2.1
+orbs:
+  atlas-orb: ariga/atlas-orb@0.0.6
+jobs:
+  push-dir:
+    docker:
+      - image: cimg/base:current
+      - image: clickhouse/clickhouse-server:23.10
+        environment:
+          CLICKHOUSE_DB: test
+          CLICKHOUSE_DEFAULT_ACCESS_MANAGEMENT: 1
+          CLICKHOUSE_PASSWORD: pass
+          CLICKHOUSE_USER: root
+    environment:
+      # Define those envs in CircleCI context to keep them secure
+      ATLAS_TOKEN: "your-atlas-token"
+    steps:
+      - checkout
+      - run:
+          name: Wait for Clickhouse
+          command: dockerize -wait tcp://127.0.0.1:9000 -timeout 60s
+      - atlas-orb/setup:
+          version: "latest"
+      - atlas-orb/schema_test:
+          url: file://schema.hcl
+          dev_url: "clickhouse://root:pass@localhost:9000/dev"
+          run: "example"
+workflows:
+  use-atlas:
+    jobs:
+      - push-dir
+```
+
+</TabItem>
+</Tabs>
+
+### Inputs
+
+All inputs are optional as they may be specified in the Atlas configuration file.
+
+* `dev_url` - The URL of the dev-database to use for analysis. For example: `mysql://root:pass@localhost:3306/dev`.
+  Read more about [dev-databases](https://atlasgo.io/concepts/dev-database).
+* `url` - The desired schema URL(s) to test. For Example: `file://schema.hcl`
+* `run` - Filter tests to run by regexp. For example, `^test_.*` will only run tests that start with `test_`.
+  Default is to run all tests.
+* `config` - The URL of the Atlas configuration file.  By default, Atlas will look for a file
+  named `atlas.hcl` in the current directory. For example, `file://config/atlas.hcl`.
+  Learn more about [Atlas configuration files](https://atlasgo.io/atlas-schema/projects). 
+* `env` - The environment to use from the Atlas configuration file.  For example, `dev`.
+* `vars` - Stringify JSON object containing variables to be used inside the Atlas configuration file.
+   For example: `'{"var1": "value1", "var2": "value2"}'`.
 * `working_directory` - The working directory to run from. Defaults to project root.
\ No newline at end of file
diff --git a/doc/md/integrations/github-actions.mdx b/doc/md/integrations/github-actions.mdx
index d7c844ca..23c9e9d4 100644
--- a/doc/md/integrations/github-actions.mdx
+++ b/doc/md/integrations/github-actions.mdx
@@ -26,6 +26,9 @@ database schema management tasks.
 | [ariga/atlas-action/migrate/lint](https://github.com/ariga/atlas-action/tree/master/migrate/lint/action.yml)        | CI for schema changes                               |
 | [ariga/atlas-action/migrate/push](https://github.com/ariga/atlas-action/tree/master/migrate/push/action.yml)   | Push your migration directory to Atlas Cloud (atlasgo.cloud) |
 | [ariga/atlas-action/migrate/apply](https://github.com/ariga/atlas-action/tree/master/migrate/apply/action.yml) | Deploy versioned migrations from GitHub Actions    |
+| [ariga/atlas-action/migrate/down](https://github.com/ariga/atlas-action/tree/master/migrate/down/action.yml) | Revert migrations to a database    |
+| [ariga/atlas-action/migrate/test](https://github.com/ariga/atlas-action/tree/master/migrate/test/action.yml) | Test migrations on a database    |
+| [ariga/atlas-action/schema/test](https://github.com/ariga/atlas-action/tree/master/schema/test/action.yml) | Test schema on a database    |
 
 ## `ariga/setup-atlas`
 
@@ -715,3 +718,885 @@ All inputs are optional as they may be specified in the Atlas configuration file
 * `target` - The target version of the database.
 * `pending_count` - The number of migrations that will be applied.
 * `applied_count` - The number of migrations that were applied.
+
+## `ariga/atlas-action/migrate/down`
+
+Revert migrations to a database.
+
+### Usage
+
+Add `.github/workflows/atlas-ci.yaml` to your repo with the following contents:
+
+<Tabs
+defaultValue="mysql"
+values={[
+    {label: 'MySQL', value: 'mysql'},
+    {label: 'MariaDB', value: 'maria'},
+    {label: 'PostgreSQL', value: 'postgres'},
+    {label: 'SQLite', value: 'sqlite'},
+    {label: 'SQL Server', value: 'sqlserver'},
+    {label: 'Clickhouse', value: 'clickhouse'},
+]}>
+<TabItem value="mysql">
+
+```yaml
+name: Atlas CI
+on:
+  # Run whenever code is changed in the master branch,
+  # change this to your root branch.
+  push:
+    branches:
+      - master
+  # Run on PRs where something changed under the `migrations/` directory.
+  pull_request:
+    paths:
+      - 'migrations/*'
+jobs:
+  down:
+    services:
+      # Spin up a mysql:8 container to be used as the dev-database for analysis.
+      mysql:
+        image: mysql:8
+        env:
+          MYSQL_DATABASE: dev
+          MYSQL_ROOT_PASSWORD: pass
+        ports:
+          - "3306:3306"
+        options: >-
+          --health-cmd "mysqladmin ping -ppass"
+          --health-interval 10s
+          --health-start-period 10s
+          --health-timeout 5s
+          --health-retries 10
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@v3
+      - uses: ariga/setup-atlas@v0
+        with:
+          cloud-token: ${{ secrets.ATLAS_CLOUD_TOKEN }}
+      - uses: ariga/atlas-action/migrate/down@v1
+        with:
+          dir: my-project
+          url: "mysql://root:pass@localhost:3306/example"
+          dev-url: "mysql://root:pass@localhost:3306/dev"
+          amount: 1
+```
+
+</TabItem>
+<TabItem value="maria">
+
+```yaml
+name: Atlas CI
+on:
+  # Run whenever code is changed in the master branch,
+  # change this to your root branch.
+  push:
+    branches:
+      - master
+  # Run on PRs where something changed under the `migrations/` directory.
+  pull_request:
+    paths:
+      - 'migrations/*'
+jobs:
+  down:
+    services:
+      # Spin up a maria:11 container to be used as the dev-database for analysis.
+      mariadb:
+        image: mariadb:11
+        env:
+          MYSQL_DATABASE: dev
+          MYSQL_ROOT_PASSWORD: pass
+        ports:
+          - 3306:3306
+        options: >-
+          --health-cmd "healthcheck.sh --su-mysql --connect --innodb_initialized"
+          --health-interval 10s
+          --health-start-period 10s
+          --health-timeout 5s
+          --health-retries 10
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@v3
+      - uses: ariga/setup-atlas@v0
+        with:
+          cloud-token: ${{ secrets.ATLAS_CLOUD_TOKEN }}
+      - uses: ariga/atlas-action/migrate/down@v1
+        with:
+          dir: atlas://my-project
+          url: "maria://root:pass@localhost:3306/example"
+          dev-url: "maria://root:pass@localhost:3306/dev"
+          amount: 1
+```
+
+</TabItem>
+<TabItem value="postgres">
+
+```yaml
+name: Atlas CI
+on:
+  # Run whenever code is changed in the master branch,
+  # change this to your root branch.
+  push:
+    branches:
+      - master
+  # Run on PRs where something changed under the `path/to/migration/dir/` directory.
+  pull_request:
+    paths:
+      - 'migrations/*'
+jobs:
+  down:
+    services:
+      # Spin up a postgres:15 container to be used as the dev-database for analysis.
+      postgres:
+        image: postgres:15
+        env:
+          POSTGRES_DB: dev
+          POSTGRES_PASSWORD: pass
+        ports:
+          - 5432:5432
+        options: >-
+          --health-cmd pg_isready
+          --health-interval 10s
+          --health-start-period 10s
+          --health-timeout 5s
+          --health-retries 5
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@v3
+      - uses: ariga/setup-atlas@v0
+        with:
+          cloud-token: ${{ secrets.ATLAS_CLOUD_TOKEN }}
+      - uses: ariga/atlas-action/migrate/down@v1
+        with:
+          dir: atlas://my-project
+          url: "postgres://postgres:pass@localhost:5432/example?sslmode=disable"
+          dev-url: "postgres://postgres:pass@localhost:5432/dev"
+          amount: 1
+```
+
+</TabItem>
+<TabItem value="sqlite">
+
+```yaml
+name: Atlas CI
+on:
+  # Run whenever code is changed in the master branch,
+  # change this to your root branch.
+  push:
+    branches:
+      - master
+  # Run on PRs where something changed under the `path/to/migration/dir/` directory.
+  pull_request:
+    paths:
+      - 'migrations/*'
+jobs:
+  down:
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@v3
+      - uses: ariga/setup-atlas@v0
+        with:
+          cloud-token: ${{ secrets.ATLAS_CLOUD_TOKEN }}
+      - uses: ariga/atlas-action/migrate/down@v1
+        with:
+          dir: atlas://my-project
+          url: "sqlite://file?mode=memory&_fk=1"
+          dev-url: "sqlite://file2?mode=memory&_fk=1"
+          amount: 1
+```
+</TabItem>
+<TabItem value="sqlserver">
+
+```yaml
+name: Atlas CI
+on:
+  # Run whenever code is changed in the master branch,
+  # change this to your root branch.
+  push:
+    branches:
+      - master
+  # Run on PRs where something changed under the `path/to/migration/dir/` directory.
+  pull_request:
+    paths:
+      - 'migrations/*'
+jobs:
+  down:
+    services:
+      # Spin up a SQL Server container to be used as the dev-database for analysis.
+      sqlserver:
+        image: mcr.microsoft.com/mssql/server:2022-latest
+        env:
+          ACCEPT_EULA: Y
+          MSSQL_PID: Developer
+          MSSQL_SA_PASSWORD: P@ssw0rd0995
+        ports:
+          - 1433:1433
+        options: >-
+          --health-cmd "/opt/mssql-tools/bin/sqlcmd -U sa -P P@ssw0rd0995 -Q \"SELECT 1\""
+          --health-interval 10s
+          --health-timeout 5s
+          --health-retries 5
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@v3
+      - uses: ariga/setup-atlas@v0
+        with:
+          cloud-token: ${{ secrets.ATLAS_CLOUD_TOKEN }}
+      - uses: ariga/atlas-action/migrate/down@v1
+        with:
+          dir: atlas://my-project
+          url: "sqlserver://sa:P@ssw0rd0995@localhost:1433?database=master"
+          dev-url: "sqlserver://sa:P@ssw0rd0995@localhost:1433?database=dev"
+          amount: 1
+```
+
+</TabItem>
+<TabItem value="clickhouse">
+
+```yaml
+name: Atlas CI
+on:
+  # Run whenever code is changed in the master branch,
+  # change this to your root branch.
+  push:
+    branches:
+      - master
+  # Run on PRs where something changed under the `path/to/migration/dir/` directory.
+  pull_request:
+    paths:
+      - 'migrations/*'
+jobs:
+  down:
+    services:
+      # Spin up a Clickhouse container to be used as the dev-database for analysis.
+      clickhouse:
+        image: clickhouse/clickhouse-server:23.10
+        env:
+          CLICKHOUSE_DB: test
+          CLICKHOUSE_DEFAULT_ACCESS_MANAGEMENT: 1
+          CLICKHOUSE_PASSWORD: pass
+          CLICKHOUSE_USER: root
+        ports:
+          - 9000:9000
+        options: >-
+          --health-cmd "clickhouse-client --host localhost --query 'SELECT 1'"
+          --health-interval 10s
+          --health-timeout 5s
+          --health-retries 5
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@v3
+      - uses: ariga/setup-atlas@v0
+        with:
+          cloud-token: ${{ secrets.ATLAS_CLOUD_TOKEN }}
+      - uses: ariga/atlas-action/migrate/down@v1
+        with:
+          dir: atlas://my-project
+          url: "clickhouse://root:pass@localhost:9000/test"
+          dev-url: "clickhouse://root:pass@localhost:9000/dev"
+          amount: 1
+```
+
+</TabItem>
+</Tabs>
+
+### Inputs
+
+All inputs are optional as they may be specified in the Atlas configuration file.
+
+* `url` - The URL of the target database. For example: `mysql://root:pass@localhost:3306/dev`.
+* `dir` - The URL of the migration directory to apply. For example: `atlas://dir-name` for cloud based directories or `file://migrations` for local ones.
+* `config` - The path to the Atlas configuration file. By default, Atlas will look for a file
+named `atlas.hcl` in the current directory. For example, `file://config/atlas.hcl`.
+Learn more about [Atlas configuration files](https://atlasgo.io/atlas-schema/projects).
+* `env` - The environment to use from the Atlas configuration file.  For example, `dev`.
+* `amount` - The number of migrations to revert. Defaults to 1.
+* `to-version` - To which version to revert.
+* `to-tag` - To which tag to revert.
+* `wait-timeout` -  Time after which no other retry attempt is made and the action exits. If not set, only one attempt is made.
+* `wait-interval` - Time in seconds between different migrate down attempts, useful when waiting for plan approval, defaults to 1s.
+* `vars` - Extra variables to pass to the Atlas configuration file. For example, `key=value key2=values`.
+
+## `ariga/atlas-action/migrate/test`
+
+Test migrations on a database.
+
+### Usage
+
+Add `.github/workflows/atlas-ci.yaml` to your repo with the following contents:
+
+<Tabs
+defaultValue="mysql"
+values={[
+    {label: 'MySQL', value: 'mysql'},
+    {label: 'MariaDB', value: 'maria'},
+    {label: 'PostgreSQL', value: 'postgres'},
+    {label: 'SQLite', value: 'sqlite'},
+    {label: 'SQL Server', value: 'sqlserver'},
+    {label: 'Clickhouse', value: 'clickhouse'},
+]}>
+<TabItem value="mysql">
+
+```yaml
+name: Atlas CI
+on:
+  # Run whenever code is changed in the master branch,
+  # change this to your root branch.
+  push:
+    branches:
+      - master
+  # Run on PRs where something changed under the `migrations/` directory.
+  pull_request:
+    paths:
+      - 'migrations/*'
+jobs:
+  test:
+    services:
+      # Spin up a mysql:8 container to be used as the dev-database for analysis.
+      mysql:
+        image: mysql:8
+        env:
+          MYSQL_DATABASE: dev
+          MYSQL_ROOT_PASSWORD: pass
+        ports:
+          - "3306:3306"
+        options: >-
+          --health-cmd "mysqladmin ping -ppass"
+          --health-interval 10s
+          --health-start-period 10s
+          --health-timeout 5s
+          --health-retries 10
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@v3
+      - uses: ariga/setup-atlas@v0
+        with:
+          cloud-token: ${{ secrets.ATLAS_CLOUD_TOKEN }}
+      - uses: ariga/atlas-action/migrate/test@v1
+        with:
+          dir: file://migrations
+          dev-url: mysql://root:pass@localhost:3306/dev
+          run: "example"
+```
+
+</TabItem>
+<TabItem value="maria">
+
+```yaml
+name: Atlas CI
+on:
+  # Run whenever code is changed in the master branch,
+  # change this to your root branch.
+  push:
+    branches:
+      - master
+  # Run on PRs where something changed under the `migrations/` directory.
+  pull_request:
+    paths:
+      - 'migrations/*'
+jobs:
+  test:
+    services:
+      # Spin up a maria:11 container to be used as the dev-database for analysis.
+      mariadb:
+        image: mariadb:11
+        env:
+          MYSQL_DATABASE: dev
+          MYSQL_ROOT_PASSWORD: pass
+        ports:
+          - 3306:3306
+        options: >-
+          --health-cmd "healthcheck.sh --su-mysql --connect --innodb_initialized"
+          --health-interval 10s
+          --health-start-period 10s
+          --health-timeout 5s
+          --health-retries 10
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@v3
+      - uses: ariga/setup-atlas@v0
+        with:
+          cloud-token: ${{ secrets.ATLAS_CLOUD_TOKEN }}
+      - uses: ariga/atlas-action/migrate/test@v1
+        with:
+          dir: file://migrations
+          dev-url: maria://root:pass@localhost:3306/dev
+          run: "example"
+```
+
+</TabItem>
+<TabItem value="postgres">
+
+```yaml
+name: Atlas CI
+on:
+  # Run whenever code is changed in the master branch,
+  # change this to your root branch.
+  push:
+    branches:
+      - master
+  # Run on PRs where something changed under the `path/to/migration/dir/` directory.
+  pull_request:
+    paths:
+      - 'migrations/*'
+jobs:
+  test:
+    services:
+      # Spin up a postgres:15 container to be used as the dev-database for analysis.
+      postgres:
+        image: postgres:15
+        env:
+          POSTGRES_DB: dev
+          POSTGRES_PASSWORD: pass
+        ports:
+          - 5432:5432
+        options: >-
+          --health-cmd pg_isready
+          --health-interval 10s
+          --health-start-period 10s
+          --health-timeout 5s
+          --health-retries 5
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@v3
+      - uses: ariga/setup-atlas@v0
+        with:
+          cloud-token: ${{ secrets.ATLAS_CLOUD_TOKEN }}
+      - uses: ariga/atlas-action/migrate/test@v1
+        with:
+          dir: file://migrations
+          dev-url: postgres://postgres:pass@localhost:5432/dev
+          run: "example"
+```
+
+</TabItem>
+<TabItem value="sqlite">
+
+```yaml
+name: Atlas CI
+on:
+  # Run whenever code is changed in the master branch,
+  # change this to your root branch.
+  push:
+    branches:
+      - master
+  # Run on PRs where something changed under the `path/to/migration/dir/` directory.
+  pull_request:
+    paths:
+      - 'migrations/*'
+jobs:
+  test:
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@v3
+      - uses: ariga/setup-atlas@v0
+        with:
+          cloud-token: ${{ secrets.ATLAS_CLOUD_TOKEN }}
+      - uses: ariga/atlas-action/migrate/test@v1
+        with:
+          dir: file://migrations
+          dev-url: sqlite://file2?mode=memory&_fk=1"
+          run: "example"
+```
+</TabItem>
+<TabItem value="sqlserver">
+
+```yaml
+name: Atlas CI
+on:
+  # Run whenever code is changed in the master branch,
+  # change this to your root branch.
+  push:
+    branches:
+      - master
+  # Run on PRs where something changed under the `path/to/migration/dir/` directory.
+  pull_request:
+    paths:
+      - 'migrations/*'
+jobs:
+  test:
+    services:
+      # Spin up a SQL Server container to be used as the dev-database for analysis.
+      sqlserver:
+        image: mcr.microsoft.com/mssql/server:2022-latest
+        env:
+          ACCEPT_EULA: Y
+          MSSQL_PID: Developer
+          MSSQL_SA_PASSWORD: P@ssw0rd0995
+        ports:
+          - 1433:1433
+        options: >-
+          --health-cmd "/opt/mssql-tools/bin/sqlcmd -U sa -P P@ssw0rd0995 -Q \"SELECT 1\""
+          --health-interval 10s
+          --health-timeout 5s
+          --health-retries 5
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@v3
+      - uses: ariga/setup-atlas@v0
+        with:
+          cloud-token: ${{ secrets.ATLAS_CLOUD_TOKEN }}
+      - uses: ariga/atlas-action/migrate/test@v1
+        with:
+          dir: file://migrations
+          dev-url: "sqlserver://sa:P@ssw0rd0995@localhost:1433?database=dev"
+          run: "example"
+```
+
+</TabItem>
+<TabItem value="clickhouse">
+
+```yaml
+name: Atlas CI
+on:
+  # Run whenever code is changed in the master branch,
+  # change this to your root branch.
+  push:
+    branches:
+      - master
+  # Run on PRs where something changed under the `path/to/migration/dir/` directory.
+  pull_request:
+    paths:
+      - 'migrations/*'
+jobs:
+  test:
+    services:
+      # Spin up a Clickhouse container to be used as the dev-database for analysis.
+      clickhouse:
+        image: clickhouse/clickhouse-server:23.10
+        env:
+          CLICKHOUSE_DB: test
+          CLICKHOUSE_DEFAULT_ACCESS_MANAGEMENT: 1
+          CLICKHOUSE_PASSWORD: pass
+          CLICKHOUSE_USER: root
+        ports:
+          - 9000:9000
+        options: >-
+          --health-cmd "clickhouse-client --host localhost --query 'SELECT 1'"
+          --health-interval 10s
+          --health-timeout 5s
+          --health-retries 5
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@v3
+      - uses: ariga/setup-atlas@v0
+        with:
+          cloud-token: ${{ secrets.ATLAS_CLOUD_TOKEN }}
+      - uses: ariga/atlas-action/migrate/test@v1
+        with:
+          dir: file://migrations
+          dev-url: "clickhouse://root:pass@localhost:9000/dev"
+          run: "example"
+```
+
+</TabItem>
+</Tabs>
+
+### Inputs
+
+All inputs are optional as they may be specified in the Atlas configuration file.
+
+* `dev-url` - The URL of the dev-database to use for analysis. For example: `mysql://root:pass@localhost:3306/dev`.
+  Read more about [dev-databases](https://atlasgo.io/concepts/dev-database).
+* `dir` - The URL of the migration directory to apply.  For example: `atlas://dir-name` for cloud
+   based directories or `file://migrations` for local ones.
+* `run` - Filter tests to run by regexp. For example, `^test_.*` will only run tests that start with `test_`.
+  Default is to run all tests.
+* `config` - The URL of the Atlas configuration file.  By default, Atlas will look for a file
+  named `atlas.hcl` in the current directory. For example, `file://config/atlas.hcl`.
+  Learn more about [Atlas configuration files](https://atlasgo.io/atlas-schema/projects). 
+* `env` - The environment to use from the Atlas configuration file.  For example, `dev`.
+* `vars` - Stringify JSON object containing variables to be used inside the Atlas configuration file.
+   For example: `'{"var1": "value1", "var2": "value2"}'`.
+
+## `ariga/atlas-action/schema/test`
+
+Test schema on a database.
+
+### Usage
+
+Add `.github/workflows/atlas-ci.yaml` to your repo with the following contents:
+
+<Tabs
+defaultValue="mysql"
+values={[
+    {label: 'MySQL', value: 'mysql'},
+    {label: 'MariaDB', value: 'maria'},
+    {label: 'PostgreSQL', value: 'postgres'},
+    {label: 'SQLite', value: 'sqlite'},
+    {label: 'SQL Server', value: 'sqlserver'},
+    {label: 'Clickhouse', value: 'clickhouse'},
+]}>
+<TabItem value="mysql">
+
+```yaml
+name: Atlas CI
+on:
+  # Run whenever code is changed in the master branch,
+  # change this to your root branch.
+  push:
+    branches:
+      - master
+  # Run on PRs where something changed under the `migrations/` directory.
+  pull_request:
+    paths:
+      - 'migrations/*'
+jobs:
+  test:
+    services:
+      # Spin up a mysql:8 container to be used as the dev-database for analysis.
+      mysql:
+        image: mysql:8
+        env:
+          MYSQL_DATABASE: dev
+          MYSQL_ROOT_PASSWORD: pass
+        ports:
+          - "3306:3306"
+        options: >-
+          --health-cmd "mysqladmin ping -ppass"
+          --health-interval 10s
+          --health-start-period 10s
+          --health-timeout 5s
+          --health-retries 10
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@v3
+      - uses: ariga/setup-atlas@v0
+        with:
+          cloud-token: ${{ secrets.ATLAS_CLOUD_TOKEN }}
+      - uses: ariga/atlas-action/schema/test@v1
+        with:
+          url: file://schema.hcl
+          dev-url: mysql://root:pass@localhost:3306/dev
+          run: "example"
+```
+
+</TabItem>
+<TabItem value="maria">
+
+```yaml
+name: Atlas CI
+on:
+  # Run whenever code is changed in the master branch,
+  # change this to your root branch.
+  push:
+    branches:
+      - master
+  # Run on PRs where something changed under the `migrations/` directory.
+  pull_request:
+    paths:
+      - 'migrations/*'
+jobs:
+  test:
+    services:
+      # Spin up a maria:11 container to be used as the dev-database for analysis.
+      mariadb:
+        image: mariadb:11
+        env:
+          MYSQL_DATABASE: dev
+          MYSQL_ROOT_PASSWORD: pass
+        ports:
+          - 3306:3306
+        options: >-
+          --health-cmd "healthcheck.sh --su-mysql --connect --innodb_initialized"
+          --health-interval 10s
+          --health-start-period 10s
+          --health-timeout 5s
+          --health-retries 10
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@v3
+      - uses: ariga/setup-atlas@v0
+        with:
+          cloud-token: ${{ secrets.ATLAS_CLOUD_TOKEN }}
+      - uses: ariga/atlas-action/schema/test@v1
+        with:
+          url: file://schema.hcl
+          dev-url: maria://root:pass@localhost:3306/dev
+          run: "example"
+```
+
+</TabItem>
+<TabItem value="postgres">
+
+```yaml
+name: Atlas CI
+on:
+  # Run whenever code is changed in the master branch,
+  # change this to your root branch.
+  push:
+    branches:
+      - master
+  # Run on PRs where something changed under the `path/to/migration/dir/` directory.
+  pull_request:
+    paths:
+      - 'migrations/*'
+jobs:
+  test:
+    services:
+      # Spin up a postgres:15 container to be used as the dev-database for analysis.
+      postgres:
+        image: postgres:15
+        env:
+          POSTGRES_DB: dev
+          POSTGRES_PASSWORD: pass
+        ports:
+          - 5432:5432
+        options: >-
+          --health-cmd pg_isready
+          --health-interval 10s
+          --health-start-period 10s
+          --health-timeout 5s
+          --health-retries 5
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@v3
+      - uses: ariga/setup-atlas@v0
+        with:
+          cloud-token: ${{ secrets.ATLAS_CLOUD_TOKEN }}
+      - uses: ariga/atlas-action/schema/test@v1
+        with:
+          url: file://schema.hcl
+          dev-url: postgres://postgres:pass@localhost:5432/dev
+          run: "example"
+```
+
+</TabItem>
+<TabItem value="sqlite">
+
+```yaml
+name: Atlas CI
+on:
+  # Run whenever code is changed in the master branch,
+  # change this to your root branch.
+  push:
+    branches:
+      - master
+  # Run on PRs where something changed under the `path/to/migration/dir/` directory.
+  pull_request:
+    paths:
+      - 'migrations/*'
+jobs:
+  test:
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@v3
+      - uses: ariga/setup-atlas@v0
+        with:
+          cloud-token: ${{ secrets.ATLAS_CLOUD_TOKEN }}
+      - uses: ariga/atlas-action/schema/test@v1
+        with:
+          url: file://schema.hcl
+          dev-url: sqlite://file2?mode=memory&_fk=1"
+          run: "example"
+```
+</TabItem>
+<TabItem value="sqlserver">
+
+```yaml
+name: Atlas CI
+on:
+  # Run whenever code is changed in the master branch,
+  # change this to your root branch.
+  push:
+    branches:
+      - master
+  # Run on PRs where something changed under the `path/to/migration/dir/` directory.
+  pull_request:
+    paths:
+      - 'migrations/*'
+jobs:
+  test:
+    services:
+      # Spin up a SQL Server container to be used as the dev-database for analysis.
+      sqlserver:
+        image: mcr.microsoft.com/mssql/server:2022-latest
+        env:
+          ACCEPT_EULA: Y
+          MSSQL_PID: Developer
+          MSSQL_SA_PASSWORD: P@ssw0rd0995
+        ports:
+          - 1433:1433
+        options: >-
+          --health-cmd "/opt/mssql-tools/bin/sqlcmd -U sa -P P@ssw0rd0995 -Q \"SELECT 1\""
+          --health-interval 10s
+          --health-timeout 5s
+          --health-retries 5
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@v3
+      - uses: ariga/setup-atlas@v0
+        with:
+          cloud-token: ${{ secrets.ATLAS_CLOUD_TOKEN }}
+      - uses: ariga/atlas-action/schema/test@v1
+        with:
+          url: file://schema.hcl
+          dev-url: "sqlserver://sa:P@ssw0rd0995@localhost:1433?database=dev"
+          run: "example"
+```
+
+</TabItem>
+<TabItem value="clickhouse">
+
+```yaml
+name: Atlas CI
+on:
+  # Run whenever code is changed in the master branch,
+  # change this to your root branch.
+  push:
+    branches:
+      - master
+  # Run on PRs where something changed under the `path/to/migration/dir/` directory.
+  pull_request:
+    paths:
+      - 'migrations/*'
+jobs:
+  test:
+    services:
+      # Spin up a Clickhouse container to be used as the dev-database for analysis.
+      clickhouse:
+        image: clickhouse/clickhouse-server:23.10
+        env:
+          CLICKHOUSE_DB: test
+          CLICKHOUSE_DEFAULT_ACCESS_MANAGEMENT: 1
+          CLICKHOUSE_PASSWORD: pass
+          CLICKHOUSE_USER: root
+        ports:
+          - 9000:9000
+        options: >-
+          --health-cmd "clickhouse-client --host localhost --query 'SELECT 1'"
+          --health-interval 10s
+          --health-timeout 5s
+          --health-retries 5
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@v3
+      - uses: ariga/setup-atlas@v0
+        with:
+          cloud-token: ${{ secrets.ATLAS_CLOUD_TOKEN }}
+      - uses: ariga/atlas-action/schema/test@v1
+        with:
+          url: file://schema.hcl
+          dev-url: clickhouse://root:pass@localhost:9000/dev
+          run: "example"
+```
+
+</TabItem>
+</Tabs>
+
+### Inputs
+
+All inputs are optional as they may be specified in the Atlas configuration file.
+
+* `dev-url` - The URL of the dev-database to use for analysis. For example: `mysql://root:pass@localhost:3306/dev`.
+  Read more about [dev-databases](https://atlasgo.io/concepts/dev-database).
+* `url` - The desired schema URL(s) to test. For Example: `file://schema.hcl`
+* `run` - Filter tests to run by regexp. For example, `^test_.*` will only run tests that start with `test_`.
+  Default is to run all tests.
+* `config` - The URL of the Atlas configuration file.  By default, Atlas will look for a file
+  named `atlas.hcl` in the current directory. For example, `file://config/atlas.hcl`.
+  Learn more about [Atlas configuration files](https://atlasgo.io/atlas-schema/projects). 
+* `env` - The environment to use from the Atlas configuration file.  For example, `dev`.
+* `vars` - Stringify JSON object containing variables to be used inside the Atlas configuration file.
+   For example: `'{"var1": "value1", "var2": "value2"}'`.
+
diff --git a/doc/md/versioned/checkpoint.mdx b/doc/md/versioned/checkpoint.mdx
index 305a4293..e788d36f 100644
--- a/doc/md/versioned/checkpoint.mdx
+++ b/doc/md/versioned/checkpoint.mdx
@@ -10,9 +10,9 @@ Using checkpoints can speed up the process of setting up a database from scratch
 
 ### Generating a Checkpoint
 
-:::info Login Required
+:::info [Atlas Pro Feature](/features#pro-plan)
 
-The `atlas migrate checkpoint` feature is only available to logged-in users. You can create a free account
+The `atlas migrate checkpoint` feature is only available to [Atlas Pro users](/features#pro-plan). You can create a free account
 using the `atlas login` command. To learn more about logged-in features, see [Features page](/features).
 
 :::
diff --git a/doc/md/versioned/intro.mdx b/doc/md/versioned/intro.mdx
index a658f445..4eee6bd9 100644
--- a/doc/md/versioned/intro.mdx
+++ b/doc/md/versioned/intro.mdx
@@ -235,30 +235,10 @@ the [Atlas Schema Registry](/cloud/features/registry), much like how Docker imag
 First, [log in to Atlas](https://auth.atlasgo.cloud/signup). If it's your first time, you'll be
 prompted to create both an account and a workspace (organization):
 
-<Tabs>
-<TabItem value="web" label="Via Web">
-
 ```bash
 atlas login
 ```
 
-</TabItem>
-<TabItem value="token" label="Via Token">
-
-```bash
-atlas login --token "ATLAS_TOKEN"
-```
-
-</TabItem>
-<TabItem value="env-var" label="Via Environment Variable">
-
-```bash
-ATLAS_TOKEN="ATLAS_TOKEN" atlas login
-```
-
-</TabItem>
-</Tabs>
-
 After logging in, let's name our new migration project `app` and run `atlas migrate push`:
 
 <MigratePushCommand />
diff --git a/doc/website/blog/2022-10-27-multi-tenant-support.mdx b/doc/website/blog/2022-10-27-multi-tenant-support.mdx
index 91ebe64b..c9ebc6bb 100644
--- a/doc/website/blog/2022-10-27-multi-tenant-support.mdx
+++ b/doc/website/blog/2022-10-27-multi-tenant-support.mdx
@@ -74,7 +74,7 @@ env "prod" {
 Once defined, a project's environment can be worked against using the `--env` flag. For example:
 
 ```bash
-atlas schema apply --env local
+atlas migrate apply --env local
 ```
 
 The command above runs the `schema apply` against the database that is defined in the `local` environment.
diff --git a/doc/website/blog/2024-07-09-v025-row-level-security.mdx b/doc/website/blog/2024-07-09-v025-row-level-security.mdx
new file mode 100644
index 00000000..21b9929a
--- /dev/null
+++ b/doc/website/blog/2024-07-09-v025-row-level-security.mdx
@@ -0,0 +1,156 @@
+---
+title: "Manage your Row-level Security Policies as Code with Atlas v0.25"
+authors: rotemtam
+tags: [release, postgres, rls, security]
+---
+
+import InstallationInstructions from '../../md/components/_installation_instructions.mdx'
+
+Hi everyone,
+
+Thanks for joining us today for our [v0.25 release](https://github.com/ariga/atlas/releases/tag/v0.25.0) announcement! In this version we are introducing a new feature that
+has been [requested by many of you](https://github.com/ariga/atlas/issues/1071): support for **Row-level Security
+Policies** in PostgreSQL.
+
+Additionally, we have made some minor changes to our pricing plans, more on that below.
+
+### What are Row-level Security Policies?
+
+Row-level security (RLS) in PostgreSQL allows tables to have policies that restrict which rows can be accessed or
+modified based on the user's role, enhancing the SQL-standard privilege system available through `GRANT`.
+
+When enabled, all normal access to the table must comply with these policies, defaulting to a deny-all approach if
+no policies are set, ensuring that no rows are visible or modifiable. Policies can be specific to commands, roles,
+or both, providing fine-grained control over data access and modification.
+
+### How does RLS work?
+
+When you create and enable a row-level security (RLS) policy in PostgreSQL, the database enforces the specified access
+control rules on a per-row basis.
+
+For example, you can create a policy that allows only employees to see their own records in an employees table.
+The policy could look like this:
+
+```postgresql
+CREATE POLICY employee_policy ON employees
+  FOR SELECT
+  USING (current_user = employee_role);
+```
+
+This SQL command creates an RLS policy named `employee_policy` on the employees table. The `FOR SELECT` clause specifies
+that this policy applies to `SELECT` queries. The `USING` clause contains the condition `current_user = employee_role`, which
+means that a user can only select rows where the employee_role column matches their PostgreSQL username.
+
+Next, database administrators typically run:
+
+```postgresql
+ALTER TABLE employees ENABLE ROW LEVEL SECURITY;
+```
+
+This command enables RLS on the employees table. With RLS enabled, PostgreSQL will check the policies defined for
+this table whenever a user attempts to access or modify existing rows, or insert new ones.
+
+When a user executes a `SELECT` query on the employees table, PostgreSQL evaluates the `employee_policy`. If the user's
+PostgreSQL role (username) matches the employee_role column value in a row, the row is included in the query result.
+Otherwise, the row is excluded.
+
+For instance, if the employees table contains the following data:
+
+| id | name    | employee_role |
+|----|---------|---------------|
+| 1  | Alice   | alice         |
+| 2  | Bob     | bob           |
+| 3  | Charlie | charlie       |
+
+When the user alice runs `SELECT * FROM employees`, PostgreSQL applies the policy:
+
+```postgresql
+SELECT * FROM employees WHERE current_user = employee_role;
+```
+
+This results in:
+
+| id | name    | employee_role |
+|----|---------|---------------|
+| 1  | Alice   | alice         |
+
+By enforcing these policies, RLS ensures that users only have access to the data they are permitted to see,
+enhancing the security and privacy of the database.
+
+### Manage your Row-level Security Policies as Code
+
+With Atlas, you can now manage your RLS policies as code, just like you manage other database resources such as tables,
+indexes, and triggers. This allows you to version control your policies, track changes, and apply them consistently
+across your environments.
+
+To get started with RLS in Atlas, first upgrade to the most recent version.
+
+<InstallationInstructions/>
+
+RLS is available to Atlas Pro users only. Get your free Atlas Pro account today by running:
+
+```shell
+atlas login
+```
+
+Next, you can define your RLS policies in your Atlas schema file (schema.hcl) using the new `policy` block:
+
+```hcl
+policy "employee_policy" {
+  on    = table.employees
+  for   = SELECT
+  to    = [PUBLIC]
+  using = "(current_user = employee_role)"
+}
+```
+
+This HCL snippet defines an RLS policy named `employee_policy` on the `employees` table, allowing only users whose
+`employee_role` matches their PostgreSQL username to `SELECT` rows from the table.
+
+Next, you need to enable RLS on the table:
+
+```hcl
+table "employees" {
+  schema = schema.public
+  column "employee_role" {
+    type = text
+  }
+  row_security {
+    enabled  = true // ENABLE ROW LEVEL SECURITY
+  }
+}
+```
+
+Finally, run `atlas schema apply` to apply the changes to your database!
+
+To learn more about RLS using Atlas, check out our [documentation](/atlas-schema/hcl#row-level-security-policies).
+
+### Introducing Atlas Pro
+
+Since launching Atlas Cloud a little over a year ago, we have been working hard with our users and customers to make
+Atlas as easy and simple to use as possible.
+
+One point of confusion we have encountered, especially around our pricing plans, was how users who currently don't
+want (or can't) use Atlas Cloud for their CI/CD pipelines can get access to the advanced CLI features that Atlas offers.
+Previously, teams needed to buy Cloud quota to get access to the CLI, which didn't make a lot of sense.
+
+To address some of these issues we are making some small changes to our pricing plans:
+
+Atlas now comes in three tiers:
+
+* **Open** - Our CLI, doesn't require creating an account and comes with a solid set of features (this is more than
+  enough for many of our users).
+* **Pro (previously "Business")** - An enhanced version of our CLI, which includes support for advanced database
+  features and drivers.
+  It will cost $9/month/user, but users get their first 3 seats per company for free when they sign up. Pro users
+  also have access to Atlas Cloud (pricing remains the same).
+* **Enterprise** - our enterprise tier, targeted mostly at larger organizations or teams in regulated industries with
+  stricter compliance requirements.
+
+To learn more about our new plans, head over to our [updated pricing page](/pricing).
+
+### Wrapping Up
+
+That's all for this release! We hope you try out (and enjoy) all of these new features and find them useful.
+As always, we would love to hear your feedback and suggestions on our [Discord server](https://discord.com/invite/zZ6sWVg6NT).
+
diff --git a/doc/website/docusaurus.config.js b/doc/website/docusaurus.config.js
index 8aee0d0b..d2e45942 100644
--- a/doc/website/docusaurus.config.js
+++ b/doc/website/docusaurus.config.js
@@ -80,21 +80,16 @@ module.exports = {
           position: 'left'
         },
         {
-          to: 'support',
-          label: 'Support',
-          position: 'left',
-        },
-        {
-          to: 'https://atlasgo.cloud/?utm_term=topnav',
-          label: 'Cloud',
+          to: 'pricing',
+          label: 'Pricing',
           position: 'left',
         },
       ],
     },
     footer,
     announcementBar: {
-      id: 'announcementBar-14', // Increment on change
-      content: `<strong>Atlas v0.24: Testing framework, Editor Support, and more. </strong><a class="cta" href="https://atlasgo.io/blog/2024/06/13/atlas-v-0-24">Read it here</a>`,
+      id: 'announcementBar-15', // Increment on change
+      content: `<strong>Atlas v0.25: PostgreSQL Row-level Security and more. </strong><a class="cta" href="https://atlasgo.io/blog/2024/07/09/v025-row-level-security">Read it here</a>`,
       isCloseable: true,
     },
   },
diff --git a/doc/website/package-lock.json b/doc/website/package-lock.json
index 3c87d48d..402e02e2 100644
--- a/doc/website/package-lock.json
+++ b/doc/website/package-lock.json
@@ -8,7 +8,7 @@
       "name": "website",
       "version": "0.0.0",
       "dependencies": {
-        "@ariga/atlas-website": "^0.0.44",
+        "@ariga/atlas-website": "0.0.48",
         "@docusaurus/core": "^2.0.0-rc.1",
         "@docusaurus/plugin-client-redirects": "^2.4.1",
         "@docusaurus/plugin-ideal-image": "^2.4.1",
@@ -306,9 +306,9 @@
       }
     },
     "node_modules/@ariga/atlas-website": {
-      "version": "0.0.44",
-      "resolved": "https://registry.npmjs.org/@ariga/atlas-website/-/atlas-website-0.0.44.tgz",
-      "integrity": "sha512-ArLRfdETtV4ATPfkyajbrpPO0l3xWdW58F0heyntkhDCcdg5KPB8rj4p2+9J/i6IyJMkfZiaax2UDVPx7MR+3w==",
+      "version": "0.0.48",
+      "resolved": "https://registry.npmjs.org/@ariga/atlas-website/-/atlas-website-0.0.48.tgz",
+      "integrity": "sha512-+KYkz/WIomKil1v/4HYzcRS21XEmixTzbuYZ6cNZjSXlObibectA0M2YX41NLFt6g8PCW4+g/E302ynsTNX2Ww==",
       "dependencies": {
         "@amplitude/analytics-browser": "^1.9.4",
         "@types/amplitude-js": "^8.16.2",
@@ -327,7 +327,8 @@
         "react-router-dom": "^6.9.0",
         "react-scroll": "^1.8.9",
         "react-scroll-parallax": "^3.3.2",
-        "react-use-intercom": "^5.0.0",
+        "react-tooltip": "^5.26.0",
+        "react-use-intercom": "^5.4.0",
         "replace-in-file": "^6.3.5",
         "tailwind-merge": "^2.2.1",
         "tailwind-scrollbar": "^2.1.0",
@@ -4488,6 +4489,28 @@
         "node": ">=12"
       }
     },
+    "node_modules/@floating-ui/core": {
+      "version": "1.6.4",
+      "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.6.4.tgz",
+      "integrity": "sha512-a4IowK4QkXl4SCWTGUR0INAfEOX3wtsYw3rKK5InQEHMGObkR8Xk44qYQD9P4r6HHw0iIfK6GUKECmY8sTkqRA==",
+      "dependencies": {
+        "@floating-ui/utils": "^0.2.4"
+      }
+    },
+    "node_modules/@floating-ui/dom": {
+      "version": "1.6.7",
+      "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.6.7.tgz",
+      "integrity": "sha512-wmVfPG5o2xnKDU4jx/m4w5qva9FWHcnZ8BvzEe90D/RpwsJaTAVYPEPdQ8sbr/N8zZTAHlZUTQdqg8ZUbzHmng==",
+      "dependencies": {
+        "@floating-ui/core": "^1.6.0",
+        "@floating-ui/utils": "^0.2.4"
+      }
+    },
+    "node_modules/@floating-ui/utils": {
+      "version": "0.2.4",
+      "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.4.tgz",
+      "integrity": "sha512-dWO2pw8hhi+WrXq1YJy2yCuWoL20PddgGaqTgVe4cOS9Q6qklXCiA1tJEqX6BEwRNSCP84/afac9hd4MS+zEUA=="
+    },
     "node_modules/@hapi/hoek": {
       "version": "9.3.0",
       "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.3.0.tgz",
@@ -21036,10 +21059,23 @@
         "react": "^16.8.0 || ^17.0.0 || ^18.0.0"
       }
     },
+    "node_modules/react-tooltip": {
+      "version": "5.27.1",
+      "resolved": "https://registry.npmjs.org/react-tooltip/-/react-tooltip-5.27.1.tgz",
+      "integrity": "sha512-a+micPXcMOMt11CYlwJD4XShcqGziasHco4NPe1OFw298WBTILMyzUgNC1LAFViAe791JdHNVSJIpzhZm2MvDA==",
+      "dependencies": {
+        "@floating-ui/dom": "^1.6.1",
+        "classnames": "^2.3.0"
+      },
+      "peerDependencies": {
+        "react": ">=16.14.0",
+        "react-dom": ">=16.14.0"
+      }
+    },
     "node_modules/react-use-intercom": {
-      "version": "5.1.4",
-      "resolved": "https://registry.npmjs.org/react-use-intercom/-/react-use-intercom-5.1.4.tgz",
-      "integrity": "sha512-37dMuCg3H5KgMqpUMiZiu7Ni1LcRAkO8T7F1Zg4Xc+hL89j8OYFHzWWBy29EFgRsTdvCEJ2X8J5wAXerU9uuqw==",
+      "version": "5.4.1",
+      "resolved": "https://registry.npmjs.org/react-use-intercom/-/react-use-intercom-5.4.1.tgz",
+      "integrity": "sha512-KfrFYTAyRuUn8hZiqzVMkVujOOWAQyclT1v3FMpqBHcdoyml31VsrvUDi95pX1hLBJ39kX4FKwzAi1OL9IsbDQ==",
       "peerDependencies": {
         "react": ">=16.8.0",
         "react-dom": ">=16.8.0"
@@ -27005,9 +27041,9 @@
       }
     },
     "@ariga/atlas-website": {
-      "version": "0.0.44",
-      "resolved": "https://registry.npmjs.org/@ariga/atlas-website/-/atlas-website-0.0.44.tgz",
-      "integrity": "sha512-ArLRfdETtV4ATPfkyajbrpPO0l3xWdW58F0heyntkhDCcdg5KPB8rj4p2+9J/i6IyJMkfZiaax2UDVPx7MR+3w==",
+      "version": "0.0.48",
+      "resolved": "https://registry.npmjs.org/@ariga/atlas-website/-/atlas-website-0.0.48.tgz",
+      "integrity": "sha512-+KYkz/WIomKil1v/4HYzcRS21XEmixTzbuYZ6cNZjSXlObibectA0M2YX41NLFt6g8PCW4+g/E302ynsTNX2Ww==",
       "requires": {
         "@amplitude/analytics-browser": "^1.9.4",
         "@types/amplitude-js": "^8.16.2",
@@ -27026,7 +27062,8 @@
         "react-router-dom": "^6.9.0",
         "react-scroll": "^1.8.9",
         "react-scroll-parallax": "^3.3.2",
-        "react-use-intercom": "^5.0.0",
+        "react-tooltip": "^5.26.0",
+        "react-use-intercom": "^5.4.0",
         "replace-in-file": "^6.3.5",
         "tailwind-merge": "^2.2.1",
         "tailwind-scrollbar": "^2.1.0",
@@ -29785,6 +29822,28 @@
       "optional": true,
       "peer": true
     },
+    "@floating-ui/core": {
+      "version": "1.6.4",
+      "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.6.4.tgz",
+      "integrity": "sha512-a4IowK4QkXl4SCWTGUR0INAfEOX3wtsYw3rKK5InQEHMGObkR8Xk44qYQD9P4r6HHw0iIfK6GUKECmY8sTkqRA==",
+      "requires": {
+        "@floating-ui/utils": "^0.2.4"
+      }
+    },
+    "@floating-ui/dom": {
+      "version": "1.6.7",
+      "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.6.7.tgz",
+      "integrity": "sha512-wmVfPG5o2xnKDU4jx/m4w5qva9FWHcnZ8BvzEe90D/RpwsJaTAVYPEPdQ8sbr/N8zZTAHlZUTQdqg8ZUbzHmng==",
+      "requires": {
+        "@floating-ui/core": "^1.6.0",
+        "@floating-ui/utils": "^0.2.4"
+      }
+    },
+    "@floating-ui/utils": {
+      "version": "0.2.4",
+      "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.4.tgz",
+      "integrity": "sha512-dWO2pw8hhi+WrXq1YJy2yCuWoL20PddgGaqTgVe4cOS9Q6qklXCiA1tJEqX6BEwRNSCP84/afac9hd4MS+zEUA=="
+    },
     "@hapi/hoek": {
       "version": "9.3.0",
       "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.3.0.tgz",
@@ -42167,10 +42226,19 @@
         "use-latest": "^1.2.1"
       }
     },
+    "react-tooltip": {
+      "version": "5.27.1",
+      "resolved": "https://registry.npmjs.org/react-tooltip/-/react-tooltip-5.27.1.tgz",
+      "integrity": "sha512-a+micPXcMOMt11CYlwJD4XShcqGziasHco4NPe1OFw298WBTILMyzUgNC1LAFViAe791JdHNVSJIpzhZm2MvDA==",
+      "requires": {
+        "@floating-ui/dom": "^1.6.1",
+        "classnames": "^2.3.0"
+      }
+    },
     "react-use-intercom": {
-      "version": "5.1.4",
-      "resolved": "https://registry.npmjs.org/react-use-intercom/-/react-use-intercom-5.1.4.tgz",
-      "integrity": "sha512-37dMuCg3H5KgMqpUMiZiu7Ni1LcRAkO8T7F1Zg4Xc+hL89j8OYFHzWWBy29EFgRsTdvCEJ2X8J5wAXerU9uuqw==",
+      "version": "5.4.1",
+      "resolved": "https://registry.npmjs.org/react-use-intercom/-/react-use-intercom-5.4.1.tgz",
+      "integrity": "sha512-KfrFYTAyRuUn8hZiqzVMkVujOOWAQyclT1v3FMpqBHcdoyml31VsrvUDi95pX1hLBJ39kX4FKwzAi1OL9IsbDQ==",
       "requires": {}
     },
     "react-waypoint": {
diff --git a/doc/website/package.json b/doc/website/package.json
index 6b7b0f8e..5838cc17 100644
--- a/doc/website/package.json
+++ b/doc/website/package.json
@@ -16,7 +16,7 @@
     "invalidate-cdn": "aws cloudfront create-invalidation --distribution-id E3VOJYSV9YO33D --paths \"/*\""
   },
   "dependencies": {
-    "@ariga/atlas-website": "^0.0.44",
+    "@ariga/atlas-website": "0.0.48",
     "@docusaurus/core": "^2.0.0-rc.1",
     "@docusaurus/plugin-client-redirects": "^2.4.1",
     "@docusaurus/plugin-ideal-image": "^2.4.1",
diff --git a/doc/website/sidebars.js b/doc/website/sidebars.js
index 304f62e5..cc805a81 100644
--- a/doc/website/sidebars.js
+++ b/doc/website/sidebars.js
@@ -126,6 +126,7 @@ module.exports = {
                         {type: 'doc', id: 'cloud/features/schema-docs', label: 'Schema Docs'},
                         {type: 'doc', id: 'cloud/features/pre-migration-checks', label: 'Pre-migration Checks'},
                         {type: 'doc', id: 'cloud/features/troubleshooting', label: 'Migration Troubleshooting'},
+                        {type: 'doc', id: 'cloud/features/monitoring', label: 'Schema Monitoring'},
                     ],
                 },
                 {type: 'doc', id: 'cloud/pricing', label: 'Pricing'},
@@ -254,6 +255,10 @@ module.exports = {
                 },
             ]
         },
+        {
+            type: 'doc',
+            id: 'guides/atlas-in-docker',
+        },
         {
             type: 'category',
             label: 'Testing',
@@ -268,6 +273,11 @@ module.exports = {
                     type: 'doc',
                     id: 'guides/testing/testing-github-actions',
                     label: 'With GitHub Actions'
+                },
+                {
+                    type: 'doc',
+                    id: 'guides/testing/testing-views',
+                    label: 'Testing Views'
                 }
             ]
         },
diff --git a/doc/website/src/pages/pricing.js b/doc/website/src/pages/pricing.js
new file mode 100644
index 00000000..067fa229
--- /dev/null
+++ b/doc/website/src/pages/pricing.js
@@ -0,0 +1,16 @@
+import React from "react";
+import LayoutProvider from "@theme/Layout/Provider";
+import AnnouncementBar from "@theme/AnnouncementBar";
+import { AtlasGoPricing } from "@ariga/atlas-website";
+import FOOTER from "../constants/footer";
+
+import "@ariga/atlas-website/style.css";
+
+export default function () {
+  return (
+    <LayoutProvider>
+      <AnnouncementBar />
+      <AtlasGoPricing footer={FOOTER} />
+    </LayoutProvider>
+  );
+}
diff --git a/go.mod b/go.mod
index e7a5391e..69bbb3e8 100644
--- a/go.mod
+++ b/go.mod
@@ -1,8 +1,6 @@
 module ariga.io/atlas
 
-go 1.22.3
-
-toolchain go1.22.4
+go 1.22.5
 
 require (
 	github.com/DATA-DOG/go-sqlmock v1.5.0
diff --git a/internal/integration/go.mod b/internal/integration/go.mod
index 3ad3e05c..c928b067 100644
--- a/internal/integration/go.mod
+++ b/internal/integration/go.mod
@@ -1,8 +1,6 @@
 module ariga.io/atlas/internal/integration
 
-go 1.22.3
-
-toolchain go1.22.4
+go 1.22.5
 
 replace ariga.io/atlas => ../../
 
@@ -17,7 +15,7 @@ require (
 	github.com/mattn/go-sqlite3 v1.14.16
 	github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e
 	github.com/rogpeppe/go-internal v1.9.0
-	github.com/stretchr/testify v1.8.4
+	github.com/stretchr/testify v1.9.0
 	github.com/zclconf/go-cty v1.14.4
 )
 
@@ -76,10 +74,9 @@ require (
 	github.com/mitchellh/go-wordwrap v1.0.1 // indirect
 	github.com/pganalyze/pg_query_go/v5 v5.1.0 // indirect
 	github.com/pmezard/go-difflib v1.0.0 // indirect
-	github.com/sergi/go-diff v1.2.0 // indirect
 	github.com/spf13/cobra v1.8.0 // indirect
 	github.com/spf13/pflag v1.0.5 // indirect
-	github.com/vektah/gqlparser/v2 v2.5.1 // indirect
+	github.com/vektah/gqlparser/v2 v2.5.16 // indirect
 	go.opencensus.io v0.24.0 // indirect
 	go.opentelemetry.io/otel/metric v1.24.0 // indirect
 	go.opentelemetry.io/otel/trace v1.24.0 // indirect
diff --git a/internal/integration/go.sum b/internal/integration/go.sum
index f49c116a..1b4886f7 100644
--- a/internal/integration/go.sum
+++ b/internal/integration/go.sum
@@ -22,7 +22,6 @@ github.com/DATA-DOG/go-sqlmock v1.5.0 h1:Shsta01QNfFxHCfpW6YH2STWB0MudeXXEWMr20O
 github.com/DATA-DOG/go-sqlmock v1.5.0/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM=
 github.com/agext/levenshtein v1.2.3 h1:YB2fHEn0UJagG8T1rrWknE3ZQzWM06O8AMAatNn7lmo=
 github.com/agext/levenshtein v1.2.3/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558=
-github.com/agnivade/levenshtein v1.0.1/go.mod h1:CURSv5d9Uaml+FovSIICkLbAUZ9S4RqaHDIsdSBg7lM=
 github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883 h1:bvNMNQO63//z+xNgfBlViaCIJKLlCJ6/fmUseuG0wVQ=
 github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8=
 github.com/antlr/antlr4/runtime/Go/antlr/v4 v4.0.0-20230512164433-5d1fd1a340c9 h1:goHVqTbFX3AIo0tzGr14pgfAW2ZfPChKO21Z9MGf/gk=
@@ -186,11 +185,8 @@ github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHm
 github.com/klauspost/compress v1.10.3/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
 github.com/klauspost/compress v1.15.15 h1:EF27CXIuDsYJ6mmvtBRlEuB2UVOqHG1tAXgZ7yIO+lw=
 github.com/klauspost/compress v1.15.15/go.mod h1:ZcK2JAFqKOpnBlxcLsJzYfrS9X1akm9fHZNnD9+Vo/4=
-github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
 github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
 github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
-github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
-github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
 github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
 github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
 github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
@@ -234,9 +230,8 @@ github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:
 github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
 github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
 github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
-github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
-github.com/sergi/go-diff v1.2.0 h1:XU+rvMAioB0UC3q1MFrIQy4Vo5/4VsRDQQXHsEya6xQ=
-github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
+github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8=
+github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I=
 github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0=
 github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho=
 github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
@@ -249,14 +244,14 @@ github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81P
 github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
 github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
 github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
-github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
-github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
+github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
+github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
 github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo=
 github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
 github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs=
 github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
-github.com/vektah/gqlparser/v2 v2.5.1 h1:ZGu+bquAY23jsxDRcYpWjttRZrUz07LbiY77gUOHcr4=
-github.com/vektah/gqlparser/v2 v2.5.1/go.mod h1:mPgqFBu/woKTVYWyNk8cO3kh4S/f4aRFZrvOnp3hmCs=
+github.com/vektah/gqlparser/v2 v2.5.16 h1:1gcmLTvs3JLKXckwCwlUagVn/IlV2bwqle0vJ0vy5p8=
+github.com/vektah/gqlparser/v2 v2.5.16/go.mod h1:1lz1OeCqgQbQepsGxPVywrjdBHW2T08PUS3pJqepRww=
 github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
 github.com/zclconf/go-cty v1.14.4 h1:uXXczd9QDGsgu0i/QFR/hzI5NYCHLf6NQw/atrbnhq8=
 github.com/zclconf/go-cty v1.14.4/go.mod h1:VvMs5i0vgZdhYawQNq5kePSpLAoz8u1xvZgrPIxfnZE=
@@ -384,11 +379,9 @@ google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqw
 google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=
 google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
-gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU=
 gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
-gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
 gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
diff --git a/schemahcl/extension.go b/schemahcl/extension.go
index 09d01399..16ac86c9 100644
--- a/schemahcl/extension.go
+++ b/schemahcl/extension.go
@@ -221,6 +221,11 @@ func (r *Resource) as(target any) error {
 		children := childrenOfType(r, childType)
 		extras.Children = append(extras.Children, children...)
 	}
+	// In case the resource contains a remain (DefaultExtension)
+	// field, attach to it the position.
+	if r.rang != nil && rem.Remain() != nil {
+		rem.Remain().SetRange(r.rang)
+	}
 	return nil
 }
 
diff --git a/schemahcl/schemahcl.go b/schemahcl/schemahcl.go
index 167f6726..c75ef282 100644
--- a/schemahcl/schemahcl.go
+++ b/schemahcl/schemahcl.go
@@ -368,7 +368,7 @@ func (s *State) Eval(parsed *hclparse.Parser, v any, input map[string]cty.Value)
 		blocks := make([]*hclsyntax.Block, 0, len(metaBlocks))
 		for name, bs := range metaBlocks {
 			for _, b := range bs {
-				nb, err := forEachBlocks(ctx, b)
+				nb, err := s.forEachBlocks(ctx, b)
 				if err != nil {
 					return err
 				}
@@ -849,18 +849,32 @@ func (s *State) writeAttr(attr *Attr, body *hclwrite.Body) error {
 		}
 		tokens := make([]hclwrite.Tokens, 0, attr.V.LengthInt())
 		for _, v := range attr.V.AsValueSlice() {
-			if v.Type().IsCapsuleType() {
-				ref, ok := v.EncapsulatedValue().(*Ref)
-				if !ok {
-					return fmt.Errorf("unsupported capsule type: %v", v.Type())
-				}
-				ts, err := hclRefTokens(ref.V)
+			if !v.Type().IsCapsuleType() {
+				tokens = append(tokens, hclwrite.TokensForValue(v))
+				continue
+			}
+			switch ev := v.EncapsulatedValue().(type) {
+			case *Ref:
+				ts, err := hclRefTokens(ev.V)
 				if err != nil {
 					return err
 				}
 				tokens = append(tokens, ts)
-			} else {
-				tokens = append(tokens, hclwrite.TokensForValue(v))
+			case *EnumString:
+				switch {
+				case ev.S != "" && ev.E != "":
+					return fmt.Errorf("enum string cannot have both a string and an expression, got: %#v", ev)
+				case ev.S != "":
+					tokens = append(tokens, hclwrite.TokensForValue(cty.StringVal(ev.S)))
+				case ev.E != "":
+					tokens = append(tokens, hclwrite.Tokens{
+						&hclwrite.Token{Type: hclsyntax.TokenIdent, Bytes: []byte(ev.E)},
+					})
+				default:
+					return fmt.Errorf("enum string must have either a string or an expression, got: %#v", ev)
+				}
+			default:
+				return fmt.Errorf("unsupported capsule type: %v", v.Type())
 			}
 		}
 		body.SetAttributeRaw(attr.K, hclList(tokens))
@@ -1006,7 +1020,7 @@ func hclList(items []hclwrite.Tokens) hclwrite.Tokens {
 	return t
 }
 
-func forEachBlocks(ctx *hcl.EvalContext, b *hclsyntax.Block) ([]*hclsyntax.Block, error) {
+func (s *State) forEachBlocks(ctx *hcl.EvalContext, b *hclsyntax.Block) ([]*hclsyntax.Block, error) {
 	forEach, diags := b.Body.Attributes[forEachAttr].Expr.Value(ctx)
 	if diags.HasErrors() {
 		return nil, diags
@@ -1025,7 +1039,7 @@ func forEachBlocks(ctx *hcl.EvalContext, b *hclsyntax.Block) ([]*hclsyntax.Block
 				"value": v,
 			}),
 		}
-		nb, err := copyBlock(nctx, b)
+		nb, err := s.copyBlock(nctx, b, []string{b.Type})
 		if err != nil {
 			return nil, fmt.Errorf("schemahcl: evaluate block for value %q: %w", v, err)
 		}
@@ -1034,7 +1048,7 @@ func forEachBlocks(ctx *hcl.EvalContext, b *hclsyntax.Block) ([]*hclsyntax.Block
 	return blocks, nil
 }
 
-func copyBlock(ctx *hcl.EvalContext, b *hclsyntax.Block) (*hclsyntax.Block, error) {
+func (s *State) copyBlock(ctx *hcl.EvalContext, b *hclsyntax.Block, scope []string) (*hclsyntax.Block, error) {
 	nb := &hclsyntax.Block{
 		Type:      b.Type,
 		Labels:    b.Labels,
@@ -1045,7 +1059,7 @@ func copyBlock(ctx *hcl.EvalContext, b *hclsyntax.Block) (*hclsyntax.Block, erro
 		},
 	}
 	for k, v := range b.Body.Attributes {
-		x, diags := v.Expr.Value(ctx)
+		x, diags := v.Expr.Value(s.mayScopeContext(ctx, append(scope, k)))
 		if diags.HasErrors() {
 			return nil, diags
 		}
@@ -1054,7 +1068,7 @@ func copyBlock(ctx *hcl.EvalContext, b *hclsyntax.Block) (*hclsyntax.Block, erro
 		nb.Body.Attributes[k] = &nv
 	}
 	for _, v := range b.Body.Blocks {
-		nv, err := copyBlock(ctx, v)
+		nv, err := s.copyBlock(ctx, v, append(scope, v.Type))
 		if err != nil {
 			return nil, err
 		}
diff --git a/schemahcl/spec.go b/schemahcl/spec.go
index 505eda05..d3f2158c 100644
--- a/schemahcl/spec.go
+++ b/schemahcl/spec.go
@@ -578,3 +578,25 @@ func RawAttr(k string, x string) *Attr {
 func RawExprValue(x *RawExpr) cty.Value {
 	return cty.CapsuleVal(ctyRawExpr, x)
 }
+
+// ctyEnumString is a capsule type for EnumString.
+var ctyEnumString = cty.Capsule("enum_string", reflect.TypeOf(EnumString{}))
+
+// EnumString is a helper type that represents
+// either an enum or a string value.
+type EnumString struct {
+	E, S string // Enum or string value.
+}
+
+// StringEnumsAttr is a helper method for constructing *schemahcl.Attr instances
+// that contain list of elements that their values can be either enum or string.
+func StringEnumsAttr(k string, elems ...*EnumString) *Attr {
+	vv := make([]cty.Value, len(elems))
+	for i, e := range elems {
+		vv[i] = cty.CapsuleVal(ctyEnumString, e)
+	}
+	return &Attr{
+		K: k,
+		V: cty.ListVal(vv),
+	}
+}
diff --git a/sql/internal/specutil/spec.go b/sql/internal/specutil/spec.go
index fb5e012f..82d7eb09 100644
--- a/sql/internal/specutil/spec.go
+++ b/sql/internal/specutil/spec.go
@@ -170,32 +170,6 @@ func QualifyObjects[T SchemaObject](specs []T) error {
 	return nil
 }
 
-// ColumnRefFinder returns a function that finds a column reference by its table and its schema.
-func ColumnRefFinder(specs []*sqlspec.Table) func(s, t, c string) (*schemahcl.Ref, error) {
-	var refs map[struct{ s, t string }]*sqlspec.Table
-	return func(s, t, c string) (*schemahcl.Ref, error) {
-		// Lazily initialize refs.
-		if refs == nil {
-			refs = make(map[struct{ s, t string }]*sqlspec.Table, len(specs))
-			for _, st := range specs {
-				ns, err := SchemaName(st.Schema)
-				if err != nil {
-					return nil, err
-				}
-				refs[struct{ s, t string }{s: ns, t: st.Name}] = st
-			}
-		}
-		r, ok := refs[struct{ s, t string }{s: s, t: t}]
-		if !ok {
-			return nil, fmt.Errorf("table %q.%q was not found", s, t)
-		}
-		if r.Qualifier != "" {
-			return QualifiedExternalColRef(c, r.Name, r.Qualifier), nil
-		}
-		return ExternalColumnRef(c, r.Name), nil
-	}
-}
-
 // QualifyReferences qualifies any reference with qualifier.
 func QualifyReferences(tableSpecs []*sqlspec.Table, realm *schema.Realm) error {
 	type cref struct{ s, t string }
diff --git a/sql/internal/sqlx/diff.go b/sql/internal/sqlx/diff.go
index 75b49477..8b393e13 100644
--- a/sql/internal/sqlx/diff.go
+++ b/sql/internal/sqlx/diff.go
@@ -82,7 +82,7 @@ type (
 	// If the DiffDriver implements the Normalizer interface, TableDiff normalizes its table
 	// inputs before starting the diff process.
 	Normalizer interface {
-		Normalize(from, to *schema.Table) error
+		Normalize(from, to *schema.Table, opts *schema.DiffOptions) error
 	}
 
 	// TableFinder wraps the FindTable method, providing more
@@ -297,7 +297,7 @@ func (d *Diff) tableDiff(from, to *schema.Table, opts *schema.DiffOptions) ([]sc
 	}
 	// Normalizing tables before starting the diff process.
 	if n, ok := d.DiffDriver.(Normalizer); ok {
-		if err := n.Normalize(from, to); err != nil {
+		if err := n.Normalize(from, to, opts); err != nil {
 			return nil, err
 		}
 	}
@@ -378,12 +378,12 @@ func addViewChange(v *schema.View) []schema.Change {
 
 // columnDiff returns the schema changes (if any) for migrating table columns.
 func (d *Diff) columnDiff(from, to *schema.Table, opts *schema.DiffOptions) ([]schema.Change, error) {
-	var changes []schema.Change
+	var all []schema.Change
 	// Drop or modify columns.
 	for _, c1 := range from.Columns {
 		c2, ok := to.Column(c1.Name)
 		if !ok {
-			changes = opts.AddOrSkip(changes, &schema.DropColumn{C: c1})
+			all = append(all, &schema.DropColumn{C: c1})
 			continue
 		}
 		change, err := d.ColumnChange(from, c1, c2)
@@ -391,7 +391,7 @@ func (d *Diff) columnDiff(from, to *schema.Table, opts *schema.DiffOptions) ([]s
 			return nil, err
 		}
 		if change != schema.NoChange {
-			changes = opts.AddOrSkip(changes, &schema.ModifyColumn{
+			all = append(all, &schema.ModifyColumn{
 				From:   c1,
 				To:     c2,
 				Change: change,
@@ -401,12 +401,22 @@ func (d *Diff) columnDiff(from, to *schema.Table, opts *schema.DiffOptions) ([]s
 	// Add columns.
 	for _, c1 := range to.Columns {
 		if _, ok := from.Column(c1.Name); !ok {
-			changes = opts.AddOrSkip(changes, &schema.AddColumn{
+			all = append(all, &schema.AddColumn{
 				C: c1,
 			})
 		}
 	}
-	return d.askForColumns(from, changes, opts)
+	var (
+		err     error
+		changes = make([]schema.Change, 0, len(all))
+	)
+	if all, err = d.askForColumns(from, all, opts); err != nil {
+		return nil, err
+	}
+	for _, c := range all {
+		changes = opts.AddOrSkip(changes, c)
+	}
+	return changes, nil
 }
 
 // pkDiff returns the schema changes (if any) for migrating table
@@ -435,8 +445,8 @@ func (d *Diff) pkDiff(from, to *schema.Table, opts *schema.DiffOptions) (changes
 // indexes from current state to the desired state.
 func (d *Diff) indexDiffT(from, to *schema.Table, opts *schema.DiffOptions) ([]schema.Change, error) {
 	var (
-		changes []schema.Change
-		exists  = make(map[*schema.Index]bool)
+		all    []schema.Change
+		exists = make(map[*schema.Index]bool)
 	)
 	// Drop or modify indexes.
 	for _, idx1 := range from.Indexes {
@@ -444,7 +454,7 @@ func (d *Diff) indexDiffT(from, to *schema.Table, opts *schema.DiffOptions) ([]s
 		// Found directly.
 		if ok {
 			if change := d.indexChange(idx1, idx2); change != schema.NoChange {
-				changes = opts.AddOrSkip(changes, &schema.ModifyIndex{
+				all = append(all, &schema.ModifyIndex{
 					From:   idx1,
 					To:     idx2,
 					Change: change,
@@ -461,7 +471,7 @@ func (d *Diff) indexDiffT(from, to *schema.Table, opts *schema.DiffOptions) ([]s
 			}
 		}
 		// Not found.
-		changes = opts.AddOrSkip(changes, &schema.DropIndex{I: idx1})
+		all = append(all, &schema.DropIndex{I: idx1})
 	}
 	// Add indexes.
 	for _, idx := range to.Indexes {
@@ -469,10 +479,20 @@ func (d *Diff) indexDiffT(from, to *schema.Table, opts *schema.DiffOptions) ([]s
 			continue
 		}
 		if _, ok := from.Index(idx.Name); !ok {
-			changes = opts.AddOrSkip(changes, &schema.AddIndex{I: idx})
+			all = append(all, &schema.AddIndex{I: idx})
 		}
 	}
-	return d.askForIndexes(from.Name, changes, opts)
+	var (
+		err     error
+		changes = make([]schema.Change, 0, len(all))
+	)
+	if all, err = d.askForIndexes(from.Name, all, opts); err != nil {
+		return nil, err
+	}
+	for _, c := range all {
+		changes = opts.AddOrSkip(changes, c)
+	}
+	return changes, nil
 }
 
 // viewDefChanged checks if the view definition has changed.
diff --git a/sql/migrate/dir.go b/sql/migrate/dir.go
index 75a605b3..af2e286c 100644
--- a/sql/migrate/dir.go
+++ b/sql/migrate/dir.go
@@ -483,11 +483,16 @@ func (d *MemDir) Checksum() (HashFile, error) {
 	return NewHashFile(files)
 }
 
+// NewVersion generates a new migration version.
+func NewVersion() string {
+	return time.Now().UTC().Format("20060102150405")
+}
+
 var (
 	// templateFunc contains the template.FuncMap for the DefaultFormatter.
 	templateFuncs = template.FuncMap{
 		"upper": strings.ToUpper,
-		"now":   func() string { return time.Now().UTC().Format("20060102150405") },
+		"now":   NewVersion,
 	}
 	// DefaultFormatter is a default implementation for Formatter.
 	DefaultFormatter = TemplateFormatter{
diff --git a/sql/migrate/migrate.go b/sql/migrate/migrate.go
index 0eccb8dc..e3f5feaa 100644
--- a/sql/migrate/migrate.go
+++ b/sql/migrate/migrate.go
@@ -135,9 +135,9 @@ func (f StateReaderFunc) ReadState(ctx context.Context) (*schema.Realm, error) {
 // List of migration planning modes.
 const (
 	PlanModeUnset        PlanMode = iota // Driver default.
-	PlanModeInPlace                      // Changes are applied inplace (e.g., 'schema diff').
-	PlanModeDeferred                     // Changes are planned for future applying (e.g., 'migrate diff').
-	PlanModeDump                         // Schema creation dump (e.g., 'schema inspect').
+	PlanModeInPlace                      // Changes are applied in place (e.g. 'schema diff').
+	PlanModeDeferred                     // Changes are planned for future applying (e.g. 'migrate diff').
+	PlanModeDump                         // Schema creation dump (e.g. 'schema inspect').
 	PlanModeUnsortedDump                 // Schema creation demo without sorting dependencies.
 )
 
diff --git a/sql/mysql/diff.go b/sql/mysql/diff.go
index c8978fd6..76fb0c3c 100644
--- a/sql/mysql/diff.go
+++ b/sql/mysql/diff.go
@@ -83,6 +83,9 @@ func (d *diff) TableAttrDiff(from, to *schema.Table) ([]schema.Change, error) {
 	if change := d.engineChange(from.Attrs, to.Attrs); change != noChange {
 		changes = append(changes, change)
 	}
+	if change := d.systemVerChange(from.Attrs, to.Attrs); change != noChange {
+		changes = append(changes, change)
+	}
 	if !d.SupportsCheck() && sqlx.Has(to.Attrs, &schema.Check{}) {
 		return nil, fmt.Errorf("version %q does not support CHECK constraints", d.V)
 	}
@@ -210,7 +213,10 @@ func (*diff) ReferenceChanged(from, to schema.ReferenceOption) bool {
 }
 
 // Normalize implements the sqlx.Normalizer interface.
-func (d *diff) Normalize(from, to *schema.Table) error {
+func (d *diff) Normalize(from, to *schema.Table, opts *schema.DiffOptions) error {
+	if opts.Mode.Is(schema.DiffModeNormalized) {
+		return nil // already normalized
+	}
 	indexes := make([]*schema.Index, 0, len(from.Indexes))
 	for _, idx := range from.Indexes {
 		// MySQL requires that foreign key columns be indexed; Therefore, if the child
@@ -334,6 +340,19 @@ func (*diff) engineChange(from, to []schema.Attr) schema.Change {
 	return noChange
 }
 
+// systemVerChange returns the schema change for migrating the system versioning
+// attributes if it was changed.
+func (d *diff) systemVerChange(from, to []schema.Attr) schema.Change {
+	switch fromHas, toHas := sqlx.Has(from, &SystemVersioned{}), sqlx.Has(to, &SystemVersioned{}); {
+	case fromHas && !toHas:
+		return &schema.DropAttr{A: &SystemVersioned{}}
+	case !fromHas && toHas:
+		return &schema.AddAttr{A: &SystemVersioned{}}
+	default:
+		return noChange
+	}
+}
+
 // charsetChange returns the schema change for migrating the collation if
 // it was changed, and it is not the default attribute inherited from its parent.
 func (*diff) charsetChange(from, top, to []schema.Attr) schema.Change {
diff --git a/sql/mysql/driver.go b/sql/mysql/driver.go
index bf437d16..e9de037d 100644
--- a/sql/mysql/driver.go
+++ b/sql/mysql/driver.go
@@ -58,7 +58,15 @@ func init() {
 		sqlclient.OpenerFunc(opener),
 		sqlclient.RegisterDriverOpener(Open),
 		sqlclient.RegisterCodec(MarshalHCL, EvalHCL),
-		sqlclient.RegisterFlavours("mysql+unix", "maria", "maria+unix", "mariadb", "mariadb+unix"),
+		sqlclient.RegisterFlavours("mysql+unix"),
+		sqlclient.RegisterURLParser(parser{}),
+	)
+	sqlclient.Register(
+		"mariadb",
+		sqlclient.OpenerFunc(opener),
+		sqlclient.RegisterDriverOpener(Open),
+		sqlclient.RegisterCodec(MarshalHCL, EvalMariaHCL),
+		sqlclient.RegisterFlavours("mariadb+unix", "maria", "maria+unix"),
 		sqlclient.RegisterURLParser(parser{}),
 	)
 }
diff --git a/sql/mysql/driver_oss.go b/sql/mysql/driver_oss.go
index e0125580..59126233 100644
--- a/sql/mysql/driver_oss.go
+++ b/sql/mysql/driver_oss.go
@@ -11,13 +11,14 @@ import (
 
 	"ariga.io/atlas/schemahcl"
 	"ariga.io/atlas/sql/internal/specutil"
+	"ariga.io/atlas/sql/internal/sqlx"
 	"ariga.io/atlas/sql/schema"
 	"ariga.io/atlas/sql/sqlspec"
 )
 
 var (
-	specOptions []schemahcl.Option
-	specFuncs   = &specutil.SchemaFuncs{
+	specOptions, mariaSpecOptions []schemahcl.Option
+	specFuncs                     = &specutil.SchemaFuncs{
 		Table: tableSpec,
 		View:  viewSpec,
 	}
@@ -31,6 +32,31 @@ func triggersSpec([]*schema.Trigger, *specutil.Doc) ([]*sqlspec.Trigger, error)
 	return nil, nil // unimplemented.
 }
 
+func (*inspect) tablesQuery(context.Context) string {
+	return tablesQuery
+}
+
+func (*inspect) tablesQueryArgs(context.Context) string {
+	return tablesQueryArgs
+}
+
+// newTable creates a new table with the given name and type.
+func (*inspect) newTable(name, _ string) *schema.Table {
+	return schema.NewTable(name)
+}
+
+func (s *state) tableAttr(*sqlx.Builder, schema.Change, schema.Attr) {
+	// unimplemented.
+}
+
+func convertTableAttrs(*sqlspec.Table, *schema.Table) error {
+	return nil // unimplemented.
+}
+
+func tableAttrsSpec(*schema.Table, *sqlspec.Table) {
+	// unimplemented.
+}
+
 func (*inspect) inspectViews(context.Context, *schema.Realm, *schema.InspectOptions) error {
 	return nil // unimplemented.
 }
diff --git a/sql/mysql/inspect.go b/sql/mysql/inspect.go
index e137a121..07662770 100644
--- a/sql/mysql/inspect.go
+++ b/sql/mysql/inspect.go
@@ -180,7 +180,7 @@ func (i *inspect) schemas(ctx context.Context, opts *schema.InspectRealmOption)
 func (i *inspect) tables(ctx context.Context, realm *schema.Realm, opts *schema.InspectOptions) error {
 	var (
 		args  []any
-		query = fmt.Sprintf(tablesQuery, nArgs(len(realm.Schemas)))
+		query = fmt.Sprintf(i.tablesQuery(ctx), nArgs(len(realm.Schemas)))
 	)
 	for _, s := range realm.Schemas {
 		args = append(args, s.Name)
@@ -189,7 +189,7 @@ func (i *inspect) tables(ctx context.Context, realm *schema.Realm, opts *schema.
 		for _, t := range opts.Tables {
 			args = append(args, t)
 		}
-		query = fmt.Sprintf(tablesQueryArgs, nArgs(len(realm.Schemas)), nArgs(len(opts.Tables)))
+		query = fmt.Sprintf(i.tablesQueryArgs(ctx), nArgs(len(realm.Schemas)), nArgs(len(opts.Tables)))
 	}
 	rows, err := i.QueryContext(ctx, query, args...)
 	if err != nil {
@@ -198,11 +198,11 @@ func (i *inspect) tables(ctx context.Context, realm *schema.Realm, opts *schema.
 	defer rows.Close()
 	for rows.Next() {
 		var (
-			defaultE                                                    sql.NullBool
-			autoinc                                                     sql.NullInt64
-			tSchema, name, charset, collation, comment, options, engine sql.NullString
+			defaultE                                                          sql.NullBool
+			autoinc                                                           sql.NullInt64
+			tSchema, name, charset, collation, comment, options, engine, ttyp sql.NullString
 		)
-		if err := rows.Scan(&tSchema, &name, &charset, &collation, &autoinc, &comment, &options, &engine, &defaultE); err != nil {
+		if err := rows.Scan(&tSchema, &name, &charset, &collation, &autoinc, &comment, &options, &engine, &defaultE, &ttyp); err != nil {
 			return fmt.Errorf("scan table information: %w", err)
 		}
 		if !sqlx.ValidString(tSchema) || !sqlx.ValidString(name) {
@@ -212,7 +212,7 @@ func (i *inspect) tables(ctx context.Context, realm *schema.Realm, opts *schema.
 		if !ok {
 			return fmt.Errorf("schema %q was not found in realm", tSchema.String)
 		}
-		t := &schema.Table{Name: name.String}
+		t := i.newTable(name.String, ttyp.String)
 		s.AddTables(t)
 		if sqlx.ValidString(charset) {
 			t.Attrs = append(t.Attrs, &schema.Charset{
@@ -729,7 +729,8 @@ SELECT
 	t1.TABLE_COMMENT,
 	t1.CREATE_OPTIONS,
 	t1.ENGINE,
-	t3.SUPPORT = 'DEFAULT' AS DEFAULT_ENGINE
+	t3.SUPPORT = 'DEFAULT' AS DEFAULT_ENGINE,
+	t1.TABLE_TYPE
 FROM
 	INFORMATION_SCHEMA.TABLES AS t1
 	LEFT JOIN INFORMATION_SCHEMA.COLLATIONS AS t2
@@ -752,7 +753,8 @@ SELECT
 	t1.TABLE_COMMENT,
 	t1.CREATE_OPTIONS,
 	t1.ENGINE,
-	t3.SUPPORT = 'DEFAULT' AS DEFAULT_ENGINE
+	t3.SUPPORT = 'DEFAULT' AS DEFAULT_ENGINE,
+	t1.TABLE_TYPE
 FROM
 	INFORMATION_SCHEMA.TABLES AS t1
 	JOIN INFORMATION_SCHEMA.COLLATIONS AS t2
@@ -854,6 +856,12 @@ type (
 		Default bool   // The default engine used by the server.
 	}
 
+	// SystemVersioned is an attribute attached to MariaDB tables indicates they are
+	// system versioned. See: https://mariadb.com/kb/en/system-versioned-tables
+	SystemVersioned struct {
+		schema.Attr
+	}
+
 	// OnUpdate attribute for columns with "ON UPDATE CURRENT_TIMESTAMP" as a default.
 	OnUpdate struct {
 		schema.Attr
diff --git a/sql/mysql/migrate.go b/sql/mysql/migrate.go
index 9462e726..96da3734 100644
--- a/sql/mysql/migrate.go
+++ b/sql/mysql/migrate.go
@@ -289,7 +289,7 @@ func (s *state) addTable(add *schema.AddTable) error {
 	if len(errs) > 0 {
 		return fmt.Errorf("create table %q: %s", add.T.Name, strings.Join(errs, ", "))
 	}
-	s.tableAttr(b, add, add.T.Attrs...)
+	s.tableAttrs(b, add, add.T.Attrs...)
 	s.append(&migrate.Change{
 		Cmd:     b.String(),
 		Source:  add,
@@ -357,8 +357,6 @@ func (s *state) modifyTable(modify *schema.ModifyTable) error {
 			changes[1] = append(changes[1], &schema.AddIndex{
 				I: change.To,
 			})
-		case *schema.DropAttr:
-			return fmt.Errorf("unsupported change type: %v", change.A)
 		default:
 			changes[1] = append(changes[1], change)
 		}
@@ -459,11 +457,11 @@ func (s *state) alterTable(t *schema.Table, changes []schema.Change) error {
 				b.P("DROP FOREIGN KEY").Ident(change.F.Symbol)
 				reverse = append(reverse, &schema.AddForeignKey{F: change.F})
 			case *schema.AddAttr:
-				s.tableAttr(b, change, change.A)
-				// Unsupported reverse operation.
-				reversible = false
+				s.tableAttrs(b, change, change.A)
+			case *schema.DropAttr:
+				s.tableAttrs(b, change, change.A)
 			case *schema.ModifyAttr:
-				s.tableAttr(b, change, change.To)
+				s.tableAttrs(b, change, change.To)
 				reverse = append(reverse, &schema.ModifyAttr{
 					From: change.To,
 					To:   change.From,
@@ -687,9 +685,9 @@ func (s *state) fks(commaF func(any, func(int, *sqlx.Builder) error) error, fks
 	})
 }
 
-// tableAttr writes the given table attribute to the SQL
+// tableAttrs writes the given table attributes to the SQL
 // statement builder when a table is created or altered.
-func (s *state) tableAttr(b *sqlx.Builder, c schema.Change, attrs ...schema.Attr) {
+func (s *state) tableAttrs(b *sqlx.Builder, c schema.Change, attrs ...schema.Attr) {
 	for _, a := range attrs {
 		switch a := a.(type) {
 		case *CreateOptions:
@@ -713,6 +711,9 @@ func (s *state) tableAttr(b *sqlx.Builder, c schema.Change, attrs ...schema.Attr
 			b.P("COLLATE", a.V)
 		case *schema.Comment:
 			b.P("COMMENT", quote(a.Text))
+		default:
+			// Driver/build specific handling.
+			s.tableAttr(b, c, a)
 		}
 	}
 }
diff --git a/sql/mysql/sqlspec.go b/sql/mysql/sqlspec.go
index 6cc8c51e..1dc7a76a 100644
--- a/sql/mysql/sqlspec.go
+++ b/sql/mysql/sqlspec.go
@@ -21,11 +21,11 @@ import (
 )
 
 // evalSpec evaluates an Atlas DDL document into v using the input.
-func evalSpec(p *hclparse.Parser, v any, input map[string]cty.Value) error {
+func evalSpec(state *schemahcl.State, p *hclparse.Parser, v any, input map[string]cty.Value) error {
 	switch v := v.(type) {
 	case *schema.Realm:
 		var d specutil.Doc
-		if err := hclState.Eval(p, &d, input); err != nil {
+		if err := state.Eval(p, &d, input); err != nil {
 			return err
 		}
 		if err := specutil.Scan(v,
@@ -45,7 +45,7 @@ func evalSpec(p *hclparse.Parser, v any, input map[string]cty.Value) error {
 		}
 	case *schema.Schema:
 		var d specutil.Doc
-		if err := hclState.Eval(p, &d, input); err != nil {
+		if err := state.Eval(p, &d, input); err != nil {
 			return err
 		}
 		if len(d.Schemas) != 1 {
@@ -65,7 +65,7 @@ func evalSpec(p *hclparse.Parser, v any, input map[string]cty.Value) error {
 	case schema.Schema, schema.Realm:
 		return fmt.Errorf("mysql: Eval expects a pointer: received %[1]T, expected *%[1]T", v)
 	default:
-		return hclState.Eval(p, v, input)
+		return state.Eval(p, v, input)
 	}
 	return nil
 }
@@ -79,19 +79,29 @@ func MarshalSpec(v any, marshaler schemahcl.Marshaler) ([]byte, error) {
 }
 
 var (
+	registrySpecs     = TypeRegistry.Specs()
+	sharedSpecOptions = []schemahcl.Option{
+		schemahcl.WithTypes("table.column.type", registrySpecs),
+		schemahcl.WithTypes("view.column.type", registrySpecs),
+		schemahcl.WithScopedEnums("view.check_option", schema.ViewCheckOptionLocal, schema.ViewCheckOptionCascaded),
+		schemahcl.WithScopedEnums("table.engine", EngineInnoDB, EngineMyISAM, EngineMemory, EngineCSV, EngineNDB),
+		schemahcl.WithScopedEnums("table.index.type", IndexTypeBTree, IndexTypeHash, IndexTypeFullText, IndexTypeSpatial),
+		schemahcl.WithScopedEnums("table.index.parser", IndexParserNGram, IndexParserMeCab),
+		schemahcl.WithScopedEnums("table.primary_key.type", IndexTypeBTree, IndexTypeHash, IndexTypeFullText, IndexTypeSpatial),
+		schemahcl.WithScopedEnums("table.column.as.type", stored, persistent, virtual),
+		schemahcl.WithScopedEnums("table.foreign_key.on_update", specutil.ReferenceVars...),
+		schemahcl.WithScopedEnums("table.foreign_key.on_delete", specutil.ReferenceVars...),
+	}
 	hclState = schemahcl.New(
 		append(
 			specOptions,
-			schemahcl.WithTypes("table.column.type", TypeRegistry.Specs()),
-			schemahcl.WithTypes("view.column.type", TypeRegistry.Specs()),
-			schemahcl.WithScopedEnums("view.check_option", schema.ViewCheckOptionLocal, schema.ViewCheckOptionCascaded),
-			schemahcl.WithScopedEnums("table.engine", EngineInnoDB, EngineMyISAM, EngineMemory, EngineCSV, EngineNDB),
-			schemahcl.WithScopedEnums("table.index.type", IndexTypeBTree, IndexTypeHash, IndexTypeFullText, IndexTypeSpatial),
-			schemahcl.WithScopedEnums("table.index.parser", IndexParserNGram, IndexParserMeCab),
-			schemahcl.WithScopedEnums("table.primary_key.type", IndexTypeBTree, IndexTypeHash, IndexTypeFullText, IndexTypeSpatial),
-			schemahcl.WithScopedEnums("table.column.as.type", stored, persistent, virtual),
-			schemahcl.WithScopedEnums("table.foreign_key.on_update", specutil.ReferenceVars...),
-			schemahcl.WithScopedEnums("table.foreign_key.on_delete", specutil.ReferenceVars...),
+			sharedSpecOptions...,
+		)...,
+	)
+	mariaHCLState = schemahcl.New(
+		append(
+			mariaSpecOptions,
+			sharedSpecOptions...,
 		)...,
 	)
 	// MarshalHCL marshals v into an Atlas HCL DDL document.
@@ -99,11 +109,17 @@ var (
 		return MarshalSpec(v, hclState)
 	})
 	// EvalHCL implements the schemahcl.Evaluator interface.
-	EvalHCL = schemahcl.EvalFunc(evalSpec)
-
-	// EvalHCLBytes is a helper that evaluates an HCL document from a byte slice instead
-	// of from an hclparse.Parser instance.
+	EvalHCL = schemahcl.EvalFunc(func(h *hclparse.Parser, v any, m map[string]cty.Value) error {
+		return evalSpec(hclState, h, v, m)
+	})
+	// EvalHCLBytes is a helper that evaluates an HCL document from a byte slice.
 	EvalHCLBytes = specutil.HCLBytesFunc(EvalHCL)
+	// EvalMariaHCL implements the schemahcl.Evaluator interface for MariaDB flavor.
+	EvalMariaHCL = schemahcl.EvalFunc(func(h *hclparse.Parser, v any, m map[string]cty.Value) error {
+		return evalSpec(mariaHCLState, h, v, m)
+	})
+	// EvalMariaHCLBytes is a helper that evaluates a MariaDB HCL document from a byte slice.
+	EvalMariaHCLBytes = specutil.HCLBytesFunc(EvalMariaHCL)
 )
 
 // convertTable converts a sqlspec.Table to a schema.Table. Table conversion is done without converting
@@ -133,7 +149,10 @@ func convertTable(spec *sqlspec.Table, parent *schema.Schema) (*schema.Table, er
 		}
 		t.AddAttrs(&Engine{V: v})
 	}
-	return t, err
+	if err := convertTableAttrs(spec, t); err != nil {
+		return nil, err
+	}
+	return t, nil
 }
 
 // convertView converts a sqlspec.View to a schema.View.
@@ -316,6 +335,7 @@ func tableSpec(t *schema.Table) (*sqlspec.Table, error) {
 		}
 		ts.Extra.Attrs = append(ts.Extra.Attrs, attr)
 	}
+	tableAttrsSpec(t, ts)
 	return ts, nil
 }
 
diff --git a/sql/postgres/convert.go b/sql/postgres/convert.go
index 8803c2a9..30bea838 100644
--- a/sql/postgres/convert.go
+++ b/sql/postgres/convert.go
@@ -288,13 +288,16 @@ func columnType(c *columnDesc) (schema.Type, error) {
 	case typeOID, typeRegClass, typeRegCollation, typeRegConfig, typeRegDictionary, typeRegNamespace,
 		typeRegOper, typeRegOperator, typeRegProc, typeRegProcedure, typeRegRole, typeRegType:
 		typ = &OIDType{T: t}
-	case TypeUserDefined:
-		typ = &UserDefinedType{T: c.fmtype}
 	case typeAny, typeAnyElement, typeAnyArray, typeAnyNonArray, typeAnyEnum, typeInternal,
 		typeRecord, typeTrigger, typeEventTrigger, typeVoid, typeUnknown:
 		typ = &PseudoType{T: t}
+	// TypeUserDefined or any other base type.
 	default:
-		typ = &schema.UnsupportedType{T: t}
+		ft := c.fmtype
+		if ft == "" {
+			ft = t
+		}
+		typ = &UserDefinedType{T: ft}
 	}
 	switch c.typtype {
 	case "d", "e":
diff --git a/sql/postgres/crdb.go b/sql/postgres/crdb.go
index 9c500348..f63f0b17 100644
--- a/sql/postgres/crdb.go
+++ b/sql/postgres/crdb.go
@@ -72,7 +72,7 @@ func (i *crdbInspect) InspectRealm(ctx context.Context, opts *schema.InspectReal
 }
 
 // Normalize implements the sqlx.Normalizer.
-func (cd *crdbDiff) Normalize(from, to *schema.Table) error {
+func (cd *crdbDiff) Normalize(from, to *schema.Table, _ *schema.DiffOptions) error {
 	cd.normalize(from)
 	cd.normalize(to)
 	return nil
diff --git a/sql/postgres/diff.go b/sql/postgres/diff.go
index 6e89fd28..4a253ac9 100644
--- a/sql/postgres/diff.go
+++ b/sql/postgres/diff.go
@@ -63,6 +63,11 @@ func (d *diff) TableAttrDiff(from, to *schema.Table) ([]schema.Change, error) {
 	if err := d.partitionChanged(from, to); err != nil {
 		return nil, err
 	}
+	change, err := d.tableAttrDiff(from, to)
+	if err != nil {
+		return nil, err
+	}
+	changes = append(changes, change...)
 	return append(changes, sqlx.CheckDiff(from, to, func(c1, c2 *schema.Check) bool {
 		return sqlx.Has(c1.Attrs, &NoInherit{}) == sqlx.Has(c2.Attrs, &NoInherit{})
 	})...), nil
diff --git a/sql/postgres/driver_oss.go b/sql/postgres/driver_oss.go
index 75e7aafe..1b756104 100644
--- a/sql/postgres/driver_oss.go
+++ b/sql/postgres/driver_oss.go
@@ -30,6 +30,34 @@ var (
 	}
 )
 
+// newTable creates a new table.
+func (*inspect) newTable(_ context.Context, name, _ string) *schema.Table {
+	return schema.NewTable(name) // Default implementation.
+}
+
+func tableAttrsSpec(*schema.Table, *sqlspec.Table) {
+	// unimplemented.
+}
+
+func convertTableAttrs(*sqlspec.Table, *schema.Table) error {
+	return nil // unimplemented.
+}
+
+// tableAttrDiff allows extending table attributes diffing with build-specific logic.
+func (*diff) tableAttrDiff(_, _ *schema.Table) ([]schema.Change, error) {
+	return nil, nil // unimplemented.
+}
+
+// addTableAttrs allows extending table attributes creation with build-specific logic.
+func (*state) addTableAttrs(_ *schema.AddTable) {
+	// unimplemented.
+}
+
+// alterTableAttr allows extending table attributes alteration with build-specific logic.
+func (s *state) alterTableAttr(*sqlx.Builder, *schema.ModifyAttr) {
+	// unimplemented.
+}
+
 func realmObjectsSpec(*doc, *schema.Realm) error {
 	return nil // unimplemented.
 }
@@ -50,7 +78,7 @@ func (*inspect) inspectTypes(context.Context, *schema.Realm, *schema.InspectOpti
 	return nil // unimplemented.
 }
 
-func (*inspect) inspectSequences(context.Context, *schema.Realm, *schema.InspectOptions) error {
+func (*inspect) inspectObjects(context.Context, *schema.Realm, *schema.InspectOptions) error {
 	return nil // unimplemented.
 }
 
@@ -242,6 +270,13 @@ func convertSequences(_ []*sqlspec.Table, seqs []*sqlspec.Sequence, _ *schema.Re
 	return nil
 }
 
+func convertPolicies(_ []*sqlspec.Table, ps []*policy, _ *schema.Realm) error {
+	if len(ps) > 0 {
+		return fmt.Errorf("postgres: policies are not supported by this version. Use: https://atlasgo.io/getting-started")
+	}
+	return nil
+}
+
 func convertExtensions(exs []*extension, _ *schema.Realm) error {
 	if len(exs) > 0 {
 		return fmt.Errorf("postgres: extensions are not supported by this version. Use: https://atlasgo.io/getting-started")
@@ -260,7 +295,7 @@ func normalizeRealm(*schema.Realm) error {
 	return nil
 }
 
-func qualifySeqRefs([]*sqlspec.Sequence, []*sqlspec.Table, *schema.Realm) error {
+func schemasObjectSpec(*doc, ...*schema.Schema) error {
 	return nil // unimplemented.
 }
 
@@ -374,10 +409,66 @@ func (*state) sortChanges(changes []schema.Change) []schema.Change {
 	return sqlx.SortChanges(changes, nil)
 }
 
-func detachCycles(changes []schema.Change) ([]schema.Change, error) {
+func (*state) detachCycles(changes []schema.Change) ([]schema.Change, error) {
 	return sqlx.DetachCycles(changes)
 }
 
 func excludeSpec(*sqlspec.Table, *sqlspec.Index, *schema.Index, *Constraint) error {
 	return nil // unimplemented.
 }
+
+const (
+	// Query to list tables information.
+	// Note, 'attrs' are not supported in this version.
+	tablesQuery = `
+SELECT
+	t3.oid,
+	t1.table_schema,
+	t1.table_name,
+	pg_catalog.obj_description(t3.oid, 'pg_class') AS comment,
+	t4.partattrs AS partition_attrs,
+	t4.partstrat AS partition_strategy,
+	pg_get_expr(t4.partexprs, t4.partrelid) AS partition_exprs,
+	'{}' AS attrs
+FROM
+	INFORMATION_SCHEMA.TABLES 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_partitioned_table AS t4 ON t4.partrelid = t3.oid
+	LEFT JOIN pg_depend AS t5 ON t5.classid = 'pg_catalog.pg_class'::regclass::oid AND t5.objid = t3.oid AND t5.deptype = 'e'
+WHERE
+	t1.table_type = 'BASE TABLE'
+	AND NOT COALESCE(t3.relispartition, false)
+	AND t1.table_schema IN (%s)
+	AND t5.objid IS NULL
+ORDER BY
+	t1.table_schema, t1.table_name
+`
+	// Query to list tables by their names.
+	// Note, 'attrs' are not supported in this version.
+	tablesQueryArgs = `
+SELECT
+	t3.oid,
+	t1.table_schema,
+	t1.table_name,
+	pg_catalog.obj_description(t3.oid, 'pg_class') AS comment,
+	t4.partattrs AS partition_attrs,
+	t4.partstrat AS partition_strategy,
+	pg_get_expr(t4.partexprs, t4.partrelid) AS partition_exprs,
+	'{}' AS attrs
+FROM
+	INFORMATION_SCHEMA.TABLES 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_partitioned_table AS t4 ON t4.partrelid = t3.oid
+	LEFT JOIN pg_depend AS t5 ON t5.classid = 'pg_catalog.pg_class'::regclass::oid AND t5.objid = t3.oid AND t5.deptype = 'e'
+WHERE
+	t1.table_type = 'BASE TABLE'
+	AND NOT COALESCE(t3.relispartition, false)
+	AND t1.table_schema IN (%s)
+	AND t1.table_name IN (%s)
+	AND t5.objid IS NULL
+ORDER BY
+	t1.table_schema, t1.table_name
+`
+)
diff --git a/sql/postgres/inspect.go b/sql/postgres/inspect.go
index 996549c1..8a63d3c4 100644
--- a/sql/postgres/inspect.go
+++ b/sql/postgres/inspect.go
@@ -72,7 +72,7 @@ func (i *inspect) InspectRealm(ctx context.Context, opts *schema.InspectRealmOpt
 			}
 		}
 		if mode.Is(schema.InspectObjects) {
-			if err := i.inspectSequences(ctx, r, nil); err != nil {
+			if err := i.inspectObjects(ctx, r, nil); err != nil {
 				return nil, err
 			}
 			if err := i.inspectRealmObjects(ctx, r, nil); err != nil {
@@ -168,7 +168,7 @@ func (i *inspect) InspectSchema(ctx context.Context, name string, opts *schema.I
 		}
 	}
 	if mode.Is(schema.InspectObjects) {
-		if err := i.inspectSequences(ctx, r, opts); err != nil {
+		if err := i.inspectObjects(ctx, r, opts); err != nil {
 			return nil, err
 		}
 	}
@@ -232,10 +232,10 @@ func (i *inspect) tables(ctx context.Context, realm *schema.Realm, opts *schema.
 	defer rows.Close()
 	for rows.Next() {
 		var (
-			oid                                                     sql.NullInt64
-			tSchema, name, comment, partattrs, partstart, partexprs sql.NullString
+			oid                                                            sql.NullInt64
+			tSchema, name, comment, partattrs, partstart, partexprs, extra sql.NullString
 		)
-		if err := rows.Scan(&oid, &tSchema, &name, &comment, &partattrs, &partstart, &partexprs); err != nil {
+		if err := rows.Scan(&oid, &tSchema, &name, &comment, &partattrs, &partstart, &partexprs, &extra); err != nil {
 			return fmt.Errorf("scan table information: %w", err)
 		}
 		if !sqlx.ValidString(tSchema) || !sqlx.ValidString(name) {
@@ -245,7 +245,7 @@ func (i *inspect) tables(ctx context.Context, realm *schema.Realm, opts *schema.
 		if !ok {
 			return fmt.Errorf("schema %q was not found in realm", tSchema.String)
 		}
-		t := schema.NewTable(name.String)
+		t := i.newTable(ctx, name.String, extra.String)
 		s.AddTables(t)
 		if oid.Valid {
 			t.AddAttrs(&OID{V: oid.Int64})
@@ -1517,54 +1517,6 @@ WHERE
 ORDER BY
     nspname`
 
-	// Query to list table information.
-	tablesQuery = `
-SELECT
-	t3.oid,
-	t1.table_schema,
-	t1.table_name,
-	pg_catalog.obj_description(t3.oid, 'pg_class') AS comment,
-	t4.partattrs AS partition_attrs,
-	t4.partstrat AS partition_strategy,
-	pg_get_expr(t4.partexprs, t4.partrelid) AS partition_exprs
-FROM
-	INFORMATION_SCHEMA.TABLES 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_partitioned_table AS t4 ON t4.partrelid = t3.oid
-	LEFT JOIN pg_depend AS t5 ON t5.classid = 'pg_catalog.pg_class'::regclass::oid AND t5.objid = t3.oid AND t5.deptype = 'e'
-WHERE
-	t1.table_type = 'BASE TABLE'
-	AND NOT COALESCE(t3.relispartition, false)
-	AND t1.table_schema IN (%s)
-	AND t5.objid IS NULL
-ORDER BY
-	t1.table_schema, t1.table_name
-`
-	tablesQueryArgs = `
-SELECT
-	t3.oid,
-	t1.table_schema,
-	t1.table_name,
-	pg_catalog.obj_description(t3.oid, 'pg_class') AS comment,
-	t4.partattrs AS partition_attrs,
-	t4.partstrat AS partition_strategy,
-	pg_get_expr(t4.partexprs, t4.partrelid) AS partition_exprs
-FROM
-	INFORMATION_SCHEMA.TABLES 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_partitioned_table AS t4 ON t4.partrelid = t3.oid
-	LEFT JOIN pg_depend AS t5 ON t5.classid = 'pg_catalog.pg_class'::regclass::oid AND t5.objid = t3.oid AND t5.deptype = 'e'
-WHERE
-	t1.table_type = 'BASE TABLE'
-	AND NOT COALESCE(t3.relispartition, false)
-	AND t1.table_schema IN (%s)
-	AND t1.table_name IN (%s)
-	AND t5.objid IS NULL
-ORDER BY
-	t1.table_schema, t1.table_name
-`
 	// Query to list table columns.
 	columnsQuery = `
 SELECT
diff --git a/sql/postgres/migrate.go b/sql/postgres/migrate.go
index fe8b5a65..ee994085 100644
--- a/sql/postgres/migrate.go
+++ b/sql/postgres/migrate.go
@@ -78,7 +78,7 @@ func (s *state) plan(changes []schema.Change) error {
 		return err
 	}
 	if s.PlanOptions.Mode != migrate.PlanModeUnsortedDump {
-		if planned, err = detachCycles(planned); err != nil {
+		if planned, err = s.detachCycles(planned); err != nil {
 			return err
 		}
 		planned = s.sortChanges(planned)
@@ -287,6 +287,7 @@ func (s *state) addTable(add *schema.AddTable) error {
 		}
 	}
 	s.addComments(add, add.T)
+	s.addTableAttrs(add)
 	return nil
 }
 
@@ -339,11 +340,23 @@ func (s *state) modifyTable(modify *schema.ModifyTable) error {
 	)
 	for _, change := range skipAutoChanges(modify.Changes) {
 		switch change := change.(type) {
-		case *schema.AddAttr, *schema.ModifyAttr:
+		case *schema.ModifyAttr:
+			if _, ok := change.From.(*schema.Comment); !ok {
+				alter = append(alter, change)
+				continue
+			}
 			from, to, err := commentChange(change)
 			if err != nil {
 				return err
 			}
+			// Comments are not part of the ALTER command.
+			changes = append(changes, s.tableComment(modify, modify.T, to, from))
+		case *schema.AddAttr:
+			from, to, err := commentChange(change)
+			if err != nil {
+				return err
+			}
+			// Comments are not part of the ALTER command.
 			changes = append(changes, s.tableComment(modify, modify.T, to, from))
 		case *schema.DropAttr:
 			return fmt.Errorf("unsupported change type: %T", change)
@@ -613,6 +626,12 @@ func (s *state) alterTable(t *schema.Table, changes []schema.Change) error {
 					From: change.To,
 					To:   change.From,
 				})
+			case *schema.ModifyAttr:
+				s.alterTableAttr(b, change)
+				reverse = append(reverse, &schema.ModifyAttr{
+					From: change.To,
+					To:   change.From,
+				})
 			}
 			return nil
 		})
diff --git a/sql/postgres/sqlspec.go b/sql/postgres/sqlspec.go
index 65a73cff..32430c7d 100644
--- a/sql/postgres/sqlspec.go
+++ b/sql/postgres/sqlspec.go
@@ -34,6 +34,7 @@ type (
 		Procs         []*sqlspec.Func     `spec:"procedure"`
 		Aggregates    []*aggregate        `spec:"aggregate"`
 		Triggers      []*sqlspec.Trigger  `spec:"trigger"`
+		Policies      []*policy           `spec:"policy"`
 		EventTriggers []*eventTrigger     `spec:"event_trigger"`
 		Extensions    []*extension        `spec:"extension"`
 		Schemas       []*sqlspec.Schema   `spec:"schema"`
@@ -105,6 +106,15 @@ type (
 		// are appended after the function arguments.
 		schemahcl.DefaultExtension
 	}
+
+	// Policy defines row-level security policy for a table.
+	// See: https://www.postgresql.org/docs/current/view-pg-policies.html.
+	policy struct {
+		Name string         `spec:",name"`
+		On   *schemahcl.Ref `spec:"on"`
+		// Rest of the attributes are appended after the table reference.
+		schemahcl.DefaultExtension
+	}
 )
 
 // merge merges the doc d1 into d.
@@ -121,6 +131,7 @@ func (d *doc) merge(d1 *doc) {
 	d.Sequences = append(d.Sequences, d1.Sequences...)
 	d.Extensions = append(d.Extensions, d1.Extensions...)
 	d.Triggers = append(d.Triggers, d1.Triggers...)
+	d.Policies = append(d.Policies, d1.Policies...)
 	d.EventTriggers = append(d.EventTriggers, d1.EventTriggers...)
 	d.Materialized = append(d.Materialized, d1.Materialized...)
 }
@@ -188,6 +199,7 @@ func (a *aggregate) SchemaRef() *schemahcl.Ref { return a.Schema }
 func init() {
 	schemahcl.Register("enum", &enum{})
 	schemahcl.Register("domain", &domain{})
+	schemahcl.Register("policy", &policy{})
 	schemahcl.Register("composite", &composite{})
 	schemahcl.Register("aggregate", &aggregate{})
 	schemahcl.Register("extension", &extension{})
@@ -214,6 +226,9 @@ func evalSpec(p *hclparse.Parser, v any, input map[string]cty.Value) error {
 		if err := convertSequences(d.Tables, d.Sequences, v); err != nil {
 			return err
 		}
+		if err := convertPolicies(d.Tables, d.Policies, v); err != nil {
+			return err
+		}
 		if err := convertExtensions(d.Extensions, v); err != nil {
 			return err
 		}
@@ -244,6 +259,9 @@ func evalSpec(p *hclparse.Parser, v any, input map[string]cty.Value) error {
 		if err := convertSequences(d.Tables, d.Sequences, r); err != nil {
 			return err
 		}
+		if err := convertPolicies(d.Tables, d.Policies, r); err != nil {
+			return err
+		}
 		// Extensions are skipped in schema scope.
 		if err := normalizeRealm(r); err != nil {
 			return err
@@ -271,6 +289,9 @@ func MarshalSpec(v any, marshaler schemahcl.Marshaler) ([]byte, error) {
 		}
 		ts = trs
 		d.merge(d1)
+		if err := schemasObjectSpec(&d, rv); err != nil {
+			return nil, err
+		}
 	case *schema.Realm:
 		for _, s := range rv.Schemas {
 			d1, trs, err := schemaSpec(s)
@@ -316,9 +337,6 @@ func MarshalSpec(v any, marshaler schemahcl.Marshaler) ([]byte, error) {
 		if err := specutil.QualifyReferences(d.Tables, rv); err != nil {
 			return nil, err
 		}
-		if err := qualifySeqRefs(d.Sequences, d.Tables, rv); err != nil {
-			return nil, err
-		}
 	default:
 		return nil, fmt.Errorf("specutil: failed marshaling spec. %T is not supported", v)
 	}
@@ -376,6 +394,9 @@ func convertTable(spec *sqlspec.Table, parent *schema.Schema) (*schema.Table, er
 	if err := convertPartition(spec.Extra, t); err != nil {
 		return nil, err
 	}
+	if err := convertTableAttrs(spec, t); err != nil {
+		return nil, err
+	}
 	return t, nil
 }
 
@@ -782,6 +803,7 @@ func tableSpec(t *schema.Table) (*sqlspec.Table, error) {
 	if p := (Partition{}); sqlx.Has(t.Attrs, &p) {
 		spec.Extra.Children = append(spec.Extra.Children, fromPartition(p))
 	}
+	tableAttrsSpec(t, spec)
 	return spec, nil
 }
 
diff --git a/sql/schema/dsl.go b/sql/schema/dsl.go
index 9f4a3b76..28a98a66 100644
--- a/sql/schema/dsl.go
+++ b/sql/schema/dsl.go
@@ -6,6 +6,8 @@ package schema
 
 import (
 	"reflect"
+	"slices"
+	"strings"
 )
 
 // The functions and methods below provide a DSL for creating schema resources using
@@ -253,9 +255,55 @@ func (t *Table) AddAttrs(attrs ...Attr) *Table {
 // AddDeps adds the given objects as dependencies to the view.
 func (t *Table) AddDeps(objs ...Object) *Table {
 	t.Deps = append(t.Deps, objs...)
+	addRefs(t, objs)
 	return t
 }
 
+// RefsAdder wraps the AddRefs method. Objects that implemented this method
+// will get their dependent objects automatically set by their AddDeps calls.
+type RefsAdder interface {
+	AddRefs(...Object)
+}
+
+// addRefs adds the dependent objects to all objects it references.
+func addRefs(dependent Object, refs []Object) {
+	for _, o := range refs {
+		if r, ok := o.(RefsAdder); ok {
+			r.AddRefs(dependent)
+		}
+	}
+}
+
+// sortRefs maintains consistent dependents list.
+func sortRefs(refs []Object) {
+	slices.SortFunc(refs, func(a, b Object) int {
+		typeA, typeB := reflect.TypeOf(a), reflect.TypeOf(b)
+		if typeA != typeB {
+			return strings.Compare(typeA.String(), typeB.String())
+		}
+		switch o1 := a.(type) {
+		case *Table:
+			return strings.Compare(o1.Name, b.(*Table).Name)
+		case *View:
+			return strings.Compare(o1.Name, b.(*View).Name)
+		case *Trigger:
+			return strings.Compare(o1.Name, b.(*Trigger).Name)
+		case *Func:
+			return strings.Compare(o1.Name, b.(*Func).Name)
+		case *Proc:
+			return strings.Compare(o1.Name, b.(*Proc).Name)
+		default:
+			return 0
+		}
+	})
+}
+
+// AddRefs adds references to the table.
+func (t *Table) AddRefs(refs ...Object) {
+	t.Refs = append(t.Refs, refs...)
+	sortRefs(t.Refs)
+}
+
 // NewView creates a new View.
 func NewView(name, def string) *View {
 	return &View{Name: name, Def: def}
@@ -295,9 +343,16 @@ func (v *View) AddAttrs(attrs ...Attr) *View {
 // AddDeps adds the given objects as dependencies to the view.
 func (v *View) AddDeps(objs ...Object) *View {
 	v.Deps = append(v.Deps, objs...)
+	addRefs(v, objs)
 	return v
 }
 
+// AddRefs adds references to the view.
+func (v *View) AddRefs(refs ...Object) {
+	v.Refs = append(v.Refs, refs...)
+	sortRefs(v.Refs)
+}
+
 // AddIndexes appends the given indexes to the table index list.
 func (v *View) AddIndexes(indexes ...*Index) *View {
 	for _, idx := range indexes {
@@ -849,15 +904,29 @@ func (f *ForeignKey) SetOnDelete(o ReferenceOption) *ForeignKey {
 // AddDeps adds the given objects as dependencies to the function.
 func (f *Func) AddDeps(objs ...Object) *Func {
 	f.Deps = append(f.Deps, objs...)
+	addRefs(f, objs)
 	return f
 }
 
+// AddRefs adds references to the function.
+func (f *Func) AddRefs(refs ...Object) {
+	f.Refs = append(f.Refs, refs...)
+	sortRefs(f.Refs)
+}
+
 // AddDeps adds the given objects as dependencies to the procedure.
 func (p *Proc) AddDeps(objs ...Object) *Proc {
 	p.Deps = append(p.Deps, objs...)
+	addRefs(p, objs)
 	return p
 }
 
+// AddRefs adds references to the procedure.
+func (p *Proc) AddRefs(refs ...Object) {
+	p.Refs = append(p.Refs, refs...)
+	sortRefs(p.Refs)
+}
+
 // ReplaceOrAppend searches an attribute of the same type as v in
 // the list and replaces it. Otherwise, v is appended to the list.
 func ReplaceOrAppend(attrs *[]Attr, v Attr) {
diff --git a/sql/schema/migrate.go b/sql/schema/migrate.go
index 6335c40e..658294cd 100644
--- a/sql/schema/migrate.go
+++ b/sql/schema/migrate.go
@@ -280,7 +280,7 @@ type (
 
 	// DropForeignKey describes a foreign-key removal change.
 	DropForeignKey struct {
-		F *ForeignKey
+		F     *ForeignKey
 		Extra []Clause // Extra clauses and options.
 	}
 
@@ -388,6 +388,13 @@ const (
 	ChangeDeleteAction
 )
 
+// List of diff modes.
+const (
+	DiffModeUnset         DiffMode = iota // Default, backwards compatability.
+	DiffModeNotNormalized                 // Diff objects are considered to be in not normalized state.
+	DiffModeNormalized                    // Diff objects are considered to be in normalized state.
+)
+
 // Is reports whether c is match the given change kind.
 func (k ChangeKind) Is(c ChangeKind) bool {
 	return k == c || k&c != 0
@@ -413,12 +420,18 @@ type (
 		TableDiff(from, to *Table, opts ...DiffOption) ([]Change, error)
 	}
 
+	// DiffMode defines the diffing mode, e.g. if objects are normalized or not.
+	DiffMode uint8
+
 	// DiffOptions defines the standard and per-driver configuration
 	// for the schema diffing process.
 	DiffOptions struct {
 		// SkipChanges defines a list of change types to skip.
 		SkipChanges []Change
 
+		// DiffMode defines the diffing mode.
+		Mode DiffMode
+
 		// Extra defines per-driver configuration. If not
 		// nil, should be set to schemahcl.Extension.
 		Extra any // avoid circular dependency with schemahcl.
@@ -432,6 +445,11 @@ type (
 	DiffOption func(*DiffOptions)
 )
 
+// Is reports whether m is match the given mode.
+func (m DiffMode) Is(m1 DiffMode) bool {
+	return m == m1 || m&m1 != 0
+}
+
 // NewDiffOptions creates a new DiffOptions from the given configuration.
 func NewDiffOptions(opts ...DiffOption) *DiffOptions {
 	o := &DiffOptions{}
@@ -451,6 +469,16 @@ func DiffSkipChanges(changes ...Change) DiffOption {
 	}
 }
 
+// DiffNormalized returns a DiffOption that sets DiffMode to DiffModeNormalized,
+// indicating the Differ should consider input objects as normalized, For example:
+//
+//	DiffNormalized()
+func DiffNormalized() DiffOption {
+	return func(o *DiffOptions) {
+		o.Mode = DiffModeNormalized
+	}
+}
+
 // Skipped reports whether the given change should be skipped.
 func (o *DiffOptions) Skipped(c Change) bool {
 	for _, s := range o.SkipChanges {
diff --git a/sql/schema/schema.go b/sql/schema/schema.go
index ec49bc43..39f6aae9 100644
--- a/sql/schema/schema.go
+++ b/sql/schema/schema.go
@@ -43,6 +43,7 @@ type (
 		Attrs       []Attr     // Attrs, constraints and options.
 		Triggers    []*Trigger // Triggers on the table.
 		Deps        []Object   // Objects this table depends on.
+		Refs        []Object   // Objects that depends on this table.
 	}
 
 	// A View represents a view definition.
@@ -55,6 +56,7 @@ type (
 		Indexes  []*Index   // Indexes on materialized view.
 		Triggers []*Trigger // Triggers on the view.
 		Deps     []Object   // Objects this view depends on.
+		Refs     []Object   // Objects that depends on this view.
 	}
 
 	// A Column represents a column definition.
@@ -124,6 +126,7 @@ type (
 		Body       string         // Trigger body only.
 		Attrs      []Attr         // WHEN, REFERENCING, etc.
 		Deps       []Object       // Objects this trigger depends on.
+		Refs       []Object       // Objects that depend on this trigger.
 	}
 
 	// TriggerTime represents the trigger action time.
@@ -148,6 +151,7 @@ type (
 		Lang   string   // Language (e.g. SQL, PL/pgSQL, etc.).
 		Attrs  []Attr   // Extra driver specific attributes.
 		Deps   []Object // Objects this function depends on.
+		Refs   []Object // Objects that depend on this function.
 	}
 
 	// Proc represents a procedure definition.
@@ -159,6 +163,7 @@ type (
 		Lang   string   // Language (e.g. SQL, PL/pgSQL, etc.).
 		Attrs  []Attr   // Extra driver specific attributes.
 		Deps   []Object // Objects this function depends on.
+		Refs   []Object // Objects that depend on this Proc.
 	}
 
 	// A FuncArg represents a single function argument.
diff --git a/sql/sqlite/convert.go b/sql/sqlite/convert.go
index f5c18604..55ef8cb0 100644
--- a/sql/sqlite/convert.go
+++ b/sql/sqlite/convert.go
@@ -95,7 +95,7 @@ func ParseType(c string) (schema.Type, error) {
 			ct.Size = int(p)
 		}
 		return ct, nil
-	case "json":
+	case "json", "jsonb":
 		return &schema.JSONType{T: t}, nil
 	case "date", "datetime", "time", "timestamp":
 		return &schema.TimeType{T: t}, nil
diff --git a/sql/sqlite/diff.go b/sql/sqlite/diff.go
index 9d698c6b..74037e41 100644
--- a/sql/sqlite/diff.go
+++ b/sql/sqlite/diff.go
@@ -167,7 +167,7 @@ func (*diff) ReferenceChanged(from, to schema.ReferenceOption) bool {
 }
 
 // Normalize implements the sqlx.Normalizer interface.
-func (d *diff) Normalize(from, to *schema.Table) error {
+func (d *diff) Normalize(from, to *schema.Table, _ *schema.DiffOptions) error {
 	used := make([]bool, len(to.ForeignKeys))
 	// In SQLite, there is no easy way to get the foreign-key constraint
 	// name, except for parsing the CREATE statement. Therefore, we check
diff --git a/sql/sqlite/sqlspec.go b/sql/sqlite/sqlspec.go
index 7581128e..5c469f58 100644
--- a/sql/sqlite/sqlspec.go
+++ b/sql/sqlite/sqlspec.go
@@ -280,6 +280,7 @@ var TypeRegistry = schemahcl.NewRegistry(
 		schemahcl.NewTypeSpec("datetime"),
 		schemahcl.NewTypeSpec("json"),
 		schemahcl.NewTypeSpec("uuid"),
+		schemahcl.NewTypeSpec("jsonb"),
 	),
 )
 


Test patch:
diff --git a/doc/md/guides/testing/testing-views.mdx b/doc/md/guides/testing/testing-views.mdx
new file mode 100644
index 00000000..c7be3c27
--- /dev/null
+++ b/doc/md/guides/testing/testing-views.mdx
@@ -0,0 +1,208 @@
+---
+id: testing-views
+title: Testing Database Views
+slug: /guides/testing/views
+---
+
+Testing your database schema and migrations is crucial to ensure code behaves as expected, catch bugs early,
+and prevent regressions. Databases enforce logic, constraints, and complex relationships,
+so testing ensures these elements work correctly and remain intact after changes.
+
+In this guide we will learn how to use Atlas's [`schema test`](/testing/schema) command to test database views.
+
+## Database Views
+A `view` is a virtual table in the database, defined by a statement that queries rows from one or more existing tables or views.
+
+:::info [Atlas Pro Feature](/features#pro-plan)
+Views are currently available only to [Atlas Pro users](/features#pro-plan). To use this feature, run:
+```
+atlas login
+```
+:::
+
+## Project Setup
+
+### Schema File
+For this example, let's assume we have the following schema, including a view:
+
+```hcl title="schema.hcl"
+schema "public" {}
+
+table "users" {
+  schema = schema.public
+  column "id" {
+    type = int
+  }
+  column "name" {
+    type = text
+  }
+  column "ssn" {
+    type = varchar(9)
+  }
+}
+
+// highlight-start
+view "clean_users" {
+  schema = schema.public
+  column "id" {
+    type = int
+  }
+  column "name" {
+    type = text
+  }
+  as         = <<-SQL
+  SELECT u.id, u.name
+    FROM ${table.users.name} AS u
+  SQL
+  depends_on = [table.users]
+  comment    = "A view to select active users without sensitive data"
+// highlight-end
+}
+```
+
+The schema has a simple `users` table, with `id`, `name`, and `ssn` as columns.
+The view, `clean_users`, selects the `id` and `name` columns from `users`, ensuring we
+aren't selecting any sensitive data (SSNs).
+
+### Project File
+Before we begin testing, create a [project file](/atlas-schema/projects#project-files) named
+`atlas.hcl`.
+
+In this file we will create an environment, specify the source of our schema,
+and a URL for our [dev database](/concepts/dev-database).
+
+We will also create a file named `schema.test.hcl` to write our tests, and
+add it to the `atlas.hcl` file in the test block.
+
+```hcl title="atlas.hcl"
+env "dev" {
+  src = "file://schema.hcl"
+  dev = "docker://postgres/15/dev?search_path=public"
+
+  # Test configuration for local development.
+  test {
+    schema {
+      src = ["schema.test.hcl"]
+    }
+  }
+}
+```
+
+## Writing Tests
+
+### Simple Test
+Let's start off with a simple test that will do the following:
+1. Seed data into the `users` table.
+2. Run a query on the `clean_users` view.
+3. Ensure the data is retrieved as expected.
+
+```hcl title="schema.test.hcl"
+test "schema" "view" {
+  # Seeding to test view.
+  exec {
+    sql = "INSERT INTO users (id, name) VALUES (1, 'Alice'), (2, 'Bob'), (3, 'Charlie');"
+  }
+  log {
+    message = "Seeded the user's table"
+  }
+
+  # Expected exec to pass.
+  exec {
+    sql = <<SQL
+    SELECT id, name
+    FROM clean_users;
+    SQL
+  }
+  log {
+    message = "Tested the view"
+  }
+
+  # Validates data.
+  exec {
+    sql = "SELECT id, name FROM clean_users;"
+    format = table
+    output = <<TAB
+ id |  name
+----+---------
+ 1  | Alice
+ 2  | Bob
+ 3  | Charlie
+TAB
+  }
+  log {
+    message = "Table is as expected"
+  }
+}
+
+```
+
+:::info
+To learn more about how to write tests, view the [testing docs](/testing/schema).
+:::
+
+To run this test, run:
+
+```
+atlas schema test --env dev
+```
+
+The output should be similar to:
+
+```testoutput title="Test Output"
+-- PASS: view (1ms)
+    schema.test.hcl:6: Seeded the user's table
+    schema.test.hcl:17: Tested the view
+    schema.test.hcl:33: Table is as expected
+PASS
+```
+
+### Table Driven Test
+
+Another alternative is to write a [table driven test](/testing/schema#table-driven-tests). This
+test uses the `for_each` meta-argument, which accepts a map or a set of values and is used to generate
+a test case for each item in the set or map.
+
+Following similar logic to the example above, our table drive test will:
+1. Seed data into the `users` table.
+2. Run a query on the `clean_users` view.
+3. Ensure the data is retrieved as expected.
+
+```hcl title="schema.test.hcl"
+test "schema" "view" {
+  for_each = [
+    {id = 1, name = "Alice"},
+    {id = 2, name = "Bob"},
+    {id = 3, name = "Charlie"}
+  ]
+  # Seed the `users` table.
+  exec {
+    sql = "INSERT INTO users (id, name) VALUES (1, 'Alice'), (2, 'Bob'), (3, 'Charlie');"
+  }
+  # Query the `clean_users` view.
+  exec {
+    sql = "SELECT id, name FROM clean_users WHERE id IN (1, 2, 3);"
+  }
+  # Check each ID returns the right user.
+  log {
+    message = "Testing ${each.value.id} -> ${each.value.name}"
+  }
+}
+```
+
+To run this test, run:
+
+```
+atlas schema test --env dev
+```
+
+The output should be similar to:
+
+```testoutput title="Test Output"
+-- PASS: view/1 (3ms)
+    schema.test.hcl:62: Testing 1 -> Alice
+-- PASS: view/2 (633µs)
+    schema.test.hcl:62: Testing 2 -> Bob
+-- PASS: view/3 (643µs)
+    schema.test.hcl:62: Testing 3 -> Charlie
+PASS
+```
diff --git a/doc/md/testing/migrate.mdx b/doc/md/testing/migrate.mdx
index 99f7c7b4..ecc0a30a 100644
--- a/doc/md/testing/migrate.mdx
+++ b/doc/md/testing/migrate.mdx
@@ -11,8 +11,8 @@ The `atlas migrate test` command allows you to write tests for your schema migra
 enables you to test logic in your migrations in a minimal and straightforward way. The command is similar to
 [`atlas schema test`](/testing/schema) but is focused on testing schema migrations.
 
-:::info Login Required
-Migrations testing works only for logged-in users, free and paid. Use the following command to use this feature:
+:::info [Atlas Pro Feature](/features#pro-plan)
+Migrations testing works only for [Atlas Pro users](/features#pro-plan), free and paid. Use the following command to use this feature:
 
 ```shell
 atlas login
diff --git a/doc/md/testing/schema.mdx b/doc/md/testing/schema.mdx
index fab4f06c..2cc9946c 100644
--- a/doc/md/testing/schema.mdx
+++ b/doc/md/testing/schema.mdx
@@ -10,8 +10,8 @@ The `atlas schema test` command allows you to write tests for your database sche
 paradigms.
 
 
-:::info Login Required
-Schema testing works only for logged-in users, free and paid. Use the following command to use this feature:
+:::info [Atlas Pro Feature](/features#pro-plan)
+Schema testing works only for [Atlas Pro users](/features#pro-plan), free and paid. Use the following command to use this feature:
 
 ```shell
 atlas login
diff --git a/internal/integration/testdata/mysql/cli-migrate-diff-mode-normalized.txt b/internal/integration/testdata/mysql/cli-migrate-diff-mode-normalized.txt
new file mode 100644
index 00000000..b1333f56
--- /dev/null
+++ b/internal/integration/testdata/mysql/cli-migrate-diff-mode-normalized.txt
@@ -0,0 +1,35 @@
+only mysql
+
+atlas migrate hash
+
+# Migrate diff wants to drop the unique index.
+atlas migrate diff --dev-url URL --to file://./schema.sql
+! stdout 'no changes'
+cmpmig 1 expected.sql
+
+-- migrations/1.sql --
+CREATE TABLE `ref` (
+  `id` bigint NOT NULL AUTO_INCREMENT,
+  PRIMARY KEY (`id`)
+) CHARSET utf8mb4 COLLATE utf8mb4_bin;
+CREATE TABLE `tbl` (
+  `ref_id` bigint NOT NULL,
+  UNIQUE INDEX `u_ref_id` (`ref_id`), -- expected to be dropped
+  INDEX `ref_id` (`ref_id`),
+  FOREIGN KEY (`ref_id`) REFERENCES `ref` (`id`)
+) CHARSET utf8mb4 COLLATE utf8mb4_bin;
+
+-- schema.sql --
+CREATE TABLE `ref` (
+  `id` bigint NOT NULL AUTO_INCREMENT,
+  PRIMARY KEY (`id`)
+) CHARSET utf8mb4 COLLATE utf8mb4_bin;
+CREATE TABLE `tbl` (
+  `ref_id` bigint NOT NULL,
+  INDEX `ref_id` (`ref_id`),
+  FOREIGN KEY (`ref_id`) REFERENCES `ref` (`id`)
+) CHARSET utf8mb4 COLLATE utf8mb4_bin;
+
+-- expected.sql --
+-- Modify "tbl" table
+ALTER TABLE `tbl` DROP INDEX `u_ref_id`;
diff --git a/schemahcl/schemahcl_test.go b/schemahcl/schemahcl_test.go
index f0c11629..315547f3 100644
--- a/schemahcl/schemahcl_test.go
+++ b/schemahcl/schemahcl_test.go
@@ -433,6 +433,7 @@ env "prod" {
 env "staging" {
   for_each = toset(var.domains)
   url = "${each.value.name}:${each.value.port}"
+  driver = MYSQL
 }
 
 env "dev" {
@@ -445,6 +446,7 @@ env "dev" {
 `)
 	)
 	require.NoError(t, New(
+		WithScopedEnums("env.driver", "MYSQL", "POSTGRES"),
 		WithDataSource("sql", func(_ context.Context, ectx *hcl.EvalContext, b *hclsyntax.Block) (cty.Value, error) {
 			attrs, diags := b.Body.JustAttributes()
 			if diags.HasErrors() {
@@ -1078,9 +1080,31 @@ bar "b1" {
 	}, doc.Bar[0])
 }
 
+func Test_MarshalAttr(t *testing.T) {
+	var doc struct {
+		DefaultExtension
+	}
+	doc.Extra.Attrs = append(
+		doc.Extra.Attrs,
+		StringEnumsAttr("mixed1", &EnumString{S: "string"}, &EnumString{E: "enum"}),
+		StringEnumsAttr("mixed2", &EnumString{E: "enum1"}, &EnumString{E: "enum2"}),
+		StringEnumsAttr("mixed3", &EnumString{S: "string1"}, &EnumString{S: "string1"}),
+	)
+	buf, err := Marshal(&doc)
+	require.NoError(t, err)
+	require.Equal(t, `mixed1 = ["string", enum]
+mixed2 = [enum1, enum2]
+mixed3 = ["string1", "string1"]
+`, string(buf))
+}
+
 func Test_WithPos(t *testing.T) {
 	var (
 		doc struct {
+			Bar struct {
+				A int `spec:"a"`
+				DefaultExtension
+			} `spec:"bar"`
 			DefaultExtension
 		}
 		b = []byte(`
@@ -1091,6 +1115,9 @@ foo {
   }
 }
 baz = 1
+bar {
+  a = 1
+}
 `)
 	)
 	require.NoError(t, New(WithPos()).EvalBytes(b, &doc, nil))
@@ -1103,6 +1130,7 @@ baz = 1
 	require.Equal(t, 3, rs[1].Range().Start.Line)
 	require.Equal(t, 4, rs[1].Children[0].Range().Start.Line)
 	require.Equal(t, 5, rs[1].Children[0].Attrs[0].Range().Start.Line)
+	require.NotNil(t, doc.Bar.Extra.Range(), "position should be attached to the resource")
 }
 
 func TestExtendedBlockDef(t *testing.T) {
diff --git a/sql/mysql/inspect_test.go b/sql/mysql/inspect_test.go
index 957899f8..4a435c92 100644
--- a/sql/mysql/inspect_test.go
+++ b/sql/mysql/inspect_test.go
@@ -43,11 +43,11 @@ func TestDriver_InspectTable(t *testing.T) {
 				m.ExpectQuery(queryTable).
 					WithArgs("public").
 					WillReturnRows(sqltest.Rows(`
-+--------------+--------------+--------------------+--------------------+----------------+---------------+-------------------+------------------+------------------+
-| TABLE_SCHEMA | TABLE_NAME   | CHARACTER_SET_NAME | TABLE_COLLATION    | AUTO_INCREMENT | TABLE_COMMENT | CREATE_OPTIONS    |      ENGINE      |  DEFAULT_ENGINE  |
-+--------------+--------------+--------------------+--------------------+----------------+---------------+-------------------+------------------+------------------+
-| public       | users        | utf8mb4            | utf8mb4_0900_ai_ci | nil            | Comment       | COMPRESSION="ZLIB"|       InnoDB     |       1          |
-+--------------+--------------+--------------------+--------------------+----------------+---------------+-------------------+------------------+------------------+
++--------------+--------------+--------------------+--------------------+----------------+---------------+-------------------+------------------+------------------+------------------+ 
+| TABLE_SCHEMA | TABLE_NAME   | CHARACTER_SET_NAME | TABLE_COLLATION    | AUTO_INCREMENT | TABLE_COMMENT | CREATE_OPTIONS    |      ENGINE      |  DEFAULT_ENGINE  |  TABLE_TYPE      | 
++--------------+--------------+--------------------+--------------------+----------------+---------------+-------------------+------------------+------------------+------------------+ 
+| public       | users        | utf8mb4            | utf8mb4_0900_ai_ci | nil            | Comment       | COMPRESSION="ZLIB"|       InnoDB     |       1          |                  | 
++--------------+--------------+--------------------+--------------------+----------------+---------------+-------------------+------------------+------------------+------------------+ 
 `))
 				m.ExpectQuery(queryColumns).
 					WithArgs("public", "users").
@@ -102,11 +102,11 @@ func TestDriver_InspectTable(t *testing.T) {
 				m.ExpectQuery(queryTable).
 					WithArgs("public").
 					WillReturnRows(sqltest.Rows(`
-+--------------+--------------+--------------------+--------------------+----------------+---------------+--------------------+------------------+------------------+
-| TABLE_SCHEMA | TABLE_NAME   | CHARACTER_SET_NAME | TABLE_COLLATION    | AUTO_INCREMENT | TABLE_COMMENT | CREATE_OPTIONS     |      ENGINE      |  DEFAULT_ENGINE  |
-+--------------+--------------+--------------------+--------------------+----------------+---------------+--------------------+------------------+------------------+
-| public       | users        | utf8mb4            | utf8mb4_0900_ai_ci | nil            | Comment       | COMPRESSION="ZLIB" |       InnoDB     |       1          |
-+--------------+--------------+--------------------+--------------------+----------------+---------------+--------------------+------------------+------------------+
++--------------+--------------+--------------------+--------------------+----------------+---------------+--------------------+------------------+------------------+------------------+ 
+| TABLE_SCHEMA | TABLE_NAME   | CHARACTER_SET_NAME | TABLE_COLLATION    | AUTO_INCREMENT | TABLE_COMMENT | CREATE_OPTIONS     |      ENGINE      |  DEFAULT_ENGINE  |  TABLE_TYPE      | 
++--------------+--------------+--------------------+--------------------+----------------+---------------+--------------------+------------------+------------------+------------------+ 
+| public       | users        | utf8mb4            | utf8mb4_0900_ai_ci | nil            | Comment       | COMPRESSION="ZLIB" |       InnoDB     |       1          |                  | 
++--------------+--------------+--------------------+--------------------+----------------+---------------+--------------------+------------------+------------------+------------------+ 
 `))
 				m.ExpectQuery(queryColumns).
 					WithArgs("public", "users").
@@ -1088,9 +1088,9 @@ func (m mock) noFKs() {
 }
 
 func (m mock) tableExists(schema, table string, exists bool) {
-	rows := sqlmock.NewRows([]string{"table_schema", "table_name", "table_collation", "character_set", "auto_increment", "table_comment", "create_options", "engine", "default_engine"})
+	rows := sqlmock.NewRows([]string{"table_schema", "table_name", "table_collation", "character_set", "auto_increment", "table_comment", "create_options", "engine", "default_engine", "table_type"})
 	if exists {
-		rows.AddRow(schema, table, nil, nil, nil, nil, nil, nil, nil)
+		rows.AddRow(schema, table, nil, nil, nil, nil, nil, nil, nil, nil)
 	}
 	m.ExpectQuery(queryTable).
 		WithArgs(schema).
@@ -1098,9 +1098,9 @@ func (m mock) tableExists(schema, table string, exists bool) {
 }
 
 func (m mock) tables(schema string, tables ...string) {
-	rows := sqlmock.NewRows([]string{"schema", "table", "charset", "collate", "inc", "comment", "options", "engine", "default_engine"})
+	rows := sqlmock.NewRows([]string{"schema", "table", "charset", "collate", "inc", "comment", "options", "engine", "default_engine", "table_type"})
 	for _, t := range tables {
-		rows.AddRow(schema, t, nil, nil, nil, nil, nil, nil, nil)
+		rows.AddRow(schema, t, nil, nil, nil, nil, nil, nil, nil, nil)
 	}
 	m.ExpectQuery(queryTable).
 		WithArgs(schema).
diff --git a/sql/postgres/inspect_test.go b/sql/postgres/inspect_test.go
index d15a60b4..7b192145 100644
--- a/sql/postgres/inspect_test.go
+++ b/sql/postgres/inspect_test.go
@@ -378,11 +378,11 @@ func TestDriver_InspectPartitionedTable(t *testing.T) {
 	m.ExpectQuery(sqltest.Escape(fmt.Sprintf(tablesQuery, "$1"))).
 		WithArgs("public").
 		WillReturnRows(sqltest.Rows(`
- oid   | table_schema | table_name  | comment | partition_attrs | partition_strategy |                  partition_exprs
--------+--------------+-------------+---------+-----------------+--------------------+----------------------------------------------------
- 112  | public       | logs1       |         |                 |                     | 
- 113  | public       | logs2       |         | 1               | r                   | 
- 114  | public       | logs3       |         | 2 0 0           | l                   | (a + b), (a + (b * 2))
+ oid   | table_schema | table_name  | comment | partition_attrs | partition_strategy |                  partition_exprs                   |                  extra                   
+-------+--------------+-------------+---------+-----------------+--------------------+----------------------------------------------------+----------------------------------------------------
+ 112  | public       | logs1       |         |                 |                     |                                                    |                                                    
+ 113  | public       | logs2       |         | 1               | r                   |                                                    |                                                    
+ 114  | public       | logs3       |         | 2 0 0           | l                   | (a + b), (a + (b * 2))                             |                              
 
 `))
 	m.ExpectQuery(sqltest.Escape(fmt.Sprintf(columnsQuery, "$2, $3, $4"))).
@@ -732,9 +732,9 @@ func (m mock) version(version string) {
 }
 
 func (m mock) tableExists(schema, table string, exists bool) {
-	rows := sqlmock.NewRows([]string{"oid", "table_schema", "table_name", "table_comment", "partition_attrs", "partition_strategy", "partition_exprs"})
+	rows := sqlmock.NewRows([]string{"oid", "table_schema", "table_name", "table_comment", "partition_attrs", "partition_strategy", "partition_exprs", "attrs"})
 	if exists {
-		rows.AddRow(nil, schema, table, nil, nil, nil, nil)
+		rows.AddRow(nil, schema, table, nil, nil, nil, nil, nil)
 	}
 	m.ExpectQuery(queryTables).
 		WithArgs(schema).
diff --git a/sql/schema/dsl_test.go b/sql/schema/dsl_test.go
index e4b52ff9..ee039ad4 100644
--- a/sql/schema/dsl_test.go
+++ b/sql/schema/dsl_test.go
@@ -141,6 +141,9 @@ func TestSchema_Views(t *testing.T) {
 	v1, v2 := schema.NewView("v1", "SELECT 1"), schema.NewView("v2", "SELECT 2")
 	s.AddViews(v1, v2)
 	require.Equal(t, []*schema.View{v1, v2}, s.Views)
+	v1.AddDeps(v2)
+	require.Equal(t, []schema.Object{v2}, v1.Deps)
+	require.Equal(t, []schema.Object{v1}, v2.Refs)
 }
 
 func TestSchema_SetCharset(t *testing.T) {
diff --git a/sql/sqlite/inspect_test.go b/sql/sqlite/inspect_test.go
index f9f021eb..7d1db781 100644
--- a/sql/sqlite/inspect_test.go
+++ b/sql/sqlite/inspect_test.go
@@ -30,6 +30,7 @@ func TestDriver_InspectTable(t *testing.T) {
 					WillReturnRows(sqltest.Rows(`
  name |   type       | nullable | dflt_value  | primary  | hidden
 ------+--------------+----------+ ------------+----------+----------
+ id   | integer       |  0      |     0x1     |  1       |  0
  c1   | int           |  1      |     a       |  0       |  0
  c2   | integer       |  0      |     97      |  0       |  0
  c3   | varchar(100)  |  1      |    'A'      |  0       |  0
@@ -44,7 +45,7 @@ func TestDriver_InspectTable(t *testing.T) {
  w    | int           |  0      |             |  0       |  2
  x    | text          |  0      |             |  0       |  3
  y    | text          |  0      |             |  0       |  2
- id   | integer       |  0      |     0x1     |  1       |  0
+ j    | jsonb         |  0      |             |  0       |  0
 `))
 				m.noIndexes("users")
 				m.noFKs("users")
@@ -52,6 +53,7 @@ func TestDriver_InspectTable(t *testing.T) {
 			expect: func(require *require.Assertions, t *schema.Table, err error) {
 				require.NoError(err)
 				columns := []*schema.Column{
+					{Name: "id", Type: &schema.ColumnType{Type: &schema.IntegerType{T: "integer"}, Raw: "integer"}, Attrs: []schema.Attr{&AutoIncrement{}}, Default: &schema.Literal{V: "0x1"}},
 					{Name: "c1", Type: &schema.ColumnType{Null: true, Type: &schema.IntegerType{T: "int"}, Raw: "int"}, Default: &schema.RawExpr{X: "a"}},
 					{Name: "c2", Type: &schema.ColumnType{Type: &schema.IntegerType{T: "integer"}, Raw: "integer"}, Default: &schema.Literal{V: "97"}},
 					{Name: "c3", Type: &schema.ColumnType{Null: true, Type: &schema.StringType{T: "varchar", Size: 100}, Raw: "varchar(100)"}, Default: &schema.Literal{V: "'A'"}},
@@ -66,14 +68,14 @@ func TestDriver_InspectTable(t *testing.T) {
 					{Name: "w", Type: &schema.ColumnType{Type: &schema.IntegerType{T: "int"}, Raw: "int"}, Attrs: []schema.Attr{&schema.GeneratedExpr{Type: "VIRTUAL", Expr: "(a*10)"}}},
 					{Name: "x", Type: &schema.ColumnType{Type: &schema.StringType{T: "text"}, Raw: "text"}, Attrs: []schema.Attr{&schema.GeneratedExpr{Type: "STORED", Expr: "(typeof(c))"}}},
 					{Name: "y", Type: &schema.ColumnType{Type: &schema.StringType{T: "text"}, Raw: "text"}, Attrs: []schema.Attr{&schema.GeneratedExpr{Type: "VIRTUAL", Expr: "(substr(b,a,a+2))"}}},
-					{Name: "id", Type: &schema.ColumnType{Type: &schema.IntegerType{T: "integer"}, Raw: "integer"}, Attrs: []schema.Attr{&AutoIncrement{}}, Default: &schema.Literal{V: "0x1"}},
+					{Name: "j", Type: &schema.ColumnType{Type: &schema.JSONType{T: "jsonb"}, Raw: "jsonb"}},
 				}
 				require.Equal(t.Columns, columns)
 				require.EqualValues(&schema.Index{
 					Name:   "PRIMARY",
 					Unique: true,
 					Table:  t,
-					Parts:  []*schema.IndexPart{{SeqNo: 1, C: columns[len(columns)-1]}},
+					Parts:  []*schema.IndexPart{{SeqNo: 1, C: columns[0]}},
 					Attrs:  []schema.Attr{&AutoIncrement{}},
 				}, t.PrimaryKey)
 			},
diff --git a/sql/sqlite/sqlspec_test.go b/sql/sqlite/sqlspec_test.go
index 4b214552..8f83dfb5 100644
--- a/sql/sqlite/sqlspec_test.go
+++ b/sql/sqlite/sqlspec_test.go
@@ -458,6 +458,10 @@ func TestTypes(t *testing.T) {
 			typeExpr: "uuid",
 			expected: &schema.UUIDType{T: "uuid"},
 		},
+		{
+			typeExpr: "jsonb",
+			expected: &schema.JSONType{T: "jsonb"},
+		},
 	} {
 		t.Run(tt.typeExpr, func(t *testing.T) {
 			var test schema.Schema