Repository: ariga/atlas
Pull request #827 (2022-05-29 08:41:50)

doc: updating text related to license


Patch:
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 6e930611..19ab4d71 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -475,4 +475,28 @@ jobs:
       - name: Run integration tests for sqlite
         working-directory: internal/integration
         run: go test -race -count=2 -v -run="SQLite.*" -dialect=sqlite ./...
+  
+  integration-cockroach:
+    runs-on: ubuntu-latest
+    services:
+      cockroach:
+        image: timveil/cockroachdb-single-node:latest
+        
+        ports:
+          - 26257:26257
+        
+    steps:
+      - uses: actions/checkout@v2.3.4
+      - uses: actions/setup-go@v2
+        with:
+          go-version: 1.18
+      - uses: actions/cache@v2.1.6
+        with:
+          path: ~/go/pkg/mod
+          key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
+          restore-keys: |
+            ${{ runner.os }}-go-
+      - name: Run integration tests for cockroach
+        working-directory: internal/integration
+        run: go test -race -count=2 -v -run="Cockroach.*/Cockroach|Cockroach.*/.*/Cockroach|Cockroach.*/.*/.*/Cockroach" -dialect=cockroach ./...
   
\ No newline at end of file
diff --git a/cmd/atlas/doc.tmpl b/cmd/atlas/doc.tmpl
index 03ae307b..1e7ef325 100644
--- a/cmd/atlas/doc.tmpl
+++ b/cmd/atlas/doc.tmpl
@@ -17,10 +17,8 @@ For a more detailed introduction to the CLI capabilities, head over to the
 
 ## Distributed Binaries
 
