Repository: ariga/atlas
Pull request #754 (2022-04-30 10:33:08)

schema/schemaspec/schemahcl: patch references to blocks with variable names
Fixes a bug in cases like:
```hcl
variable "tenant" {
	type = string
	default = "test"
}
schema "tenant" {
	name = var.tenant
}
table "users" {
	schema = schema.tenant
	column "id" {
		type = int
	}
}
```
When provided an input var such as `tenant=rotemtam`, the value of the reference to `schema.tenant` was `$schema.tenant` instead of `$schema.rotemtam`


Patch:
diff --git a/README.md b/README.md
index 0abb2b8b..39cb5e64 100644
--- a/README.md
+++ b/README.md
@@ -1,5 +1,6 @@
 
 # Atlas -  A Database Toolkit
+[![Discord](https://img.shields.io/discord/930720389120794674?label=discord&logo=discord&style=flat-square&logoColor=white)](https://discord.gg/zZ6sWVg6NT)
 
 <img width="50%" align="right" style="display: block; margin:40px auto;"
      src="https://atlasgo.io/uploads/images/gopher.png"/>
diff --git a/sql/migrate/ent/client.go b/cmd/atlascmd/migrate/ent/client.go
similarity index 96%
rename from sql/migrate/ent/client.go
rename to cmd/atlascmd/migrate/ent/client.go
index 93867c81..6f748bfd 100644
--- a/sql/migrate/ent/client.go
+++ b/cmd/atlascmd/migrate/ent/client.go
@@ -11,7 +11,9 @@ import (
 	"fmt"
 	"log"
 
-	"ariga.io/atlas/sql/migrate/ent/revision"
+	"ariga.io/atlas/cmd/atlascmd/migrate/ent/migrate"
+
+	"ariga.io/atlas/cmd/atlascmd/migrate/ent/revision"
 
 	"entgo.io/ent/dialect"
 	"entgo.io/ent/dialect/sql"
@@ -20,6 +22,8 @@ import (
 // Client is the client that holds all ent builders.
 type Client struct {
 	config
+	// Schema is the client for creating, migrating and dropping schema.
+	Schema *migrate.Schema
 	// Revision is the client for interacting with the Revision builders.
 	Revision *RevisionClient
 }
@@ -34,6 +38,7 @@ func NewClient(opts ...Option) *Client {
 }
 
 func (c *Client) init() {
+	c.Schema = migrate.NewSchema(c.driver)
 	c.Revision = NewRevisionClient(c.config)
 }
 
diff --git a/sql/migrate/ent/config.go b/cmd/atlascmd/migrate/ent/config.go
similarity index 83%
rename from sql/migrate/ent/config.go
rename to cmd/atlascmd/migrate/ent/config.go
index e257ffad..01f2a8d8 100644
--- a/sql/migrate/ent/config.go
+++ b/cmd/atlascmd/migrate/ent/config.go
@@ -11,6 +11,7 @@ import (
 	stdsql "database/sql"
 	"fmt"
 
+	"ariga.io/atlas/cmd/atlascmd/migrate/ent/internal"
 	"entgo.io/ent"
 	"entgo.io/ent/dialect"
 )
@@ -28,6 +29,8 @@ type config struct {
 	log func(...interface{})
 	// hooks to execute on mutations.
 	hooks *hooks
+	// schemaConfig contains alternative names for all tables.
+	schemaConfig SchemaConfig
 }
 
 // hooks per client, for fast access.
@@ -66,6 +69,18 @@ func Driver(driver dialect.Driver) Option {
 	}
 }
 
+// SchemaConfig represents alternative schema names for all tables
+// that can be passed at runtime.
+type SchemaConfig = internal.SchemaConfig
+
+// AlternateSchemas allows alternate schema names to be
+// passed into ent operations.
+func AlternateSchema(schemaConfig SchemaConfig) Option {
+	return func(c *config) {
+		c.schemaConfig = schemaConfig
+	}
+}
+
 // ExecContext allows calling the underlying ExecContext method of the driver if it is supported by it.
 // See, database/sql#DB.ExecContext for more information.
 func (c *config) ExecContext(ctx context.Context, query string, args ...interface{}) (stdsql.Result, error) {
diff --git a/sql/migrate/ent/context.go b/cmd/atlascmd/migrate/ent/context.go
similarity index 100%
rename from sql/migrate/ent/context.go
rename to cmd/atlascmd/migrate/ent/context.go
diff --git a/sql/migrate/ent/ent.go b/cmd/atlascmd/migrate/ent/ent.go
similarity index 99%
rename from sql/migrate/ent/ent.go
rename to cmd/atlascmd/migrate/ent/ent.go
index e89f4a8d..6b7aa7a1 100644
--- a/sql/migrate/ent/ent.go
+++ b/cmd/atlascmd/migrate/ent/ent.go
@@ -11,7 +11,7 @@ import (
 	"errors"
 	"fmt"
 
-	"ariga.io/atlas/sql/migrate/ent/revision"
+	"ariga.io/atlas/cmd/atlascmd/migrate/ent/revision"
 	"entgo.io/ent"
 	"entgo.io/ent/dialect/sql"
 	"entgo.io/ent/dialect/sql/sqlgraph"
diff --git a/sql/migrate/ent/entc.go b/cmd/atlascmd/migrate/ent/entc.go
similarity index 76%
rename from sql/migrate/ent/entc.go
rename to cmd/atlascmd/migrate/ent/entc.go
index c50767b1..6047873a 100644
--- a/sql/migrate/ent/entc.go
+++ b/cmd/atlascmd/migrate/ent/entc.go
@@ -7,16 +7,12 @@
 package main
 
 import (
-	_ "embed"
 	"log"
 
 	"entgo.io/ent/entc"
 	"entgo.io/ent/entc/gen"
 )
 
-//go:embed templates.tmpl
-var tpls string
-
 func main() {
 	err := entc.Generate("./schema", &gen.Config{
 		Header: `// Copyright 2021-present The Atlas Authors. All rights reserved.
@@ -25,8 +21,11 @@ func main() {
 
 // Code generated by entc, DO NOT EDIT.
 `,
-		Features:  []gen.Feature{gen.FeatureUpsert, gen.FeatureExecQuery},
-		Templates: []*gen.Template{gen.MustParse(gen.NewTemplate("client/init").Parse(tpls))},
+		Features: []gen.Feature{
+			gen.FeatureUpsert,
+			gen.FeatureExecQuery,
+			gen.FeatureSchemaConfig,
+		},
 	})
 	if err != nil {
 		log.Fatalf("running ent codegen: %v", err)
diff --git a/sql/migrate/ent/generate.go b/cmd/atlascmd/migrate/ent/generate.go
similarity index 100%
rename from sql/migrate/ent/generate.go
rename to cmd/atlascmd/migrate/ent/generate.go
diff --git a/sql/migrate/ent/hook/hook.go b/cmd/atlascmd/migrate/ent/hook/hook.go
similarity index 99%
rename from sql/migrate/ent/hook/hook.go
rename to cmd/atlascmd/migrate/ent/hook/hook.go
index e9ff248e..4f3b54b6 100644
--- a/sql/migrate/ent/hook/hook.go
+++ b/cmd/atlascmd/migrate/ent/hook/hook.go
@@ -10,7 +10,7 @@ import (
 	"context"
 	"fmt"
 
-	"ariga.io/atlas/sql/migrate/ent"
+	"ariga.io/atlas/cmd/atlascmd/migrate/ent"
 )
 
 // The RevisionFunc type is an adapter to allow the use of ordinary
diff --git a/cmd/atlascmd/migrate/ent/internal/schemaconfig.go b/cmd/atlascmd/migrate/ent/internal/schemaconfig.go
new file mode 100644
index 00000000..71cf37c3
--- /dev/null
+++ b/cmd/atlascmd/migrate/ent/internal/schemaconfig.go
@@ -0,0 +1,28 @@
+// Copyright 2021-present The Atlas Authors. All rights reserved.
+// This source code is licensed under the Apache 2.0 license found
+// in the LICENSE file in the root directory of this source tree.
+
+// Code generated by entc, DO NOT EDIT.
+
+package internal
+
+import "context"
+
+// SchemaConfig represents alternative schema names for all tables
+// that can be passed at runtime.
+type SchemaConfig struct {
+	Revision string // Revision table.
+}
+
+type schemaCtxKey struct{}
+
+// SchemaConfigFromContext returns a SchemaConfig stored inside a context, or empty if there isn't one.
+func SchemaConfigFromContext(ctx context.Context) SchemaConfig {
+	config, _ := ctx.Value(schemaCtxKey{}).(SchemaConfig)
+	return config
+}
+
+// NewSchemaConfigContext returns a new context with the given SchemaConfig attached.
+func NewSchemaConfigContext(parent context.Context, config SchemaConfig) context.Context {
+	return context.WithValue(parent, schemaCtxKey{}, config)
+}
diff --git a/sql/migrate/ent/migrate/migrate.go b/cmd/atlascmd/migrate/ent/migrate/migrate.go
similarity index 100%
rename from sql/migrate/ent/migrate/migrate.go
rename to cmd/atlascmd/migrate/ent/migrate/migrate.go
diff --git a/sql/migrate/ent/migrate/schema.go b/cmd/atlascmd/migrate/ent/migrate/schema.go
similarity index 100%
rename from sql/migrate/ent/migrate/schema.go
rename to cmd/atlascmd/migrate/ent/migrate/schema.go
diff --git a/sql/migrate/ent/mutation.go b/cmd/atlascmd/migrate/ent/mutation.go
similarity index 99%
rename from sql/migrate/ent/mutation.go
rename to cmd/atlascmd/migrate/ent/mutation.go
index 1beca6ae..997cff5d 100644
--- a/sql/migrate/ent/mutation.go
+++ b/cmd/atlascmd/migrate/ent/mutation.go
@@ -13,8 +13,8 @@ import (
 	"sync"
 	"time"
 
-	"ariga.io/atlas/sql/migrate/ent/predicate"
-	"ariga.io/atlas/sql/migrate/ent/revision"
+	"ariga.io/atlas/cmd/atlascmd/migrate/ent/predicate"
+	"ariga.io/atlas/cmd/atlascmd/migrate/ent/revision"
 
 	"entgo.io/ent"
 )
diff --git a/sql/migrate/ent/predicate/predicate.go b/cmd/atlascmd/migrate/ent/predicate/predicate.go
similarity index 100%
rename from sql/migrate/ent/predicate/predicate.go
rename to cmd/atlascmd/migrate/ent/predicate/predicate.go
diff --git a/sql/migrate/ent/revision.go b/cmd/atlascmd/migrate/ent/revision.go
similarity index 99%
rename from sql/migrate/ent/revision.go
rename to cmd/atlascmd/migrate/ent/revision.go
index c3de9e3b..ad1d5378 100644
--- a/sql/migrate/ent/revision.go
+++ b/cmd/atlascmd/migrate/ent/revision.go
@@ -12,7 +12,7 @@ import (
 	"strings"
 	"time"
 
-	"ariga.io/atlas/sql/migrate/ent/revision"
+	"ariga.io/atlas/cmd/atlascmd/migrate/ent/revision"
 	"entgo.io/ent/dialect/sql"
 )
 
diff --git a/sql/migrate/ent/revision/revision.go b/cmd/atlascmd/migrate/ent/revision/revision.go
similarity index 100%
rename from sql/migrate/ent/revision/revision.go
rename to cmd/atlascmd/migrate/ent/revision/revision.go
diff --git a/sql/migrate/ent/revision/where.go b/cmd/atlascmd/migrate/ent/revision/where.go
similarity index 99%
rename from sql/migrate/ent/revision/where.go
rename to cmd/atlascmd/migrate/ent/revision/where.go
index 05348194..adca2b50 100644
--- a/sql/migrate/ent/revision/where.go
+++ b/cmd/atlascmd/migrate/ent/revision/where.go
@@ -9,7 +9,7 @@ package revision
 import (
 	"time"
 
-	"ariga.io/atlas/sql/migrate/ent/predicate"
+	"ariga.io/atlas/cmd/atlascmd/migrate/ent/predicate"
 	"entgo.io/ent/dialect/sql"
 )
 
diff --git a/sql/migrate/ent/revision_create.go b/cmd/atlascmd/migrate/ent/revision_create.go
similarity index 99%
rename from sql/migrate/ent/revision_create.go
rename to cmd/atlascmd/migrate/ent/revision_create.go
index 1b043b62..aa545480 100644
--- a/sql/migrate/ent/revision_create.go
+++ b/cmd/atlascmd/migrate/ent/revision_create.go
@@ -12,7 +12,7 @@ import (
 	"fmt"
 	"time"
 
-	"ariga.io/atlas/sql/migrate/ent/revision"
+	"ariga.io/atlas/cmd/atlascmd/migrate/ent/revision"
 	"entgo.io/ent/dialect"
 	"entgo.io/ent/dialect/sql"
 	"entgo.io/ent/dialect/sql/sqlgraph"
@@ -203,6 +203,7 @@ func (rc *RevisionCreate) createSpec() (*Revision, *sqlgraph.CreateSpec) {
 			},
 		}
 	)
+	_spec.Schema = rc.schemaConfig.Revision
 	_spec.OnConflict = rc.conflict
 	if id, ok := rc.mutation.ID(); ok {
 		_node.ID = id
diff --git a/sql/migrate/ent/revision_delete.go b/cmd/atlascmd/migrate/ent/revision_delete.go
similarity index 91%
rename from sql/migrate/ent/revision_delete.go
rename to cmd/atlascmd/migrate/ent/revision_delete.go
index 1d9410fc..1e0565b5 100644
--- a/sql/migrate/ent/revision_delete.go
+++ b/cmd/atlascmd/migrate/ent/revision_delete.go
@@ -10,8 +10,9 @@ import (
 	"context"
 	"fmt"
 
-	"ariga.io/atlas/sql/migrate/ent/predicate"
-	"ariga.io/atlas/sql/migrate/ent/revision"
+	"ariga.io/atlas/cmd/atlascmd/migrate/ent/internal"
+	"ariga.io/atlas/cmd/atlascmd/migrate/ent/predicate"
+	"ariga.io/atlas/cmd/atlascmd/migrate/ent/revision"
 	"entgo.io/ent/dialect/sql"
 	"entgo.io/ent/dialect/sql/sqlgraph"
 	"entgo.io/ent/schema/field"
@@ -81,6 +82,8 @@ func (rd *RevisionDelete) sqlExec(ctx context.Context) (int, error) {
 			},
 		},
 	}
+	_spec.Node.Schema = rd.schemaConfig.Revision
+	ctx = internal.NewSchemaConfigContext(ctx, rd.schemaConfig)
 	if ps := rd.mutation.predicates; len(ps) > 0 {
 		_spec.Predicate = func(selector *sql.Selector) {
 			for i := range ps {
diff --git a/sql/migrate/ent/revision_query.go b/cmd/atlascmd/migrate/ent/revision_query.go
similarity index 96%
rename from sql/migrate/ent/revision_query.go
rename to cmd/atlascmd/migrate/ent/revision_query.go
index 8f397a34..abd13ec9 100644
--- a/sql/migrate/ent/revision_query.go
+++ b/cmd/atlascmd/migrate/ent/revision_query.go
@@ -11,8 +11,9 @@ import (
 	"fmt"
 	"math"
 
-	"ariga.io/atlas/sql/migrate/ent/predicate"
-	"ariga.io/atlas/sql/migrate/ent/revision"
+	"ariga.io/atlas/cmd/atlascmd/migrate/ent/internal"
+	"ariga.io/atlas/cmd/atlascmd/migrate/ent/predicate"
+	"ariga.io/atlas/cmd/atlascmd/migrate/ent/revision"
 	"entgo.io/ent/dialect/sql"
 	"entgo.io/ent/dialect/sql/sqlgraph"
 	"entgo.io/ent/schema/field"
@@ -330,6 +331,8 @@ func (rq *RevisionQuery) sqlAll(ctx context.Context, hooks ...queryHook) ([]*Rev
 		nodes = append(nodes, node)
 		return node.assignValues(columns, values)
 	}
+	_spec.Node.Schema = rq.schemaConfig.Revision
+	ctx = internal.NewSchemaConfigContext(ctx, rq.schemaConfig)
 	for i := range hooks {
 		hooks[i](ctx, _spec)
 	}
@@ -344,6 +347,8 @@ func (rq *RevisionQuery) sqlAll(ctx context.Context, hooks ...queryHook) ([]*Rev
 
 func (rq *RevisionQuery) sqlCount(ctx context.Context) (int, error) {
 	_spec := rq.querySpec()
+	_spec.Node.Schema = rq.schemaConfig.Revision
+	ctx = internal.NewSchemaConfigContext(ctx, rq.schemaConfig)
 	_spec.Node.Columns = rq.fields
 	if len(rq.fields) > 0 {
 		_spec.Unique = rq.unique != nil && *rq.unique
@@ -422,6 +427,9 @@ func (rq *RevisionQuery) sqlQuery(ctx context.Context) *sql.Selector {
 	if rq.unique != nil && *rq.unique {
 		selector.Distinct()
 	}
+	t1.Schema(rq.schemaConfig.Revision)
+	ctx = internal.NewSchemaConfigContext(ctx, rq.schemaConfig)
+	selector.WithContext(ctx)
 	for _, p := range rq.predicates {
 		p(selector)
 	}
diff --git a/sql/migrate/ent/revision_update.go b/cmd/atlascmd/migrate/ent/revision_update.go
similarity index 97%
rename from sql/migrate/ent/revision_update.go
rename to cmd/atlascmd/migrate/ent/revision_update.go
index 64d9e6fa..7e10581e 100644
--- a/sql/migrate/ent/revision_update.go
+++ b/cmd/atlascmd/migrate/ent/revision_update.go
@@ -12,8 +12,9 @@ import (
 	"fmt"
 	"time"
 
-	"ariga.io/atlas/sql/migrate/ent/predicate"
-	"ariga.io/atlas/sql/migrate/ent/revision"
+	"ariga.io/atlas/cmd/atlascmd/migrate/ent/internal"
+	"ariga.io/atlas/cmd/atlascmd/migrate/ent/predicate"
+	"ariga.io/atlas/cmd/atlascmd/migrate/ent/revision"
 	"entgo.io/ent/dialect/sql"
 	"entgo.io/ent/dialect/sql/sqlgraph"
 	"entgo.io/ent/schema/field"
@@ -230,6 +231,8 @@ func (ru *RevisionUpdate) sqlSave(ctx context.Context) (n int, err error) {
 			Column: revision.FieldMeta,
 		})
 	}
+	_spec.Node.Schema = ru.schemaConfig.Revision
+	ctx = internal.NewSchemaConfigContext(ctx, ru.schemaConfig)
 	if n, err = sqlgraph.UpdateNodes(ctx, ru.driver, _spec); err != nil {
 		if _, ok := err.(*sqlgraph.NotFoundError); ok {
 			err = &NotFoundError{revision.Label}
@@ -471,6 +474,8 @@ func (ruo *RevisionUpdateOne) sqlSave(ctx context.Context) (_node *Revision, err
 			Column: revision.FieldMeta,
 		})
 	}
+	_spec.Node.Schema = ruo.schemaConfig.Revision
+	ctx = internal.NewSchemaConfigContext(ctx, ruo.schemaConfig)
 	_node = &Revision{config: ruo.config}
 	_spec.Assign = _node.assignValues
 	_spec.ScanValues = _node.scanValues
diff --git a/sql/migrate/ent/runtime.go b/cmd/atlascmd/migrate/ent/runtime.go
similarity index 100%
rename from sql/migrate/ent/runtime.go
rename to cmd/atlascmd/migrate/ent/runtime.go
diff --git a/sql/migrate/ent/runtime/runtime.go b/cmd/atlascmd/migrate/ent/runtime/runtime.go
similarity index 58%
rename from sql/migrate/ent/runtime/runtime.go
rename to cmd/atlascmd/migrate/ent/runtime/runtime.go
index 508d8569..729f0d13 100644
--- a/sql/migrate/ent/runtime/runtime.go
+++ b/cmd/atlascmd/migrate/ent/runtime/runtime.go
@@ -6,9 +6,9 @@
 
 package runtime
 
-// The schema-stitching logic is generated in ariga.io/atlas/sql/migrate/ent/runtime.go
+// The schema-stitching logic is generated in ariga.io/atlas/cmd/atlascmd/migrate/ent/runtime.go
 
 const (
-	Version = "v0.10.2-0.20220428114225-0c8f9a977c99"           // Version of ent codegen.
-	Sum     = "h1:++Y53GODlpz4OZOJ4VjANc9OxKUR6NZRgXcFg+CEtOc=" // Sum of ent codegen.
+	Version = "v0.10.2-0.20220502113020-4ac82f5bb3f0"           // Version of ent codegen.
+	Sum     = "h1:qHA4+ANAzDj6BcDLxNgZuzKxFre/RI9r5wwsI2O+1M4=" // Sum of ent codegen.
 )
diff --git a/sql/migrate/ent/schema/revision.go b/cmd/atlascmd/migrate/ent/schema/revision.go
similarity index 100%
rename from sql/migrate/ent/schema/revision.go
rename to cmd/atlascmd/migrate/ent/schema/revision.go
diff --git a/sql/migrate/ent/tx.go b/cmd/atlascmd/migrate/ent/tx.go
similarity index 100%
rename from sql/migrate/ent/tx.go
rename to cmd/atlascmd/migrate/ent/tx.go
diff --git a/cmd/atlascmd/migrate/migrate.go b/cmd/atlascmd/migrate/migrate.go
new file mode 100644
index 00000000..5aaafa45
--- /dev/null
+++ b/cmd/atlascmd/migrate/migrate.go
@@ -0,0 +1,76 @@
+// Copyright 2021-present The Atlas Authors. All rights reserved.
+// This source code is licensed under the Apache 2.0 license found
+// in the LICENSE file in the root directory of this source tree.
+
+package migrate
+
+import (
+	"context"
+
+	"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"
+	"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 }
+
+// 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{c: ent.NewClient(opts...)}
+}
+
+// Init makes sure the revisions table does exist in the connected database.
+func (r *EntRevisions) Init(ctx context.Context) error {
+	return r.c.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)
+	if err != nil {
+		return nil, err
+	}
+	ret := make(migrate.Revisions, len(revs))
+	for i, r := range revs {
+		ret[i] = &migrate.Revision{
+			Version:         r.ID,
+			Description:     r.Description,
+			ExecutionState:  string(r.ExecutionState),
+			ExecutedAt:      r.ExecutedAt,
+			ExecutionTime:   r.ExecutionTime,
+			Hash:            r.Hash,
+			OperatorVersion: r.OperatorVersion,
+			Meta:            r.Meta,
+		}
+	}
+	return ret, nil
+}
+
+// WriteRevisions writes the revisions to the revisions table.
+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().
+			SetID(rev.Version).
+			SetDescription(rev.Description).
+			SetExecutionState(revision.ExecutionState(rev.ExecutionState)).
+			SetExecutedAt(rev.ExecutedAt).
+			SetExecutionTime(rev.ExecutionTime).
+			SetHash(rev.Hash).
+			SetOperatorVersion(rev.OperatorVersion).
+			SetMeta(rev.Meta)
+	}
+	return r.c.Revision.CreateBulk(bulk...).
+		OnConflict(
+			sql.ConflictColumns(revision.FieldID),
+		).
+		UpdateNewValues().
+		Exec(ctx)
+}
+
+var _ migrate.RevisionReadWriter = (*EntRevisions)(nil)
diff --git a/cmd/atlascmd/migrations/20220318104614_initial.sql b/cmd/atlascmd/testdata/20220318104614_initial.sql
similarity index 100%
rename from cmd/atlascmd/migrations/20220318104614_initial.sql
rename to cmd/atlascmd/testdata/20220318104614_initial.sql
diff --git a/cmd/atlascmd/migrations/atlas.sum b/cmd/atlascmd/testdata/atlas.sum
similarity index 100%
rename from cmd/atlascmd/migrations/atlas.sum
rename to cmd/atlascmd/testdata/atlas.sum
diff --git a/doc/md/cli/url.mdx b/doc/md/cli/url.mdx
index 43a01d36..a7626351 100644
--- a/doc/md/cli/url.mdx
+++ b/doc/md/cli/url.mdx
@@ -74,7 +74,7 @@ sqlite://file?cache=shared&mode=memory
 <TabItem value="docker">
 
 Atlas can spin up an ephemeral local docker container for you by specifying a special URL like below. This can be useful
-if you need a [dev database](dev.md) for schema validation or diffing. However, some images like `mysql` /
+if you need a [dev database](../dev.md) for schema validation or diffing. However, some images like `mysql` /
 `mariadb` take quite some time to "boot", before they are ready to be used. For a smoother developing experience
 consider spinning up a longer lived container by yourself.
 
diff --git a/doc/md/ddl/sql.mdx b/doc/md/ddl/sql.mdx
index 4cef9801..055cd33b 100644
--- a/doc/md/ddl/sql.mdx
+++ b/doc/md/ddl/sql.mdx
@@ -256,7 +256,7 @@ table "users" {
 </Tabs>
 
 :::info
-Note, it is recommended to use the [`--dev-url`](../cli/dev-database) option when generated columns are used.
+Note, it is recommended to use the [`--dev-url`](../dev-database) option when generated columns are used.
 :::
 
 ## Column Types
@@ -394,7 +394,7 @@ table "t" {
 ```
 
 :::info
-Note, it is recommended to use the [`--dev-url`](../cli/dev-database) option when index expressions are used.
+Note, it is recommended to use the [`--dev-url`](../dev-database) option when index expressions are used.
 :::
 
 ### Partial Indexes
@@ -419,7 +419,7 @@ table "t" {
 ```
 
 :::info
-Note, it is recommended to use the [`--dev-url`](../cli/dev-database) option when partial indexes are used.
+Note, it is recommended to use the [`--dev-url`](../dev-database) option when partial indexes are used.
 :::
 
 ### Index Prefixes
diff --git a/doc/md/cli/dev.md b/doc/md/dev.md
similarity index 93%
rename from doc/md/cli/dev.md
rename to doc/md/dev.md
index 02fca4ad..1212fd7e 100644
--- a/doc/md/cli/dev.md
+++ b/doc/md/dev.md
@@ -1,15 +1,15 @@
 ---
 title: Dev Database
 id: dev-database
-slug: /cli/dev-database
+slug: /dev-database
 ---
 ## Introduction
 
-Various Atlas commands accept the `--dev-url` flag. This option uses the [URL](url.mdx) format, and allows passing
+Atlas uses the concept of "Dev Database" to provide extra safety and correctness to the migration process.
 a development database (a twin environment) to validate schemas, simulate migrations and calculate the state of the
 migration directory by replaying the historical changes. Let's go over a few examples to explain the benefits of using a
 dev/twin database. For a one-time use Atlas can spin up an ephemeral local docker container for you with a special
-[docker driver](url.mdx).
+[docker driver](cli/url.mdx).
 
 ## Validation
 
@@ -27,7 +27,7 @@ table "t" {
 }
 ```
 
-After running [`schema apply`](reference.md#atlas-schema-apply), we get the following error because the `CHECK`
+After running [`schema apply`](cli/reference.md#atlas-schema-apply), we get the following error because the `CHECK`
 constraint is invalid, as column `d` does not exist.
 
 ```shell
@@ -136,7 +136,7 @@ Similarly to the previous example, we will use the `--dev-url` option to solve t
 
 ```shell
 $ atlas schema apply --url "mysql://root:pass@:3308/test" -f test.hcl
-  --dev-url="mysql://root:pass@:3308/test"
+  --dev-url="mysql://root:pass@:3307/test"
 ```
 ```text
 Schema is synced, no changes to be made
diff --git a/doc/website/docusaurus.config.js b/doc/website/docusaurus.config.js
index 9c17f8fa..b4365529 100644
--- a/doc/website/docusaurus.config.js
+++ b/doc/website/docusaurus.config.js
@@ -54,6 +54,13 @@ module.exports = {
             {"label": "Discord", "to": "https://discord.gg/zZ6sWVg6NT"},
           ]
         },
+        {
+          "title": "Legal",
+          "items": [
+            {"label": "Privacy Policy", "to": "https://ariga.io/legal/privacy"},
+            {"label": "Terms of Service", "to": "https://ariga.io/legal/tos"},
+          ]
+        }
       ],
       copyright: `
       Copyright © ${new Date().getFullYear()} The Atlas Authors.
@@ -65,6 +72,19 @@ module.exports = {
       `,
     },
   },
+  plugins: [
+    [
+      '@docusaurus/plugin-client-redirects',
+      {
+        redirects: [
+          {
+            to: '/dev-database',
+            from: '/cli/dev-database',
+          },
+        ],
+      },
+    ],
+  ],
   presets: [
     [
       '@docusaurus/preset-classic',
diff --git a/doc/website/package-lock.json b/doc/website/package-lock.json
index 32a1872f..bda7f4fe 100644
--- a/doc/website/package-lock.json
+++ b/doc/website/package-lock.json
@@ -9,6 +9,7 @@
       "version": "0.0.0",
       "dependencies": {
         "@docusaurus/core": "2.0.0-beta.15",
+        "@docusaurus/plugin-client-redirects": "2.0.0-beta.15",
         "@docusaurus/preset-classic": "2.0.0-beta.15",
         "@mdx-js/react": "^1.6.21",
         "@svgr/webpack": "^5.5.0",
@@ -2322,6 +2323,29 @@
         "react-dom": "^16.8.4 || ^17.0.0"
       }
     },
+    "node_modules/@docusaurus/plugin-client-redirects": {
+      "version": "2.0.0-beta.15",
+      "resolved": "https://registry.npmjs.org/@docusaurus/plugin-client-redirects/-/plugin-client-redirects-2.0.0-beta.15.tgz",
+      "integrity": "sha512-FcH1y+a+Bmc5x50kZn5RvLr2a6iPiGnRK4iUP7G1eNozJC+8+93OwyZYSCdEOf2LyoVOtEq06xlfORFP2mfQDQ==",
+      "dependencies": {
+        "@docusaurus/core": "2.0.0-beta.15",
+        "@docusaurus/logger": "2.0.0-beta.15",
+        "@docusaurus/utils": "2.0.0-beta.15",
+        "@docusaurus/utils-common": "2.0.0-beta.15",
+        "@docusaurus/utils-validation": "2.0.0-beta.15",
+        "eta": "^1.12.3",
+        "fs-extra": "^10.0.0",
+        "lodash": "^4.17.20",
+        "tslib": "^2.3.1"
+      },
+      "engines": {
+        "node": ">=14"
+      },
+      "peerDependencies": {
+        "react": "^16.8.4 || ^17.0.0",
+        "react-dom": "^16.8.4 || ^17.0.0"
+      }
+    },
     "node_modules/@docusaurus/plugin-content-blog": {
       "version": "2.0.0-beta.15",
       "resolved": "https://registry.npmjs.org/@docusaurus/plugin-content-blog/-/plugin-content-blog-2.0.0-beta.15.tgz",
@@ -15036,6 +15060,22 @@
         "webpack": "^5.61.0"
       }
     },
+    "@docusaurus/plugin-client-redirects": {
+      "version": "2.0.0-beta.15",
+      "resolved": "https://registry.npmjs.org/@docusaurus/plugin-client-redirects/-/plugin-client-redirects-2.0.0-beta.15.tgz",
+      "integrity": "sha512-FcH1y+a+Bmc5x50kZn5RvLr2a6iPiGnRK4iUP7G1eNozJC+8+93OwyZYSCdEOf2LyoVOtEq06xlfORFP2mfQDQ==",
+      "requires": {
+        "@docusaurus/core": "2.0.0-beta.15",
+        "@docusaurus/logger": "2.0.0-beta.15",
+        "@docusaurus/utils": "2.0.0-beta.15",
+        "@docusaurus/utils-common": "2.0.0-beta.15",
+        "@docusaurus/utils-validation": "2.0.0-beta.15",
+        "eta": "^1.12.3",
+        "fs-extra": "^10.0.0",
+        "lodash": "^4.17.20",
+        "tslib": "^2.3.1"
+      }
+    },
     "@docusaurus/plugin-content-blog": {
       "version": "2.0.0-beta.15",
       "resolved": "https://registry.npmjs.org/@docusaurus/plugin-content-blog/-/plugin-content-blog-2.0.0-beta.15.tgz",
diff --git a/doc/website/package.json b/doc/website/package.json
index f1380491..836b8c8b 100644
--- a/doc/website/package.json
+++ b/doc/website/package.json
@@ -17,6 +17,7 @@
   },
   "dependencies": {
     "@docusaurus/core": "2.0.0-beta.15",
+    "@docusaurus/plugin-client-redirects": "2.0.0-beta.15",
     "@docusaurus/preset-classic": "2.0.0-beta.15",
     "@mdx-js/react": "^1.6.21",
     "@svgr/webpack": "^5.5.0",
diff --git a/doc/website/sidebars.js b/doc/website/sidebars.js
index 146834b7..c7c26c40 100644
--- a/doc/website/sidebars.js
+++ b/doc/website/sidebars.js
@@ -33,13 +33,16 @@ module.exports = {
                 'ddl/ddl-intro',
             ],
         },
+        {
+            type: 'doc',
+            id: 'dev-database'
+        },
         {
             type: 'category',
             label: 'CLI',
             items: [
                 {type: 'doc', id: 'cli/cli-reference', label: 'Reference'},
                 {type: 'doc', id: 'cli/cli-url', label: 'URLs'},
-                {type: 'doc', id: 'cli/dev-database', label: 'Dev Database'},
             ]
         },
         {
diff --git a/doc/website/src/pages/index.js b/doc/website/src/pages/index.js
index ccef4376..34dc8419 100644
--- a/doc/website/src/pages/index.js
+++ b/doc/website/src/pages/index.js
@@ -162,7 +162,6 @@ export default function () {
                 <section className={index.section}>
                     <div className={index.subtitleWithChipWrapper}>
                         <h2 style={{ marginRight: "10px" }} className={index.subtitleMargin}>Versioned Migrations</h2>
-                        <Chip text="Coming soon" />
                     </div>
                     <p className={index.paragraphSecondary}>Atlas offers you an alternative workflow, in which migrations are explicitly defined and
                         assigned a version. Atlas can then bring a schema to the desired version by following
diff --git a/go.mod b/go.mod
index 5e053f3b..9ae6dcd2 100644
--- a/go.mod
+++ b/go.mod
@@ -3,7 +3,7 @@ module ariga.io/atlas
 go 1.17
 
 require (
-	entgo.io/ent v0.10.2-0.20220428114225-0c8f9a977c99
+	entgo.io/ent v0.10.2-0.20220502113020-4ac82f5bb3f0
 	github.com/DATA-DOG/go-sqlmock v1.5.0
 	github.com/go-openapi/inflect v0.19.0
 	github.com/go-sql-driver/mysql v1.6.0
diff --git a/go.sum b/go.sum
index fc7575cd..af44c269 100644
--- a/go.sum
+++ b/go.sum
@@ -1,5 +1,7 @@
 entgo.io/ent v0.10.2-0.20220428114225-0c8f9a977c99 h1:++Y53GODlpz4OZOJ4VjANc9OxKUR6NZRgXcFg+CEtOc=
 entgo.io/ent v0.10.2-0.20220428114225-0c8f9a977c99/go.mod h1:rUH4yiUVADR2XHnv0ZERDnwF/YjR42m3Qra6CZ4ZxBA=
+entgo.io/ent v0.10.2-0.20220502113020-4ac82f5bb3f0 h1:qHA4+ANAzDj6BcDLxNgZuzKxFre/RI9r5wwsI2O+1M4=
+entgo.io/ent v0.10.2-0.20220502113020-4ac82f5bb3f0/go.mod h1:Zh61BPvB+cL6VWEyN8f1YoDacrMjQf2KDlDeX26xq2k=
 github.com/DATA-DOG/go-sqlmock v1.5.0 h1:Shsta01QNfFxHCfpW6YH2STWB0MudeXXEWMr20OEh60=
 github.com/DATA-DOG/go-sqlmock v1.5.0/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM=
 github.com/agext/levenshtein v1.2.1 h1:QmvMAjj2aEICytGiWzmxoE0x2KZvE0fvmqMOfy2tjT8=
diff --git a/internal/integration/go.mod b/internal/integration/go.mod
index dfa380b8..e100e4ae 100644
--- a/internal/integration/go.mod
+++ b/internal/integration/go.mod
@@ -5,8 +5,8 @@ go 1.17
 replace ariga.io/atlas => ../../
 
 require (
-	ariga.io/atlas v0.3.8-0.20220424181913-f64001131c0e
-	entgo.io/ent v0.10.2-0.20220428114225-0c8f9a977c99
+	ariga.io/atlas v0.3.8-0.20220502090649-c495296ad28c
+	entgo.io/ent v0.10.2-0.20220502113020-4ac82f5bb3f0
 	github.com/go-sql-driver/mysql v1.6.0
 	github.com/hashicorp/hcl/v2 v2.10.1 // indirect
 	github.com/lib/pq v1.10.5
diff --git a/internal/integration/go.sum b/internal/integration/go.sum
index c57d47b9..c635689f 100644
--- a/internal/integration/go.sum
+++ b/internal/integration/go.sum
@@ -1,5 +1,7 @@
 entgo.io/ent v0.10.2-0.20220428114225-0c8f9a977c99 h1:++Y53GODlpz4OZOJ4VjANc9OxKUR6NZRgXcFg+CEtOc=
 entgo.io/ent v0.10.2-0.20220428114225-0c8f9a977c99/go.mod h1:rUH4yiUVADR2XHnv0ZERDnwF/YjR42m3Qra6CZ4ZxBA=
+entgo.io/ent v0.10.2-0.20220502113020-4ac82f5bb3f0 h1:qHA4+ANAzDj6BcDLxNgZuzKxFre/RI9r5wwsI2O+1M4=
+entgo.io/ent v0.10.2-0.20220502113020-4ac82f5bb3f0/go.mod h1:Zh61BPvB+cL6VWEyN8f1YoDacrMjQf2KDlDeX26xq2k=
 github.com/DATA-DOG/go-sqlmock v1.5.0 h1:Shsta01QNfFxHCfpW6YH2STWB0MudeXXEWMr20OEh60=
 github.com/agext/levenshtein v1.2.1/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558=
 github.com/agext/levenshtein v1.2.3 h1:YB2fHEn0UJagG8T1rrWknE3ZQzWM06O8AMAatNn7lmo=
@@ -8,6 +10,7 @@ github.com/apparentlymart/go-dump v0.0.0-20180507223929-23540a00eaa3/go.mod h1:o
 github.com/apparentlymart/go-textseg v1.0.0/go.mod h1:z96Txxhf3xSFMPmb5X/1W05FF/Nj9VFpLOpjS5yuumk=
 github.com/apparentlymart/go-textseg/v13 v13.0.0 h1:Y+KvPE1NYz0xl601PVImeQfFyEy6iT90AvPUL1NNfNw=
 github.com/apparentlymart/go-textseg/v13 v13.0.0/go.mod h1:ZK2fH7c4NqDTLtiYLvIkEghdlcqw7yxLeM89kiTRPUo=
+github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e h1:fY5BOSpyZCqRo5OhCuC+XN+r/bBCmeuuJtjz+bCNIf8=
 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
 github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@@ -36,8 +39,10 @@ github.com/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348 h1:MtvEpTB6LX3v
 github.com/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348/go.mod h1:B69LEHPfb2qLo0BaaOLcbitczOKLWTsrBG9LczfCD4k=
 github.com/lib/pq v1.10.5 h1:J+gdV2cUmX7ZqL2B0lFcW0m+egaHC2V3lpO8nWxyYiQ=
 github.com/lib/pq v1.10.5/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
+github.com/manifoldco/promptui v0.9.0 h1:3V4HzJk1TtXW1MTZMP7mdlwbBpIinw3HztaIlYthEiA=
 github.com/mattn/go-sqlite3 v1.14.10 h1:MLn+5bFRlWMGoSRmJour3CL1w/qL96mvipqpwQW/Sfk=
 github.com/mattn/go-sqlite3 v1.14.10/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
+github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
 github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo=
 github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0=
 github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0=
@@ -49,7 +54,9 @@ github.com/rogpeppe/go-internal v1.8.2-0.20220112175052-f3cb5c2c6412 h1:BVgfta5l
 github.com/rogpeppe/go-internal v1.8.2-0.20220112175052-f3cb5c2c6412/go.mod h1:JeRgkft04UBgHMgCIwADu4Pn6Mtm5d4nPKWu0nJ5d+o=
 github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ=
 github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
+github.com/spf13/cobra v1.4.0 h1:y+wJpx64xcgO1V+RcnwW0LEHxTKRi2ZDPSBjWnrg88Q=
 github.com/spf13/pflag v1.0.2/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
+github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
 github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
 github.com/stretchr/testify v1.7.1-0.20210427113832-6241f9ab9942 h1:t0lM6y/M5IiUZyvbBTcngso8SZEZICH7is9B6g/obVU=
diff --git a/internal/typedoc/go.mod b/internal/typedoc/go.mod
index aa62a234..31bbef35 100644
--- a/internal/typedoc/go.mod
+++ b/internal/typedoc/go.mod
@@ -4,18 +4,17 @@ go 1.17
 
 replace ariga.io/atlas => ../../
 
-require ariga.io/atlas v0.3.8-0.20220424181913-f64001131c0e
+require ariga.io/atlas v0.3.8-0.20220502090649-c495296ad28c
 
 require (
-	entgo.io/ent v0.10.2-0.20220428114225-0c8f9a977c99 // indirect
 	github.com/agext/levenshtein v1.2.1 // indirect
 	github.com/apparentlymart/go-textseg/v13 v13.0.0 // indirect
 	github.com/go-openapi/inflect v0.19.0 // indirect
 	github.com/google/go-cmp v0.5.6 // indirect
-	github.com/google/uuid v1.3.0 // indirect
 	github.com/hashicorp/hcl/v2 v2.10.0 // indirect
 	github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7 // indirect
 	github.com/zclconf/go-cty v1.8.0 // indirect
 	golang.org/x/mod v0.5.1 // indirect
 	golang.org/x/text v0.3.7 // indirect
+	golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
 )
diff --git a/internal/typedoc/go.sum b/internal/typedoc/go.sum
index 14920b72..fc9efc2f 100644
--- a/internal/typedoc/go.sum
+++ b/internal/typedoc/go.sum
@@ -1,5 +1,3 @@
-entgo.io/ent v0.10.2-0.20220428114225-0c8f9a977c99 h1:++Y53GODlpz4OZOJ4VjANc9OxKUR6NZRgXcFg+CEtOc=
-entgo.io/ent v0.10.2-0.20220428114225-0c8f9a977c99/go.mod h1:rUH4yiUVADR2XHnv0ZERDnwF/YjR42m3Qra6CZ4ZxBA=
 github.com/DATA-DOG/go-sqlmock v1.5.0 h1:Shsta01QNfFxHCfpW6YH2STWB0MudeXXEWMr20OEh60=
 github.com/agext/levenshtein v1.2.1 h1:QmvMAjj2aEICytGiWzmxoE0x2KZvE0fvmqMOfy2tjT8=
 github.com/agext/levenshtein v1.2.1/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558=
@@ -19,8 +17,6 @@ github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaW
 github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
 github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ=
 github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
-github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
-github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
 github.com/hashicorp/hcl/v2 v2.10.0 h1:1S1UnuhDGlv3gRFV4+0EdwB+znNP5HmcGbIqwnSCByg=
 github.com/hashicorp/hcl/v2 v2.10.0/go.mod h1:FwWsfWEjyV/CMj8s/gqAuiviY72rJ1/oayI9WftqcKg=
 github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
@@ -66,6 +62,7 @@ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
 golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
 golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
+golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
 google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
 gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
diff --git a/schema/schemaspec/extension.go b/schema/schemaspec/extension.go
index 35748a49..8d820ae6 100644
--- a/schema/schemaspec/extension.go
+++ b/schema/schemaspec/extension.go
@@ -200,6 +200,30 @@ func (r *Resource) As(target interface{}) error {
 	return nil
 }
 
+// FinalName returns the final name for the resource by examining the struct tags for
+// the extension of the Resource's type. If no such extension is registered or the
+// extension struct does not have a name field, an error is returned.
+func (r *Resource) FinalName() (string, error) {
+	extensionsMu.RLock()
+	defer extensionsMu.RUnlock()
+	t, ok := extensions[r.Type]
+	if !ok {
+		return "", fmt.Errorf("no extension registered for %q", r.Type)
+	}
+	for _, fd := range specFields(t) {
+		if fd.isName() {
+			if fd.tag != "" {
+				name, ok := r.Attr(fd.tag)
+				if ok {
+					return name.String()
+				}
+			}
+			return r.Name, nil
+		}
+	}
+	return "", fmt.Errorf("extension %q has no name field", r.Type)
+}
+
 func validateStructPtr(target interface{}) error {
 	typeOf := reflect.TypeOf(target)
 	if typeOf.Kind() != reflect.Ptr {
diff --git a/schema/schemaspec/schemahcl/hcl.go b/schema/schemaspec/schemahcl/hcl.go
index 93c50649..8f2be5cb 100644
--- a/schema/schemaspec/schemahcl/hcl.go
+++ b/schema/schemaspec/schemahcl/hcl.go
@@ -72,12 +72,80 @@ func (s *State) Eval(data []byte, v interface{}, input map[string]string) error
 	if err != nil {
 		return fmt.Errorf("schemahcl: failed decoding: %w", err)
 	}
+	if err := patchRefs(spec); err != nil {
+		return err
+	}
 	if err := spec.As(v); err != nil {
 		return fmt.Errorf("schemahcl: failed reading spec as %T: %w", v, err)
 	}
 	return nil
 }
 
+// addrRef maps addresses to their referenced resource.
+type addrRef map[string]*schemaspec.Resource
+
+// patchRefs recursively searches for schemaspec.Ref under the provided schemaspec.Resource
+// and patches any variables with their concrete names.
+func patchRefs(spec *schemaspec.Resource) error {
+	return make(addrRef).patch(spec)
+}
+
+func (r addrRef) patch(resource *schemaspec.Resource) error {
+	cp := r.copy().load(resource, "")
+	for _, attr := range resource.Attrs {
+		if ref, ok := attr.V.(*schemaspec.Ref); ok {
+			referenced, ok := cp[ref.V]
+			if !ok {
+				fmt.Println(cp)
+				return fmt.Errorf("broken reference to %q", ref.V)
+			}
+			if name, err := referenced.FinalName(); err == nil {
+				ref.V = strings.ReplaceAll(ref.V, referenced.Name, name)
+			}
+		}
+	}
+	for _, ch := range resource.Children {
+		if err := cp.patch(ch); err != nil {
+			return err
+		}
+	}
+	return nil
+}
+
+func (r addrRef) copy() addrRef {
+	n := make(addrRef)
+	for k, v := range r {
+		n[k] = v
+	}
+	return n
+}
+
+// load loads the references from the children of the resource.
+func (r addrRef) load(res *schemaspec.Resource, track string) addrRef {
+	unlabeled := 0
+	for _, ch := range res.Children {
+		current := rep(ch)
+		if ch.Name == "" {
+			current += strconv.Itoa(unlabeled)
+			unlabeled++
+		}
+		if track != "" {
+			current = track + "." + current
+		}
+		r[current] = ch
+		r.load(ch, current)
+	}
+	return r
+}
+
+func rep(r *schemaspec.Resource) string {
+	n := r.Name
+	if r.Qualifier != "" {
+		n = r.Qualifier + "." + n
+	}
+	return fmt.Sprintf("$%s.%s", r.Type, n)
+}
+
 // eval evaluates the input Atlas HCL document with the given input and returns a
 // *schemaspec.Resource representing it.
 func (s *State) eval(body []byte, input map[string]string) (*schemaspec.Resource, error) {
diff --git a/sql/migrate/ent/runtime/migrate.go b/sql/migrate/ent/runtime/migrate.go
deleted file mode 100644
index bcb58b40..00000000
--- a/sql/migrate/ent/runtime/migrate.go
+++ /dev/null
@@ -1,29 +0,0 @@
-// Copyright 2021-present The Atlas Authors. All rights reserved.
-// This source code is licensed under the Apache 2.0 license found
-// in the LICENSE file in the root directory of this source tree.
-
-// Code generated by entc, DO NOT EDIT.
-
-package runtime
-
-import (
-	"context"
-	"database/sql"
-
-	"ariga.io/atlas/sql/migrate/ent/migrate"
-	entsql "entgo.io/ent/dialect/sql"
-	"entgo.io/ent/dialect/sql/schema"
-)
-
-type initSchemaMigratorer interface {
-	InitSchemaMigrator(func(ctx context.Context) error)
-}
-
-// InitSchemaMigrator stitches in the Ent migration engine to the mysql.Driver at runtime. This is necessary
-// because the Ent migration engine imports atlas and therefore would introduce a cyclic dependency.
-func InitSchemaMigrator(drv initSchemaMigratorer, db *sql.DB, dialect string) {
-	mgr := migrate.NewSchema(entsql.NewDriver(entsql.Conn{ExecQuerier: db}, dialect))
-	drv.InitSchemaMigrator(func(ctx context.Context) error {
-		return mgr.Create(ctx, schema.WithAtlas(true))
-	})
-}
diff --git a/sql/migrate/ent/templates.tmpl b/sql/migrate/ent/templates.tmpl
deleted file mode 100644
index c8064d3d..00000000
--- a/sql/migrate/ent/templates.tmpl
+++ /dev/null
@@ -1,66 +0,0 @@
-{{ define "client/init" }}
-	// Client is the client that holds all ent builders.
-	type Client struct {
-		config
-		{{- range $n := $.Nodes }}
-			// {{ $n.Name }} is the client for interacting with the {{ $n.Name }} builders.
-			{{ $n.Name }} *{{ $n.Name }}Client
-		{{- end }}
-		{{- template "client/fields/additional" $ }}
-		{{- with $tmpls := matchTemplate "client/fields/additional/*" }}
-			{{- range $tmpl := $tmpls }}
-				{{- xtemplate $tmpl $ }}
-			{{- end }}
-		{{- end }}
-	}
-
-	// NewClient creates a new client configured with the given options.
-	func NewClient(opts ...Option) *Client {
-		cfg := config{log: log.Println, hooks: &hooks{}}
-		cfg.options(opts...)
-		client := &Client{config: cfg}
-		client.init()
-		return client
-	}
-
-	func (c *Client) init() {
-		{{- range $n := $.Nodes }}
-			c.{{ $n.Name }} = New{{ $n.Name }}Client(c.config)
-		{{- end }}
-	}
-{{ end }}
-
-{{ define "enttest" }}
-	{{ with extend $ "Package" "enttest" -}}
-		{{ template "header" . }}
-	{{ end }}
-
-	// Overridden by a custom template because it is unused in this project.
-{{ end }}
-
-{{ define "runtime/migrate" }}
-	{{ $.Header }}
-	package {{ "runtime" }}
-
-	import(
-		"context"
-		"database/sql"
-
-		"ariga.io/atlas/sql/migrate/ent/migrate"
-		entsql "entgo.io/ent/dialect/sql"
-		"entgo.io/ent/dialect/sql/schema"
-	)
-
-	type initSchemaMigratorer interface {
-		InitSchemaMigrator(func(ctx context.Context) error)
-	}
-
-	// InitSchemaMigrator stitches in the Ent migration engine to the mysql.Driver at runtime. This is necessary
-	// because the Ent migration engine imports atlas and therefore would introduce a cyclic dependency.
-	func InitSchemaMigrator(drv initSchemaMigratorer, db *sql.DB, dialect string) {
-		mgr := migrate.NewSchema(entsql.NewDriver(entsql.Conn{ExecQuerier: db}, dialect))
-		drv.InitSchemaMigrator(func(ctx context.Context) error {
-			return mgr.Create(ctx, schema.WithAtlas(true))
-		})
-	}
-{{ end }}
\ No newline at end of file
diff --git a/sql/migrate/migrate.go b/sql/migrate/migrate.go
index c2ce6538..107d20c3 100644
--- a/sql/migrate/migrate.go
+++ b/sql/migrate/migrate.go
@@ -21,7 +21,6 @@ import (
 	"text/template"
 	"time"
 
-	"ariga.io/atlas/sql/migrate/ent/revision"
 	"ariga.io/atlas/sql/schema"
 )
 
@@ -172,6 +171,8 @@ type (
 	}
 
 	// A RevisionReadWriter reads and writes information about a Revisions to a persistent storage.
+	// If the implementation happens provide an "Init() error" method,
+	// it will be called once before attempting to read or write.
 	//
 	// Atlas drivers provide a sql based implementation of this interface by default.
 	RevisionReadWriter interface {
@@ -222,8 +223,9 @@ type (
 
 	// Executor is responsible to manage and execute a set of migration files against a database.
 	Executor struct {
-		drv Driver // The Driver to access and manage the database.
-		dir Dir    // The Dir with migration files to use.
+		drv Driver             // The Driver to access and manage the database.
+		dir Dir                // The Dir with migration files to use.
+		rrw RevisionReadWriter // The RevisionReadWriter to read and write database revisions to.
 	}
 
 	// ExecutorOption allows configuring an Executor using functional arguments.
@@ -312,12 +314,30 @@ func (p *Planner) WritePlan(plan *Plan) error {
 	return nil
 }
 
+const (
+	// stateOngoing is set once a migration file has been started to be applied.
+	stateOngoing = "ongoing"
+	// stateOK is set once a migration file is applied without errors.
+	stateOK = "ok"
+	// stateError  is set once a migration file could not be applied due to an error.
+	stateError = "error"
+)
+
 // ErrNoPendingFiles is returned when there are no pending migration files to execute on the managed database.
 var ErrNoPendingFiles = errors.New("sql/migrate: execute: nothing to do")
 
 // NewExecutor creates a new Executor with default values. // TODO(masseelch): Operator Version and other Meta
-func NewExecutor(drv Driver, dir Dir, opts ...ExecutorOption) (*Executor, error) {
-	p := &Executor{drv: drv, dir: dir}
+func NewExecutor(drv Driver, dir Dir, rrw RevisionReadWriter, opts ...ExecutorOption) (*Executor, error) {
+	if drv == nil {
+		return nil, errors.New("sql/migrate: execute: drv cannot be nil")
+	}
+	if dir == nil {
+		return nil, errors.New("sql/migrate: execute: dir cannot be nil")
+	}
+	if rrw == nil {
+		return nil, errors.New("sql/migrate: execute: rrw cannot be nil")
+	}
+	p := &Executor{drv: drv, dir: dir, rrw: rrw}
 	for _, opt := range opts {
 		if err := opt(p); err != nil {
 			return nil, err
@@ -333,18 +353,13 @@ func (e *Executor) Execute(ctx context.Context, n int) (err error) {
 	if err := Validate(e.dir); err != nil {
 		return fmt.Errorf("sql/migrate: execute: validate migration directory: %w", err)
 	}
-	// Check if the Driver implements RevisionReadWriter interface.
-	rrw, ok := e.drv.(RevisionReadWriter)
-	if !ok {
-		return errors.New("sql/migrate: execute: no revisions reader available")
-	}
 	// Check if the Dir implements Scanner interface.
 	sc, ok := e.dir.(Scanner)
 	if !ok {
 		return errors.New("sql/migrate: execute: no scanner available")
 	}
 	// Read all applied database revisions.
-	revisions, err := rrw.ReadRevisions(ctx)
+	revisions, err := e.rrw.ReadRevisions(ctx)
 	if err != nil {
 		return fmt.Errorf("sql/migrate: execute: read revisions: %w", err)
 	}
@@ -388,7 +403,7 @@ func (e *Executor) Execute(ctx context.Context, n int) (err error) {
 			}
 			err = dErr
 		}
-	}(rrw, ctx, &revisions)
+	}(e.rrw, ctx, &revisions)
 	// Determine what migrations to run.
 	var pending []File
 	if n <= 0 {
@@ -404,7 +419,7 @@ func (e *Executor) Execute(ctx context.Context, n int) (err error) {
 	}
 	// TODO(masseelch): run in a transaction
 	for _, m := range pending {
-		r := &Revision{ExecutedAt: time.Now(), ExecutionState: string(revision.ExecutionStateOngoing)}
+		r := &Revision{ExecutedAt: time.Now(), ExecutionState: stateOngoing}
 		revisions = append(revisions, r)
 		h, err := hf.sumByName(m.Name())
 		if err != nil {
@@ -541,9 +556,9 @@ func GlobStateReader(dir Dir, drv Driver, glob string) StateReaderFunc {
 func (r *Revision) done(ok bool) {
 	r.ExecutionTime = time.Now().Sub(r.ExecutedAt)
 	if ok {
-		r.ExecutionState = string(revision.ExecutionStateOk)
+		r.ExecutionState = stateOK
 	} else {
-		r.ExecutionState = string(revision.ExecutionStateError)
+		r.ExecutionState = stateError
 	}
 }
 
diff --git a/sql/mysql/driver.go b/sql/mysql/driver.go
index d8496b85..ded8c798 100644
--- a/sql/mysql/driver.go
+++ b/sql/mysql/driver.go
@@ -13,12 +13,8 @@ import (
 
 	"ariga.io/atlas/sql/internal/sqlx"
 	"ariga.io/atlas/sql/migrate"
-	"ariga.io/atlas/sql/migrate/ent"
 	"ariga.io/atlas/sql/schema"
 	"ariga.io/atlas/sql/sqlclient"
-	"entgo.io/ent/dialect"
-	"entgo.io/ent/dialect/sql"
-
 	"golang.org/x/mod/semver"
 )
 
@@ -30,7 +26,6 @@ type (
 		schema.Differ
 		schema.Inspector
 		migrate.PlanApplier
-		migrate.RevisionReadWriter
 	}
 
 	// database connection and its information.
@@ -78,18 +73,9 @@ func Open(db schema.ExecQuerier) (migrate.Driver, error) {
 		Differ:      &sqlx.Diff{DiffDriver: &diff{c}},
 		Inspector:   &inspect{c},
 		PlanApplier: &planApply{c},
-		RevisionReadWriter: &revReadWrite{
-			c: ent.NewClient(ent.Driver(sql.NewDriver(sql.Conn{ExecQuerier: db}, dialect.MySQL))),
-		},
 	}, nil
 }
 
-// InitSchemaMigrator stitches in the Ent migration engine to the mysql.Driver at runtime. This is necessary
-// because the Ent migration engine imports atlas and therefore would introduce a cyclic dependency.
-func (d *Driver) InitSchemaMigrator(sc func(context.Context) error) {
-	d.RevisionReadWriter.(*revReadWrite).sc = sc
-}
-
 func (d *Driver) dev() *sqlx.DevDriver {
 	return &sqlx.DevDriver{Driver: d, MaxNameLen: 64}
 }
diff --git a/sql/mysql/migrate.go b/sql/mysql/migrate.go
index 9d66b22a..ec3496c1 100644
--- a/sql/mysql/migrate.go
+++ b/sql/mysql/migrate.go
@@ -13,8 +13,6 @@ import (
 
 	"ariga.io/atlas/sql/internal/sqlx"
 	"ariga.io/atlas/sql/migrate"
-	"ariga.io/atlas/sql/migrate/ent"
-	"ariga.io/atlas/sql/migrate/ent/revision"
 	"ariga.io/atlas/sql/schema"
 )
 
@@ -54,69 +52,6 @@ func (p *planApply) ApplyChanges(ctx context.Context, changes []schema.Change) e
 	return sqlx.ApplyChanges(ctx, changes, p)
 }
 
-// A revReadWrite provides implementation for the migrate.RevisionReadWriter interface.
-type revReadWrite struct {
-	c *ent.Client
-	// sc is the function signature of the Ent migration engine.
-	// Due to cyclic dependencies between Ent and Atlas, this value is stitched in at runtime.
-	sc func(context.Context) error
-}
-
-// ReadRevisions read the revisions from the revisions table.
-func (r *revReadWrite) ReadRevisions(ctx context.Context) (migrate.Revisions, error) {
-	rs, err := r.c.QueryContext(ctx, fmt.Sprintf("SHOW TABLES LIKE '%s'", revision.Table))
-	if err != nil {
-		return nil, err
-	}
-	if !rs.Next() {
-		// If there are no results the table does not exist and there are no revisions yet.
-		return migrate.Revisions{}, nil
-	}
-	if err := rs.Close(); err != nil {
-		return nil, err
-	}
-	revs, err := r.c.Revision.Query().Order(ent.Asc(revision.FieldID)).All(ctx)
-	if err != nil {
-		return nil, err
-	}
-	ret := make(migrate.Revisions, len(revs))
-	for i, r := range revs {
-		ret[i] = &migrate.Revision{
-			Version:         r.ID,
-			Description:     r.Description,
-			ExecutionState:  string(r.ExecutionState),
-			ExecutedAt:      r.ExecutedAt,
-			ExecutionTime:   r.ExecutionTime,
-			Hash:            r.Hash,
-			OperatorVersion: r.OperatorVersion,
-			Meta:            r.Meta,
-		}
-	}
-	return ret, nil
-}
-
-// WriteRevisions stores the revisions in the revisions table.
-func (r *revReadWrite) WriteRevisions(ctx context.Context, rs migrate.Revisions) error {
-	if err := r.sc(ctx); err != nil {
-		return err
-	}
-	bulk := make([]*ent.RevisionCreate, len(rs))
-	for i, rev := range rs {
-		bulk[i] = r.c.Revision.Create().
-			SetID(rev.Version).
-			SetDescription(rev.Description).
-			SetExecutionState(revision.ExecutionState(rev.ExecutionState)).
-			SetExecutedAt(rev.ExecutedAt).
-			SetExecutionTime(rev.ExecutionTime).
-			SetHash(rev.Hash).
-			SetOperatorVersion(rev.OperatorVersion).
-			SetMeta(rev.Meta)
-	}
-	return r.c.Revision.CreateBulk(bulk...).OnConflict().UpdateNewValues().Exec(ctx)
-}
-
-var _ migrate.RevisionReadWriter = (*revReadWrite)(nil)
-
 // state represents the state of a planning. It is not part of
 // planApply so that multiple planning/applying can be called
 // in parallel.
diff --git a/sql/postgres/driver.go b/sql/postgres/driver.go
index f4d50410..2e175cc5 100644
--- a/sql/postgres/driver.go
+++ b/sql/postgres/driver.go
@@ -16,7 +16,6 @@ import (
 	"ariga.io/atlas/sql/migrate"
 	"ariga.io/atlas/sql/schema"
 	"ariga.io/atlas/sql/sqlclient"
-
 	"golang.org/x/mod/semver"
 )
 


Test patch:
diff --git a/cmd/atlascmd/migrate/ent/enttest/enttest.go b/cmd/atlascmd/migrate/ent/enttest/enttest.go
new file mode 100644
index 00000000..916cab9c
--- /dev/null
+++ b/cmd/atlascmd/migrate/ent/enttest/enttest.go
@@ -0,0 +1,82 @@
+// Copyright 2021-present The Atlas Authors. All rights reserved.
+// This source code is licensed under the Apache 2.0 license found
+// in the LICENSE file in the root directory of this source tree.
+
+// Code generated by entc, DO NOT EDIT.
+
+package enttest
+
+import (
+	"context"
+
+	"ariga.io/atlas/cmd/atlascmd/migrate/ent"
+	// required by schema hooks.
+	_ "ariga.io/atlas/cmd/atlascmd/migrate/ent/runtime"
+
+	"entgo.io/ent/dialect/sql/schema"
+)
+
+type (
+	// TestingT is the interface that is shared between
+	// testing.T and testing.B and used by enttest.
+	TestingT interface {
+		FailNow()
+		Error(...interface{})
+	}
+
+	// Option configures client creation.
+	Option func(*options)
+
+	options struct {
+		opts        []ent.Option
+		migrateOpts []schema.MigrateOption
+	}
+)
+
+// WithOptions forwards options to client creation.
+func WithOptions(opts ...ent.Option) Option {
+	return func(o *options) {
+		o.opts = append(o.opts, opts...)
+	}
+}
+
+// WithMigrateOptions forwards options to auto migration.
+func WithMigrateOptions(opts ...schema.MigrateOption) Option {
+	return func(o *options) {
+		o.migrateOpts = append(o.migrateOpts, opts...)
+	}
+}
+
+func newOptions(opts []Option) *options {
+	o := &options{}
+	for _, opt := range opts {
+		opt(o)
+	}
+	return o
+}
+
+// Open calls ent.Open and auto-run migration.
+func Open(t TestingT, driverName, dataSourceName string, opts ...Option) *ent.Client {
+	o := newOptions(opts)
+	c, err := ent.Open(driverName, dataSourceName, o.opts...)
+	if err != nil {
+		t.Error(err)
+		t.FailNow()
+	}
+	if err := c.Schema.Create(context.Background(), o.migrateOpts...); err != nil {
+		t.Error(err)
+		t.FailNow()
+	}
+	return c
+}
+
+// NewClient calls ent.NewClient and auto-run migration.
+func NewClient(t TestingT, opts ...Option) *ent.Client {
+	o := newOptions(opts)
+	c := ent.NewClient(o.opts...)
+	if err := c.Schema.Create(context.Background(), o.migrateOpts...); err != nil {
+		t.Error(err)
+		t.FailNow()
+	}
+	return c
+}
diff --git a/cmd/atlascmd/migrate_test.go b/cmd/atlascmd/migrate_test.go
index a09d2caa..69f18018 100644
--- a/cmd/atlascmd/migrate_test.go
+++ b/cmd/atlascmd/migrate_test.go
@@ -83,17 +83,17 @@ func TestMigrate_Diff(t *testing.T) {
 }
 
 func TestMigrate_Validate(t *testing.T) {
-	s, err := runCmd(Root, "migrate", "validate", "--dir", "file://migrations")
+	s, err := runCmd(Root, "migrate", "validate", "--dir", "file://testdata")
 	require.Zero(t, s)
 	require.NoError(t, err)
 }
 
 func TestMigrate_ValidateError(t *testing.T) {
 	if os.Getenv("DO_VALIDATE") == "1" {
-		runCmd(Root, "migrate", "validate", "--dir", "file://migrations")
+		runCmd(Root, "migrate", "validate", "--dir", "file://testdata")
 		return
 	}
-	f := filepath.Join("migrations", "new.sql")
+	f := filepath.Join("testdata", "new.sql")
 	require.NoError(t, os.WriteFile(f, []byte("contents"), 0600))
 	defer os.Remove(f)
 	cmd := exec.Command(os.Args[0], "-test.run=TestMigrate_ValidateError") //nolint:gosec
@@ -106,12 +106,12 @@ func TestMigrate_ValidateError(t *testing.T) {
 }
 
 func TestMigrate_Hash(t *testing.T) {
-	s, err := runCmd(Root, "migrate", "hash", "--dir", "file://migrations")
+	s, err := runCmd(Root, "migrate", "hash", "--dir", "file://testdata")
 	require.Zero(t, s)
 	require.NoError(t, err)
 
 	p := t.TempDir()
-	err = copyFile(filepath.Join("migrations", "20220318104614_initial.sql"), filepath.Join(p, "20220318104614_initial.sql"))
+	err = copyFile(filepath.Join("testdata", "20220318104614_initial.sql"), filepath.Join(p, "20220318104614_initial.sql"))
 	require.NoError(t, err)
 
 	s, err = runCmd(Root, "migrate", "hash", "--dir", "file://"+p, "--force")
@@ -135,7 +135,7 @@ func TestMigrate_HashError(t *testing.T) {
 		return
 	}
 	p := t.TempDir()
-	err := copyFile(filepath.Join("migrations", "20220318104614_initial.sql"), filepath.Join(p, "20220318104614_initial.sql"))
+	err := copyFile(filepath.Join("testdata", "20220318104614_initial.sql"), filepath.Join(p, "20220318104614_initial.sql"))
 	require.NoError(t, err)
 	cmd := exec.Command(os.Args[0], "-test.run=TestMigrate_HashError") //nolint:gosec
 	cmd.Env = append(os.Environ(), "DO_HASH=1", "MIGRATION_DIR="+p)
diff --git a/internal/integration/integration_test.go b/internal/integration/integration_test.go
index 84ab8e1b..a698d83e 100644
--- a/internal/integration/integration_test.go
+++ b/internal/integration/integration_test.go
@@ -45,6 +45,7 @@ func TestMain(m *testing.M) {
 type T interface {
 	testing.TB
 	driver() migrate.Driver
+	revisionsStorage() migrate.RevisionReadWriter
 	realm() *schema.Realm
 	loadRealm() *schema.Realm
 	users() *schema.Table
@@ -471,7 +472,7 @@ func testAdvisoryLock(t *testing.T, l schema.Locker) {
 }
 
 func testExecutor(t T) {
-	usersT, postsT, revisionsT := t.users(), t.posts(), t.revisions()
+	usersT, postsT := t.users(), t.posts()
 	petsT := &schema.Table{
 		Name:   "pets",
 		Schema: usersT.Schema,
@@ -485,7 +486,10 @@ func testExecutor(t T) {
 		{Symbol: "owner_id", Table: petsT, Columns: petsT.Columns[1:], RefTable: usersT, RefColumns: usersT.Columns[:1]},
 	}
 
-	t.dropTables(petsT.Name, postsT.Name, usersT.Name, revisionsT.Name)
+	t.dropTables(petsT.Name, postsT.Name, usersT.Name)
+	t.Cleanup(func() {
+		t.revisionsStorage().(*rrw).clean()
+	})
 
 	dir, err := migrate.NewLocalDir(t.TempDir())
 	require.NoError(t, err)
@@ -503,12 +507,14 @@ func testExecutor(t T) {
 	require.NoError(t, pl.WritePlan(plan(t, "2_posts", &schema.AddTable{T: postsT})))
 	require.NoError(t, pl.WritePlan(plan(t, "3_pets", &schema.AddTable{T: petsT})))
 
-	ex, err := migrate.NewExecutor(t.driver(), dir)
+	ex, err := migrate.NewExecutor(t.driver(), dir, t.revisionsStorage())
 	require.NoError(t, err)
 	require.NoError(t, ex.Execute(context.Background(), 2)) // usersT and postsT
-	ensureNoChange(t, revisionsT, postsT, usersT)
+	require.Len(t, *t.revisionsStorage().(*rrw), 2)
+	ensureNoChange(t, postsT, usersT)
 	require.NoError(t, ex.Execute(context.Background(), 1)) // petsT
-	ensureNoChange(t, revisionsT, petsT, postsT, usersT)
+	require.Len(t, *t.revisionsStorage().(*rrw), 3)
+	ensureNoChange(t, petsT, postsT, usersT)
 
 	require.ErrorIs(t, ex.Execute(context.Background(), 1), migrate.ErrNoPendingFiles)
 }
@@ -518,3 +524,18 @@ func plan(t T, name string, changes ...schema.Change) *migrate.Plan {
 	require.NoError(t, err)
 	return p
 }
+
+type rrw migrate.Revisions
+
+func (r *rrw) WriteRevisions(ctx context.Context, revs migrate.Revisions) error {
+	*r = rrw(revs)
+	return nil
+}
+
+func (r *rrw) ReadRevisions(ctx context.Context) (migrate.Revisions, error) {
+	return migrate.Revisions(*r), nil
+}
+
+func (r *rrw) clean() {
+	*r = rrw(migrate.Revisions{})
+}
diff --git a/internal/integration/mysql_test.go b/internal/integration/mysql_test.go
index 8dbf5158..43ba56e3 100644
--- a/internal/integration/mysql_test.go
+++ b/internal/integration/mysql_test.go
@@ -14,7 +14,6 @@ import (
 	"testing"
 
 	"ariga.io/atlas/sql/migrate"
-	"ariga.io/atlas/sql/migrate/ent/runtime"
 	"ariga.io/atlas/sql/mysql"
 	"ariga.io/atlas/sql/schema"
 	"entgo.io/ent/dialect"
@@ -26,6 +25,7 @@ type myTest struct {
 	*testing.T
 	db      *sql.DB
 	drv     migrate.Driver
+	rrw     migrate.RevisionReadWriter
 	version string
 	port    int
 }
@@ -45,15 +45,6 @@ var myTests = struct {
 	},
 }
 
-func myRun(t *testing.T, fn func(*myTest)) {
-	for version, tt := range myTests.drivers {
-		t.Run(version, func(t *testing.T) {
-			tt := &myTest{T: t, db: tt.db, drv: tt.drv, version: version, port: tt.port}
-			fn(tt)
-		})
-	}
-}
-
 func myInit(d string) []io.Closer {
 	var cs []io.Closer
 	if d != "" {
@@ -74,12 +65,20 @@ func myInit(d string) []io.Closer {
 		if err != nil {
 			log.Fatalln(err)
 		}
-		runtime.InitSchemaMigrator(drv.(*mysql.Driver), db, dialect.MySQL)
-		myTests.drivers[version] = &myTest{db: db, drv: drv, version: version, port: port}
+		myTests.drivers[version] = &myTest{db: db, drv: drv, version: version, port: port, rrw: &rrw{}}
 	}
 	return cs
 }
 
+func myRun(t *testing.T, fn func(*myTest)) {
+	for version, tt := range myTests.drivers {
+		t.Run(version, func(t *testing.T) {
+			tt := &myTest{T: t, db: tt.db, drv: tt.drv, version: version, port: tt.port, rrw: tt.rrw}
+			fn(tt)
+		})
+	}
+}
+
 func TestMySQL_Executor(t *testing.T) {
 	myRun(t, func(t *myTest) {
 		testExecutor(t)
@@ -1156,6 +1155,10 @@ func (t *myTest) driver() migrate.Driver {
 	return t.drv
 }
 
+func (t *myTest) revisionsStorage() migrate.RevisionReadWriter {
+	return t.rrw
+}
+
 func (t *myTest) applyHcl(spec string) {
 	realm := t.loadRealm()
 	var desired schema.Schema
diff --git a/internal/integration/postgres_test.go b/internal/integration/postgres_test.go
index 9de91ac9..c4af85da 100644
--- a/internal/integration/postgres_test.go
+++ b/internal/integration/postgres_test.go
@@ -27,6 +27,7 @@ type pgTest struct {
 	*testing.T
 	db      *sql.DB
 	drv     migrate.Driver
+	rrw     migrate.RevisionReadWriter
 	version string
 	port    int
 }
@@ -45,21 +46,12 @@ var pgTests = struct {
 	},
 }
 
-func pgRun(t *testing.T, fn func(*pgTest)) {
-	for version, tt := range pgTests.drivers {
-		t.Run(version, func(t *testing.T) {
-			tt := &pgTest{T: t, db: tt.db, drv: tt.drv, version: version, port: tt.port}
-			fn(tt)
-		})
-	}
-}
-
-func pgInit(dialect string) []io.Closer {
+func pgInit(d string) []io.Closer {
 	var cs []io.Closer
-	if dialect != "" {
-		p, ok := pgTests.ports[dialect]
+	if d != "" {
+		p, ok := pgTests.ports[d]
 		if ok {
-			pgTests.ports = map[string]int{dialect: p}
+			pgTests.ports = map[string]int{d: p}
 		} else {
 			pgTests.ports = make(map[string]int)
 		}
@@ -74,11 +66,26 @@ func pgInit(dialect string) []io.Closer {
 		if err != nil {
 			log.Fatalln(err)
 		}
-		pgTests.drivers[version] = &pgTest{db: db, drv: drv, version: version, port: port}
+		pgTests.drivers[version] = &pgTest{db: db, drv: drv, version: version, port: port, rrw: &rrw{}}
 	}
 	return cs
 }
 
+func pgRun(t *testing.T, fn func(*pgTest)) {
+	for version, tt := range pgTests.drivers {
+		t.Run(version, func(t *testing.T) {
+			tt := &pgTest{T: t, db: tt.db, drv: tt.drv, version: version, port: tt.port, rrw: tt.rrw}
+			fn(tt)
+		})
+	}
+}
+
+func TestPostgres_Executor(t *testing.T) {
+	pgRun(t, func(t *pgTest) {
+		testExecutor(t)
+	})
+}
+
 func TestPostgres_AddDropTable(t *testing.T) {
 	pgRun(t, func(t *pgTest) {
 		testAddDrop(t)
@@ -999,6 +1006,10 @@ func (t *pgTest) driver() migrate.Driver {
 	return t.drv
 }
 
+func (t *pgTest) revisionsStorage() migrate.RevisionReadWriter {
+	return t.rrw
+}
+
 func (t *pgTest) applyHcl(spec string) {
 	realm := t.loadRealm()
 	var desired schema.Schema
@@ -1105,14 +1116,14 @@ func (t *pgTest) revisions() *schema.Table {
 	versionsT := &schema.Table{
 		Name: "atlas_schema_revisions",
 		Columns: []*schema.Column{
-			{Name: "version", Type: &schema.ColumnType{Type: &schema.StringType{T: t.valueByVersion(map[string]string{"mysql56": "varchar(191)"}, "varchar(255)")}}},
-			{Name: "description", Type: &schema.ColumnType{Type: &schema.StringType{T: "varchar(255)"}}},
-			{Name: "execution_state", Type: &schema.ColumnType{Type: &schema.StringType{T: "varchar(7)"}}},
-			{Name: "executed_at", Type: &schema.ColumnType{Type: &schema.TimeType{T: "datetime"}}},
-			{Name: "execution_time", Type: &schema.ColumnType{Type: &schema.IntegerType{T: "int"}}},
-			{Name: "hash", Type: &schema.ColumnType{Type: &schema.StringType{T: "varchar(44)"}}},
-			{Name: "operator_version", Type: &schema.ColumnType{Type: &schema.StringType{T: "varchar(255)"}}},
-			{Name: "meta", Type: &schema.ColumnType{Type: &schema.StringType{T: "varchar(255)"}}},
+			{Name: "version", Type: &schema.ColumnType{Type: &schema.StringType{T: "character varying"}}},
+			{Name: "description", Type: &schema.ColumnType{Type: &schema.StringType{T: "character varying"}}},
+			{Name: "execution_state", Type: &schema.ColumnType{Type: &schema.StringType{T: "character varying"}}},
+			{Name: "executed_at", Type: &schema.ColumnType{Type: &schema.TimeType{T: "timestamp with time zone"}}},
+			{Name: "execution_time", Type: &schema.ColumnType{Type: &schema.IntegerType{T: "bigint"}}},
+			{Name: "hash", Type: &schema.ColumnType{Type: &schema.StringType{T: "character varying"}}},
+			{Name: "operator_version", Type: &schema.ColumnType{Type: &schema.StringType{T: "character varying"}}},
+			{Name: "meta", Type: &schema.ColumnType{Type: &schema.JSONType{T: "jsonb"}, Raw: "jsonb"}},
 		},
 	}
 	versionsT.PrimaryKey = &schema.Index{Parts: []*schema.IndexPart{{C: versionsT.Columns[0]}}}
diff --git a/internal/integration/sqlite_test.go b/internal/integration/sqlite_test.go
index 1f14b079..a471a3cf 100644
--- a/internal/integration/sqlite_test.go
+++ b/internal/integration/sqlite_test.go
@@ -27,6 +27,7 @@ type liteTest struct {
 	*testing.T
 	db   *sql.DB
 	drv  migrate.Driver
+	rrw  migrate.RevisionReadWriter
 	file string
 }
 
@@ -40,10 +41,16 @@ func liteRun(t *testing.T, fn func(test *liteTest)) {
 	})
 	drv, err := sqlite.Open(db)
 	require.NoError(t, err)
-	tt := &liteTest{T: t, db: db, drv: drv, file: f}
+	tt := &liteTest{T: t, db: db, drv: drv, file: f, rrw: &rrw{}}
 	fn(tt)
 }
 
+func TestSQLite_Executor(t *testing.T) {
+	liteRun(t, func(t *liteTest) {
+		testExecutor(t)
+	})
+}
+
 func TestSQLite_AddDropTable(t *testing.T) {
 	liteRun(t, func(t *liteTest) {
 		testAddDrop(t)
@@ -718,6 +725,10 @@ func (t *liteTest) driver() migrate.Driver {
 	return t.drv
 }
 
+func (t *liteTest) revisionsStorage() migrate.RevisionReadWriter {
+	return t.rrw
+}
+
 func (t *liteTest) applyHcl(spec string) {
 	realm := t.loadRealm()
 	var desired schema.Schema
@@ -823,7 +834,7 @@ func (t *liteTest) revisions() *schema.Table {
 			{Name: "execution_time", Type: &schema.ColumnType{Type: &schema.IntegerType{T: "int"}}},
 			{Name: "hash", Type: &schema.ColumnType{Type: &schema.StringType{T: "varchar(44)"}}},
 			{Name: "operator_version", Type: &schema.ColumnType{Type: &schema.StringType{T: "varchar(255)"}}},
-			{Name: "meta", Type: &schema.ColumnType{Type: &schema.StringType{T: "varchar(255)"}}},
+			{Name: "meta", Type: &schema.ColumnType{Type: &schema.JSONType{T: "json"}, Raw: "json"}},
 		},
 	}
 	versionsT.PrimaryKey = &schema.Index{Parts: []*schema.IndexPart{{C: versionsT.Columns[0]}}}
diff --git a/internal/integration/tidb_test.go b/internal/integration/tidb_test.go
index 4bf00c22..30c48acd 100644
--- a/internal/integration/tidb_test.go
+++ b/internal/integration/tidb_test.go
@@ -33,12 +33,12 @@ var tidbTests = struct {
 	ports:   map[string]int{"tidb5": 4309, "tidb6": 4310},
 }
 
-func tidbInit(dialect string) []io.Closer {
+func tidbInit(d string) []io.Closer {
 	var cs []io.Closer
-	if dialect != "" {
-		p, ok := tidbTests.ports[dialect]
+	if d != "" {
+		p, ok := tidbTests.ports[d]
 		if ok {
-			tidbTests.ports = map[string]int{dialect: p}
+			tidbTests.ports = map[string]int{d: p}
 		} else {
 			tidbTests.ports = make(map[string]int)
 		}
@@ -53,7 +53,7 @@ func tidbInit(dialect string) []io.Closer {
 		if err != nil {
 			log.Fatalln(err)
 		}
-		tidbTests.drivers[version] = &myTest{db: db, drv: drv, version: version, port: port}
+		tidbTests.drivers[version] = &myTest{db: db, drv: drv, version: version, port: port, rrw: &rrw{}}
 	}
 	return cs
 }
@@ -61,12 +61,18 @@ func tidbInit(dialect string) []io.Closer {
 func tidbRun(t *testing.T, fn func(*myTest)) {
 	for version, tt := range tidbTests.drivers {
 		t.Run(version, func(t *testing.T) {
-			tt := &myTest{T: t, db: tt.db, drv: tt.drv, version: version, port: tt.port}
+			tt := &myTest{T: t, db: tt.db, drv: tt.drv, version: version, port: tt.port, rrw: tt.rrw}
 			fn(tt)
 		})
 	}
 }
 
+func TestTiDB_Executor(t *testing.T) {
+	tidbRun(t, func(t *myTest) {
+		testExecutor(t)
+	})
+}
+
 func TestTiDB_AddDropTable(t *testing.T) {
 	tidbRun(t, func(t *myTest) {
 		testAddDrop(t)
diff --git a/schema/schemaspec/extension_test.go b/schema/schemaspec/extension_test.go
index 9f4daa8d..23af261d 100644
--- a/schema/schemaspec/extension_test.go
+++ b/schema/schemaspec/extension_test.go
@@ -131,6 +131,9 @@ func TestNested(t *testing.T) {
 	err = scan.Scan(&pb)
 	require.NoError(t, err)
 	require.EqualValues(t, pet, scan)
+	name, err := pet.FinalName()
+	require.NoError(t, err)
+	require.EqualValues(t, "donut", name)
 }
 
 func TestRef(t *testing.T) {
@@ -202,6 +205,7 @@ func TestNameAttr(t *testing.T) {
 	schemaspec.Register("named", &Named{})
 	resource := &schemaspec.Resource{
 		Name: "id",
+		Type: "named",
 		Attrs: []*schemaspec.Attr{
 			schemautil.StrLitAttr("name", "atlas"),
 		},
@@ -210,4 +214,7 @@ func TestNameAttr(t *testing.T) {
 	err := resource.As(&tgt)
 	require.NoError(t, err)
 	require.EqualValues(t, "atlas", tgt.Name)
+	name, err := resource.FinalName()
+	require.NoError(t, err)
+	require.EqualValues(t, name, "atlas")
 }
diff --git a/schema/schemaspec/schemahcl/hcl_test.go b/schema/schemaspec/schemahcl/hcl_test.go
index 96ba4032..ce54a6e1 100644
--- a/schema/schemaspec/schemahcl/hcl_test.go
+++ b/schema/schemaspec/schemahcl/hcl_test.go
@@ -271,3 +271,37 @@ ref = named.block_id.name
 	}, test.Named)
 	require.EqualValues(t, "atlas", test.Ref)
 }
+
+func TestRefPatch(t *testing.T) {
+	type (
+		Family struct {
+			Name string `spec:"name,name"`
+		}
+		Person struct {
+			Name   string          `spec:",name"`
+			Family *schemaspec.Ref `spec:"family"`
+		}
+	)
+	schemaspec.Register("family", &Family{})
+	schemaspec.Register("person", &Person{})
+	var test struct {
+		Families []*Family `spec:"family"`
+		People   []*Person `spec:"person"`
+	}
+	h := `
+variable "family_name" {
+  type = string
+}
+
+family "default" {
+	name = var.family_name
+}
+
+person "rotem" {
+	family = family.default
+}
+`
+	err := New().Eval([]byte(h), &test, map[string]string{"family_name": "tam"})
+	require.NoError(t, err)
+	require.EqualValues(t, "$family.tam", test.People[0].Family.V)
+}
diff --git a/sql/internal/spectest/spectest.go b/sql/internal/spectest/spectest.go
index 41bc2fe0..8b12014b 100644
--- a/sql/internal/spectest/spectest.go
+++ b/sql/internal/spectest/spectest.go
@@ -53,12 +53,19 @@ table "users" {
 	column "id" {
 		type = int
 	}
+	index "user_name" {
+    on {
+      column = column.id
+      unique = true
+    }
+  }
 }
 `
 	var test schema.Realm
 	err := evaluator.Eval([]byte(h), &test, map[string]string{"tenant": "rotemtam"})
 	require.NoError(t, err)
 	require.EqualValues(t, "rotemtam", test.Schemas[0].Name)
+	require.Len(t, test.Schemas[0].Tables, 1)
 }
 
 func contains(s string, l []string) bool {
diff --git a/sql/migrate/ent/enttest/enttest.go b/sql/migrate/ent/enttest/enttest.go
deleted file mode 100644
index 2241af4c..00000000
--- a/sql/migrate/ent/enttest/enttest.go
+++ /dev/null
@@ -1,9 +0,0 @@
-// Copyright 2021-present The Atlas Authors. All rights reserved.
-// This source code is licensed under the Apache 2.0 license found
-// in the LICENSE file in the root directory of this source tree.
-
-// Code generated by entc, DO NOT EDIT.
-
-package enttest
-
-// Overridden by a custom template because it is unused in this project.