-Starting [v0.3.0](https://github.com/ariga/atlas/releases/tag/v0.3.0),
-ֿthe distributed binaries include code for a [Management UI](ui/intro.md) wrapping the
-core atlas engine that is not currently released publicly. The binaries
-themselves are still released under the same [Apache License 2.0](https://github.com/ariga/atlas/blob/master/LICENSE).
+The binaries distributed in official releases are released under the  [Ariga End User License](https://ariga.io/legal/atlas/eula).
+If you would like to build Atlas from source follow the instructions [here](https://atlasgo.io/cli/reference#building-from-source).
 
 ### Building from Source
 
diff --git a/cmd/atlascmd/migrate/migrate.go b/cmd/atlascmd/migrate/migrate.go
index 8fbaf51e..9ee413d8 100644
--- a/cmd/atlascmd/migrate/migrate.go
+++ b/cmd/atlascmd/migrate/migrate.go
@@ -6,32 +6,90 @@ package migrate
 
 import (
 	"context"
+	"errors"
 
 	"ariga.io/atlas/cmd/atlascmd/migrate/ent"
 	"ariga.io/atlas/cmd/atlascmd/migrate/ent/revision"
 	"ariga.io/atlas/sql/migrate"
 	"ariga.io/atlas/sql/schema"
+	"ariga.io/atlas/sql/sqlclient"
 	"entgo.io/ent/dialect/sql"
 	entschema "entgo.io/ent/dialect/sql/schema"
 )
 
-// A EntRevisions provides implementation for the migrate.RevisionReadWriter interface.
-type EntRevisions struct{ c *ent.Client }
+type (
+	// A EntRevisions provides implementation for the migrate.RevisionReadWriter interface.
+	EntRevisions struct {
+		ac     *sqlclient.Client // underlying Atlas client
+		sc     *sqlclient.Client // underlying Atlas client connected to the named schema
+		ec     *ent.Client       // underlying Ent client
+		schema string            // name of the schema the revision table resides in
+	}
+
+	// Option allows to configure EntRevisions by using functional arguments.
+	Option func(*EntRevisions) error
+)
 
-// NewEntRevisions creates a new EntRevisions with the given ent.Client.
-func NewEntRevisions(db schema.ExecQuerier, dialect string, opts ...ent.Option) *EntRevisions {
-	opts = append(opts, ent.Driver(sql.NewDriver(dialect, sql.Conn{ExecQuerier: db})))
-	return &EntRevisions{ent.NewClient(opts...)}
+// NewEntRevisions creates a new EntRevisions with the given sqlclient.Client. It is important to call
+// EntRevisions.Init to initialize the underlying Ent client.
+func NewEntRevisions(ac *sqlclient.Client, opts ...Option) (*EntRevisions, error) {
+	r := &EntRevisions{ac: ac}
+	for _, opt := range opts {
+		if err := opt(r); err != nil {
+			return nil, err
+		}
+	}
+	if r.schema == "" {
+		r.schema = "atlas_schema_revisions"
+	}
+	return r, nil
+}
+
+// WithSchema configures the schema to use for the revision table.
+func WithSchema(s string) Option {
+	return func(r *EntRevisions) error {
+		r.schema = s
+		return nil
+	}
 }
 
 // Init makes sure the revision table does exist in the connected database.
 func (r *EntRevisions) Init(ctx context.Context) error {
-	return r.c.Schema.Create(ctx, entschema.WithAtlas(true))
+	// Try to open a connection to the schema we are storing the revision table in.
+	var err error
+	r.sc, err = sqlclient.OpenURL(ctx, r.ac.URL.URL, sqlclient.OpenSchema(r.schema))
+	// If the driver does not support changing the schema (most likely SQLite) use the existing connection.
+	if err != nil && errors.Is(err, sqlclient.ErrUnsupported) {
+		r.ec = ent.NewClient(ent.Driver(sql.OpenDB(r.ac.Name, r.ac.DB)))
+		return r.ec.Schema.Create(ctx, entschema.WithAtlas(true))
+	}
+	// Driver does support changing schemas. Make sure the schema does exist before proceeding.
+	_, err2 := r.ac.InspectSchema(ctx, r.schema, &schema.InspectOptions{Mode: schema.InspectSchemas})
+	if err2 != nil && !schema.IsNotExistError(err2) {
+		return err2
+	}
+	if schema.IsNotExistError(err2) {
+		if err := r.ac.ApplyChanges(ctx, []schema.Change{
+			&schema.AddSchema{S: &schema.Schema{Name: r.schema}},
+		}); err != nil {
+			return err
+		}
+	}
+	// If the previous connection attempt was unsuccessful, re-try with the schema present.
+	if r.sc == nil {
+		r.sc, err = sqlclient.OpenURL(ctx, r.ac.URL.URL, sqlclient.OpenSchema(r.schema))
+		if err != nil {
+			return err
+		}
+	}
+	r.ac.AddClosers(r.sc)
+	r.ec = ent.NewClient(ent.Driver(sql.OpenDB(r.sc.Name, r.sc.DB)))
+	return r.ec.Schema.Create(ctx, entschema.WithAtlas(true))
 }
 
 // ReadRevisions reads the revisions from the revisions table.
 func (r *EntRevisions) ReadRevisions(ctx context.Context) (migrate.Revisions, error) {
-	revs, err := r.c.Revision.Query().Order(ent.Asc(revision.FieldID)).All(ctx)
+	revs, err := r.ec.Revision.Query().Order(ent.Asc(revision.FieldID)).All(ctx)
 	if err != nil {
 		return nil, err
 	}
@@ -55,7 +113,7 @@ func (r *EntRevisions) ReadRevisions(ctx context.Context) (migrate.Revisions, er
 func (r *EntRevisions) WriteRevisions(ctx context.Context, rs migrate.Revisions) error {
 	bulk := make([]*ent.RevisionCreate, len(rs))
 	for i, rev := range rs {
-		bulk[i] = r.c.Revision.Create().
+		bulk[i] = r.ec.Revision.Create().
 			SetID(rev.Version).
 			SetDescription(rev.Description).
 			SetExecutionState(revision.ExecutionState(rev.ExecutionState)).
@@ -65,7 +123,7 @@ func (r *EntRevisions) WriteRevisions(ctx context.Context, rs migrate.Revisions)
 			SetOperatorVersion(rev.OperatorVersion).
 			SetMeta(rev.Meta)
 	}
-	return r.c.Revision.CreateBulk(bulk...).
+	return r.ec.Revision.CreateBulk(bulk...).
 		OnConflict(
 			sql.ConflictColumns(revision.FieldID),
 		).
diff --git a/doc/md/cli/reference.md b/doc/md/cli/reference.md
index 217e8c15..1b6952b6 100644
--- a/doc/md/cli/reference.md
+++ b/doc/md/cli/reference.md
@@ -14,10 +14,8 @@ For a more detailed introduction to the CLI capabilities, head over to the
 
 ## Distributed Binaries
 
-Starting [v0.3.0](https://github.com/ariga/atlas/releases/tag/v0.3.0),
-ֿthe distributed binaries include code for a [Management UI](ui/intro.md) wrapping the
-core atlas engine that is not currently released publicly. The binaries
-themselves are still released under the same [Apache License 2.0](https://github.com/ariga/atlas/blob/master/LICENSE).
+The binaries distributed in official releases are released under the  [Ariga End User License](https://ariga.io/legal/atlas/eula).
+If you would like to build Atlas from source follow the instructions [here](https://atlasgo.io/cli/reference#building-from-source).
 
 ### Building from Source
 
diff --git a/doc/md/getting-started/01-introduction.mdx b/doc/md/getting-started/01-introduction.mdx
index 4ff86567..716608d2 100644
--- a/doc/md/getting-started/01-introduction.mdx
+++ b/doc/md/getting-started/01-introduction.mdx
@@ -83,10 +83,9 @@ move the atlas binary to a file location on your system PATH.
 Latest release is updated twice a day and is based on the most recent tagged release.
 :::
 
-The distributed binaries include code for a [Management UI](../ui/intro.md) wrapping the
-core Atlas engine that is not currently released publicly. The binaries
-themselves are released under the [Ariga End User License](https://ariga.io/legal/atlas/eula).
-If you would like to build Atlas from source without the UI follow the instructions [here](../cli/reference.md#building-from-source).
+The binaries distributed in official releases are released under the  [Ariga End User License](https://ariga.io/legal/atlas/eula).
+If you would like to build Atlas from source follow the instructions [here](https://atlasgo.io/cli/reference#building-from-source).
+
 
 To verify that the CLI is installed correctly run:
 ```shell
diff --git a/doc/md/getting-started/03-applying.md b/doc/md/getting-started/03-applying.md
index 0d03fccd..607cba38 100644
--- a/doc/md/getting-started/03-applying.md
+++ b/doc/md/getting-started/03-applying.md
@@ -51,7 +51,7 @@ Flags:
       --addr string      used with -w, local address to bind the server to. (default ":5800")
   -h, --help             help for apply
 ```
-As you can see, similar to the `inspect` command, the `-d` flag is used to define the
+As you can see, similar to the `inspect` command, the `-u` flag is used to define the
 URL to connect to the database, and an additional flag `-f` specifies the path to
 the file containing the desired schema. 
 
@@ -158,4 +158,4 @@ CREATE TABLE `example`.`post_categories` (`post_id` int NOT NULL, `category_id`
 ### Conclusion 
 
 In this section, we've seen how to use the `atlas schema apply` command to migrate
-the schema of an existing database to our desired state.
\ No newline at end of file
+the schema of an existing database to our desired state.
diff --git a/doc/website/src/css/custom.css b/doc/website/src/css/custom.css
index 5ef830e8..8bf26e6e 100644
--- a/doc/website/src/css/custom.css
+++ b/doc/website/src/css/custom.css
@@ -19,6 +19,7 @@
 }
 
 .footer__title {
+  color: #000939;
   font-size: 22px;
 }
 .footer__link-item {
diff --git a/internal/ci/main.go b/internal/ci/main.go
index 7823e544..3b8f5604 100644
--- a/internal/ci/main.go
+++ b/internal/ci/main.go
@@ -156,6 +156,12 @@ var (
 			Version: "sqlite",
 			Regex:   "SQLite.*",
 		},
+		{
+			Version: "cockroach",
+			Image:   "timveil/cockroachdb-single-node:latest",
+			Regex:   re("Cockroach.*/%sCockroach"),
+			Ports:   []string{"26257:26257"},
+		},
 	}
 )
 
diff --git a/internal/integration/docker-compose.yaml b/internal/integration/docker-compose.yaml
index 08138e82..4e9934d9 100644
--- a/internal/integration/docker-compose.yaml
+++ b/internal/integration/docker-compose.yaml
@@ -134,3 +134,10 @@ services:
     image: pingcap/tidb:v6.0.0
     ports:
       - "4310:4000"
+  
+  cockroach21.2.11:
+    platform: linux/amd64
+    image: cockroachdb/cockroach:v21.2.11
+    ports:
+      - "26257:26257"
+    command: "start-single-node --insecure"
\ No newline at end of file
diff --git a/sql/mysql/driver.go b/sql/mysql/driver.go
index ded8c798..cda7ce14 100644
--- a/sql/mysql/driver.go
+++ b/sql/mysql/driver.go
@@ -44,9 +44,7 @@ func init() {
 		sqlclient.DriverOpener(Open),
 		sqlclient.RegisterCodec(MarshalHCL, EvalHCL),
 		sqlclient.RegisterFlavours("maria", "mariadb"),
-		sqlclient.RegisterURLParser(func(u *url.URL) *sqlclient.URL {
-			return &sqlclient.URL{URL: u, DSN: dsn(u), Schema: strings.TrimPrefix(u.Path, "/")}
-		}),
+		sqlclient.RegisterURLParser(parser{}),
 	)
 }
 
@@ -235,6 +233,20 @@ func unescape(s string) string {
 	return b.String()
 }
 
+type parser struct{}
+
+// ParseURL implements the sqlclient.URLParser interface.
+func (parser) ParseURL(u *url.URL) *sqlclient.URL {
+	return &sqlclient.URL{URL: u, DSN: dsn(u), Schema: strings.TrimPrefix(u.Path, "/")}
+}
+
+// ChangeSchema implements the sqlclient.SchemaChanger interface.
+func (parser) ChangeSchema(u *url.URL, s string) *url.URL {
+	nu := *u
+	nu.Path = "/" + s
+	return &nu
+}
+
 // dsn returns the MySQL standard DSN for opening
 // the sql.DB from the user provided URL.
 func dsn(u *url.URL) string {
diff --git a/sql/postgres/crdb.go b/sql/postgres/crdb.go
index f3deda11..80bb4d25 100644
--- a/sql/postgres/crdb.go
+++ b/sql/postgres/crdb.go
@@ -8,72 +8,89 @@ import (
 	"context"
 	"database/sql"
 	"fmt"
-	"net/url"
 	"regexp"
 	"strings"
 
 	"ariga.io/atlas/sql/internal/sqlx"
-	"ariga.io/atlas/sql/migrate"
 	"ariga.io/atlas/sql/schema"
 	"ariga.io/atlas/sql/sqlclient"
 )
 
-// OpenCRDB opens a new CockroachDB driver.
-func OpenCRDB(db schema.ExecQuerier) (migrate.Driver, error) {
-	drv, err := open(db)
-	if err != nil {
-		return nil, err
-	}
-	c := drv.conn
-	c.crdb = true
-	return &Driver{
-		conn:        c,
-		Differ:      &sqlx.Diff{DiffDriver: &crdbDiff{diff{c}}},
-		Inspector:   &inspect{c},
-		PlanApplier: &planApply{c},
-	}, nil
-}
-
 func init() {
 	sqlclient.Register(
 		"cockroach",
-		sqlclient.DriverOpener(OpenCRDB),
+		sqlclient.DriverOpener(Open),
 		sqlclient.RegisterCodec(MarshalHCL, EvalHCL),
 		sqlclient.RegisterFlavours("crdb"),
-		sqlclient.RegisterURLParser(func(u *url.URL) *sqlclient.URL {
-			return &sqlclient.URL{URL: u, DSN: u.String(), Schema: u.Query().Get("search_path")}
-		}),
+		sqlclient.RegisterURLParser(parser{}),
 	)
 }
 
 // crdbDiff implements the sqlx.DiffDriver for CockroachDB.
-type crdbDiff struct{ diff }
+type (
+	crdbDiff    struct{ diff }
+	crdbInspect struct{ inspect }
+)
 
 var _ sqlx.DiffDriver = (*crdbDiff)(nil)
 
-// IndexAttrChanged reports if the index attributes were changed.
-func (cd *crdbDiff) IndexAttrChanged(from, to []schema.Attr) bool {
-	t1 := &IndexType{T: IndexTypeBTree}
-	if sqlx.Has(from, t1) {
-		t1.T = strings.ToUpper(t1.T)
+// pathSchema fixes: https://github.com/cockroachdb/cockroach/issues/82040.
+func (i *crdbInspect) patchSchema(s *schema.Schema) {
+	for _, t := range s.Tables {
+		for _, c := range t.Columns {
+			id, ok := identity(c.Attrs)
+			if !ok {
+				continue
+			}
+			if g := strings.ToUpper(id.Generation); strings.Contains(g, "ALWAYS") {
+				id.Generation = "ALWAYS"
+				c.Default = nil
+			} else if strings.Contains(g, "BY DEFAULT") {
+				id.Generation = "BY DEFAULT"
+				c.Default = nil
+			}
+			schema.ReplaceOrAppend(&c.Attrs, id)
+		}
 	}
-	t2 := &IndexType{T: IndexTypeBTree}
-	if sqlx.Has(to, t2) {
-		t2.T = strings.ToUpper(t2.T)
+}
+
+func (i *crdbInspect) InspectSchema(ctx context.Context, name string, opts *schema.InspectOptions) (*schema.Schema, error) {
+	s, err := i.inspect.InspectSchema(ctx, name, opts)
+	if err != nil {
+		return nil, err
+	}
+	i.patchSchema(s)
+	return s, err
+}
+
+func (i *crdbInspect) InspectRealm(ctx context.Context, opts *schema.InspectRealmOption) (*schema.Realm, error) {
+	r, err := i.inspect.InspectRealm(ctx, opts)
+	if err != nil {
+		return nil, err
 	}
-	if t1.T != t2.T {
-		return true
+	for _, s := range r.Schemas {
+		i.patchSchema(s)
 	}
-	var p1, p2 IndexPredicate
-	return sqlx.Has(from, &p1) != sqlx.Has(to, &p2) || (p1.P != p2.P && p1.P != sqlx.MayWrap(p2.P))
+	return r, nil
 }
 
 // Normalize implements the sqlx.Normalizer.
 func (cd *crdbDiff) Normalize(from, to *schema.Table) {
 	cd.normalize(from)
 	cd.normalize(to)
-	cd.diff.normalize(from)
-	cd.diff.normalize(to)
+}
+
+func (cd *crdbDiff) ColumnChange(from, to *schema.Column) (schema.ChangeKind, error) {
+	// All serial types in crdb become bigints under the hood, see: https://www.cockroachlabs.com/docs/stable/serial.html#generated-values-for-mode-sql_sequence-and-sql_sequence_cached.
+	if s, ok := to.Type.Type.(*SerialType); ok {
+		to.Type.Type = &schema.IntegerType{
+			T: TypeBigInt,
+		}
+		defer func() {
+			to.Type.Type = s
+		}()
+	}
+	return cd.diff.ColumnChange(from, to)
 }
 
 func (cd *crdbDiff) normalize(table *schema.Table) {
@@ -82,7 +99,8 @@ func (cd *crdbDiff) normalize(table *schema.Table) {
 		if !ok {
 			prim = schema.NewColumn("rowid").
 				AddAttrs(Identity{}).
-				SetType(&schema.IntegerType{})
+				SetType(&schema.IntegerType{T: TypeBigInt}).
+				SetDefault(&schema.RawExpr{X: "unique_rowid()"})
 			table.AddColumns(prim)
 		}
 		table.PrimaryKey = &schema.Index{
@@ -96,17 +114,10 @@ func (cd *crdbDiff) normalize(table *schema.Table) {
 		}
 	}
 	for _, c := range table.Columns {
-		if id, ok := identity(c.Attrs); ok && c.Default == nil {
-			c.Default = &schema.RawExpr{
-				X: fmt.Sprintf("nextval('%s.%s_%s_seq'::REGCLASS)", table.Schema.Name, table.Name, c.Name),
-			}
-			id.Generation = "generated by default as identity"
-			id.Sequence = &Sequence{Start: defaultSeqStart, Increment: defaultSeqIncrement}
-			for j := 0; j < len(c.Attrs); j++ {
-				switch c.Attrs[j].(type) {
-				case *Identity:
-					c.Attrs[j] = id
-				}
+		if _, ok := identity(c.Attrs); ok {
+			if c.Default != nil {
+				c.Default = nil
+				continue
 			}
 		}
 		switch t := c.Type.Type.(type) {
@@ -125,6 +136,45 @@ func (cd *crdbDiff) normalize(table *schema.Table) {
 			case TypeJSON:
 				t.T = TypeJSONB
 			}
+		case *SerialType:
+			c.Default = &schema.RawExpr{
+				X: "unique_rowid()",
+			}
+		case *schema.TimeType:
+			// "timestamp" and "timestamptz" are accepted as
+			// abbreviations for timestamp with(out) time zone.
+			switch t.T {
+			case "timestamp with time zone":
+				t.T = "timestamptz"
+			case "timestamp without time zone":
+				t.T = "timestamp"
+			}
+		case *schema.FloatType:
+			// The same numeric precision is used in all platform.
+			// See: https://www.postgresql.org/docs/current/datatype-numeric.html
+			switch {
+			case t.T == "float" && t.Precision < 25:
+				// float(1) to float(24) are selected as "real" type.
+				t.T = "real"
+				fallthrough
+			case t.T == "real":
+				t.Precision = 24
+			case t.T == "float" && t.Precision >= 25:
+				// float(25) to float(53) are selected as "double precision" type.
+				t.T = "double precision"
+				fallthrough
+			case t.T == "double precision":
+				t.Precision = 53
+			}
+		case *schema.StringType:
+			switch t.T {
+			case "character", "char":
+				// Character without length specifier
+				// is equivalent to character(1).
+				t.Size = 1
+			}
+		case *enumType:
+			c.Type.Type = &schema.EnumType{T: t.T, Values: t.Values}
 		}
 	}
 }
@@ -239,7 +289,7 @@ SELECT
 	JOIN pg_class t ON t.oid = idx.indrelid
 	JOIN pg_namespace n ON n.oid = t.relnamespace
 	LEFT JOIN pg_constraint c ON idx.indexrelid = c.conindid
-	LEFT JOIN pg_indexes pgi ON pgi.tablename = t.relname AND indexname = i.relname
+	LEFT JOIN pg_indexes pgi ON pgi.tablename = t.relname AND indexname = i.relname AND n.nspname = pgi.schemaname
 	LEFT JOIN pg_attribute a ON (a.attrelid, a.attnum) = (idx.indrelid, idx.key)
 WHERE
 	n.nspname = $1
@@ -248,3 +298,39 @@ WHERE
 ORDER BY
 	table_name, index_name, idx.ord
 `
+
+const crdbColumnsQuery = `
+SELECT
+	t1.table_name,
+	t1.column_name,
+	t1.data_type,
+	t1.is_nullable,
+	t1.column_default,
+	t1.character_maximum_length,
+	t1.numeric_precision,
+	t1.datetime_precision,
+	t1.numeric_scale,
+	t1.character_set_name,
+	t1.collation_name,
+	t1.udt_name,
+	t1.is_identity,
+	t5.start_value as identity_start,
+	t5.increment_by as identity_increment,
+	t1.identity_generation,
+	t1.generation_expression,
+	col_description(t3.oid, "ordinal_position") AS comment,
+	t4.typtype,
+	t4.oid
+FROM
+	"information_schema"."columns" AS t1
+	JOIN pg_catalog.pg_namespace AS t2 ON t2.nspname = t1.table_schema
+	JOIN pg_catalog.pg_class AS t3 ON t3.relnamespace = t2.oid AND t3.relname = t1.table_name
+	LEFT JOIN pg_catalog.pg_type AS t4
+	ON t1.udt_name = t4.typname
+	LEFT JOIN pg_sequences AS t5
+	ON t5.schemaname || '.' || t5.sequencename = btrim(btrim(t1.column_default, 'nextval('''), '''::REGCLASS)')
+WHERE
+	table_schema = $1 AND table_name IN (%s)
+ORDER BY
+	t1.table_name, t1.ordinal_position
+`
diff --git a/sql/postgres/driver.go b/sql/postgres/driver.go
index 455af8c0..dc5e2914 100644
--- a/sql/postgres/driver.go
+++ b/sql/postgres/driver.go
@@ -45,18 +45,12 @@ func init() {
 		"postgres",
 		sqlclient.DriverOpener(Open),
 		sqlclient.RegisterCodec(MarshalHCL, EvalHCL),
-		sqlclient.RegisterURLParser(func(u *url.URL) *sqlclient.URL {
-			return &sqlclient.URL{URL: u, DSN: u.String(), Schema: u.Query().Get("search_path")}
-		}),
+		sqlclient.RegisterURLParser(parser{}),
 	)
 }
 
 // Open opens a new PostgreSQL driver.
 func Open(db schema.ExecQuerier) (migrate.Driver, error) {
-	return open(db)
-}
-
-func open(db schema.ExecQuerier) (*Driver, error) {
 	c := conn{ExecQuerier: db}
 	rows, err := db.QueryContext(context.Background(), paramsQuery)
 	if err != nil {
@@ -66,10 +60,10 @@ func open(db schema.ExecQuerier) (*Driver, error) {
 	if err != nil {
 		return nil, fmt.Errorf("postgres: failed scanning rows: %w", err)
 	}
-	if len(params) != 3 {
+	if len(params) != 3 && len(params) != 4 {
 		return nil, fmt.Errorf("postgres: unexpected number of rows: %d", len(params))
 	}
-	c.collate, c.ctype, c.version = params[0], params[1], params[2]
+	c.version, c.ctype, c.collate = params[0], params[1], params[2]
 	if len(c.version) != 6 {
 		return nil, fmt.Errorf("postgres: malformed version: %s", c.version)
 	}
@@ -77,6 +71,15 @@ func open(db schema.ExecQuerier) (*Driver, error) {
 	if semver.Compare("v"+c.version, "v10.0.0") != -1 {
 		return nil, fmt.Errorf("postgres: unsupported postgres version: %s", c.version)
 	}
+	// Means we are connected to CockroachDB because we have a result for name='crdb_version'. see `paramsQuery`.
+	if c.crdb = len(params) == 4; c.crdb {
+		return &Driver{
+			conn:        c,
+			Differ:      &sqlx.Diff{DiffDriver: &crdbDiff{diff{c}}},
+			Inspector:   &crdbInspect{inspect{c}},
+			PlanApplier: &planApply{c},
+		}, nil
+	}
 	return &Driver{
 		conn:        c,
 		Differ:      &sqlx.Diff{DiffDriver: &diff{c}},
@@ -163,6 +166,22 @@ func acquire(ctx context.Context, conn schema.ExecQuerier, id uint32, timeout ti
 	}
 }
 
+type parser struct{}
+
+// ParseURL implements the sqlclient.URLParser interface.
+func (parser) ParseURL(u *url.URL) *sqlclient.URL {
+	return &sqlclient.URL{URL: u, DSN: u.String(), Schema: u.Query().Get("search_path")}
+}
+
+// ChangeSchema implements the sqlclient.SchemaChanger interface.
+func (parser) ChangeSchema(u *url.URL, s string) *url.URL {
+	nu := *u
+	q := nu.Query()
+	q.Set("search_path", s)
+	nu.RawQuery = q.Encode()
+	return &nu
+}
+
 // Standard column types (and their aliases) as defined in
 // PostgreSQL codebase/website.
 const (
diff --git a/sql/postgres/inspect.go b/sql/postgres/inspect.go
index cf181e6d..6e8a6c4d 100644
--- a/sql/postgres/inspect.go
+++ b/sql/postgres/inspect.go
@@ -139,7 +139,11 @@ func (i *inspect) tables(ctx context.Context, realm *schema.Realm, opts *schema.
 
 // columns queries and appends the columns of the given table.
 func (i *inspect) columns(ctx context.Context, s *schema.Schema) error {
-	rows, err := i.querySchema(ctx, columnsQuery, s)
+	query := columnsQuery
+	if i.crdb {
+		query = crdbColumnsQuery
+	}
+	rows, err := i.querySchema(ctx, query, s)
 	if err != nil {
 		return fmt.Errorf("postgres: querying schema %q columns: %w", s.Name, err)
 	}
@@ -822,7 +826,7 @@ func newIndexStorage(opts string) (*IndexStorageParams, error) {
 
 const (
 	// Query to list runtime parameters.
-	paramsQuery = `SELECT setting FROM pg_settings WHERE name IN ('lc_collate', 'lc_ctype', 'server_version_num') ORDER BY name`
+	paramsQuery = `SELECT setting FROM pg_settings WHERE name IN ('lc_collate', 'lc_ctype', 'server_version_num', 'crdb_version') ORDER BY name DESC`
 
 	// Query to list database schemas.
 	schemasQuery = "SELECT schema_name FROM information_schema.schemata WHERE schema_name NOT IN ('information_schema', 'pg_catalog', 'pg_toast') AND schema_name NOT LIKE 'pg_%temp_%' ORDER BY schema_name"
@@ -846,6 +850,7 @@ FROM
 	LEFT JOIN pg_catalog.pg_partitioned_table AS t4 ON t4.partrelid = t3.oid
 WHERE
 	t1.table_type = 'BASE TABLE'
+	AND NOT COALESCE(t3.relispartition, false)
 	AND t1.table_schema IN (%s)
 ORDER BY
 	t1.table_schema, t1.table_name
@@ -854,16 +859,18 @@ ORDER BY
 SELECT
 	t1.table_schema,
 	t1.table_name,
-	pg_catalog.obj_description(t2.oid, 'pg_class') AS comment,
-	t3.partattrs AS partition_attrs,
-	t3.partstrat AS partition_strategy,
-	pg_get_expr(t3.partexprs, t3.partrelid) AS partition_exprs
+	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_class AS t2 ON t2.oid = to_regclass(quote_ident(t1.table_schema) || '.' || quote_ident(t1.table_name))::oid
-	LEFT JOIN pg_catalog.pg_partitioned_table AS t3 ON t3.partrelid = t2.oid
+	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
 WHERE
 	t1.table_type = 'BASE TABLE'
+	AND NOT COALESCE(t3.relispartition, false)
 	AND t1.table_schema IN (%s)
 	AND t1.table_name IN (%s)
 ORDER BY
@@ -916,9 +923,9 @@ SELECT
 	c.contype AS constraint_type,
 	pg_get_expr(idx.indpred, idx.indrelid) AS predicate,
 	pg_get_indexdef(idx.indexrelid, idx.ord, false) AS expression,
-	pg_index_column_has_property(idx.indexrelid, a.attnum, 'desc') AS desc,
-	pg_index_column_has_property(idx.indexrelid, a.attnum, 'nulls_first') AS nulls_first,
-	pg_index_column_has_property(idx.indexrelid, a.attnum, 'nulls_last') AS nulls_last,
+	pg_index_column_has_property(idx.indexrelid, idx.ord, 'desc') AS desc,
+	pg_index_column_has_property(idx.indexrelid, idx.ord, 'nulls_first') AS nulls_first,
+	pg_index_column_has_property(idx.indexrelid, idx.ord, 'nulls_last') AS nulls_last,
 	obj_description(to_regclass($1 || i.relname)::oid) AS comment,
 	i.reloptions AS options
 FROM
diff --git a/sql/postgres/migrate.go b/sql/postgres/migrate.go
index 72dcd91e..c5fa5e6c 100644
--- a/sql/postgres/migrate.go
+++ b/sql/postgres/migrate.go
@@ -400,7 +400,7 @@ func (s *state) alterTable(t *schema.Table, changes []schema.Change) error {
 		// a reversed order they were created.
 		sqlx.ReverseChanges(reverse)
 		if change.Reverse, err = build(reverse); err != nil {
-			return fmt.Errorf("reversd alter table %q: %v", t.Name, err)
+			return fmt.Errorf("reverse alter table %q: %v", t.Name, err)
 		}
 	}
 	s.append(change)
diff --git a/sql/sqlclient/client.go b/sql/sqlclient/client.go
index 4eb623d1..bed6dcb2 100644
--- a/sql/sqlclient/client.go
+++ b/sql/sqlclient/client.go
@@ -7,6 +7,7 @@ package sqlclient
 import (
 	"context"
 	"database/sql"
+	"errors"
 	"fmt"
 	"io"
 	"net/url"
@@ -44,6 +45,15 @@ type (
 		schemahcl.Evaluator
 	}
 
+	// TxClient is returned by calling Client.Tx. It behaves the same as Client,
+	// but wraps all operations within a transaction.
+	TxClient struct {
+		*Client
+
+		// The transaction this Client wraps.
+		Tx *sql.Tx
+	}
+
 	// URL extends the standard url.URL with additional
 	// connection information attached by the Opener (if any).
 	URL struct {
@@ -57,6 +67,35 @@ type (
 	}
 )
 
+// Tx returns a transactional client.
+func (c *Client) Tx(ctx context.Context, opts *sql.TxOptions) (*TxClient, error) {
+	tx, err := c.DB.BeginTx(ctx, opts)
+	if err != nil {
+		return nil, fmt.Errorf("sql/sqlclient: starting transaction: %w", err)
+	}
+	v, ok := drivers.Load(c.Name)
+	if !ok {
+		return nil, fmt.Errorf("sql/sqlclient: unexpected missing opener %q", c.Name)
+	}
+	drv, err := v.(*driver).openDriver(tx)
+	if err != nil {
+		return nil, fmt.Errorf("sql/sqlclient: opengin atlas driver: %w", err)
+	}
+	ic := *c
+	ic.Driver = drv
+	return &TxClient{Client: &ic, Tx: tx}, nil
+}
+
+// Commit the transaction.
+func (c *TxClient) Commit() error {
+	return c.Tx.Commit()
+}
+
+// Rollback the transaction.
+func (c *TxClient) Rollback() error {
+	return c.Tx.Rollback()
+}
+
 // AddClosers adds list of closers to close at the end of the client lifetime.
 func (c *Client) AddClosers(closers ...io.Closer) {
 	c.closers = append(c.closers, closers...)
@@ -85,10 +124,24 @@ type (
 	// OpenerFunc allows using a function as an Opener.
 	OpenerFunc func(context.Context, *url.URL) (*Client, error)
 
+	// URLParser parses an url.URL into an enriched URL and attaches additional info to it.
+	URLParser interface {
+		ParseURL(*url.URL) *URL
+	}
+
+	// URLParserFunc allows using a function as an URLParser.
+	URLParserFunc func(*url.URL) *URL
+
+	// SchemaChanger is implemented by a driver if it how to change the connection URL to represent another schema.
+	SchemaChanger interface {
+		ChangeSchema(*url.URL, string) *url.URL
+	}
+
 	driver struct {
 		Opener
-		name  string
-		parse func(*url.URL) *URL
+		name       string
+		parser     URLParser
+		openDriver func(schema.ExecQuerier) (migrate.Driver, error)
 	}
 )
 
@@ -97,33 +150,80 @@ func (f OpenerFunc) Open(ctx context.Context, u *url.URL) (*Client, error) {
 	return f(ctx, u)
 }
 
+// ParseURL calls f(u).
+func (f URLParserFunc) ParseURL(u *url.URL) *URL {
+	return f(u)
+}
+
 var drivers sync.Map
 
+type (
+	// openOptions holds additional configuration values for opening a Client.
+	openOptions struct {
+		schema string
+	}
+
+	// OpenOption allows to configure a openOptions using functional arguments.
+	OpenOption func(*openOptions) error
+)
+
+// ErrUnsupported is returned if a registered driver does not support changing the schema.
+var ErrUnsupported = errors.New("sql/sqlclient: driver does not support changing connected schema")
+
 // Open opens an Atlas client by its provided url string.
-func Open(ctx context.Context, s string) (*Client, error) {
+func Open(ctx context.Context, s string, opts ...OpenOption) (*Client, error) {
 	u, err := url.Parse(s)
 	if err != nil {
 		return nil, fmt.Errorf("sql/sqlclient: parse open url: %w", err)
 	}
+	return OpenURL(ctx, u, opts...)
+}
+
+// OpenURL opens an Atlas client by its provided url.URL.
+func OpenURL(ctx context.Context, u *url.URL, opts ...OpenOption) (*Client, error) {
+	cfg := &openOptions{}
+	for _, opt := range opts {
+		if err := opt(cfg); err != nil {
+			return nil, err
+		}
+	}
 	v, ok := drivers.Load(u.Scheme)
 	if !ok {
 		return nil, fmt.Errorf("sql/sqlclient: no opener was register with name %q", u.Scheme)
 	}
-	client, err := v.(driver).Open(ctx, u)
+	// If there is a schema given and the driver allows to change the schema for the url, do it.
+	if cfg.schema != "" {
+		sc, ok := v.(*driver).parser.(SchemaChanger)
+		if !ok {
+			return nil, ErrUnsupported
+		}
+		u = sc.ChangeSchema(u, cfg.schema)
+	}
+	client, err := v.(*driver).Open(ctx, u)
 	if err != nil {
 		return nil, err
 	}
 	if client.URL == nil {
-		client.URL = v.(driver).parse(u)
+		client.URL = v.(*driver).parser.ParseURL(u)
 	}
 	return client, nil
 }
 
+// OpenSchema opens the connection to the given schema.
+// If the registered driver does not support this, ErrUnsupported is returned instead.
+func OpenSchema(s string) OpenOption {
+	return func(c *openOptions) error {
+		c.schema = s
+		return nil
+	}
+}
+
 type (
 	registerOptions struct {
-		parse    func(*url.URL) *URL
-		flavours []string
-		codec    interface {
+		openDriver func(schema.ExecQuerier) (migrate.Driver, error)
+		parser     URLParser
+		flavours   []string
+		codec      interface {
 			schemaspec.Marshaler
 			schemahcl.Evaluator
 		}
@@ -143,9 +243,9 @@ func RegisterFlavours(flavours ...string) RegisterOption {
 
 // RegisterURLParser allows registering a function for parsing
 // the url.URL and attach additional info to the extended URL.
-func RegisterURLParser(p func(*url.URL) *URL) RegisterOption {
+func RegisterURLParser(p URLParser) RegisterOption {
 	return func(opts *registerOptions) {
-		opts.parse = p
+		opts.parser = p
 	}
 }
 
@@ -163,6 +263,14 @@ func RegisterCodec(m schemaspec.Marshaler, e schemahcl.Evaluator) RegisterOption
 	}
 }
 
+// RegisterDriverOpener registers a func to create a migrate.Driver from a schema.ExecQuerier.
+// Registering this function is implicitly done when using DriverOpener.
+func RegisterDriverOpener(open func(schema.ExecQuerier) (migrate.Driver, error)) RegisterOption {
+	return func(opts *registerOptions) {
+		opts.openDriver = open
+	}
+}
+
 // DriverOpener is a helper Opener creator for sharing between all drivers.
 func DriverOpener(open func(schema.ExecQuerier) (migrate.Driver, error)) Opener {
 	return OpenerFunc(func(ctx context.Context, u *url.URL) (*Client, error) {
@@ -170,8 +278,11 @@ func DriverOpener(open func(schema.ExecQuerier) (migrate.Driver, error)) Opener
 		if !ok {
 			return nil, fmt.Errorf("sql/sqlclient: unexpected missing opener %q", u.Scheme)
 		}
-		ur := v.(driver).parse(u)
-		db, err := sql.Open(v.(driver).name, ur.DSN)
+		if v.(*driver).openDriver == nil {
+			v.(*driver).openDriver = open
+		}
+		ur := v.(*driver).parser.ParseURL(u)
+		db, err := sql.Open(v.(*driver).name, ur.DSN)
 		if err != nil {
 			return nil, err
 		}
@@ -183,7 +294,7 @@ func DriverOpener(open func(schema.ExecQuerier) (migrate.Driver, error)) Opener
 			return nil, err
 		}
 		return &Client{
-			Name:   v.(driver).name,
+			Name:   v.(*driver).name,
 			DB:     db,
 			URL:    ur,
 			Driver: drv,
@@ -198,7 +309,7 @@ func Register(name string, opener Opener, opts ...RegisterOption) {
 	}
 	opt := &registerOptions{
 		// Default URL parser uses the URL as the DSN.
-		parse: func(u *url.URL) *URL { return &URL{URL: u, DSN: u.String()} },
+		parser: URLParserFunc(func(u *url.URL) *URL { return &URL{URL: u, DSN: u.String()} }),
 	}
 	for i := range opts {
 		opts[i](opt)
@@ -218,10 +329,11 @@ func Register(name string, opener Opener, opts ...RegisterOption) {
 		if _, ok := drivers.Load(f); ok {
 			panic("sql/sqlclient: Register called twice for " + f)
 		}
-		drivers.Store(f, driver{
-			name:   name,
-			parse:  opt.parse,
-			Opener: opener,
+		drivers.Store(f, &driver{
+			name:       name,
+			parser:     opt.parser,
+			openDriver: opt.openDriver,
+			Opener:     opener,
 		})
 	}
 }
diff --git a/sql/sqlite/driver.go b/sql/sqlite/driver.go
index 7cd54b9f..4debfb47 100644
--- a/sql/sqlite/driver.go
+++ b/sql/sqlite/driver.go
@@ -48,9 +48,9 @@ func init() {
 		sqlclient.DriverOpener(Open),
 		sqlclient.RegisterCodec(MarshalHCL, EvalHCL),
 		sqlclient.RegisterFlavours("sqlite"),
-		sqlclient.RegisterURLParser(func(u *url.URL) *sqlclient.URL {
+		sqlclient.RegisterURLParser(sqlclient.URLParserFunc(func(u *url.URL) *sqlclient.URL {
 			return &sqlclient.URL{URL: u, DSN: strings.TrimPrefix(u.String(), u.Scheme+"://"), Schema: mainFile}
-		}),
+		})),
 	)
 }
 


Test patch:
diff --git a/cmd/atlascmd/migrate/migrate_test.go b/cmd/atlascmd/migrate/migrate_test.go
index 65d59863..cc7e6060 100644
--- a/cmd/atlascmd/migrate/migrate_test.go
+++ b/cmd/atlascmd/migrate/migrate_test.go
@@ -6,26 +6,28 @@ package migrate
 
 import (
 	"context"
-	"database/sql"
+	"fmt"
+	"path/filepath"
 	"testing"
 
 	"ariga.io/atlas/cmd/atlascmd/migrate/ent/revision"
-	"ariga.io/atlas/sql/sqlite"
-	"entgo.io/ent/dialect"
+	"ariga.io/atlas/sql/sqlclient"
 	_ "github.com/mattn/go-sqlite3"
 	"github.com/stretchr/testify/require"
 )
 
 func TestEntRevisions_Init(t *testing.T) {
-	db, err := sql.Open(dialect.SQLite, "file:revisions?cache=shared&mode=memory&_fk=true")
+	c, err := sqlclient.Open(
+		context.Background(),
+		fmt.Sprintf("sqlite://%s?cache=shared&mode=memory&_fk=true", filepath.Join(t.TempDir(), "revision")),
+	)
+	require.NoError(t, err)
+	r, err := NewEntRevisions(c)
 	require.NoError(t, err)
-	r := NewEntRevisions(db, dialect.SQLite)
 
 	require.NoError(t, r.Init(context.Background()))
 
-	drv, err := sqlite.Open(db)
-	require.NoError(t, err)
-	s, err := drv.InspectSchema(context.Background(), "main", nil)
+	s, err := c.Driver.InspectSchema(context.Background(), "", nil)
 	require.NoError(t, err)
 	require.Len(t, s.Tables, 1)
 	_, ok := s.Table(revision.Table)
diff --git a/internal/integration/cockroach_test.go b/internal/integration/cockroach_test.go
index 25d0ccdb..3bc38042 100644
--- a/internal/integration/cockroach_test.go
+++ b/internal/integration/cockroach_test.go
@@ -16,6 +16,9 @@ import (
 	"ariga.io/atlas/sql/migrate"
 	"ariga.io/atlas/sql/postgres"
 	"ariga.io/atlas/sql/schema"
+	"entgo.io/ent/dialect"
+	entschema "entgo.io/ent/dialect/sql/schema"
+	entmigrate "entgo.io/ent/entc/integration/ent/migrate"
 
 	_ "github.com/lib/pq"
 	"github.com/stretchr/testify/require"
@@ -57,7 +60,7 @@ func crdbInit(d string) []io.Closer {
 		}
 		cs = append(cs, db)
 
-		drv, err := postgres.OpenCRDB(db)
+		drv, err := postgres.Open(db)
 		if err != nil {
 			log.Fatalln(err)
 		}
@@ -75,14 +78,761 @@ func crdbRun(t *testing.T, fn func(*crdbTest)) {
 	}
 }
 
+func TestCockroach_EntRevisions(t *testing.T) {
+	crdbRun(t, func(t *crdbTest) {
+		testEntRevisions(t)
+	})
+}
+
+func TestCockroach_Executor(t *testing.T) {
+	crdbRun(t, func(t *crdbTest) {
+		testExecutor(t)
+	})
+}
+
 func TestCockroach_AddDropTable(t *testing.T) {
 	crdbRun(t, func(t *crdbTest) {
 		testAddDrop(t)
 	})
 }
 
+func TestCockroach_Relation(t *testing.T) {
+	crdbRun(t, func(t *crdbTest) {
+		testRelation(t)
+	})
+}
+
+func TestCockroach_AddIndexedColumns(t *testing.T) {
+	crdbRun(t, func(t *crdbTest) {
+		s := &schema.Schema{
+			Name: "public",
+		}
+		usersT := &schema.Table{
+			Name:    "users",
+			Schema:  s,
+			Columns: []*schema.Column{{Name: "id", Type: &schema.ColumnType{Type: &schema.IntegerType{T: "int"}}, Attrs: []schema.Attr{&postgres.Identity{}}}},
+		}
+		usersT.PrimaryKey = &schema.Index{Parts: []*schema.IndexPart{{C: usersT.Columns[0]}}}
+		t.migrate(&schema.AddTable{T: usersT})
+		t.dropTables(usersT.Name)
+		usersT.Columns = append(usersT.Columns, &schema.Column{
+			Name:    "a",
+			Type:    &schema.ColumnType{Raw: "bigint", Type: &schema.IntegerType{T: "bigint"}, Null: true},
+			Default: &schema.Literal{V: "10"},
+		}, &schema.Column{
+			Name:    "b",
+			Type:    &schema.ColumnType{Raw: "bigint", Type: &schema.IntegerType{T: "bigint"}, Null: true},
+			Default: &schema.Literal{V: "10"},
+		}, &schema.Column{
+			Name:    "c",
+			Type:    &schema.ColumnType{Raw: "bigint", Type: &schema.IntegerType{T: "bigint"}, Null: true},
+			Default: &schema.Literal{V: "10"},
+		})
+		parts := usersT.Columns[len(usersT.Columns)-3:]
+		usersT.Indexes = append(usersT.Indexes, &schema.Index{
+			Unique: true,
+			Name:   "a_b_c_unique",
+			Parts:  []*schema.IndexPart{{C: parts[0]}, {C: parts[1]}, {C: parts[2]}},
+		})
+		changes := t.diff(t.loadUsers(), usersT)
+		require.NotEmpty(t, changes, "usersT contains 3 new columns and 1 new index")
+		t.migrate(&schema.ModifyTable{T: usersT, Changes: changes})
+		ensureNoChange(t, usersT)
+
+		// Dropping a column involves in a multi-column
+		// index causes the index to be dropped as well.
+		usersT.Columns = usersT.Columns[:len(usersT.Columns)-1]
+		changes = t.diff(t.loadUsers(), usersT)
+		t.migrate(&schema.ModifyTable{T: usersT, Changes: changes})
+		ensureNoChange(t, t.loadUsers())
+		usersT = t.loadUsers()
+		_, ok := usersT.Index("a_b_c_unique")
+		require.False(t, ok)
+	})
+}
+
+func TestCockroach_AddColumns(t *testing.T) {
+	crdbRun(t, func(t *crdbTest) {
+		usersT := t.users()
+		t.dropTables(usersT.Name)
+		t.migrate(&schema.AddTable{T: usersT})
+		usersT.Columns = append(
+			usersT.Columns,
+			&schema.Column{Name: "a", Type: &schema.ColumnType{Type: &schema.BinaryType{T: "bytea"}}},
+			&schema.Column{Name: "b", Type: &schema.ColumnType{Type: &schema.FloatType{T: "double precision", Precision: 10}}, Default: &schema.Literal{V: "10.1"}},
+			&schema.Column{Name: "c", Type: &schema.ColumnType{Type: &schema.StringType{T: "character"}}, Default: &schema.Literal{V: "'y'"}},
+			&schema.Column{Name: "d", Type: &schema.ColumnType{Type: &schema.DecimalType{T: "numeric", Precision: 10, Scale: 2}}, Default: &schema.Literal{V: "0.99"}},
+			&schema.Column{Name: "e", Type: &schema.ColumnType{Type: &schema.JSONType{T: "json"}}, Default: &schema.Literal{V: "'{}'"}},
+			&schema.Column{Name: "f", Type: &schema.ColumnType{Type: &schema.JSONType{T: "jsonb"}}, Default: &schema.Literal{V: "'1'"}},
+			&schema.Column{Name: "g", Type: &schema.ColumnType{Type: &schema.FloatType{T: "float", Precision: 10}}, Default: &schema.Literal{V: "'1'"}},
+			&schema.Column{Name: "h", Type: &schema.ColumnType{Type: &schema.FloatType{T: "float", Precision: 30}}, Default: &schema.Literal{V: "'1'"}},
+			&schema.Column{Name: "i", Type: &schema.ColumnType{Type: &schema.FloatType{T: "float", Precision: 53}}, Default: &schema.Literal{V: "1"}},
+			&schema.Column{Name: "j", Type: &schema.ColumnType{Type: &postgres.SerialType{T: "serial"}}},
+			&schema.Column{Name: "m", Type: &schema.ColumnType{Type: &schema.BoolType{T: "boolean"}, Null: true}, Default: &schema.Literal{V: "false"}},
+			&schema.Column{Name: "n", Type: &schema.ColumnType{Type: &schema.SpatialType{T: "geometry"}, Null: true}, Default: &schema.Literal{V: "'POINT(1 2)'"}},
+			&schema.Column{Name: "o", Type: &schema.ColumnType{Type: &schema.SpatialType{T: "geometry"}, Null: true}, Default: &schema.Literal{V: "'LINESTRING(0 0, 1440 900)'"}},
+			&schema.Column{Name: "q", Type: &schema.ColumnType{Type: &postgres.ArrayType{T: "text[]"}, Null: true}, Default: &schema.Literal{V: "'{}'"}},
+		)
+		changes := t.diff(t.loadUsers(), usersT)
+		require.Len(t, changes, 14)
+		t.migrate(&schema.ModifyTable{T: usersT, Changes: changes})
+		ensureNoChange(t, usersT)
+	})
+}
+
+func TestCockroach_ColumnInt(t *testing.T) {
+	ctx := context.Background()
+	run := func(t *testing.T, change func(*schema.Column)) {
+		crdbRun(t, func(t *crdbTest) {
+			usersT := &schema.Table{
+				Name:    "users",
+				Columns: []*schema.Column{{Name: "a", Type: &schema.ColumnType{Type: &schema.IntegerType{T: "bigint"}}}},
+			}
+			err := t.drv.ApplyChanges(ctx, []schema.Change{&schema.AddTable{T: usersT}})
+			require.NoError(t, err)
+			t.dropTables(usersT.Name)
+			change(usersT.Columns[0])
+			changes := t.diff(t.loadUsers(), usersT)
+			require.Len(t, changes, 1)
+			t.migrate(&schema.ModifyTable{T: usersT, Changes: changes})
+			ensureNoChange(t, usersT)
+		})
+	}
+
+	t.Run("ChangeNull", func(t *testing.T) {
+		run(t, func(c *schema.Column) {
+			c.Type.Null = true
+		})
+	})
+
+	t.Run("ChangeDefault", func(t *testing.T) {
+		run(t, func(c *schema.Column) {
+			c.Default = &schema.RawExpr{X: "0"}
+		})
+	})
+}
+
+func TestCockroach_ColumnArray(t *testing.T) {
+	crdbRun(t, func(t *crdbTest) {
+		usersT := t.users()
+		t.dropTables(usersT.Name)
+		t.migrate(&schema.AddTable{T: usersT})
+
+		// Add column.
+		usersT.Columns = append(
+			usersT.Columns,
+			&schema.Column{Name: "a", Type: &schema.ColumnType{Raw: "int[]", Type: &postgres.ArrayType{T: "int[]"}}, Default: &schema.Literal{V: "'{1}'"}},
+		)
+		changes := t.diff(t.loadUsers(), usersT)
+		require.Len(t, changes, 1)
+		t.migrate(&schema.ModifyTable{T: usersT, Changes: changes})
+		ensureNoChange(t, usersT)
+
+		// Check default.
+		usersT.Columns[2].Default = &schema.RawExpr{X: "ARRAY[1]"}
+		ensureNoChange(t, usersT)
+
+		// Change default.
+		usersT.Columns[2].Default = &schema.RawExpr{X: "ARRAY[1,2]"}
+		changes = t.diff(t.loadUsers(), usersT)
+		require.Len(t, changes, 1)
+		t.migrate(&schema.ModifyTable{T: usersT, Changes: changes})
+		ensureNoChange(t, usersT)
+	})
+}
+
+func TestCockroach_Enums(t *testing.T) {
+	crdbRun(t, func(t *crdbTest) {
+		ctx := context.Background()
+		usersT := &schema.Table{
+			Name:   "users",
+			Schema: t.realm().Schemas[0],
+			Columns: []*schema.Column{
+				{Name: "state", Type: &schema.ColumnType{Type: &schema.EnumType{T: "state", Values: []string{"on", "off"}}}},
+			},
+		}
+		t.Cleanup(func() {
+			_, err := t.drv.ExecContext(ctx, "DROP TYPE IF EXISTS state, day")
+			require.NoError(t, err)
+		})
+
+		// Create table with an enum column.
+		err := t.drv.ApplyChanges(ctx, []schema.Change{&schema.AddTable{T: usersT}})
+		require.NoError(t, err, "create a new table with an enum column")
+		t.dropTables(usersT.Name)
+		ensureNoChange(t, usersT)
+
+		// Add another enum column.
+		usersT.Columns = append(
+			usersT.Columns,
+			&schema.Column{Name: "day", Type: &schema.ColumnType{Type: &schema.EnumType{T: "day", Values: []string{"sunday", "monday"}}}},
+		)
+		changes := t.diff(t.loadUsers(), usersT)
+		require.Len(t, changes, 1)
+		err = t.drv.ApplyChanges(ctx, []schema.Change{&schema.ModifyTable{T: usersT, Changes: changes}})
+		require.NoError(t, err, "add a new enum column to existing table")
+		ensureNoChange(t, usersT)
+
+		// Add a new value to an existing enum.
+		e := usersT.Columns[2].Type.Type.(*schema.EnumType)
+		e.Values = append(e.Values, "tuesday")
+		changes = t.diff(t.loadUsers(), usersT)
+		require.Len(t, changes, 1)
+		err = t.drv.ApplyChanges(ctx, []schema.Change{&schema.ModifyTable{T: usersT, Changes: changes}})
+		require.NoError(t, err, "append a value to existing enum")
+		ensureNoChange(t, usersT)
+
+		// Add multiple new values to an existing enum.
+		e = usersT.Columns[2].Type.Type.(*schema.EnumType)
+		e.Values = append(e.Values, "wednesday", "thursday", "friday", "saturday")
+		changes = t.diff(t.loadUsers(), usersT)
+		require.Len(t, changes, 1)
+		err = t.drv.ApplyChanges(ctx, []schema.Change{&schema.ModifyTable{T: usersT, Changes: changes}})
+		require.NoError(t, err, "append multiple values to existing enum")
+		ensureNoChange(t, usersT)
+	})
+}
+
+func TestCockroach_Ent(t *testing.T) {
+	crdbRun(t, func(t *crdbTest) {
+		// Cockroach doesn't support macaddr but its in the integration tests of ent
+		for _, ff := range entmigrate.FieldTypesColumns {
+			if st := ff.SchemaType; st[dialect.Postgres] == "macaddr" {
+				c := ff
+				t.Cleanup(func() {
+					c.SchemaType = st
+				})
+				c.SchemaType = nil
+			}
+		}
+		testEntIntegration(t, dialect.Postgres, t.db, entschema.WithAtlas(true))
+	})
+}
+
+func TestCockroach_HCL(t *testing.T) {
+	full := `
+schema "public" {
+}
+table "users" {
+	schema = schema.public
+	column "id" {
+		type = int
+	}
+	primary_key {
+		columns = [table.users.column.id]
+	}
+}
+table "posts" {
+	schema = schema.public
+	column "id" {
+		type = int
+	}
+	column "tags" {
+		type = sql("text[]")
+	}
+	column "author_id" {
+		type = int
+	}
+	foreign_key "author" {
+		columns = [
+			table.posts.column.author_id,
+		]
+		ref_columns = [
+			table.users.column.id,
+		]
+	}
+	primary_key {
+		columns = [table.users.column.id]
+	}
+}
+`
+	empty := `
+schema "public" {
+}
+`
+	crdbRun(t, func(t *crdbTest) {
+		testHCLIntegration(t, full, empty)
+	})
+}
+
+func TestCockroach_HCL_Realm(t *testing.T) {
+	crdbRun(t, func(t *crdbTest) {
+		t.dropSchemas("second")
+		realm := t.loadRealm()
+		hcl, err := postgres.MarshalHCL(realm)
+		require.NoError(t, err)
+		wa := string(hcl) + `
+schema "second" {
+}
+`
+		t.applyRealmHcl(wa)
+		realm, err = t.drv.InspectRealm(context.Background(), &schema.InspectRealmOption{})
+		require.NoError(t, err)
+		_, ok := realm.Schema("public")
+		require.True(t, ok)
+		_, ok = realm.Schema("second")
+		require.True(t, ok)
+	})
+}
+
+func TestCockroach_CLI(t *testing.T) {
+	h := `
+			schema "public" {
+			}
+			table "users" {
+				schema = schema.public
+				column "id" {
+					type = bigint
+				}
+				primary_key {
+					columns = [table.users.column.id]
+				}
+			}`
+	t.Run("SchemaInspect", func(t *testing.T) {
+		crdbRun(t, func(t *crdbTest) {
+			testCLISchemaInspect(t, h, t.dsn(), postgres.UnmarshalHCL, "-s", "public")
+		})
+	})
+	t.Run("SchemaApply", func(t *testing.T) {
+		crdbRun(t, func(t *crdbTest) {
+			testCLISchemaApply(t, h, t.dsn(), "-s", "public")
+		})
+	})
+	t.Run("SchemaApplyDryRun", func(t *testing.T) {
+		crdbRun(t, func(t *crdbTest) {
+			testCLISchemaApplyDry(t, h, t.dsn())
+		})
+	})
+	t.Run("SchemaApplyWithVars", func(t *testing.T) {
+		h := `
+variable "tenant" {
+	type = string
+}
+schema "tenant" {
+	name = var.tenant
+}
+table "users" {
+	schema = schema.tenant
+	column "id" {
+		type = int
+	}
+}
+`
+		crdbRun(t, func(t *crdbTest) {
+			testCLISchemaApply(t, h, t.dsn(), "--var", "tenant=public", "-s", "public")
+		})
+	})
+	t.Run("SchemaDiffRun", func(t *testing.T) {
+		crdbRun(t, func(t *crdbTest) {
+			testCLISchemaDiff(t, t.dsn())
+		})
+	})
+	t.Run("SchemaApplyAutoApprove", func(t *testing.T) {
+		crdbRun(t, func(t *crdbTest) {
+			testCLISchemaApplyAutoApprove(t, h, t.dsn(), "-s", "public")
+		})
+	})
+}
+
+func TestCockroach_CLI_MultiSchema(t *testing.T) {
+	h := `
+			schema "public" {	
+			}
+			table "users" {
+				schema = schema.public
+				column "id" {
+					type = bigint
+				}
+				primary_key {
+					columns = [table.users.column.id]
+				}
+			}
+			schema "test2" {	
+			}
+			table "users" {
+				schema = schema.test2
+				column "id" {
+					type = bigint
+				}
+				primary_key {
+					columns = [table.users.column.id]
+				}
+			}`
+	t.Run("SchemaInspect", func(t *testing.T) {
+		crdbRun(t, func(t *crdbTest) {
+			t.dropSchemas("test2")
+			t.dropTables("users")
+			testCLIMultiSchemaInspect(t, h, t.dsn(), []string{"public", "test2"}, postgres.UnmarshalHCL)
+		})
+	})
+	t.Run("SchemaApply", func(t *testing.T) {
+		crdbRun(t, func(t *crdbTest) {
+			t.dropSchemas("test2")
+			t.dropTables("users")
+			testCLIMultiSchemaApply(t, h, t.dsn(), []string{"public", "test2"}, postgres.UnmarshalHCL)
+		})
+	})
+}
+
+func TestCockroach_DefaultsHCL(t *testing.T) {
+	n := "atlas_defaults"
+	crdbRun(t, func(t *crdbTest) {
+		ddl := `
+create table atlas_defaults
+(
+	string varchar(255) default 'hello_world',
+	quoted varchar(100) default 'never say "never"',
+	tBit bit(10) default B'10101',
+	ts timestamp default CURRENT_TIMESTAMP,
+	tstz timestamp with time zone default CURRENT_TIMESTAMP,
+	number int default 42
+)
+`
+		t.dropTables(n)
+		_, err := t.db.Exec(ddl)
+		require.NoError(t, err)
+		realm := t.loadRealm()
+		spec, err := postgres.MarshalHCL(realm.Schemas[0])
+		require.NoError(t, err)
+		var s schema.Schema
+		err = postgres.UnmarshalHCL(spec, &s)
+		require.NoError(t, err)
+		t.dropTables(n)
+		t.applyHcl(string(spec))
+		ensureNoChange(t, realm.Schemas[0].Tables[0])
+	})
+}
+
+func TestCockroach_Sanity(t *testing.T) {
+	n := "atlas_types_sanity"
+	ddl := `
+create table atlas_types_sanity
+(
+    "tBit"                 bit(10)                     default B'100'                                   null,
+    "tBitVar"              bit varying(10)             default B'100'                                   null,
+    "tBoolean"             boolean                     default false                                not null,
+    "tBool"                bool                        default false                                not null,
+    "tBytea"               bytea                       default E'\\001'                             not null,
+    "tCharacter"           character(10)               default 'atlas'                                  null,
+    "tChar"                char(10)                    default 'atlas'                                  null,
+    "tCharVar"             character varying(10)       default 'atlas'                                  null,
+    "tVarChar"             varchar(10)                 default 'atlas'                                  null,
+    "tText"                text                        default 'atlas'                                  null,
+    "tSmallInt"            smallint                    default '10'                                     null,
+    "tInteger"             integer                     default '10'                                     null,
+    "tBigInt"              bigint                      default '10'                                     null,
+    "tInt"                 int                         default '10'                                     null,
+    "tInt2"                int2                        default '10'                                     null,
+    "tInt4"                int4                        default '10'                                     null,
+    "tInt8"                int8                        default '10'                                     null,
+    "tInet"                inet                        default '127.0.0.1'                              null,
+    "tGeometry"            geometry                    default                                          null,
+    "tDate"                date                        default current_date                             null,
+    "tTime"                time                        default current_time                             null,
+    "tTimeWTZ"             time with time zone         default current_time                             null,
+    "tTimeWOTZ"            time without time zone      default current_time                             null,
+    "tTimestamp"           timestamp                   default now()                                    null,
+    "tTimestampTZ"         timestamptz                 default now()                                    null,
+    "tTimestampWTZ"        timestamp with time zone    default now()                                    null,
+    "tTimestampWOTZ"       timestamp without time zone default now()                                    null,
+    "tTimestampPrec"       timestamp(4)                default now()                                    null,
+    "tDouble"              double precision            default 0                                        null,
+    "tReal"                real                        default 0                                        null,
+    "tFloat8"              float8                      default 0                                        null,
+    "tFloat4"              float4                      default 0                                        null,
+    "tNumeric"             numeric                     default 0                                        null,
+    "tDecimal"             decimal                     default 0                                        null,
+    "tSmallSerial"         smallserial                                                                      ,
+    "tSerial"              serial                                                                           ,
+    "tBigSerial"           bigserial                                                                        ,
+    "tSerial2"             serial2                                                                          ,
+    "tSerial4"             serial4                                                                          ,
+    "tSerial8"             serial8                                                                          ,
+    "tArray"               text[10]                     default '{}'                                    null,
+    "tJSON"                json                         default '{"key":"value"}'                       null,
+    "tJSONB"               jsonb                        default '{"key":"value"}'                       null,
+    "tUUID"                uuid                         default  'a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11' null,
+    "tInterval"            interval                     default '4 hours'                               null
+);
+`
+	crdbRun(t, func(t *crdbTest) {
+		t.dropTables(n)
+		_, err := t.db.Exec(ddl)
+		require.NoError(t, err)
+		realm := t.loadRealm()
+		require.Len(t, realm.Schemas, 1)
+		ts, ok := realm.Schemas[0].Table(n)
+		require.True(t, ok)
+		expected := schema.Table{
+			Name:   n,
+			Schema: realm.Schemas[0],
+			Columns: []*schema.Column{
+				{
+					Name:    "tBit",
+					Type:    &schema.ColumnType{Type: &postgres.BitType{T: "bit", Len: 10}, Raw: "bit", Null: true},
+					Default: &schema.RawExpr{X: "B'100'"},
+				},
+				{
+					Name:    "tBitVar",
+					Type:    &schema.ColumnType{Type: &postgres.BitType{T: "bit varying", Len: 10}, Raw: "bit varying", Null: true},
+					Default: &schema.RawExpr{X: "B'100'"},
+				},
+				{
+					Name:    "tBoolean",
+					Type:    &schema.ColumnType{Type: &schema.BoolType{T: "boolean"}, Raw: "boolean", Null: false},
+					Default: &schema.Literal{V: "false"},
+				},
+				{
+					Name:    "tBool",
+					Type:    &schema.ColumnType{Type: &schema.BoolType{T: "boolean"}, Raw: "boolean", Null: false},
+					Default: &schema.Literal{V: "false"},
+				},
+				{
+					Name:    "tBytea",
+					Type:    &schema.ColumnType{Type: &schema.BinaryType{T: "bytea"}, Raw: "bytea", Null: false},
+					Default: &schema.RawExpr{X: "'\\x01':::BYTES"},
+				},
+				{
+					Name:    "tCharacter",
+					Type:    &schema.ColumnType{Type: &schema.StringType{T: "character", Size: 10}, Raw: "character", Null: true},
+					Default: &schema.RawExpr{X: "'atlas':::STRING"},
+				},
+				{
+					Name:    "tChar",
+					Type:    &schema.ColumnType{Type: &schema.StringType{T: "character", Size: 10}, Raw: "character", Null: true},
+					Default: &schema.RawExpr{X: "'atlas':::STRING"},
+				},
+				{
+					Name:    "tCharVar",
+					Type:    &schema.ColumnType{Type: &schema.StringType{T: "character varying", Size: 10}, Raw: "character varying", Null: true},
+					Default: &schema.RawExpr{X: "'atlas':::STRING"},
+				},
+				{
+					Name:    "tVarChar",
+					Type:    &schema.ColumnType{Type: &schema.StringType{T: "character varying", Size: 10}, Raw: "character varying", Null: true},
+					Default: &schema.RawExpr{X: "'atlas':::STRING"},
+				},
+				{
+					Name:    "tText",
+					Type:    &schema.ColumnType{Type: &schema.StringType{T: "text"}, Raw: "text", Null: true},
+					Default: &schema.RawExpr{X: "'atlas':::STRING"},
+				},
+				{
+					Name:    "tSmallInt",
+					Type:    &schema.ColumnType{Type: &schema.IntegerType{T: "smallint"}, Raw: "smallint", Null: true},
+					Default: &schema.RawExpr{X: "10:::INT8"},
+				},
+				{
+					Name:    "tInteger",
+					Type:    &schema.ColumnType{Type: &schema.IntegerType{T: "bigint"}, Raw: "bigint", Null: true},
+					Default: &schema.RawExpr{X: "10:::INT8"},
+				},
+				{
+					Name:    "tBigInt",
+					Type:    &schema.ColumnType{Type: &schema.IntegerType{T: "bigint"}, Raw: "bigint", Null: true},
+					Default: &schema.RawExpr{X: "10:::INT8"},
+				},
+				{
+					Name:    "tInt",
+					Type:    &schema.ColumnType{Type: &schema.IntegerType{T: "bigint"}, Raw: "bigint", Null: true},
+					Default: &schema.RawExpr{X: "10:::INT8"},
+				},
+				{
+					Name:    "tInt2",
+					Type:    &schema.ColumnType{Type: &schema.IntegerType{T: "smallint"}, Raw: "smallint", Null: true},
+					Default: &schema.RawExpr{X: "10:::INT8"},
+				},
+				{
+					Name:    "tInt4",
+					Type:    &schema.ColumnType{Type: &schema.IntegerType{T: "integer"}, Raw: "integer", Null: true},
+					Default: &schema.RawExpr{X: "10:::INT8"},
+				},
+				{
+					Name:    "tInt8",
+					Type:    &schema.ColumnType{Type: &schema.IntegerType{T: "bigint"}, Raw: "bigint", Null: true},
+					Default: &schema.RawExpr{X: "10:::INT8"},
+				},
+				{
+					Name:    "tInet",
+					Type:    &schema.ColumnType{Type: &postgres.NetworkType{T: "inet"}, Raw: "inet", Null: true},
+					Default: &schema.RawExpr{X: "'127.0.0.1':::INET"},
+				},
+				{
+					Name: "tGeometry",
+					Type: &schema.ColumnType{Type: &schema.SpatialType{T: "geometry"}, Raw: "geometry", Null: true},
+				},
+				{
+					Name:    "tDate",
+					Type:    &schema.ColumnType{Type: &schema.TimeType{T: "date"}, Raw: "date", Null: true},
+					Default: &schema.RawExpr{X: "current_date()"},
+				},
+				{
+					Name:    "tTime",
+					Type:    &schema.ColumnType{Type: &schema.TimeType{T: "time without time zone", Precision: intp(6)}, Raw: "time without time zone", Null: true},
+					Default: &schema.RawExpr{X: "current_time():::TIME"},
+				},
+				{
+					Name:    "tTimeWTZ",
+					Type:    &schema.ColumnType{Type: &schema.TimeType{T: "time with time zone", Precision: intp(6)}, Raw: "time with time zone", Null: true},
+					Default: &schema.RawExpr{X: "current_time():::TIMETZ"},
+				},
+				{
+					Name:    "tTimeWOTZ",
+					Type:    &schema.ColumnType{Type: &schema.TimeType{T: "time without time zone", Precision: intp(6)}, Raw: "time without time zone", Null: true},
+					Default: &schema.RawExpr{X: "current_time():::TIME"},
+				},
+				{
+					Name:    "tTimestamp",
+					Type:    &schema.ColumnType{Type: &schema.TimeType{T: "timestamp without time zone", Precision: intp(6)}, Raw: "timestamp without time zone", Null: true},
+					Default: &schema.RawExpr{X: "now():::TIMESTAMP"},
+				},
+				{
+					Name:    "tTimestampTZ",
+					Type:    &schema.ColumnType{Type: &schema.TimeType{T: "timestamp with time zone", Precision: intp(6)}, Raw: "timestamp with time zone", Null: true},
+					Default: &schema.RawExpr{X: "now():::TIMESTAMPTZ"},
+				},
+				{
+					Name:    "tTimestampWTZ",
+					Type:    &schema.ColumnType{Type: &schema.TimeType{T: "timestamp with time zone", Precision: intp(6)}, Raw: "timestamp with time zone", Null: true},
+					Default: &schema.RawExpr{X: "now():::TIMESTAMPTZ"},
+				},
+				{
+					Name:    "tTimestampWOTZ",
+					Type:    &schema.ColumnType{Type: &schema.TimeType{T: "timestamp without time zone", Precision: intp(6)}, Raw: "timestamp without time zone", Null: true},
+					Default: &schema.RawExpr{X: "now():::TIMESTAMP"},
+				},
+				{
+					Name:    "tTimestampPrec",
+					Type:    &schema.ColumnType{Type: &schema.TimeType{T: "timestamp without time zone", Precision: intp(4)}, Raw: "timestamp without time zone", Null: true},
+					Default: &schema.RawExpr{X: "now():::TIMESTAMP"},
+				},
+				{
+					Name:    "tDouble",
+					Type:    &schema.ColumnType{Type: &schema.FloatType{T: "double precision", Precision: 53}, Raw: "double precision", Null: true},
+					Default: &schema.RawExpr{X: "0.0:::FLOAT8"},
+				},
+				{
+					Name:    "tReal",
+					Type:    &schema.ColumnType{Type: &schema.FloatType{T: "real", Precision: 24}, Raw: "real", Null: true},
+					Default: &schema.RawExpr{X: "0.0:::FLOAT8"},
+				},
+				{
+					Name:    "tFloat8",
+					Type:    &schema.ColumnType{Type: &schema.FloatType{T: "double precision", Precision: 53}, Raw: "double precision", Null: true},
+					Default: &schema.RawExpr{X: "0.0:::FLOAT8"},
+				},
+				{
+					Name:    "tFloat4",
+					Type:    &schema.ColumnType{Type: &schema.FloatType{T: "real", Precision: 24}, Raw: "real", Null: true},
+					Default: &schema.RawExpr{X: "0.0:::FLOAT8"},
+				},
+				{
+					Name:    "tNumeric",
+					Type:    &schema.ColumnType{Type: &schema.DecimalType{T: "numeric", Precision: 0}, Raw: "numeric", Null: true},
+					Default: &schema.RawExpr{X: "0:::DECIMAL"},
+				},
+				{
+					Name:    "tDecimal",
+					Type:    &schema.ColumnType{Type: &schema.DecimalType{T: "numeric", Precision: 0}, Raw: "numeric", Null: true},
+					Default: &schema.RawExpr{X: "0:::DECIMAL"},
+				},
+				{
+					Name: "tSmallSerial",
+					Type: &schema.ColumnType{Type: &schema.IntegerType{T: "bigint", Unsigned: false}, Raw: "bigint", Null: false},
+					Default: &schema.RawExpr{
+						X: "unique_rowid()",
+					},
+				},
+				{
+					Name: "tSerial",
+					Type: &schema.ColumnType{Type: &schema.IntegerType{T: "bigint", Unsigned: false}, Raw: "bigint", Null: false},
+					Default: &schema.RawExpr{
+						X: "unique_rowid()",
+					},
+				},
+				{
+					Name: "tBigSerial",
+					Type: &schema.ColumnType{Type: &schema.IntegerType{T: "bigint", Unsigned: false}, Raw: "bigint", Null: false},
+					Default: &schema.RawExpr{
+						X: "unique_rowid()",
+					},
+				},
+				{
+					Name: "tSerial2",
+					Type: &schema.ColumnType{Type: &schema.IntegerType{T: "bigint", Unsigned: false}, Raw: "bigint", Null: false},
+					Default: &schema.RawExpr{
+						X: "unique_rowid()",
+					},
+				},
+				{
+					Name: "tSerial4",
+					Type: &schema.ColumnType{Type: &schema.IntegerType{T: "bigint", Unsigned: false}, Raw: "bigint", Null: false},
+					Default: &schema.RawExpr{
+						X: "unique_rowid()",
+					},
+				},
+				{
+					Name: "tSerial8",
+					Type: &schema.ColumnType{Type: &schema.IntegerType{T: "bigint", Unsigned: false}, Raw: "bigint", Null: false},
+					Default: &schema.RawExpr{
+						X: "unique_rowid()",
+					},
+				},
+				{
+					Name: "tArray",
+					Type: &schema.ColumnType{Type: &postgres.ArrayType{T: "text[]"}, Raw: "ARRAY", Null: true},
+					Default: &schema.RawExpr{
+						X: "ARRAY[]:::STRING[]",
+					},
+				},
+				{
+					Name: "tJSON",
+					Type: &schema.ColumnType{Type: &schema.JSONType{T: "jsonb"}, Raw: "jsonb", Null: true},
+					Default: &schema.RawExpr{
+						X: "'{\"key\": \"value\"}':::JSONB",
+					},
+				},
+				{
+					Name: "tJSONB",
+					Type: &schema.ColumnType{Type: &schema.JSONType{T: "jsonb"}, Raw: "jsonb", Null: true},
+					Default: &schema.RawExpr{
+						X: "'{\"key\": \"value\"}':::JSONB",
+					},
+				},
+				{
+					Name: "tUUID",
+					Type: &schema.ColumnType{Type: &postgres.UUIDType{T: "uuid"}, Raw: "uuid", Null: true},
+					Default: &schema.RawExpr{
+						X: "'a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11':::UUID",
+					},
+				},
+				{
+					Name: "tInterval",
+					Type: &schema.ColumnType{Type: &schema.UnsupportedType{T: "interval"}, Raw: "interval", Null: true},
+					Default: &schema.RawExpr{
+						X: "'04:00:00':::INTERVAL",
+					},
+				},
+			},
+		}
+		for i, c := range expected.Columns {
+			require.EqualValues(t, ts.Columns[i], c, c.Name)
+		}
+	})
+
+	t.Run("ImplicitIndexes", func(t *testing.T) {
+		crdbRun(t, func(t *crdbTest) {
+			testImplicitIndexes(t, t.db)
+		})
+	})
+}
+
+func (t *crdbTest) url() string {
+	return t.dsn()
+}
+
 func (t *crdbTest) dsn() string {
-	return fmt.Sprintf("postgres://postgres:pass@localhost:%d/test?sslmode=disable", t.port)
+	return fmt.Sprintf("postgres://root:pass@localhost:%d/defaultdb?sslmode=disable", t.port)
 }
 
 func (t *crdbTest) driver() migrate.Driver {
diff --git a/internal/integration/integration_test.go b/internal/integration/integration_test.go
index cad5fea9..7b32489a 100644
--- a/internal/integration/integration_test.go
+++ b/internal/integration/integration_test.go
@@ -20,9 +20,12 @@ import (
 	"text/template"
 	"time"
 
+	entmigrate "ariga.io/atlas/cmd/atlascmd/migrate"
+	"ariga.io/atlas/cmd/atlascmd/migrate/ent/revision"
 	"ariga.io/atlas/schema/schemaspec"
 	"ariga.io/atlas/sql/migrate"
 	"ariga.io/atlas/sql/schema"
+	"ariga.io/atlas/sql/sqlclient"
 	entsql "entgo.io/ent/dialect/sql"
 	entschema "entgo.io/ent/dialect/sql/schema"
 	"entgo.io/ent/entc/integration/ent"
@@ -49,6 +52,7 @@ func TestMain(m *testing.M) {
 // T holds the elements common between dialect tests.
 type T interface {
 	testing.TB
+	url() string
 	driver() migrate.Driver
 	revisionsStorage() migrate.RevisionReadWriter
 	realm() *schema.Realm
@@ -60,6 +64,7 @@ type T interface {
 	revisions() *schema.Table
 	loadTable(string) *schema.Table
 	dropTables(...string)
+	dropSchemas(...string)
 	migrate(...schema.Change)
 	diff(*schema.Table, *schema.Table) []schema.Change
 	applyHcl(spec string)
@@ -174,7 +179,7 @@ func testHCLIntegration(t T, full string, empty string) {
 	require.Empty(t, t.realm().Schemas[0].Tables)
 }
 
-func testCLISchemaInspect(t T, h string, dsn string, unmarshaler schemaspec.Unmarshaler) {
+func testCLISchemaInspect(t T, h string, dsn string, unmarshaler schemaspec.Unmarshaler, args ...string) {
 	err := initCLI()
 	require.NoError(t, err)
 	t.dropTables("users")
@@ -182,12 +187,15 @@ func testCLISchemaInspect(t T, h string, dsn string, unmarshaler schemaspec.Unma
 	err = unmarshaler.UnmarshalSpec([]byte(h), &expected)
 	require.NoError(t, err)
 	t.applyHcl(h)
-	cmd := exec.Command("go", "run", "ariga.io/atlas/cmd/atlas",
+	runArgs := []string{
+		"run", "ariga.io/atlas/cmd/atlas",
 		"schema",
 		"inspect",
 		"-d",
 		dsn,
-	)
+	}
+	runArgs = append(runArgs, args...)
+	cmd := exec.Command("go", runArgs...)
 	stdout, stderr := bytes.NewBuffer(nil), bytes.NewBuffer(nil)
 	cmd.Stderr = stderr
 	cmd.Stdout = stdout
@@ -266,7 +274,6 @@ func testCLIMultiSchemaApply(t T, h string, dsn string, schemas []string, unmars
 }
 
 func testCLIMultiSchemaInspect(t T, h string, dsn string, schemas []string, unmarshaler schemaspec.Unmarshaler) {
-
 	err := initCLI()
 	require.NoError(t, err)
 	var expected schema.Realm
@@ -293,7 +300,6 @@ func testCLIMultiSchemaInspect(t T, h string, dsn string, schemas []string, unma
 }
 
 func testCLISchemaApply(t T, h string, dsn string, args ...string) {
-
 	err := initCLI()
 	require.NoError(t, err)
 	t.dropTables("users")
@@ -329,7 +335,6 @@ func testCLISchemaApply(t T, h string, dsn string, args ...string) {
 }
 
 func testCLISchemaApplyDry(t T, h string, dsn string) {
-
 	err := initCLI()
 	require.NoError(t, err)
 	t.dropTables("users")
@@ -362,15 +367,15 @@ func testCLISchemaApplyDry(t T, h string, dsn string) {
 	require.False(t, ok, "expected users table not to be created")
 }
 
-func testCLISchemaApplyAutoApprove(t T, h string, dsn string) {
-
+func testCLISchemaApplyAutoApprove(t T, h string, dsn string, args ...string) {
 	err := initCLI()
 	require.NoError(t, err)
 	t.dropTables("users")
 	f := filepath.Join(t.TempDir(), "atlas.hcl")
 	err = ioutil.WriteFile(f, []byte(h), 0644)
 	require.NoError(t, err)
-	cmd := exec.Command("go", "run", "ariga.io/atlas/cmd/atlas",
+	runArgs := []string{
+		"run", "ariga.io/atlas/cmd/atlas",
 		"schema",
 		"apply",
 		"-d",
@@ -378,7 +383,9 @@ func testCLISchemaApplyAutoApprove(t T, h string, dsn string) {
 		"-f",
 		f,
 		"--auto-approve",
-	)
+	}
+	runArgs = append(runArgs, args...)
+	cmd := exec.Command("go", runArgs...)
 	stdout, stderr := bytes.NewBuffer(nil), bytes.NewBuffer(nil)
 	cmd.Stderr = stderr
 	cmd.Stdout = stdout
@@ -391,7 +398,6 @@ func testCLISchemaApplyAutoApprove(t T, h string, dsn string) {
 }
 
 func testCLISchemaDiff(t T, dsn string) {
-
 	err := initCLI()
 
 	require.NoError(t, err)
@@ -466,6 +472,25 @@ func testAdvisoryLock(t *testing.T, l schema.Locker) {
 	})
 }
 
+// testEntRevisions will not be run for SQLite. It has its own version of this test.
+func testEntRevisions(t T) {
+	const name = "my_revisions_schema"
+
+	c, err := sqlclient.Open(context.Background(), t.url())
+	require.NoError(t, err)
+
+	r, err := entmigrate.NewEntRevisions(c, entmigrate.WithSchema(name))
+	require.NoError(t, err)
+
+	require.NoError(t, r.Init(context.Background()))
+	t.dropSchemas(name)
+
+	s, err := c.InspectSchema(context.Background(), name, nil)
+	require.NoError(t, err)
+	_, ok := s.Table(revision.Table)
+	require.True(t, ok)
+}
+
 func testExecutor(t T) {
 	usersT, postsT := t.users(), t.posts()
 	petsT := &schema.Table{
diff --git a/internal/integration/mysql_test.go b/internal/integration/mysql_test.go
index a65c58fc..9ea2d7f7 100644
--- a/internal/integration/mysql_test.go
+++ b/internal/integration/mysql_test.go
@@ -79,6 +79,12 @@ func myRun(t *testing.T, fn func(*myTest)) {
 	}
 }
 
+func TestMySQL_EntRevisions(t *testing.T) {
+	myRun(t, func(t *myTest) {
+		testEntRevisions(t)
+	})
+}
+
 func TestMySQL_Executor(t *testing.T) {
 	myRun(t, func(t *myTest) {
 		testExecutor(t)
@@ -661,7 +667,7 @@ func TestMySQL_CLI_MultiSchema(t *testing.T) {
 			}`
 	t.Run("SchemaInspect", func(t *testing.T) {
 		myRun(t, func(t *myTest) {
-			t.dropDB("test2")
+			t.dropSchemas("test2")
 			t.dropTables("users")
 			attrs := t.defaultAttrs()
 			charset, collate := attrs[0].(*schema.Charset), attrs[1].(*schema.Collation)
@@ -670,7 +676,7 @@ func TestMySQL_CLI_MultiSchema(t *testing.T) {
 	})
 	t.Run("SchemaApply", func(t *testing.T) {
 		myRun(t, func(t *myTest) {
-			t.dropDB("test2")
+			t.dropSchemas("test2")
 			t.dropTables("users")
 			attrs := t.defaultAttrs()
 			charset, collate := attrs[0].(*schema.Charset), attrs[1].(*schema.Collation)
@@ -681,7 +687,7 @@ func TestMySQL_CLI_MultiSchema(t *testing.T) {
 
 func TestMySQL_HCL_Realm(t *testing.T) {
 	myRun(t, func(t *myTest) {
-		t.dropDB("second")
+		t.dropSchemas("second")
 		realm := t.loadRealm()
 		hcl, err := mysql.MarshalHCL(realm)
 		require.NoError(t, err)
@@ -1178,6 +1184,10 @@ create table atlas_types_sanity
 	})
 }
 
+func (t *myTest) url() string {
+	return t.dsn("")
+}
+
 func (t *myTest) dsn(dbname string) string {
 	d := "mysql"
 	pass := ":pass"
@@ -1240,7 +1250,7 @@ func (t *myTest) dropTables(names ...string) {
 	})
 }
 
-func (t *myTest) dropDB(names ...string) {
+func (t *myTest) dropSchemas(names ...string) {
 	t.Cleanup(func() {
 		for _, n := range names {
 			_, err := t.db.Exec("DROP DATABASE IF EXISTS " + n)
diff --git a/internal/integration/postgres_test.go b/internal/integration/postgres_test.go
index c9143a00..052e67ef 100644
--- a/internal/integration/postgres_test.go
+++ b/internal/integration/postgres_test.go
@@ -14,7 +14,6 @@ import (
 	"testing"
 
 	"ariga.io/atlas/sql/migrate"
-	"ariga.io/atlas/sql/mysql"
 	"ariga.io/atlas/sql/postgres"
 	"ariga.io/atlas/sql/schema"
 
@@ -80,6 +79,12 @@ func pgRun(t *testing.T, fn func(*pgTest)) {
 	}
 }
 
+func TestPostgres_EntRevisions(t *testing.T) {
+	pgRun(t, func(t *pgTest) {
+		testEntRevisions(t)
+	})
+}
+
 func TestPostgres_Executor(t *testing.T) {
 	pgRun(t, func(t *pgTest) {
 		testExecutor(t)
@@ -487,7 +492,7 @@ func TestPostgres_HCL_Realm(t *testing.T) {
 	pgRun(t, func(t *pgTest) {
 		t.dropSchemas("second")
 		realm := t.loadRealm()
-		hcl, err := mysql.MarshalHCL(realm)
+		hcl, err := postgres.MarshalHCL(realm)
 		require.NoError(t, err)
 		wa := string(hcl) + `
 schema "second" {
@@ -1033,6 +1038,10 @@ create table atlas_types_sanity
 	})
 }
 
+func (t *pgTest) url() string {
+	return t.dsn()
+}
+
 func (t *pgTest) dsn() string {
 	return fmt.Sprintf("postgres://postgres:pass@localhost:%d/test?sslmode=disable", t.port)
 }
diff --git a/internal/integration/script_test.go b/internal/integration/script_test.go
index 7181d22c..51cf2f1b 100644
--- a/internal/integration/script_test.go
+++ b/internal/integration/script_test.go
@@ -58,6 +58,7 @@ func TestPostgres_Script(t *testing.T) {
 				"synced":  t.cmdSynced,
 				"cmphcl":  t.cmdCmpHCL,
 				"cmpshow": t.cmdCmpShow,
+				"execsql": t.cmdExec,
 			},
 		})
 	})
@@ -356,6 +357,10 @@ func (t *myTest) cmdExec(ts *testscript.TestScript, _ bool, args []string) {
 	cmdExec(ts, args, t.db)
 }
 
+func (t *pgTest) cmdExec(ts *testscript.TestScript, _ bool, args []string) {
+	cmdExec(ts, args, t.db)
+}
+
 func (t *liteTest) cmdExec(ts *testscript.TestScript, _ bool, args []string) {
 	cmdExec(ts, args, ts.Value(keyDB).(*sql.DB))
 }
diff --git a/internal/integration/sqlite_test.go b/internal/integration/sqlite_test.go
index 8740dfbb..3e04ea45 100644
--- a/internal/integration/sqlite_test.go
+++ b/internal/integration/sqlite_test.go
@@ -15,9 +15,12 @@ import (
 	"strings"
 	"testing"
 
+	entmigrate "ariga.io/atlas/cmd/atlascmd/migrate"
+	"ariga.io/atlas/cmd/atlascmd/migrate/ent/revision"
 	"ariga.io/atlas/sql/migrate"
 	"ariga.io/atlas/sql/postgres"
 	"ariga.io/atlas/sql/schema"
+	"ariga.io/atlas/sql/sqlclient"
 	"ariga.io/atlas/sql/sqlite"
 
 	"entgo.io/ent/dialect"
@@ -47,6 +50,23 @@ func liteRun(t *testing.T, fn func(test *liteTest)) {
 	fn(tt)
 }
 
+func TestSQLite_EntRevisions(t *testing.T) {
+	liteRun(t, func(t *liteTest) {
+		c, err := sqlclient.Open(context.Background(), t.url())
+		require.NoError(t, err)
+
+		r, err := entmigrate.NewEntRevisions(c)
+		require.NoError(t, err)
+
+		require.NoError(t, r.Init(context.Background()))
+
+		s, err := c.InspectSchema(context.Background(), "", nil)
+		require.NoError(t, err)
+		_, ok := s.Table(revision.Table)
+		require.True(t, ok)
+	})
+}
+
 func TestSQLite_Executor(t *testing.T) {
 	liteRun(t, func(t *liteTest) {
 		testExecutor(t)
@@ -785,6 +805,8 @@ func (t *liteTest) revisionsStorage() migrate.RevisionReadWriter {
 	return t.rrw
 }
 
+func (t *liteTest) dropSchemas(...string) {}
+
 func (t *liteTest) applyHcl(spec string) {
 	realm := t.loadRealm()
 	var desired schema.Schema
@@ -936,6 +958,10 @@ func (t *liteTest) dsn() string {
 	return fmt.Sprintf("sqlite://file:%s?cache=shared&_fk=1", t.file)
 }
 
+func (t *liteTest) url() string {
+	return t.dsn()
+}
+
 func (t *liteTest) applyRealmHcl(spec string) {
 	t.applyHcl(spec)
 }
diff --git a/internal/integration/testdata/postgres/index-desc.txt b/internal/integration/testdata/postgres/index-desc.txt
index 422d11ab..3f0967df 100644
--- a/internal/integration/testdata/postgres/index-desc.txt
+++ b/internal/integration/testdata/postgres/index-desc.txt
@@ -10,6 +10,9 @@ synced 2-no-change.hcl
 apply 3.hcl
 cmpshow users 3.sql
 
+apply 4.hcl
+cmpshow users 4.sql
+
 -- 1.hcl --
 schema "$db" {}
 
@@ -21,7 +24,7 @@ table "users" {
     index "rank_idx" {
         on {
             desc   = true
-            column = table.users.column.rank
+            column = column.rank
         }
     }
 }
@@ -73,7 +76,6 @@ table "users" {
     }
 }
 
-
 -- 3.hcl --
 schema "$db" {}
 
@@ -104,3 +106,53 @@ table "users" {
  score  | integer |           | not null |
 Indexes:
     "rank_score_idx" btree (rank, score DESC)
+
+
+-- 4.hcl --
+schema "$db" {}
+
+table "users" {
+    schema = schema.$db
+    column "rank" {
+        type = int
+    }
+    column "score" {
+        type = int
+    }
+    index "double_rank_desc_idx" {
+        on {
+            expr = "rank * 2"
+            desc = true
+        }
+    }
+    index "double_score_desc_idx" {
+        on {
+            expr = "score * 2"
+            desc = true
+        }
+    }
+    index "double_rank_idx" {
+        on {
+            expr = "rank * 2"
+            desc = false
+        }
+    }
+    index "double_score_idx" {
+        on {
+            expr = "score * 2"
+            desc = false
+        }
+    }
+}
+
+-- 4.sql --
+      Table "script_index_desc.users"
+ Column |  Type   | Collation | Nullable | Default
+--------+---------+-----------+----------+---------
+ rank   | integer |           | not null |
+ score  | integer |           | not null |
+Indexes:
+    "double_rank_desc_idx" btree ((rank * 2) DESC)
+    "double_rank_idx" btree ((rank * 2))
+    "double_score_desc_idx" btree ((score * 2) DESC)
+    "double_score_idx" btree ((score * 2))
\ No newline at end of file
diff --git a/internal/integration/testdata/postgres/table-partition.txt b/internal/integration/testdata/postgres/table-partition.txt
index d1b219ab..5fdb857c 100644
--- a/internal/integration/testdata/postgres/table-partition.txt
+++ b/internal/integration/testdata/postgres/table-partition.txt
@@ -17,6 +17,8 @@ apply drop.hcl
 apply 3.hcl
 cmpshow measurement 3.sql
 cmphcl 3.inspect.hcl
+execsql 'CREATE TABLE measurement_y2006m02 PARTITION OF $db.measurement FOR VALUES FROM (''2006-02-01'') TO (''2006-03-01'')'
+cmpshow measurement 3.partition.sql
 
 # Drop all tables.
 apply drop.hcl
@@ -173,6 +175,40 @@ Partitioned table "script_table_partition.measurement"
 Partition key: RANGE (logdate)
 Number of partitions: 0
 
+
+-- postgres10/3.partition.sql --
+      Table "script_table_partition.measurement"
+  Column   |  Type   | Collation | Nullable | Default
+-----------+---------+-----------+----------+---------
+ city_id   | integer |           | not null |
+ logdate   | date    |           | not null |
+ peaktemp  | integer |           |          |
+ unitsales | integer |           |          |
+Partition key: RANGE (logdate)
+Number of partitions: 1 (Use \d+ to list them.)
+
+-- postgres11/3.partition.sql --
+      Table "script_table_partition.measurement"
+  Column   |  Type   | Collation | Nullable | Default
+-----------+---------+-----------+----------+---------
+ city_id   | integer |           | not null |
+ logdate   | date    |           | not null |
+ peaktemp  | integer |           |          |
+ unitsales | integer |           |          |
+Partition key: RANGE (logdate)
+Number of partitions: 1 (Use \d+ to list them.)
+
+-- 3.partition.sql --
+Partitioned table "script_table_partition.measurement"
+  Column   |  Type   | Collation | Nullable | Default
+-----------+---------+-----------+----------+---------
+ city_id   | integer |           | not null |
+ logdate   | date    |           | not null |
+ peaktemp  | integer |           |          |
+ unitsales | integer |           |          |
+Partition key: RANGE (logdate)
+Number of partitions: 1 (Use \d+ to list them.)
+
 -- 3.inspect.hcl --
 table "measurement" {
   schema = schema.script_table_partition
diff --git a/internal/integration/tidb_test.go b/internal/integration/tidb_test.go
index 0291f450..9d1535c2 100644
--- a/internal/integration/tidb_test.go
+++ b/internal/integration/tidb_test.go
@@ -67,6 +67,12 @@ func tidbRun(t *testing.T, fn func(*myTest)) {
 	}
 }
 
+func TestTiDB_EntRevisions(t *testing.T) {
+	tidbRun(t, func(t *myTest) {
+		testEntRevisions(t)
+	})
+}
+
 func TestTiDB_AddDropTable(t *testing.T) {
 	tidbRun(t, func(t *myTest) {
 		testAddDrop(t)
@@ -471,7 +477,7 @@ func TestTiDB_ForeignKey(t *testing.T) {
 
 func TestTiDB_HCL_Realm(t *testing.T) {
 	tidbRun(t, func(t *myTest) {
-		t.dropDB("second")
+		t.dropSchemas("second")
 		realm := t.loadRealm()
 		hcl, err := mysql.MarshalHCL(realm)
 		require.NoError(t, err)
@@ -547,7 +553,7 @@ func TestTiDB_CLI_MultiSchema(t *testing.T) {
 			}`
 	t.Run("SchemaInspect", func(t *testing.T) {
 		tidbRun(t, func(t *myTest) {
-			t.dropDB("test2")
+			t.dropSchemas("test2")
 			t.dropTables("users")
 			attrs := t.defaultAttrs()
 			charset, collate := attrs[0].(*schema.Charset), attrs[1].(*schema.Collation)
@@ -556,7 +562,7 @@ func TestTiDB_CLI_MultiSchema(t *testing.T) {
 	})
 	t.Run("SchemaApply", func(t *testing.T) {
 		tidbRun(t, func(t *myTest) {
-			t.dropDB("test2")
+			t.dropSchemas("test2")
 			t.dropTables("users")
 			attrs := t.defaultAttrs()
 			charset, collate := attrs[0].(*schema.Charset), attrs[1].(*schema.Collation)
diff --git a/sql/postgres/inspect_test.go b/sql/postgres/inspect_test.go
index 7e7bc600..eba79b9f 100644
--- a/sql/postgres/inspect_test.go
+++ b/sql/postgres/inspect_test.go
@@ -23,6 +23,7 @@ var (
 	queryTables      = sqltest.Escape(fmt.Sprintf(tablesQuery, "$1"))
 	queryChecks      = sqltest.Escape(fmt.Sprintf(checksQuery, "$2"))
 	queryColumns     = sqltest.Escape(fmt.Sprintf(columnsQuery, "$2"))
+	queryCrdbColumns = sqltest.Escape(fmt.Sprintf(crdbColumnsQuery, "$2"))
 	queryIndexes     = sqltest.Escape(fmt.Sprintf(indexesQuery, "$2"))
 	queryCrdbIndexes = sqltest.Escape(fmt.Sprintf(crdbIndexesQuery, "$2"))
 )
@@ -30,7 +31,6 @@ var (
 func TestDriver_InspectTable(t *testing.T) {
 	tests := []struct {
 		name   string
-		crdb   bool
 		before func(mock)
 		expect func(*require.Assertions, *schema.Table, error)
 	}{
@@ -185,61 +185,6 @@ users           | idx6            | brin        | c1          | f       | t
 				require.EqualValues(pk, t.PrimaryKey)
 			},
 		},
-		{
-			name: "crdb table indexes",
-			crdb: true,
-			before: func(m mock) {
-				m.tableExists("public", "users", true)
-				m.ExpectQuery(queryColumns).
-					WithArgs("public", "users").
-					WillReturnRows(sqltest.Rows(`
-table_name  | column_name | data_type | is_nullable |              column_default               | character_maximum_length | numeric_precision | datetime_precision | numeric_scale | character_set_name | collation_name | udt_name | is_identity | identity_start | identity_increment |       identity_generation        | generation_expression | comment | typtype | oid 
-------------+-------------+-----------+-------------+-------------------------------------------+--------------------------+-------------------+--------------------+---------------+--------------------+----------------+----------+-------------+----------------+--------------------+----------------------------------+-----------------------+---------+---------+-----
-users       | a           | bigint    | NO          |                                           |                          |                64 |                    |             0 |                    |                | int8     | NO          |                |                    |                                  |                       |         | b       | 20 
-users       | b           | bigint    | NO          |                                           |                          |                64 |                    |             0 |                    |                | int8     | NO          |                |                    |                                  |                       |         | b       | 20 
-users       | c           | bigint    | NO          |                                           |                          |                64 |                    |             0 |                    |                | int8     | NO          |                |                    |                                  |                       |         | b       | 20 
-users       | d           | bigint    | NO          |                                           |                          |                64 |                    |             0 |                    |                | int8     | NO          |                |                    |                                  |                       |         | b       | 20 
-`))
-				m.ExpectQuery(queryCrdbIndexes).
-					WithArgs("public", "users").
-					WillReturnRows(sqltest.Rows(`
-table_name  | index_name | column_name | primary | unique | constraint_type |                                   create_stmt                                   | predicate | expression | comment 
-------------+------------+-------------+---------+--------+-----------------+---------------------------------------------------------------------------------+-----------+------------+---------
-users       | idx1       | a           | false   | false  |                 | CREATE INDEX idx1 ON defaultdb.public.serial USING btree (a ASC)                |           | a          |  
-users       | idx2       | b           | false   | true   | u               | CREATE UNIQUE INDEX idx2 ON defaultdb.public.serial USING btree (b ASC)         |           | b          |  
-users       | idx3       | c           | false   | false  |                 | CREATE INDEX idx3 ON defaultdb.public.serial USING btree (c DESC)               |           | c          | boring 
-users       | idx4       | d           | false   | false  |                 | CREATE INDEX idx5 ON defaultdb.public.serial USING btree (d ASC) WHERE (d < 10) | d < 10    | d          |  
-users       | idx5       | a           | false   | false  |                 | CREATE INDEX idx5 ON defaultdb.public.serial USING btree (a ASC, b ASC, c ASC)  |           | a          |  
-users       | idx5       | b           | false   | false  |                 | CREATE INDEX idx5 ON defaultdb.public.serial USING btree (a ASC, b ASC, c ASC)  |           | b          |  
-users       | idx5       | c           | false   | false  |                 | CREATE INDEX idx5 ON defaultdb.public.serial USING btree (a ASC, b ASC, c ASC)  |           | c          |  
-`))
-				m.noFKs()
-				m.noChecks()
-			},
-			expect: func(require *require.Assertions, t *schema.Table, err error) {
-				require.NoError(err)
-				require.Equal("users", t.Name)
-				columns := []*schema.Column{
-					{Name: "a", Type: &schema.ColumnType{Raw: "bigint", Type: &schema.IntegerType{T: "bigint"}}},
-					{Name: "b", Type: &schema.ColumnType{Raw: "bigint", Type: &schema.IntegerType{T: "bigint"}}},
-					{Name: "c", Type: &schema.ColumnType{Raw: "bigint", Type: &schema.IntegerType{T: "bigint"}}},
-					{Name: "d", Type: &schema.ColumnType{Raw: "bigint", Type: &schema.IntegerType{T: "bigint"}}},
-				}
-				indexes := []*schema.Index{
-					{Name: "idx1", Table: t, Attrs: []schema.Attr{&IndexType{T: "btree"}}, Parts: []*schema.IndexPart{{SeqNo: 1, C: columns[0]}}},
-					{Name: "idx2", Unique: true, Table: t, Attrs: []schema.Attr{&IndexType{T: "btree"}, &ConType{T: "u"}}, Parts: []*schema.IndexPart{{SeqNo: 1, C: columns[1]}}},
-					{Name: "idx3", Table: t, Attrs: []schema.Attr{&IndexType{T: "btree"}, &schema.Comment{Text: "boring"}}, Parts: []*schema.IndexPart{{SeqNo: 1, C: columns[2], Desc: true}}},
-					{Name: "idx4", Table: t, Attrs: []schema.Attr{&IndexType{T: "btree"}, &IndexPredicate{P: `d < 10`}}, Parts: []*schema.IndexPart{{SeqNo: 1, C: columns[3]}}},
-					{Name: "idx5", Table: t, Attrs: []schema.Attr{&IndexType{T: "btree"}}, Parts: []*schema.IndexPart{{SeqNo: 1, C: columns[0]}, {SeqNo: 2, C: columns[1]}, {SeqNo: 3, C: columns[2]}}},
-				}
-				columns[0].Indexes = []*schema.Index{indexes[0], indexes[4]}
-				columns[1].Indexes = []*schema.Index{indexes[1], indexes[4]}
-				columns[2].Indexes = []*schema.Index{indexes[2], indexes[4]}
-				columns[3].Indexes = []*schema.Index{indexes[3]}
-				require.EqualValues(columns, t.Columns)
-				require.EqualValues(indexes, t.Indexes)
-			},
-		},
 		{
 			name: "fks",
 			before: func(m mock) {
@@ -344,11 +289,7 @@ users        | users_check1       | (((c2 + c1) + c3) > 10) | c3          | {2,1
 			mk := mock{m}
 			mk.version("130000")
 			var drv migrate.Driver
-			if tt.crdb {
-				drv, err = OpenCRDB(db)
-			} else {
-				drv, err = Open(db)
-			}
+			drv, err = Open(db)
 			require.NoError(t, err)
 			mk.ExpectQuery(sqltest.Escape(fmt.Sprintf(schemasQueryArgs, "= $1"))).
 				WithArgs("public").
@@ -433,6 +374,79 @@ logs3      | c5         | integer   | NO          |                |
 	}, key.Parts)
 }
 
+func TestDriver_InspectCRDBSchema(t *testing.T) {
+	db, m, err := sqlmock.New()
+	require.NoError(t, err)
+	mk := mock{m}
+	mk.ExpectQuery(sqltest.Escape(paramsQuery)).
+		WillReturnRows(sqltest.Rows(`
+					setting
+				------------
+				130000
+				en_US.utf8
+				en_US.utf8
+				cockroach
+				`))
+	drv, err := Open(db)
+	require.NoError(t, err)
+	mk.ExpectQuery(sqltest.Escape(fmt.Sprintf(schemasQueryArgs, "= $1"))).
+		WithArgs("public").
+		WillReturnRows(sqltest.Rows(`
+schema_name
+--------------------
+public
+`))
+	mk.tableExists("public", "users", true)
+	mk.ExpectQuery(queryCrdbColumns).
+		WithArgs("public", "users").
+		WillReturnRows(sqltest.Rows(`
+table_name  | column_name | data_type | is_nullable |              column_default               | character_maximum_length | numeric_precision | datetime_precision | numeric_scale | character_set_name | collation_name | udt_name | is_identity | identity_start | identity_increment |       identity_generation        | generation_expression | comment | typtype | oid 
+------------+-------------+-----------+-------------+-------------------------------------------+--------------------------+-------------------+--------------------+---------------+--------------------+----------------+----------+-------------+----------------+--------------------+----------------------------------+-----------------------+---------+---------+-----
+users       | a           | bigint    | NO          |                                           |                          |                64 |                    |             0 |                    |                | int8     | NO          |                |                    |                                  |                       |         | b       | 20 
+users       | b           | bigint    | NO          |                                           |                          |                64 |                    |             0 |                    |                | int8     | NO          |                |                    |                                  |                       |         | b       | 20 
+users       | c           | bigint    | NO          |                                           |                          |                64 |                    |             0 |                    |                | int8     | NO          |                |                    |                                  |                       |         | b       | 20 
+users       | d           | bigint    | NO          |                                           |                          |                64 |                    |             0 |                    |                | int8     | NO          |                |                    |                                  |                       |         | b       | 20 
+`))
+	mk.ExpectQuery(queryCrdbIndexes).
+		WithArgs("public", "users").
+		WillReturnRows(sqltest.Rows(`
+table_name  | index_name | column_name | primary | unique | constraint_type |                                   create_stmt                                   | predicate | expression | comment 
+------------+------------+-------------+---------+--------+-----------------+---------------------------------------------------------------------------------+-----------+------------+---------
+users       | idx1       | a           | false   | false  |                 | CREATE INDEX idx1 ON defaultdb.public.serial USING btree (a ASC)                |           | a          |  
+users       | idx2       | b           | false   | true   | u               | CREATE UNIQUE INDEX idx2 ON defaultdb.public.serial USING btree (b ASC)         |           | b          |  
+users       | idx3       | c           | false   | false  |                 | CREATE INDEX idx3 ON defaultdb.public.serial USING btree (c DESC)               |           | c          | boring 
+users       | idx4       | d           | false   | false  |                 | CREATE INDEX idx5 ON defaultdb.public.serial USING btree (d ASC) WHERE (d < 10) | d < 10    | d          |  
+users       | idx5       | a           | false   | false  |                 | CREATE INDEX idx5 ON defaultdb.public.serial USING btree (a ASC, b ASC, c ASC)  |           | a          |  
+users       | idx5       | b           | false   | false  |                 | CREATE INDEX idx5 ON defaultdb.public.serial USING btree (a ASC, b ASC, c ASC)  |           | b          |  
+users       | idx5       | c           | false   | false  |                 | CREATE INDEX idx5 ON defaultdb.public.serial USING btree (a ASC, b ASC, c ASC)  |           | c          |  
+`))
+	mk.noFKs()
+	mk.noChecks()
+	s, err := drv.InspectSchema(context.Background(), "public", nil)
+	require.NoError(t, err)
+	tbl := s.Tables[0]
+	require.Equal(t, "users", tbl.Name)
+	columns := []*schema.Column{
+		{Name: "a", Type: &schema.ColumnType{Raw: "bigint", Type: &schema.IntegerType{T: "bigint"}}},
+		{Name: "b", Type: &schema.ColumnType{Raw: "bigint", Type: &schema.IntegerType{T: "bigint"}}},
+		{Name: "c", Type: &schema.ColumnType{Raw: "bigint", Type: &schema.IntegerType{T: "bigint"}}},
+		{Name: "d", Type: &schema.ColumnType{Raw: "bigint", Type: &schema.IntegerType{T: "bigint"}}},
+	}
+	indexes := []*schema.Index{
+		{Name: "idx1", Table: tbl, Attrs: []schema.Attr{&IndexType{T: "btree"}}, Parts: []*schema.IndexPart{{SeqNo: 1, C: columns[0]}}},
+		{Name: "idx2", Unique: true, Table: tbl, Attrs: []schema.Attr{&IndexType{T: "btree"}, &ConType{T: "u"}}, Parts: []*schema.IndexPart{{SeqNo: 1, C: columns[1]}}},
+		{Name: "idx3", Table: tbl, Attrs: []schema.Attr{&IndexType{T: "btree"}, &schema.Comment{Text: "boring"}}, Parts: []*schema.IndexPart{{SeqNo: 1, C: columns[2], Desc: true}}},
+		{Name: "idx4", Table: tbl, Attrs: []schema.Attr{&IndexType{T: "btree"}, &IndexPredicate{P: `d < 10`}}, Parts: []*schema.IndexPart{{SeqNo: 1, C: columns[3]}}},
+		{Name: "idx5", Table: tbl, Attrs: []schema.Attr{&IndexType{T: "btree"}}, Parts: []*schema.IndexPart{{SeqNo: 1, C: columns[0]}, {SeqNo: 2, C: columns[1]}, {SeqNo: 3, C: columns[2]}}},
+	}
+	columns[0].Indexes = []*schema.Index{indexes[0], indexes[4]}
+	columns[1].Indexes = []*schema.Index{indexes[1], indexes[4]}
+	columns[2].Indexes = []*schema.Index{indexes[2], indexes[4]}
+	columns[3].Indexes = []*schema.Index{indexes[3]}
+	require.EqualValues(t, columns, tbl.Columns)
+	require.EqualValues(t, indexes, tbl.Indexes)
+}
+
 func TestDriver_InspectSchema(t *testing.T) {
 	db, m, err := sqlmock.New()
 	require.NoError(t, err)
@@ -639,9 +653,9 @@ func (m mock) version(version string) {
 		WillReturnRows(sqltest.Rows(`
   setting
 ------------
+ ` + version + `
  en_US.utf8
  en_US.utf8
- ` + version + `
 `))
 }
 
diff --git a/sql/sqlclient/client_test.go b/sql/sqlclient/client_test.go
index 548826fd..971de93b 100644
--- a/sql/sqlclient/client_test.go
+++ b/sql/sqlclient/client_test.go
@@ -10,6 +10,9 @@ import (
 	"net/url"
 	"testing"
 
+	"ariga.io/atlas/sql/migrate"
+	"ariga.io/atlas/sql/schema"
+	"github.com/DATA-DOG/go-sqlmock"
 	"github.com/stretchr/testify/require"
 
 	"ariga.io/atlas/sql/sqlclient"
@@ -23,9 +26,9 @@ func TestRegisterOpen(t *testing.T) {
 			return c, nil
 		}),
 		sqlclient.RegisterFlavours("maria"),
-		sqlclient.RegisterURLParser(func(u *url.URL) *sqlclient.URL {
+		sqlclient.RegisterURLParser(sqlclient.URLParserFunc(func(u *url.URL) *sqlclient.URL {
 			return &sqlclient.URL{URL: u, DSN: "dsn", Schema: "schema"}
-		}),
+		})),
 	)
 	require.PanicsWithValue(
 		t,
@@ -71,3 +74,55 @@ func TestClient_AddClosers(t *testing.T) {
 type closerFunc func() error
 
 func (f closerFunc) Close() error { return f() }
+
+func TestClient_Tx(t *testing.T) {
+	db, mock, err := sqlmock.New()
+	require.NoError(t, err)
+	defer db.Close()
+
+	const stmt = "create database `test`"
+	mock.ExpectBegin()
+	mock.ExpectExec(stmt).WillReturnResult(sqlmock.NewResult(0, 1))
+	mock.ExpectCommit()
+	mock.ExpectBegin()
+	mock.ExpectExec(stmt).WillReturnResult(sqlmock.NewResult(0, 1))
+	mock.ExpectRollback()
+
+	sqlclient.Register(
+		"tx",
+		sqlclient.OpenerFunc(func(context.Context, *url.URL) (*sqlclient.Client, error) {
+			return &sqlclient.Client{Name: "tx", DB: db, Driver: &mockDriver{db: db}}, nil
+		}),
+		sqlclient.RegisterDriverOpener(func(db schema.ExecQuerier) (migrate.Driver, error) {
+			return &mockDriver{db: db}, nil
+		}),
+	)
+
+	c, err := sqlclient.Open(context.Background(), "tx://")
+	require.NoError(t, err)
+
+	// Commit works.
+	tx, err := c.Tx(context.Background(), nil)
+	require.NoError(t, err)
+	_, err = tx.ExecContext(context.Background(), stmt)
+	require.NoError(t, err)
+	require.NoError(t, tx.Commit())
+
+	// Rollback works as well.
+	tx, err = c.Tx(context.Background(), nil)
+	require.NoError(t, err)
+	_, err = tx.ExecContext(context.Background(), stmt)
+	require.NoError(t, err)
+	require.NoError(t, tx.Rollback())
+
+	require.NoError(t, mock.ExpectationsWereMet())
+}
+
+type mockDriver struct {
+	migrate.Driver
+	db schema.ExecQuerier
+}
+
+func (m *mockDriver) ExecContext(ctx context.Context, query string, args ...interface{}) (sql.Result, error) {
+	return m.db.ExecContext(ctx, query, args...)
+}