Repository: ariga/atlas
Pull request #1223 (2022-10-21 13:25:47)

doc/md/guides: added article "Partial Indexes in SQLite"


Patch:
diff --git a/cmd/atlas/go.mod b/cmd/atlas/go.mod
index 857101c3..d8c926a7 100644
--- a/cmd/atlas/go.mod
+++ b/cmd/atlas/go.mod
@@ -3,7 +3,7 @@ module ariga.io/atlas/cmd/atlas
 go 1.18
 
 require (
-	ariga.io/atlas v0.7.3-0.20221010133632-5e5aca152e8a
+	ariga.io/atlas v0.8.1
 	entgo.io/ent v0.11.1
 	github.com/antlr/antlr4/runtime/Go/antlr v0.0.0-20220816024939-bc8df83d7b9d
 	github.com/auxten/postgresql-parser v1.0.1
diff --git a/cmd/atlas/go.sum b/cmd/atlas/go.sum
index 006bd6d1..00493309 100644
--- a/cmd/atlas/go.sum
+++ b/cmd/atlas/go.sum
@@ -1,5 +1,7 @@
 ariga.io/atlas v0.7.3-0.20221010133632-5e5aca152e8a h1:/5g/0+IO3umGAxt/ROhJ0/BaTjn3rGWjZyJwt+t4UAI=
 ariga.io/atlas v0.7.3-0.20221010133632-5e5aca152e8a/go.mod h1:ft47uSh5hWGDCmQC9DsztZg6Xk+KagM5Ts/mZYKb9JE=
+ariga.io/atlas v0.8.1 h1:eTZS+qyBpqMTkCbatFquYXIqGPAIEzjQzkVXlOwkcEY=
+ariga.io/atlas v0.8.1/go.mod h1:ft47uSh5hWGDCmQC9DsztZg6Xk+KagM5Ts/mZYKb9JE=
 cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
 cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
 entgo.io/ent v0.11.1 h1:im67R+2W3Nee2bNS2YnoYz8oAF0Qz4AOlIvKRIAEISY=
diff --git a/cmd/atlas/internal/cmdapi/cmdapi.go b/cmd/atlas/internal/cmdapi/cmdapi.go
index b5206e9f..df4a257a 100644
--- a/cmd/atlas/internal/cmdapi/cmdapi.go
+++ b/cmd/atlas/internal/cmdapi/cmdapi.go
@@ -367,11 +367,11 @@ type (
 	}
 	// stateReaderConfig is given to stateReader.
 	stateReaderConfig struct {
-		urls      []string          // urls to create a migrate.StateReader from
-		norm, dev *sqlclient.Client // database connections, while dev is considered a dev database, norm is not
-		schemas   []string          // schemas to work on
-		exclude   []string          // exclude flag values
-		vars      Vars
+		urls        []string          // urls to create a migrate.StateReader from
+		client, dev *sqlclient.Client // database connections, while dev is considered a dev database, client is not
+		schemas     []string          // schemas to work on
+		exclude     []string          // exclude flag values
+		vars        Vars
 	}
 )
 
@@ -492,8 +492,8 @@ func hclStateReader(ctx context.Context, config *stateReaderConfig, urls []*url.
 	switch {
 	case config.dev != nil:
 		client = config.dev
-	case config.norm != nil:
-		client = config.norm
+	case config.client != nil:
+		client = config.client
 	default:
 		return nil, errors.New("no database connection available")
 	}
@@ -530,7 +530,7 @@ func hclStateReader(ctx context.Context, config *stateReaderConfig, urls []*url.
 			config.dev.URL.Schema,
 		)
 	}
-	if norm, ok := client.Driver.(schema.Normalizer); ok && len(realm.Schemas) > 0 {
+	if norm, ok := client.Driver.(schema.Normalizer); ok && config.dev != nil { // only normalize on a dev database
 		realm, err = norm.NormalizeRealm(ctx, realm)
 		if err != nil {
 			return nil, err
diff --git a/cmd/atlas/internal/cmdapi/migrate.go b/cmd/atlas/internal/cmdapi/migrate.go
index 9b9d60e5..cfa31088 100644
--- a/cmd/atlas/internal/cmdapi/migrate.go
+++ b/cmd/atlas/internal/cmdapi/migrate.go
@@ -895,6 +895,7 @@ func migrateStatusCmd() *cobra.Command {
 	)
 	cmd.Flags().SortFlags = false
 	addFlagURL(cmd.Flags(), &flags.url)
+	addFlagLog(cmd.Flags(), &flags.logFormat)
 	addFlagDirURL(cmd.Flags(), &flags.dirURL)
 	addFlagDirFormat(cmd.Flags(), &flags.dirFormat)
 	addFlagRevisionSchema(cmd.Flags(), &flags.revisionSchema)
@@ -1461,6 +1462,9 @@ func setFlagsFromEnv(cmd *cobra.Command, env *Env) error {
 	if err := inputValsFromEnv(cmd, env); err != nil {
 		return err
 	}
+	if err := maySetFlag(cmd, flagURL, env.URL); err != nil {
+		return err
+	}
 	if err := maySetFlag(cmd, flagDevURL, env.DevURL); err != nil {
 		return err
 	}
@@ -1470,7 +1474,7 @@ func setFlagsFromEnv(cmd *cobra.Command, env *Env) error {
 	if err := maySetFlag(cmd, flagDirFormat, env.Migration.Format); err != nil {
 		return err
 	}
-	if err := maySetFlag(cmd, flagURL, env.URL); err != nil {
+	if err := maySetFlag(cmd, flagBaseline, env.Migration.Baseline); err != nil {
 		return err
 	}
 	if err := maySetFlag(cmd, flagRevisionSchema, env.Migration.RevisionsSchema); err != nil {
@@ -1532,9 +1536,11 @@ func migrateEnvsRun[F any](run func(*cobra.Command, []string, F) error, cmd *cob
 	}
 	var (
 		w     bytes.Buffer
+		out   = cmd.OutOrStdout()
 		reset = resetFromEnv(cmd)
 	)
-	cmd.SetOut(io.MultiWriter(cmd.OutOrStdout(), &w))
+	cmd.SetOut(io.MultiWriter(out, &w))
+	defer cmd.SetOut(out)
 	for i, e := range envs {
 		if err := setFlagsFromEnv(cmd, e); err != nil {
 			return err
@@ -1542,10 +1548,10 @@ func migrateEnvsRun[F any](run func(*cobra.Command, []string, F) error, cmd *cob
 		if err := run(cmd, args, *flags); err != nil {
 			return err
 		}
-		out := bytes.TrimLeft(w.Bytes(), " \t\r")
+		b := bytes.TrimLeft(w.Bytes(), " \t\r")
 		// In case a custom logging was configured, ensure there is
 		// a newline separator between the different environments.
-		if cmd.Flags().Changed(flagLog) && bytes.LastIndexByte(out, '\n') != len(out)-1 && i != len(envs)-1 {
+		if cmd.Flags().Changed(flagLog) && bytes.LastIndexByte(b, '\n') != len(b)-1 && i != len(envs)-1 {
 			cmd.Println()
 		}
 		reset()
diff --git a/cmd/atlas/internal/cmdapi/project.go b/cmd/atlas/internal/cmdapi/project.go
index 15577b0f..2a8762fa 100644
--- a/cmd/atlas/internal/cmdapi/project.go
+++ b/cmd/atlas/internal/cmdapi/project.go
@@ -16,6 +16,7 @@ import (
 	"ariga.io/atlas/sql/sqlclient"
 
 	"github.com/hashicorp/hcl/v2"
+	"github.com/hashicorp/hcl/v2/gohcl"
 	"github.com/hashicorp/hcl/v2/hclsyntax"
 	"github.com/zclconf/go-cty/cty"
 )
@@ -77,6 +78,7 @@ type (
 	Migration struct {
 		Dir             string `spec:"dir"`
 		Format          string `spec:"format"`
+		Baseline        string `spec:"baseline"`
 		RevisionsSchema string `spec:"revisions_schema"`
 	}
 
@@ -186,9 +188,6 @@ func (e *Env) asMap() (map[string]string, error) {
 			m[attr.K] = v
 			continue
 		}
-		if lv, ok := attr.V.(*schemahcl.LiteralValue); ok {
-			m[attr.K] = lv.V
-		}
 		return nil, fmt.Errorf("expecting attr %q to be a literal, got: %T", attr.K, attr.V)
 	}
 	return m, nil
@@ -260,48 +259,54 @@ type sqlsrc struct {
 
 // exec executes the source block for getting the data.
 func (s *sqlsrc) exec() (cty.Value, error) {
-	attrs, diags := s.block.Body.JustAttributes()
-	if diags.HasErrors() {
-		return cty.NilVal, diags
-	}
-	u, err := s.stringAttr(attrs, "url")
-	if err != nil {
-		return cty.NilVal, err
+	var (
+		in struct {
+			URL    string   `hcl:"url"`
+			Query  string   `hcl:"query"`
+			Remain hcl.Body `hcl:",remain"`
+			Args   []any
+		}
+		values []cty.Value
+	)
+	if diags := gohcl.DecodeBody(s.block.Body, s.ctx, &in); diags.HasErrors() {
+		return cty.NilVal, s.errorf("decoding body: %v", diags)
 	}
-	query, err := s.stringAttr(attrs, "query")
-	if err != nil {
-		return cty.NilVal, err
+	attrs, diags := in.Remain.JustAttributes()
+	if diags.HasErrors() {
+		return cty.NilVal, s.errorf("getting attributes: %v", diags)
 	}
-	var args []any
 	if at, ok := attrs["args"]; ok {
 		switch v, diags := at.Expr.Value(s.ctx); {
 		case diags.HasErrors():
-			return cty.NilVal, s.errorf(`evaluating "args": %w`, err)
+			return cty.NilVal, s.errorf(`evaluating "args": %w`, diags)
 		case !v.CanIterateElements():
 			return cty.NilVal, s.errorf(`attribute "args" must be a list, got: %s`, v.Type())
 		default:
 			for it := v.ElementIterator(); it.Next(); {
 				switch _, v := it.Element(); v.Type() {
 				case cty.String:
-					args = append(args, v.AsString())
+					in.Args = append(in.Args, v.AsString())
 				case cty.Number:
 					f, _ := v.AsBigFloat().Float64()
-					args = append(args, f)
+					in.Args = append(in.Args, f)
 				case cty.Bool:
-					args = append(args, v.True())
+					in.Args = append(in.Args, v.True())
 				default:
-					return cty.NilVal, fmt.Errorf(`attribute "args" must contain primitive types, got: %s`, v.Type())
+					return cty.NilVal, s.errorf(`attribute "args" must be a list of strings, numbers or booleans, got: %s`, v.Type())
 				}
 			}
 		}
+		delete(attrs, "args")
+	}
+	if len(attrs) > 0 {
+		return cty.NilVal, s.errorf("unexpected attributes: %v", attrs)
 	}
-	var values []cty.Value
-	c, err := sqlclient.Open(context.Background(), u)
+	c, err := sqlclient.Open(context.Background(), in.URL)
 	if err != nil {
 		return cty.NilVal, s.errorf("opening connection: %w", err)
 	}
 	defer c.Close()
-	rows, err := c.QueryContext(context.Background(), query, args...)
+	rows, err := c.QueryContext(context.Background(), in.Query, in.Args...)
 	if err != nil {
 		return cty.NilVal, s.errorf("executing query: %w", err)
 	}
@@ -338,21 +343,6 @@ func (s *sqlsrc) exec() (cty.Value, error) {
 	return cty.ObjectVal(obj), nil
 }
 
-func (s *sqlsrc) stringAttr(attrs hcl.Attributes, name string) (string, error) {
-	at, ok := attrs[name]
-	if !ok {
-		return "", s.errorf("missing %q attribute", name)
-	}
-	u, diags := at.Expr.Value(s.ctx)
-	if diags.HasErrors() {
-		return "", s.errorf("evaluating %q attribute: %w", name, diags)
-	}
-	if u.Type() != cty.String {
-		return "", s.errorf("attribute %q must be a string, got: %s", name, u.Type())
-	}
-	return u.AsString(), nil
-}
-
 func (s *sqlsrc) errorf(format string, args ...any) error {
 	return fmt.Errorf("data.sql.%s: %w", s.block.Labels[1], fmt.Errorf(format, args...))
 }
diff --git a/cmd/atlas/internal/cmdapi/schema.go b/cmd/atlas/internal/cmdapi/schema.go
index 6aa59b88..3b7534fe 100644
--- a/cmd/atlas/internal/cmdapi/schema.go
+++ b/cmd/atlas/internal/cmdapi/schema.go
@@ -149,7 +149,7 @@ func schemaApplyRun(cmd *cobra.Command, _ []string, flags schemaApplyFlags) erro
 	to, err := stateReader(ctx, &stateReaderConfig{
 		urls:    flags.toURLs,
 		dev:     dev,
-		norm:    client,
+		client:  client,
 		schemas: flags.schemas,
 		exclude: flags.exclude,
 		vars:    GlobalFlags.Vars,
diff --git a/doc/md/atlas-schema/projects.md b/doc/md/atlas-schema/projects.md
index 1f9bb750..0a09adf1 100644
--- a/doc/md/atlas-schema/projects.md
+++ b/doc/md/atlas-schema/projects.md
@@ -225,6 +225,7 @@ set or map item. See the example [below](#multi-environment-example).
 
 - `migration` - A block defines the migration configuration of the env.
   - `dir` - The [URL](../concepts/url.mdx) to the migration directory.
+  - `baseline` - An optional version to start the migration history from. Read more [here](../versioned/apply.mdx#existing-databases).
   - `revisions_schema` - An optional name to control the schema that the revisions table resides in.
 
 - `log` - A block defines the logging configuration of the env per command.
diff --git a/doc/md/guides/guides.md b/doc/md/guides/guides.md
index 54f162f4..1ed41556 100644
--- a/doc/md/guides/guides.md
+++ b/doc/md/guides/guides.md
@@ -16,6 +16,8 @@ Welcome to the Atlas Guides.
 
 [Managing MySQL schemas on RDS with Terraform](mysql/terraform.md)
 
+[Dropping non-virtual columns (DS103)](mysql/ds-103.md)
+
 ### PostgreSQL
 
 [Serial Type Columns](postgres/serial-columns.md)
@@ -24,9 +26,15 @@ Welcome to the Atlas Guides.
 
 [Included Columns](postgres/included-columns.md)
 
+### SQLite
+
+[Partial Indexes](sqlite/partial-indexes.md)
+
 ## Migration tools
 
-[`golang-migrate`](migration-tools/golang-migrate.md)
+Automatic migration planning for [`golang-migrate`](migration-tools/golang-migrate.md)
+
+Importing from [`pressly/goose`](migration-tools/goose-import.md)
 
 ## ORMs
 
diff --git a/doc/md/guides/migration-tools/goose-import.md b/doc/md/guides/migration-tools/goose-import.md
new file mode 100644
index 00000000..d25422c0
--- /dev/null
+++ b/doc/md/guides/migration-tools/goose-import.md
@@ -0,0 +1,115 @@
+---
+id: goose-import
+title: Importing a Goose project to Atlas
+slug: /guides/migration-tools/goose-import
+---
+
+## TL;DR
+* [`pressly/goose`](https://github.com/pressly/goose) is a popular database migration
+  CLI tool and Go library that's widely used in the Go community.
+* [Atlas](https://atlasgo.io) is an open-source tool for inspecting, planning, linting and
+  executing schema changes to your database.
+* Developers using `goose` that want to start managing their database schema using Atlas
+  can follow this guide to import an existing project to Atlas. 
+
+## Prerequisites
+
+* [Install Goose](https://github.com/pressly/goose#install)
+* An existing project with a `goose` migrations directory.
+* Docker
+* Atlas ([installation guide](https://atlasgo.io/getting-started/#installation))
+
+## Convert the migration files
+
+The first step in importing a project to Atlas is to convert the existing
+migration files from the Goose [SQL format](https://github.com/pressly/goose#sql-migrations)
+to the Atlas format. 
+
+To automate this process Atlas supports the `atlas migrate import` command. To read 
+more about this command, [read the docs](/versioned/import).
+
+Suppose your migrations are located in a directory named `goose` and you would like to
+convert them and store them in a new directory named `atlas`. For this example, let's 
+assume we have a simple Goose project with only two files:
+```text
+.
+├── 20221027094633_init.sql
+└── 20221027094642_new.sql
+```
+
+Run:
+
+```text
+atlas migrate import --from file://goose?format=goose --to file://atlas
+```
+
+Observe that a new directory named `atlas` was created with 3 files:
+```text
+.
+├── 20221027094633_init.sql
+├── 20221027094642_new.sql
+└── atlas.sum
+```
+
+A few things to note about the new directory:
+* When converting the migration files, Atlas ignores the `down` migrations as those are not
+  supported by Atlas.
+* Atlas created a file named `atlas.sum`. To ensure migration history is correct while multiple developers work on the same project
+  in parallel, Atlas enforces [migration directory integrity](/concepts/migration-directory-integrity) using
+  this file.
+* Comments not directly preceding a SQL statement are dropped as well.
+
+## Set the baseline on the target database
+
+Like many other database schema management tools, Atlas uses a metadata table
+on the target database to keep track of which migrations were already applied.
+In the case where we start using Atlas on an existing database, we must somehow
+inform Atlas that all migrations up to a certain version were already applied.
+
+To illustrate this, let's try to run Atlas's `migrate apply` command on a database
+that is currently managed by Goose using the migration directory that we just
+converted:
+
+```text
+atlas migrate apply --dir file://atlas --url mysql://root:pass@localhost:3306/dev
+```
+Atlas returns an error:
+```text
+Error: sql/migrate: connected database is not clean: found table "atlas_schema_revisions" in schema "dev". baseline version or allow-dirty is required
+```
+To fix this, we use the `--baseline` flag to tell Atlas that the database is already at
+a certain version:
+
+```text
+atlas migrate apply --dir file://atlas --url mysql://root:pass@localhost:3306/dev  --baseline 20221027094642
+```
+
+Atlas reports that there's nothing new to run:
+
+```text
+No migration files to execute
+```
+
+That's better! Next, let's verify that Atlas is aware of what migrations 
+were already applied by using the `migrate status` command:
+
+```text
+atlas migrate status --dir file://atlas --url mysql://root:pass@localhost:3306/dev
+```
+Atlas reports:
+```text
+Migration Status: OK
+  -- Current Version: 20221027094642
+  -- Next Version:    Already at latest version
+  -- Executed Files:  1
+  -- Pending Files:   0
+```
+Great! We have successfully imported our existing Goose project to Atlas.
+
+## Wrapping up
+
+In this guide we have demonstrated how to take an existing project that is
+managed by [`pressly/goose`](https://github.com/pressly/goose), a popular
+schema migration tool to be managed by Atlas. 
+
+Have questions? Feedback? Find our team [on our Discord server](https://discord.gg/zZ6sWVg6NT).
diff --git a/doc/md/guides/mysql/ds-103.md b/doc/md/guides/mysql/ds-103.md
new file mode 100644
index 00000000..1a7bef7b
--- /dev/null
+++ b/doc/md/guides/mysql/ds-103.md
@@ -0,0 +1,136 @@
+---
+id: mysql-ds-103
+title: Preventing accidental column deletion on MySQL
+slug: /guides/mysql/checks/DS103
+tags: [linting, ci, destructive-change, mysql, drop-column]
+---
+
+## Introduction
+
+This document describes in detail the detection of dropping non-virtual
+columns for MySQL databases by Atlas's linting capabilities.
+
+MySQL allows users to modify existing table structures using the 
+[`ALTER TABLE`](https://dev.mysql.com/doc/refman/8.0/en/alter-table.html)
+statement. Using this statement, users can drop columns that are
+no longer needed. For example:
+
+```text
+ALTER TABLE t DROP COLUMN c;
+```
+
+While this is sometimes desired, deleting a column from an active database
+can have various forms of negative impact on an application which we describe below.
+
+To prevent damage to production databases, many teams employ a policy that
+prohibits dropping columns (or database resources altogether).  Atlas's 
+[migrate lint](/versioned/lint) can help teams automatically detect migrations
+that drop columns during continuous integration. For example:
+
+```text
+atlas migrate lint --dev-url mysql://root:pass@localhost:3306/dev --latest 1
+```
+
+May detect:
+
+```text
+20221023123017.sql: destructive changes detected:
+
+	L1: Dropping non-virtual column "c2"
+```
+
+## Impact of dropping columns
+
+### Data loss
+
+Destructive changes are changes made to a database schema that result in loss of data.
+For instance, consider a statement such as:
+
+```sql
+ALTER TABLE `users` DROP COLUMN `email_address`;
+```
+
+This statement is considered destructive because whatever data is stored 
+in the email_address column will be deleted from disk, with no way to recover it.
+If this happens accidentally during a schema change, the business
+impact of this change may be dire. 
+
+Contrary to many other types of changes, dropping a column is not a reversible
+operation. In other words, the "reverse" statement of the one above is
+not a symmetric operation. Consider this statement:
+
+```sql
+ALTER TABLE `users` ADD COLUMN `email_address` varchar(255);
+```
+
+Running this query may "roll-back" the column drop operation, but any
+data stored in this column will be lost.
+
+### Backwards Incompatibility
+
+In the past, it was acceptable in many situations to stop an application
+for short maintenance windows during which database administrators could
+apply changes to the database schema. After the changes were applied, a new
+version of the application, depending on the new schema, would be deployed. 
+
+Today, most applications that serve traffic via the internet are expected 
+to be available 24/7, making such maintenance windows unacceptable. Dropping
+a column while running an application that relies on it will cause
+the database to return errors. This type of change is also known as a 
+backwards-incompatible change. 
+
+#### ERROR 1054 (42S22): Unknown column
+
+Suppose we dropped a column named "name". A running application may rely
+on this column to be present and try to read or write records including it:
+
+```sql
+insert into  t (id, name) values (1,2);
+--- OR
+select name from t;
+```
+MySQL will return:
+```text
+ERROR 1054 (42S22): Unknown column 'name' in 'field list'
+```
+
+## Prevention
+
+Preventing accidental column deletion is easy with Atlas's [`migrate lint`](/versioned/lint)
+command. With the `atlas migrate lint` command, users can analyze the migration directory to 
+detect potentially dangerous changes to the database schema. This command may be 
+incorporated in continuous integration pipelines to enable teams to enforce 
+desired policies with regard to schema changes.
+
+When using migrate lint to analyze migrations, users must supply multiple parameters:
+
+* `--dev-url` - a URL to a Dev-database that will be used to simulate the changes and verify their correctness.
+* `--dir` - the URL of the migration directory, by default it is file://migrations, e.g a directory named migrations in the current working directory.
+
+### Changeset detection
+
+When we run the lint command, we need to instruct Atlas on how to decide what set of migration files to analyze. 
+Currently, two modes are supported:
+
+* `--git-base <branchName>`: which selects the diff between the provided branch and the current one as the changeset.
+* `--latest <n>` which selects the latest n migration files as the changeset.
+
+### Examples
+
+Analyze all changes relative to the `master` Git branch:
+
+```text
+atlas migrate lint \
+  --dir "file://my/project/migrations" \
+  --dev-url "mysql://root:pass@localhost:3306/dev" \
+  --git-base "master"
+```
+
+Analyze the latest 2 migration files:
+
+```text
+atlas migrate lint \
+  --dir "file://my/project/migrations" \
+  --dev-url "mysql://root:pass@localhost:3306/dev" \
+  --latest 2
+```
\ No newline at end of file
diff --git a/doc/md/guides/postgres/partial-indexes.md b/doc/md/guides/postgres/partial-indexes.md
index fa570e8c..38c58ca7 100644
--- a/doc/md/guides/postgres/partial-indexes.md
+++ b/doc/md/guides/postgres/partial-indexes.md
@@ -25,7 +25,7 @@ You can learn more about partial indexes in PostgreSQL [here](https://www.postgr
 In cases where we know ahead of time the access pattern to a table and can reduce the size of an index by making it partial:
 1. Response time for SELECT operations is improved because the database searches through a smaller index.
 2. On average, response time for UPDATE operations is also improved as the index is not going to get updated in all cases.
-3. Index is smaller in size and can fit into RAM more easily. s
+3. Index is smaller in size and can fit into RAM more easily.
 4. Less space is required to store the index on disk.
 
 #### Basic PostgreSQL syntax for using Partial Index
diff --git a/doc/md/guides/sqlite/partial-indexes.md b/doc/md/guides/sqlite/partial-indexes.md
new file mode 100644
index 00000000..8b3ca8d7
--- /dev/null
+++ b/doc/md/guides/sqlite/partial-indexes.md
@@ -0,0 +1,260 @@
+---
+id: partial-indexes
+title: Partial Indexes in SQLite
+slug: /guides/sqlite/partial-indexes
+---
+
+### Overview of Partial Indexes
+
+#### What are Partial Indexes?
+
+With SQLite, users may create _partial indexes_, which are types of indexes that exist on a subset of a table, rather than the entire table itself. If used correctly, partial indexes improve performance and reduce costs, all while minimizing the amount of storage space they take up on the disk.
+
+#### Why do we need them?
+
+Let's demonstrate a case where partial indexes may be useful by contrasting them with a non-partial index. ​​If you have many records in an indexed table, the number of records the index needs to track also grows. If the index grows in size, the disk space needed to store the index itself increases as well.
+In many tables, different records are not accessed with uniform frequency. A subset of a table's records might not be searched very frequently or not searched at all. Records take up precious space in your index whether they are queried or not, and are updated when a new entry is added to the field.
+
+Partial indexes come into the picture to filter unsearched values and give you, as an engineer, a tool to index only what's important.
+
+#### Advantages of using Partial Indexes
+1. Partial indexses have index entries only for a defined subset of rows, compared to ordinary indexes which have exactly one index entry for every row in the table.
+2. When used wisely, partial indexes result in smaller database files with improved query and write performance.
+
+#### Basic SQLite syntax for using Partial Indexes
+
+```sql
+CREATE INDEX 
+    index_name
+ON 
+    table_name(column_list)
+WHERE 
+    expression;
+```
+
+#### Example of Non-partial Indexes vs Partial Indexes in SQLite
+
+Let's see this in action by creating a table with the following command:
+
+```sql
+CREATE TABLE vaccination_data (
+  id INTEGER NOT NULL,
+  country varchar(100) default NULL,
+  title TEXT default NULL,
+  names varchar(255) default NULL,
+  vaccinated varchar(255) default NULL,
+  PRIMARY KEY (id)
+);
+```
+
+Here is how a portion of the table might look like after inserting values:
+
+```sql
+SELECT * FROM vaccination_data;
+```
+```console title="Output"
++----+--------------------+-------+----------------+------------+
+| id |      country       | title |     names      | vaccinated |
++----+--------------------+-------+----------------+------------+
+| 1  | Poland             | Er.   | Travis Freeman | No         |
+| 2  | Australia          | Mr.   | Hu Dodson      | No         |
+| 3  | Vietnam            | Ms.   | Amery Herman   | No         |
+| 4  | Peru               | Mr.   | Brynne Mann    | Yes        |
+| 5  | Chile              | Er.   | Nora Mitchell  | No         |
+| 6  | Brazil             | Er.   | Tanner Oneal   | No         |
+| 7  | Vietnam            | Mr.   | Ora Conway     | Yes        |
+| 8  | United Kingdom     | Er.   | Quinn Waters   | No         |
+| 9  | Russian Federation | Er.   | Xyla Holloway  | No         |
+| 10 | Norway             | Mr.   | Macy Sullivan  | No         |
+.
+.
+.
+| 576000 | Ukraine   | Dr.   | Kuame Gay         | Yes        |
++--------+-----------+-------+-------------------+------------+
+```
+
+:::info
+You can also beautify tables in SQLite like shown above, by using the command `.mode table`
+:::
+
+In the following example, suppose we want a list of doctors from India that have taken the vaccine. If we want to use a non-partial index, we can create it on the "vaccinated" column with the following command:
+
+```sql
+CREATE INDEX 
+    vaccinated_idx 
+ON 
+    vaccination_data(vaccinated);
+```
+
+Now, let's check the size of the index that we created, with the following command:
+
+```sql
+SELECT NAME, sum(pgsize) AS size FROM dbstat GROUP BY NAME ORDER BY size DESC;
+```
+```console title="Output"
++------------------+----------+
+|       name       |   size   |
++------------------+----------+
+| vaccination_data | 22224896 |
+| vaccinated_idx   | 6492160  |
+| sqlite_schema    | 4096     |
++------------------+----------+
+```
+Notice that the total size of our index vaccinated_idx is 6492160 bytes (~6 MB).
+
+:::info
+The DBSTAT virtual table is a read-only eponymous virtual table that returns information about the amount of disk space used to store the content of an SQLite database. To know more about DBSTAT, visit the official documentation page [here](https://www.sqlite.org/dbstat.html)
+:::
+
+Now, suppose we want to accelerate the same query using the partial index. Let's begin by dropping the existing index that we created earlier:
+
+```sql
+DROP INDEX vaccinated_idx;
+```
+
+In the following command, we will create an index with a `WHERE` clause that precisely describes the list of doctors from India that have taken the vaccine.
+
+```sql
+CREATE INDEX 
+    vaccinated_idx
+ON 
+    vaccination_data(vaccinated)
+WHERE 
+    vaccinated = 'Yes' AND country = 'India' AND title = 'Dr';
+```
+Let’s verify if the index we created is being used in the query with a `WHERE` clause by running the following command:
+
+```sql
+EXPLAIN QUERY PLAN 
+SELECT
+        *
+FROM    
+        vaccination_data
+WHERE
+        vaccinated = 'Yes' AND country = 'India' AND title = 'Dr';
+```
+
+```console title="Output"
+QUERY PLAN
+`--SEARCH vaccination_data USING INDEX vaccinated_idx
+```
+
+We confirmed that the index vaccinated_idx is being used while running the query above. Let's check the size of the index that we created again, with the following command:
+
+```sql
+SELECT NAME, sum(pgsize) AS size FROM dbstat GROUP BY NAME ORDER BY size DESC;
+```
+```console title="Output"
++------------------+----------+
+|       name       |   size   |
++------------------+----------+
+| vaccination_data | 22224896 |
+| vaccinated_idx   | 4096     |
+| sqlite_schema    | 4096     |
++------------------+----------+
+```
+(Note: The results will vary, ​​depending on the data that is stored in the database)
+
+Notice that the total size of our index vaccinated_idx is just 4096 bytes. In our example, the index size for the partial index took significantly less space (4 KB) compared to the non-partial index that we created earlier on the 'vaccinated' column (~6 MB).
+
+:::info
+Learn more about partial indexes in SQLite from official documentation [here](https://www.sqlite.org/partialindex.html)
+:::
+
+We have seen that creating a partial index is a better choice where only a small subset of the values stored in the database are accessed frequently. Now, let's see how we can easily manage partial indexes using Atlas.
+
+### Managing Partial Indexes is easy with Atlas
+
+Managing partial indexes and database schemas in SQLite can be confusing and error-prone. Atlas is an open-source project which allows us to manage our database using a simple and easy-to-understand declarative syntax (similar to Terraform). We will now learn how to manage partial indexes using Atlas.
+
+:::info
+If you are just getting started, install the latest version of Atlas using the guide to [setting up Atlas](https://atlasgo.io/cli/getting-started/setting-up).
+:::
+
+#### Managing Partial Indexes in Atlas
+
+We will first use the `atlas schema inspect` command to get an HCL representation of the table which we created earlier by using the Atlas CLI:
+
+```console
+atlas schema inspect -u "sqlite://vaccination_data.db" > schema.hcl
+```
+```schema title="schema.hcl"
+table "vaccination_data" {
+  schema = schema.main
+  column "id" {
+    null = false
+    type = integer
+  }
+  column "country" {
+    null    = true
+    type    = varchar(100)
+  }
+  column "title" {
+    null    = true
+    type    = text
+  }
+  column "names" {
+    null    = true
+    type    = varchar(255)
+  }
+  column "vaccinated" {
+    null    = true
+    type    = varchar(255)
+  }
+  primary_key {
+    columns = [column.id]
+  }
+}
+schema "main" {
+}
+```
+
+Now, lets add the following index definition to the file:
+
+```schema
+  index "vaccinated_idx" {
+    columns = [column.vaccinated]
+    where   = "(vaccinated = 'Yes' AND country = 'India' AND title = 'Dr.')"
+  }
+```
+
+Save the changes in the schema.hcl file and apply the changes on the database by using the following command:
+
+```console
+atlas schema apply -u "sqlite://vaccination_data.db" -f schema.hcl
+```
+
+Atlas generates the necessary SQL statements to add the new partial index to the database schema. Press Enter while the `Apply` option is highlighted to apply the changes:
+
+```console
+-- Planned Changes:
+-- Create index "vaccinated_idx" to table: "vaccination_data"
+CREATE INDEX `vaccinated_idx` ON `vaccination_data` (`vaccinated`) WHERE (vaccinated = 'Yes' AND country = 'India' AND title = 'Dr.')
+✔ Apply
+  Abort
+```
+
+To verify that our new index was created, open the database command line tool and run:
+
+```sql
+.index
+```
+```console title="Output"
+vaccinated_idx
+```
+
+Amazing! Our new partial index is now created!
+
+### Limitation of using Partial Indexes
+
+Partial indexes are useful in cases where we know ahead of time that a table is most frequently queried with a certain WHERE clause. As applications evolve, access patterns to the database also change. Consequently, we may find ourselves in a situation where our index no longer covers many queries, causing them to become resource-consuming and slow.
+
+### Conclusion
+
+In this section, we learned about SQLite partial indexes and how we can easily create partial indexes in our database by using Atlas.
+
+## Need More Help?​
+
+[Join the Ariga Discord Server](https://discord.com/invite/zZ6sWVg6NT) for early access to features and the ability to provide exclusive feedback that improves your Database Management Tooling. 
+
+[Sign up](https://www.getrevue.co/profile/ariga) to our newsletter to stay up to date about [Atlas](http://atlasgo.io), our OSS database schema management tool, and our cloud platform [Ariga Cloud](http://Ariga.cloud).
\ No newline at end of file
diff --git a/doc/md/reference.md b/doc/md/reference.md
index f83a8316..4c300904 100644
--- a/doc/md/reference.md
+++ b/doc/md/reference.md
@@ -287,6 +287,7 @@ atlas migrate status [flags]
 #### Flags
 ```
   -u, --url string                [driver://username:password@address/dbname?param=value] select a resource using the URL format
+      --log string                go template to use to format logs
       --dir string                select migration directory using URL format (default "file://migrations")
       --dir-format string         select migration file format (default "atlas")
       --revisions-schema string   name of the schema the revisions table resides in
diff --git a/doc/website/blog/2022-10-27-multi-tenant-support.mdx b/doc/website/blog/2022-10-27-multi-tenant-support.mdx
new file mode 100644
index 00000000..bd7b62d4
--- /dev/null
+++ b/doc/website/blog/2022-10-27-multi-tenant-support.mdx
@@ -0,0 +1,392 @@
+---
+title: Migrate Multi-Tenant Environments With Atlas
+authors: a8m
+tags: [atlas, migrations, versioned, multi-tenant]
+---
+
+import Tabs from '@theme/Tabs';
+import TabItem from '@theme/TabItem';
+
+Wikipedia defines [Multi-tenancy](https://en.wikipedia.org/wiki/Multitenancy) as:
+> a software architecture in which a single instance of software runs on a server
+> and serves multiple tenants.
+
+In recent years, multitenancy has become a common topic in our industry as many organizations provide service
+to multiple customers using the same infrastructure. Multitenancy usually becomes an issue in software
+architecture because tenants often expect a decent level of isolation from one another.
+
+In this post, I will go over different known approaches for achieving multi-tenancy
+and discuss the approach we took to build [Ariga's cloud platform](https://ariga.io).
+In addition, I will demonstrate how we added built-in support for multi-tenant
+environments in Atlas to overcome some of the challenges we faced.
+
+
+### Introduction
+
+Throughout the last few years, I have had the opportunity to implement multi-tenancy in various ways. Some of them
+might be familiar to you:
+
+1. A separate environment (deployment) per tenant, where isolation is achieved at both compute and data layers.
+2. A schema (named database) per tenant, where there is one environment for compute (e.g., a K8S cluster), but tenants
+are stored in different databases or schemas. Isolation is achieved at the data layer while compute resources are shared.
+3. One environment for all tenants, including the data layer. Typically, in this case, each table holds a `tenant_id`
+column that is used to filter statements by the tenant. Both data and compute layers are shared, with isolation achieved
+at the logical, database query level.
+
+Each approach has pros and cons, but I want to briefly list the main reasons we chose to build
+our cloud platform based on the second option: schema per tenant.
+
+1. **Management**: Easily delete, backup tenants, and allow them to export their data without affecting others.
+2. **Isolation**: Limit credentials, connection pooling, and quotas per tenant. This way, one tenant cannot cause the
+database to choke and interrupt other tenants in case they share the same physical database.
+3. **Security and data privacy**: In case it is required, some tenants can be physically separated from others.
+For example, data can be stored in the tenant's AWS account, and the application can connect to it using a secure
+connection, like VPC peering in AWS.
+4. **Code-maintenance**: Most of the application code is written in a way that it is unaware of the multi-tenancy. In our case,
+there is one layer "at the top" that attaches the tenant connection to the context, and the API layer (e.g., GraphQL
+resolver) extracts the connection from the context to read/write data. As a result, we are not concerned that API
+changes will cross tenant boundaries.
+5. **Migration**: Schema changes can be executed first on _"test tenants"_ and _fail-fast__ in case of error.
+
+The primary con to this approach was that there was no elegant way to execute migrations on multiple databases
+(_N_ times) in Atlas. In the rest of the post, I'll cover how we solved this problem in Ariga and added built-in
+support for multi-tenancy in Atlas.
+
+### Atlas config file
+
+Atlas provides a convenient way to describe and interact with multiple environments using project files. A project file
+is a file named `atlas.hcl` and contains one or more `env` blocks. For example:
+
+```hcl title="atlas.hcl"
+env "local" {
+  url = "mysql://root:pass@:3306/"
+  migrations {
+    dir = "file://migrations"
+  }
+}
+
+env "prod" {
+  // ... a different env
+}
+```
+
+Once defined, a project's environment can be worked against using the `--env` flag. For example:
+
+```bash
+atlas schema apply --env local
+```
+
+The command above runs the `schema apply` against the database that is defined in the `local` environment.
+
+### Multi-Tenant environments
+
+The Atlas configuration language provides a few capabilities adopted from Terraform to facilitate the definition of
+multi-tenant environments. The first is the `for_each` meta-argument that allows defining a single `env` block that
+is expanded to _N_ instances, one for each tenant. For example:
+
+```hcl title="atlas.hcl"
+variable "url" {
+  type    = string
+  default = "mysql://root:pass@:3306/"
+}
+
+variable "tenants" {
+  type = list(string)
+}
+
+env "local" {
+  for_each = toset(var.tenants)
+  url      = urlsetpath(var.url, each.value)
+  migration {
+    dir = "file://migrations"
+  }
+}
+```
+
+The above configuration expects a list of tenants to be provided as a variable. This can be useful when the list of
+tenants is dynamic and can be injected into the Atlas command. The `urlsetpath` function is a helper function that sets
+the path of the database URL to the tenant name. For example, if `url` is set to `mysql://root:pass@:3306/?param=value`
+and the tenant name is `tenant1`, the resulting URL will be `mysql://root:pass@:3306/tenant1?param=value`.
+
+The second capability is [_Data Sources_](/atlas-schema/projects#data-sources). This option enables users to retrieve
+information stored in an external service or database. For the sake of this example, let's extend the configuration
+above to use the SQL data source to retrieve the list of tenants from the _INFORMATION_SCHEMA_ in MySQL:
+
+```hcl title="atlas.hcl"
+// The URL of the database we operate on.
+variable "url" {
+  type    = string
+  default = "mysql://root:pass@:3306/"
+}
+
+// Schemas that match this pattern will be considered tenants.
+variable "pattern" {
+  type    = string
+  default = "tenant_%"
+}
+
+data "sql" "tenants" {
+  url   = var.url
+  query = <<EOS
+SELECT `schema_name`
+  FROM `information_schema`.`schemata`
+  WHERE `schema_name` LIKE ?
+EOS
+  args = [var.pattern]
+}
+
+env "local" {
+  for_each = toset(data.sql.tenants.values)
+  url      = urlsetpath(var.url, each.value)
+}
+```
+
+### Example
+
+Let's demonstrate how managing migrations in a multi-tenant architecture is made simple with Atlas.
+
+1\. Install Atlas:
+<Tabs
+defaultValue="apple-intel-brew"
+values={[
+{label: 'macOS (Homebrew)', value: 'apple-intel-brew'},
+{label: 'macOS (Plain)', value: 'apple-intel-plain'},
+{label: 'Linux', value: 'linux'},
+{label: 'Windows', value: 'windows'},
+]}>
+<TabItem value="apple-intel-brew">
+
+Get the latest release with [Homebrew](https://brew.sh/):
+
+```shell
+brew install ariga/tap/atlas
+```
+
+</TabItem>
+<TabItem value="apple-intel-plain">
+
+Download latest release.
+```shell
+curl -LO https://release.ariga.io/atlas/atlas-darwin-amd64-latest
+```
+
+Make the atlas binary executable.
+```shell
+chmod +x ./atlas-darwin-amd64-latest
+```
+
+Move the atlas binary to a file location on your system PATH.
+```shell
+sudo mv ./atlas-darwin-amd64-latest /usr/local/bin/atlas
+```
+```shell
+sudo chown root: /usr/local/bin/atlas
+```
+
+</TabItem>
+<TabItem value="linux">
+
+Download latest release.
+```shell
+curl -LO https://release.ariga.io/atlas/atlas-linux-amd64-latest
+```
+
+Move the atlas binary to a file location on your system PATH.
+```shell
+sudo install -o root -g root -m 0755 ./atlas-linux-amd64-latest /usr/local/bin/atlas
+```
+
+</TabItem>
+<TabItem value="windows">
+
+Download the [latest release](https://release.ariga.io/atlas/atlas-windows-amd64-latest.exe) and
+move the atlas binary to a file location on your system PATH.
+
+
+</TabItem>
+</Tabs>
+
+2\. Create a migration directory with the following example content:
+
+<Tabs
+defaultValue="migration_file1"
+values={[
+{label: '20220811074144_create_users.sql', value: 'migration_file1'},
+{label: '20220811074314_add_users_name.sql', value: 'migration_file2'},
+{label: 'atlas.sum', value: 'sum_file'},
+]}>
+<TabItem value="migration_file1">
+
+```sql
+-- create "users" table
+CREATE TABLE `users` (`id` int NOT NULL) CHARSET utf8mb4 COLLATE utf8mb4_0900_ai_ci;
+```
+
+</TabItem>
+<TabItem value="migration_file2">
+
+```sql
+-- modify "users" table
+ALTER TABLE `users` ADD COLUMN `name` varchar(255) NOT NULL;
+```
+
+</TabItem>
+<TabItem value="sum_file">
+
+```text
+h1:w2ODzVxhTKdBVBdzqntHw7rHV8lKQF98TmNevOEZfIo=
+20220811074144_create_users_table.sql h1:KnMSZM/E4TBGidYCZ+UHxkHEWaRWeyuPIUjSHRybQqA=
+20220811074314_add_users_name.sql h1:jUpaANgD0SjI5DjaHuJxtHZ6Wq98act0MmE5oZ+NRU0=
+```
+
+</TabItem>
+</Tabs>
+
+3\. Create two example tenants on a local database:
+
+```sql
+create database tenant_a8m;
+create database tenant_rotemtam;
+```
+
+4\. Run Atlas to execute the migration scripts on the tenants' databases:
+
+```
+atlas migrate apply --env local
+```
+
+```text title="tenant_a8m"
+Migrating to version 20220811074314 (2 migrations in total):
+
+  -- migrating version 20220811074144
+    -> CREATE TABLE `users` (`id` int NOT NULL) CHARSET utf8mb4 COLLATE utf8mb4_0900_ai_ci;
+  -- ok (36.803179ms)
+
+  -- migrating version 20220811074314
+    -> ALTER TABLE `users` ADD COLUMN `name` varchar(255) NOT NULL;
+  -- ok (26.184177ms)
+
+  -------------------------
+  -- 72.899146ms
+  -- 2 migrations
+  -- 2 sql statements
+```
+```text title="tenant_rotemtam"
+Migrating to version 20220811074314 (2 migrations in total):
+
+  -- migrating version 20220811074144
+    -> CREATE TABLE `users` (`id` int NOT NULL) CHARSET utf8mb4 COLLATE utf8mb4_0900_ai_ci;
+  -- ok (61.987153ms)
+
+  -- migrating version 20220811074314
+    -> ALTER TABLE `users` ADD COLUMN `name` varchar(255) NOT NULL;
+  -- ok (24.656515ms)
+
+  -------------------------
+  -- 95.233384ms
+  -- 2 migrations
+  -- 2 sql statements
+```
+
+Running the command again will not execute any migrations:
+
+```shell
+No migration files to execute
+No migration files to execute
+```
+
+### Migration logging
+
+At Ariga, our services print structured logs (JSON) to feed our observability tools. That is why we felt obligated to
+add support for custom log formatting in Atlas. To continue the example from above, we present how we configure Atlas to
+emit JSON lines with the tenant name attached to them.
+
+1\. Add the `log` configuration to the `local` environment block:
+
+```hcl title="atlas.hcl" {4-15}
+env "local" {
+  for_each = toset(data.sql.tenants.values)
+  url      = urlsetpath(var.url, each.value)
+  // Emit JSON logs to stdout and add the
+  // tenant name to each log line.
+  log {
+    migrate {
+      apply = format(
+        "{{ json . | json_merge %q }}",
+        jsonencode({
+          Tenant : each.value
+        })
+      )
+    }
+  }
+}
+```
+
+2\. Create a new script file in the migration directory:
+
+<Tabs
+defaultValue="migration_file1"
+values={[
+{label: '20220811074144_create_users.sql', value: 'migration_file1'},
+{label: '20220811074314_add_users_name.sql', value: 'migration_file2'},
+{label: '20221027125605_create_pets.sql', value: 'migration_file3'},
+{label: 'atlas.sum', value: 'sum_file'},
+]}>
+<TabItem value="migration_file1">
+
+```sql
+-- create "users" table
+CREATE TABLE `users` (`id` int NOT NULL) CHARSET utf8mb4 COLLATE utf8mb4_0900_ai_ci;
+```
+
+</TabItem>
+<TabItem value="migration_file2">
+
+```sql
+-- modify "users" table
+ALTER TABLE `users` ADD COLUMN `name` varchar(255) NOT NULL;
+```
+
+</TabItem>
+<TabItem value="migration_file3">
+
+```sql
+-- create "pets" table
+CREATE TABLE `pets` (`id` bigint, PRIMARY KEY (`id`));
+```
+
+</TabItem>
+<TabItem value="sum_file">
+
+```text
+h1:coAZ5kxfT6EpZLjsMRWZv+lYAuChLTnhOnRjAVbZOao=
+20220811074144_create_users.sql h1:INH5s7+VEs2pO4+YqL1XNvJUPnF5abhReSEz75L6fz0=
+20220811074314_add_users_name.sql h1:EHo+0gM1Cpfl1pe0Cid35qr9mWUhQ9FSXSr+pHUwQwk=
+20221027125605_create_pets.sql h1:glWEZaQjERCv1N5evUDz154XvJ84ivlfORrQp/pL7yY=
+```
+
+</TabItem>
+</Tabs>
+
+3\. Run `migrate apply` in our "local" environment:
+
+```shell
+atlas migrate apply --env local
+```
+```json
+{"Applied":[{"Applied":["CREATE TABLE `pets` (`id` bigint, PRIMARY KEY (`id`));"],"Description":"create_pets","End":"2022-10-27T16:03:03.685899+03:00","Name":"20221027125605_create_pets.sql","Start":"2022-10-27T16:03:03.655879+03:00","Version":"20221027125605"}],"Current":"20220811074314","Dir":"migrations","Driver":"mysql","End":"2022-10-27T16:03:03.685899+03:00","Pending":[{"Description":"create_pets","Name":"20221027125605_create_pets.sql","Version":"20221027125605"}],"Start":"2022-10-27T16:03:03.647091+03:00","Target":"20221027125605","Tenant":"tenant_a8m","URL":{"ForceQuery":false,"Fragment":"","Host":":3308","OmitHost":false,"Opaque":"","Path":"/tenant_a8m","RawFragment":"","RawPath":"","RawQuery":"parseTime=true","Schema":"tenant_a8m","Scheme":"mysql","User":{}}}
+{"Applied":[{"Applied":["CREATE TABLE `pets` (`id` bigint, PRIMARY KEY (`id`));"],"Description":"create_pets","End":"2022-10-27T16:03:03.787476+03:00","Name":"20221027125605_create_pets.sql","Start":"2022-10-27T16:03:03.757463+03:00","Version":"20221027125605"}],"Current":"20220811074314","Dir":"migrations","Driver":"mysql","End":"2022-10-27T16:03:03.787476+03:00","Pending":[{"Description":"create_pets","Name":"20221027125605_create_pets.sql","Version":"20221027125605"}],"Start":"2022-10-27T16:03:03.748399+03:00","Target":"20221027125605","Tenant":"tenant_rotemtam","URL":{"ForceQuery":false,"Fragment":"","Host":":3308","OmitHost":false,"Opaque":"","Path":"/tenant_rotemtam","RawFragment":"","RawPath":"","RawQuery":"parseTime=true","Schema":"tenant_rotemtam","Scheme":"mysql","User":{}}}
+```
+
+### Next steps
+
+Currently, Atlas uses a _fail-fast_ policy, which means the process exits on the first tenant that returns an error.
+We built it this way because we find it helpful to execute migrations first on _"test tenants"_ and stop in case the
+operation fails on any of them. However, this means the execution is serial and may be slow in cases where there is a
+large amount of tenants. Therefore, we aim to add more advanced approaches that will allow executing the first _M_
+tenants serially and the rest of the _N-M_ tenants in parallel.
+
+Have questions? Feedback? Feel free to reach out [on our Discord server](https://discord.gg/zZ6sWVg6NT).
+
+
+
diff --git a/doc/website/sidebars.js b/doc/website/sidebars.js
index 64ff64bb..4f83b3b5 100644
--- a/doc/website/sidebars.js
+++ b/doc/website/sidebars.js
@@ -111,6 +111,11 @@ module.exports = {
                     id: 'guides/mysql/terraform-mysql-rds',
                     label: 'Managing with Terraform'
                 },
+                {
+                    type: 'doc',
+                    id: 'guides/mysql/mysql-ds-103',
+                    label: 'Column Drop (DS103)'
+                },
             ],
         },
         {
@@ -135,6 +140,18 @@ module.exports = {
                 },
             ],
         },
+        {
+            type: 'category',
+            label: 'SQLite',
+            collapsed: false,
+            items: [
+                {
+                    type: 'doc',
+                    id: 'guides/sqlite/partial-indexes',
+                    label: 'Partial Indexes'
+                },
+            ],
+        },
         {
             type: 'category',
             label: 'Migration tools',
@@ -144,6 +161,11 @@ module.exports = {
                     type: 'doc',
                     id:'guides/migration-tools/golang-migrate',
                     label: 'golang-migrate'
+                },
+                {
+                    type: 'doc',
+                    id:'guides/migration-tools/goose-import',
+                    label: 'Importing from goose'
                 }
             ]
         },
diff --git a/internal/integration/go.mod b/internal/integration/go.mod
index f3920d26..7078545b 100644
--- a/internal/integration/go.mod
+++ b/internal/integration/go.mod
@@ -7,7 +7,7 @@ replace ariga.io/atlas => ../../
 replace ariga.io/atlas/cmd/atlas => ../../cmd/atlas
 
 require (
-	ariga.io/atlas v0.7.3-0.20221010133632-5e5aca152e8a
+	ariga.io/atlas v0.8.1
 	entgo.io/ent v0.11.1
 	github.com/go-sql-driver/mysql v1.6.0
 	github.com/hashicorp/hcl/v2 v2.13.0
@@ -20,7 +20,7 @@ require (
 )
 
 require (
-	ariga.io/atlas/cmd/atlas v0.7.3-0.20221013064003-c2e1121fc4e1 // indirect
+	ariga.io/atlas/cmd/atlas v0.8.1 // indirect
 	github.com/agext/levenshtein v1.2.3 // indirect
 	github.com/antlr/antlr4/runtime/Go/antlr v0.0.0-20220816024939-bc8df83d7b9d // indirect
 	github.com/apparentlymart/go-textseg/v13 v13.0.0 // indirect
diff --git a/schemahcl/context.go b/schemahcl/context.go
index 5ef2da3b..663cec15 100644
--- a/schemahcl/context.go
+++ b/schemahcl/context.go
@@ -19,9 +19,10 @@ import (
 
 // blockVar is an HCL resource that defines an input variable to the Atlas DDL document.
 type blockVar struct {
-	Name    string    `hcl:",label"`
-	Type    cty.Value `hcl:"type"`
-	Default cty.Value `hcl:"default,optional"`
+	Name        string    `hcl:",label"`
+	Type        cty.Value `hcl:"type"`
+	Default     cty.Value `hcl:"default,optional"`
+	Description string    `hcl:"description,optional"`
 }
 
 // setInputVals sets the input values into the evaluation context. HCL documents can define
@@ -234,7 +235,7 @@ func blockVars(blocks hclsyntax.Blocks, parentAddr string, defs *blockDef) (map[
 			// Fill missing attributes with zero values.
 			for n := range def.fields {
 				if _, ok := attrs[n]; !ok {
-					attrs[n] = cty.NullVal(ctySchemaLit)
+					attrs[n] = cty.NullVal(ctyNilType)
 				}
 			}
 			self := addr(parentAddr, name, blkName, qualifier)
@@ -308,51 +309,27 @@ func attrMap(attrs hclsyntax.Attributes) map[string]cty.Value {
 		if diag.HasErrors() {
 			continue
 		}
-		literalValue, err := extractValue(value)
-		if err != nil {
-			continue
-		}
-		out[v.Name] = cty.CapsuleVal(ctySchemaLit, literalValue)
+		out[v.Name] = value
 	}
 	return out
 }
 
-// ctySchemaLit is a cty.Capsule type the encapsulates a schemahcl.LiteralValue.
 var (
-	ctySchemaLit = cty.CapsuleWithOps("lit", reflect.TypeOf(LiteralValue{}), &cty.CapsuleOps{
-		// ConversionFrom facilitates reading the encapsulated type as a string, as is needed, for example,
-		// when interpolating it in a string expression.
-		ConversionFrom: func(src cty.Type) func(any, cty.Path) (cty.Value, error) {
-			if src != cty.String {
-				return nil
-			}
-			return func(i any, path cty.Path) (cty.Value, error) {
-				lit, ok := i.(*LiteralValue)
-				if !ok {
-					return cty.Value{}, fmt.Errorf("schemahcl: expected *schemahcl.LiteralValue got %T", i)
-				}
-				uq, err := strconv.Unquote(lit.V)
-				if err != nil {
-					return cty.StringVal(lit.V), nil
-				}
-				return cty.StringVal(uq), nil
-			}
-		},
-	})
 	ctyNilType  = cty.Capsule("type", reflect.TypeOf(cty.NilType))
 	ctyTypeSpec = cty.Capsule("type", reflect.TypeOf(Type{}))
-	ctyRawExpr  = cty.Capsule("raw_expr", reflect.TypeOf(RawExpr{}))
+	ctyRefType  = cty.Capsule("ref", reflect.TypeOf(Ref{}))
+	ctyRawExpr  = cty.Capsule("raw", reflect.TypeOf(RawExpr{}))
 )
 
 // Built-in blocks.
 const (
-	varBlock     = "variable"
-	dataBlock    = "data"
-	localsBlock  = "locals"
-	varRef       = "var"
-	localRef     = "local"
-	dynamicBlock = "dynamic"
-	forEachAttr  = "for_each"
+	varBlock    = "variable"
+	dataBlock   = "data"
+	localsBlock = "locals"
+	forEachAttr = "for_each"
+	eachRef     = "each"
+	varRef      = "var"
+	localRef    = "local"
 )
 
 // defRegistry returns a tree of blockDef structs representing the schema of the
@@ -400,7 +377,7 @@ func (t *blockDef) child(c *blockDef) {
 func (t *blockDef) asCty() cty.Type {
 	f := make(map[string]cty.Type)
 	for attr := range t.fields {
-		f[attr] = ctySchemaLit
+		f[attr] = ctyNilType
 	}
 	f["__ref"] = cty.String
 	for _, c := range t.children {
diff --git a/schemahcl/extension.go b/schemahcl/extension.go
index 04b9acb6..4b71c361 100644
--- a/schemahcl/extension.go
+++ b/schemahcl/extension.go
@@ -8,9 +8,11 @@ import (
 	"errors"
 	"fmt"
 	"reflect"
-	"strconv"
 	"strings"
 	"sync"
+
+	"github.com/zclconf/go-cty/cty"
+	"github.com/zclconf/go-cty/cty/gocty"
 )
 
 // Remainer is the interface that is implemented by types that can store
@@ -318,12 +320,14 @@ func setField(field reflect.Value, attr *Attr) error {
 	case reflect.Interface:
 		field.Set(reflect.ValueOf(attr.V))
 	default:
-		return fmt.Errorf("schemahcl: unsupported field kind %q", field.Kind())
+		if err := gocty.FromCtyValue(attr.V, field.Addr().Interface()); err != nil {
+			return fmt.Errorf("schemahcl: failed setting field %q of type %T: %w", attr.K, field, err)
+		}
 	}
 	return nil
 }
 
-func setPtr(field reflect.Value, val Value) error {
+func setPtr(field reflect.Value, val cty.Value) error {
 	rt := reflect.TypeOf(val)
 	if field.Type() == rt {
 		field.Set(reflect.ValueOf(val))
@@ -331,7 +335,7 @@ func setPtr(field reflect.Value, val Value) error {
 	}
 	// If we are setting a Type field handle RawExpr and Ref specifically.
 	if _, ok := field.Interface().(*Type); ok {
-		switch t := val.(type) {
+		switch t := val.EncapsulatedValue().(type) {
 		case *RawExpr:
 			field.Set(reflect.ValueOf(&Type{T: t.X}))
 			return nil
@@ -347,32 +351,21 @@ func setPtr(field reflect.Value, val Value) error {
 		field.Set(reflect.New(field.Type().Elem()))
 	}
 	switch e := field.Interface().(type) {
-	case *bool:
-		b, err := BoolVal(val)
-		if err != nil {
-			return err
-		}
-		*e = b
-	case *string:
-		s, err := StrVal(val)
-		if err != nil {
-			return err
-		}
-		*e = s
-	case *LiteralValue:
-		s, err := StrVal(val)
-		if err != nil {
-			return err
-		}
-		e.V = s
 	case *Ref:
-		s, err := StrVal(val)
-		if err != nil {
-			return err
+		switch {
+		case val.Type() == cty.String:
+			e.V = val.AsString()
+		case val.Type().IsCapsuleType():
+			ref, ok := val.EncapsulatedValue().(*Ref)
+			if !ok {
+				return fmt.Errorf("schemahcl: expected value to be a *Ref, got: %T", val.EncapsulatedValue())
+			}
+			e.V = ref.V
 		}
-		e.V = s
 	default:
-		return fmt.Errorf("unhandled pointer type %T", val)
+		if err := gocty.FromCtyValue(val, e); err != nil {
+			return fmt.Errorf("converting cty.Value to %T: %w", e, err)
+		}
 	}
 	return nil
 }
@@ -380,18 +373,16 @@ func setPtr(field reflect.Value, val Value) error {
 // setSliceAttr sets the value of attr to the slice field. This function expects both the target field
 // and the source attr to be slices.
 func setSliceAttr(field reflect.Value, attr *Attr) error {
-	lst, ok := attr.V.(*ListValue)
-	if !ok {
-		return fmt.Errorf("schemahcl: field is of type slice but attr %q does not contain a ListValue", attr.K)
+	if !attr.V.Type().IsListType() && !attr.V.Type().IsTupleType() {
+		return fmt.Errorf("schemahcl: field is of type slice but attr %q is type: %s", attr.K, attr.V.Type().FriendlyName())
 	}
 	typ := field.Type().Elem()
-
-	slc := reflect.MakeSlice(reflect.SliceOf(typ), 0, len(lst.V))
+	slc := reflect.MakeSlice(reflect.SliceOf(typ), 0, attr.V.LengthInt())
 	switch typ.Kind() {
 	case reflect.String:
 		s, err := attr.Strings()
 		if err != nil {
-			return fmt.Errorf("cannot read attribute %q as string list: %w", attr.K, err)
+			return fmt.Errorf("cannot read attribute %q of type %q as string list: %w", attr.K, attr.V.Type().FriendlyName(), err)
 		}
 		for _, item := range s {
 			slc = reflect.Append(slc, reflect.ValueOf(item))
@@ -408,8 +399,15 @@ func setSliceAttr(field reflect.Value, attr *Attr) error {
 		if typ != reflect.TypeOf(&Ref{}) {
 			return fmt.Errorf("only pointers to refs supported, got %s", typ)
 		}
-		for _, c := range lst.V {
-			slc = reflect.Append(slc, reflect.ValueOf(c))
+		for _, v := range attr.V.AsValueSlice() {
+			switch {
+			case v.Type().IsCapsuleType():
+				slc = reflect.Append(slc, reflect.ValueOf(v.EncapsulatedValue().(*Ref)))
+			case isRef(v):
+				slc = reflect.Append(slc, reflect.ValueOf(&Ref{V: v.GetAttr("__ref").AsString()}))
+			default:
+				return fmt.Errorf("schemahcl: unsupported type %s in slice", v.Type().FriendlyName())
+			}
 		}
 	default:
 		return fmt.Errorf("slice of unsupported kind: %q", typ.Kind())
@@ -488,90 +486,61 @@ func (r *Resource) Scan(ext any) error {
 func scanPtr(key string, r *Resource, field reflect.Value) error {
 	attr := &Attr{K: key}
 	switch e := field.Interface().(type) {
-	case *LiteralValue:
-		attr.V = e
 	case *Ref:
-		attr.V = e
+		attr.V = cty.CapsuleVal(ctyRefType, e)
 	case *Type:
-		attr.V = e
-	case *bool:
-		attr.V = &LiteralValue{V: strconv.FormatBool(*e)}
-	case *string:
-		attr.V = &LiteralValue{V: strconv.Quote(*e)}
+		attr.V = cty.CapsuleVal(ctyTypeSpec, e)
 	default:
-		return fmt.Errorf("schemahcl: unsupported pointer to %s", e)
+		t, err := gocty.ImpliedType(e)
+		if err != nil {
+			return fmt.Errorf("schemahcl: cannot infer type for field %q when scaning pointer: %w", key, err)
+		}
+		attr.V, err = gocty.ToCtyValue(e, t)
+		if err != nil {
+			return fmt.Errorf("schemahcl: cannot convert value for field %q: %w", key, err)
+		}
 	}
 	r.SetAttr(attr)
 	return nil
 }
 
 func scanAttr(key string, r *Resource, field reflect.Value) error {
-	var lit string
-	switch field.Kind() {
-	case reflect.Slice:
-		return scanSliceAttr(key, r, field)
-	case reflect.String:
-		lit = strconv.Quote(field.String())
-	case reflect.Int:
-		lit = fmt.Sprintf("%d", field.Int())
-	case reflect.Bool:
-		lit = strconv.FormatBool(field.Bool())
-	case reflect.Interface:
+	switch k := field.Kind(); {
+	case k == reflect.Interface:
 		if field.IsNil() {
-			return nil
+			break
 		}
 		i := field.Interface()
-		v, ok := i.(Value)
+		v, ok := i.(cty.Value)
 		if !ok {
 			return fmt.Errorf("schemahcl: unsupported interface type %T for field %q", i, key)
 		}
-		r.SetAttr(&Attr{
-			K: key,
-			V: v,
-		})
-		return nil
-	default:
-		return fmt.Errorf("schemahcl: unsupported field kind %q", field.Kind())
-	}
-	r.SetAttr(&Attr{
-		K: key,
-		V: &LiteralValue{V: lit},
-	})
-	return nil
-}
-
-// scanSliceAttr sets an Attr named "key" into the Resource r, by converting
-// the value stored in "field" into a *ListValue.
-func scanSliceAttr(key string, r *Resource, field reflect.Value) error {
-	typ := field.Type()
-	lst := &ListValue{}
-
-	switch typ.Elem().Kind() {
-	case reflect.String:
-		for i := 0; i < field.Len(); i++ {
-			item := field.Index(i).Interface().(string)
-			lst.V = append(lst.V, &LiteralValue{V: strconv.Quote(item)})
+		r.SetAttr(&Attr{K: key, V: v})
+	case field.Type() == reflect.TypeOf([]*Ref{}):
+		if field.Len() > 0 {
+			r.SetAttr(RefsAttr(key, field.Interface().([]*Ref)...))
 		}
-	case reflect.Bool:
-		for i := 0; i < field.Len(); i++ {
-			item := field.Index(i).Interface().(bool)
-			lst.V = append(lst.V, &LiteralValue{V: strconv.FormatBool(item)})
+	case k == reflect.Int, k == reflect.Int64:
+		r.SetAttr(Int64Attr(key, field.Int()))
+	case k == reflect.Struct:
+		if v, ok := field.Interface().(cty.Value); ok && v.IsNull() {
+			break
 		}
-	case reflect.Ptr:
-		if typ.Elem() != reflect.TypeOf(&Ref{}) {
-			return fmt.Errorf("schemahcl: currently on ref slice values supported, got %s", typ)
+		fallthrough
+	default:
+		t, err := gocty.ImpliedType(field.Interface())
+		if err != nil {
+			return fmt.Errorf("schemahcl: cannot infer type for field %q when scanning attribute: %w", key, err)
 		}
-		for i := 0; i < field.Len(); i++ {
-			item := field.Index(i).Interface().(*Ref)
-			lst.V = append(lst.V, item)
+		v, err := gocty.ToCtyValue(field.Interface(), t)
+		if err != nil {
+			return fmt.Errorf("schemahcl: cannot convert value for field %q: %w", key, err)
 		}
-	default:
-		return fmt.Errorf("unsupported kind %q for %q", typ.Kind(), key)
+		r.SetAttr(&Attr{
+			K: key,
+			V: v,
+		})
 	}
-	r.SetAttr(&Attr{
-		K: key,
-		V: lst,
-	})
 	return nil
 }
 
diff --git a/schemahcl/hcl.go b/schemahcl/hcl.go
index fe66c029..1bb1c72c 100644
--- a/schemahcl/hcl.go
+++ b/schemahcl/hcl.go
@@ -6,15 +6,13 @@ package schemahcl
 
 import (
 	"bytes"
-	"errors"
 	"fmt"
+	"reflect"
 	"sort"
 	"strconv"
 	"strings"
 
 	"github.com/hashicorp/hcl/v2"
-	"github.com/hashicorp/hcl/v2/ext/dynblock"
-	"github.com/hashicorp/hcl/v2/hcldec"
 	"github.com/hashicorp/hcl/v2/hclparse"
 	"github.com/hashicorp/hcl/v2/hclsyntax"
 	"github.com/hashicorp/hcl/v2/hclwrite"
@@ -78,7 +76,6 @@ func (s *State) Eval(parsed *hclparse.Parser, v any, input map[string]cty.Value)
 	files := parsed.Files()
 	fileNames := make([]string, 0, len(files))
 	allBlocks := make([]*hclsyntax.Block, 0, len(files))
-	// Prepare reg and allBlocks.
 	for name, file := range files {
 		fileNames = append(fileNames, name)
 		if err := s.setInputVals(ctx, file.Body, input); err != nil {
@@ -88,14 +85,26 @@ func (s *State) Eval(parsed *hclparse.Parser, v any, input map[string]cty.Value)
 		if err := s.evalReferences(ctx, body); err != nil {
 			return err
 		}
-		for _, blk := range body.Blocks {
-			// Variable blocks are available in the HCL
-			// source but not reachable by reference.
-			if blk.Type != varBlock {
-				allBlocks = append(allBlocks, blk)
-				reg.child(extractDef(blk, reg))
+		blocks := make(hclsyntax.Blocks, 0, len(body.Blocks))
+		for _, b := range body.Blocks {
+			switch {
+			// Variable blocks are not reachable by reference.
+			case b.Type == varBlock:
+				continue
+			// Semi-evaluate blocks with the for_each meta argument.
+			case b.Body != nil && b.Body.Attributes[forEachAttr] != nil:
+				nb, err := forEachBlocks(ctx, b)
+				if err != nil {
+					return err
+				}
+				blocks = append(blocks, nb...)
+			default:
+				blocks = append(blocks, b)
 			}
+			reg.child(extractDef(b, reg))
 		}
+		body.Blocks = blocks
+		allBlocks = append(allBlocks, blocks...)
 	}
 	vars, err := blockVars(allBlocks, "", reg)
 	if err != nil {
@@ -151,14 +160,16 @@ func patchRefs(spec *Resource) error {
 func (r addrRef) patch(resource *Resource) error {
 	cp := r.copy().load(resource, "")
 	for _, attr := range resource.Attrs {
-		if ref, ok := attr.V.(*Ref); ok {
-			referenced, ok := cp[ref.V]
-			if !ok {
-				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)
-			}
+		if !attr.IsRef() {
+			continue
+		}
+		ref := attr.V.EncapsulatedValue().(*Ref)
+		referenced, ok := cp[ref.V]
+		if !ok {
+			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 {
@@ -225,33 +236,11 @@ func (s *State) resource(ctx *hcl.EvalContext, file *hcl.File) (*Resource, error
 		if err != nil {
 			return nil, err
 		}
-		switch {
-		case blk.Body != nil && blk.Body.Attributes[forEachAttr] != nil:
-			forEach, diags := blk.Body.Attributes[forEachAttr].Expr.Value(ctx)
-			if diags.HasErrors() {
-				return nil, diags
-			}
-			if t := forEach.Type(); !t.IsSetType() && !t.IsObjectType() {
-				return nil, fmt.Errorf("schemahcl: for_each does not support %s type", t.FriendlyName())
-			}
-			delete(blk.Body.Attributes, forEachAttr)
-			// Iterate over the set and create a resource for each element.
-			for it := forEach.ElementIterator(); it.Next(); {
-				k, v := it.Element()
-				ctx.Variables["each"] = cty.ObjectVal(map[string]cty.Value{"key": k, "value": v})
-				resource, err := s.toResource(ctx, blk, []string{blk.Type})
-				if err != nil {
-					return nil, err
-				}
-				res.Children = append(res.Children, resource)
-			}
-		default:
-			resource, err := s.toResource(ctx, blk, []string{blk.Type})
-			if err != nil {
-				return nil, err
-			}
-			res.Children = append(res.Children, resource)
+		resource, err := s.toResource(ctx, blk, []string{blk.Type})
+		if err != nil {
+			return nil, err
 		}
+		res.Children = append(res.Children, resource)
 	}
 	return res, nil
 }
@@ -277,21 +266,21 @@ func (s *State) toAttrs(ctx *hcl.EvalContext, hclAttrs hclsyntax.Attributes, sco
 		if diag.HasErrors() {
 			return nil, s.typeError(diag)
 		}
-		var err error
-		switch {
+		switch t := value.Type(); {
 		case isRef(value):
-			at.V = &Ref{V: value.GetAttr("__ref").AsString()}
-		case value.Type() == ctyRawExpr:
-			at.V = value.EncapsulatedValue().(*RawExpr)
-		case value.Type() == ctyTypeSpec:
-			at.V = value.EncapsulatedValue().(*Type)
-		case value.Type().IsTupleType():
-			at.V, err = extractListValue(value)
+			at.V = cty.CapsuleVal(ctyRefType, &Ref{V: value.GetAttr("__ref").AsString()})
+		case (t.IsTupleType() || t.IsListType() || t.IsSetType()) && value.LengthInt() > 0:
+			values := make([]cty.Value, 0, value.LengthInt())
+			for it := value.ElementIterator(); it.Next(); {
+				_, v := it.Element()
+				if isRef(v) {
+					v = cty.CapsuleVal(ctyRefType, &Ref{V: v.GetAttr("__ref").AsString()})
+				}
+				values = append(values, v)
+			}
+			at.V = cty.ListVal(values)
 		default:
-			at.V, err = extractValue(value)
-		}
-		if err != nil {
-			return nil, err
+			at.V = value
 		}
 		attrs = append(attrs, at)
 	}
@@ -329,53 +318,6 @@ func isRef(v cty.Value) bool {
 	return v.Type().IsObjectType() && v.Type().HasAttribute("__ref")
 }
 
-func extractListValue(value cty.Value) (*ListValue, error) {
-	lst := &ListValue{}
-	it := value.ElementIterator()
-	for it.Next() {
-		_, v := it.Element()
-		if isRef(v) {
-			lst.V = append(lst.V, &Ref{V: v.GetAttr("__ref").AsString()})
-			continue
-		}
-		litv, err := extractValue(v)
-		if err != nil {
-			return nil, err
-		}
-		lst.V = append(lst.V, litv)
-	}
-	return lst, nil
-}
-
-func extractValue(value cty.Value) (Value, error) {
-	switch value.Type() {
-	case ctySchemaLit:
-		return value.EncapsulatedValue().(*LiteralValue), nil
-	case cty.String:
-		return &LiteralValue{V: strconv.Quote(value.AsString())}, nil
-	case cty.Number:
-		bf := value.AsBigFloat()
-		num, _ := bf.Float64()
-		return &LiteralValue{V: strconv.FormatFloat(num, 'f', -1, 64)}, nil
-	case cty.Bool:
-		return &LiteralValue{V: strconv.FormatBool(value.True())}, nil
-	case cty.List(cty.String), cty.List(cty.Number), cty.List(cty.Bool), cty.List(cty.NilType),
-		cty.Set(cty.String), cty.Set(cty.Number), cty.Set(cty.Bool), cty.Set(cty.NilType):
-		l := &ListValue{V: make([]Value, 0, value.LengthInt())}
-		for it := value.ElementIterator(); it.Next(); {
-			_, v := it.Element()
-			ev, err := extractValue(v)
-			if err != nil {
-				return nil, err
-			}
-			l.V = append(l.V, ev)
-		}
-		return l, nil
-	default:
-		return nil, fmt.Errorf("schemahcl: unsupported type %q", value.Type().GoString())
-	}
-}
-
 func (s *State) toResource(ctx *hcl.EvalContext, block *hclsyntax.Block, scope []string) (*Resource, error) {
 	spec := &Resource{
 		Type: block.Type,
@@ -397,37 +339,11 @@ func (s *State) toResource(ctx *hcl.EvalContext, block *hclsyntax.Block, scope [
 	}
 	spec.Attrs = attrs
 	for _, blk := range block.Body.Blocks {
-		switch blk.Type {
-		case dynamicBlock:
-			dec, err := dynamicSpec(blk)
-			if err != nil {
-				return nil, err
-			}
-			v, _, diag := hcldec.PartialDecode(dynblock.Expand(block.Body, ctx), dec, ctx)
-			if diag.HasErrors() {
-				return nil, diag
-			}
-			for _, e := range v.GetAttr(blk.Labels[0]).AsValueSlice() {
-				r := &Resource{Type: blk.Labels[0]}
-				for k, v := range e.AsValueMap() {
-					av, err := extractValue(v)
-					if err != nil {
-						return nil, err
-					}
-					r.Attrs = append(r.Attrs, &Attr{
-						K: k,
-						V: av,
-					})
-				}
-				spec.Children = append(spec.Children, r)
-			}
-		default:
-			r, err := s.toResource(ctx, blk, append(scope, blk.Type))
-			if err != nil {
-				return nil, err
-			}
-			spec.Children = append(spec.Children, r)
+		r, err := s.toResource(ctx, blk, append(scope, blk.Type))
+		if err != nil {
+			return nil, err
 		}
+		spec.Children = append(spec.Children, r)
 	}
 	return spec, nil
 }
@@ -485,71 +401,66 @@ func labels(r *Resource) []string {
 }
 
 func (s *State) writeAttr(attr *Attr, body *hclwrite.Body) error {
-	attr = normalizeLiterals(attr)
-	switch v := attr.V.(type) {
-	case *Ref:
-		body.SetAttributeRaw(attr.K, hclRefTokens(v.V))
-	case *Type:
-		if v.IsRef {
-			body.SetAttributeRaw(attr.K, hclRefTokens(v.T))
+	switch {
+	case attr.IsRef():
+		v, err := attr.Ref()
+		if err != nil {
+			return err
+		}
+		body.SetAttributeRaw(attr.K, hclRefTokens(v))
+	case attr.IsType():
+		t, err := attr.Type()
+		if err != nil {
+			return err
+		}
+		if t.IsRef {
+			body.SetAttributeRaw(attr.K, hclRefTokens(t.T))
 			break
 		}
-		spec, ok := s.findTypeSpec(v.T)
+		spec, ok := s.findTypeSpec(t.T)
 		if !ok {
-			v := fmt.Sprintf("sql(%q)", v.T)
+			v := fmt.Sprintf("sql(%q)", t.T)
 			body.SetAttributeRaw(attr.K, hclRawTokens(v))
 			break
 		}
-		st, err := hclType(spec, v)
+		st, err := hclType(spec, t)
 		if err != nil {
 			return err
 		}
 		body.SetAttributeRaw(attr.K, hclRawTokens(st))
-	case *LiteralValue:
-		body.SetAttributeRaw(attr.K, hclRawTokens(v.V))
-	case *RawExpr:
+	case attr.IsRawExpr():
+		v, err := attr.RawExpr()
+		if err != nil {
+			return err
+		}
 		// TODO(rotemtam): the func name should be decided on contextual basis.
 		fnc := fmt.Sprintf("sql(%q)", v.X)
 		body.SetAttributeRaw(attr.K, hclRawTokens(fnc))
-	case *ListValue:
+	case attr.V.Type().IsListType():
 		// Skip scanning nil slices ([]T(nil)) by default. Users that
 		// want to print empty lists, should use make([]T, 0) instead.
-		if v.V == nil {
+		if attr.V.LengthInt() == 0 {
 			return nil
 		}
-		lst := make([]hclwrite.Tokens, 0, len(v.V))
-		for _, item := range v.V {
-			switch v := item.(type) {
-			case *Ref:
-				lst = append(lst, hclRefTokens(v.V))
-			case *LiteralValue:
-				lst = append(lst, hclRawTokens(v.V))
-			default:
-				return fmt.Errorf("cannot write elem type %T of attr %q to HCL list", v, attr)
+		tokens := make([]hclwrite.Tokens, 0, attr.V.LengthInt())
+		for _, v := range attr.V.AsValueSlice() {
+			if v.Type().IsCapsuleType() {
+				ref, ok := v.EncapsulatedValue().(*Ref)
+				if !ok {
+					return fmt.Errorf("unsupported capsule type: %v", v.Type())
+				}
+				tokens = append(tokens, hclRefTokens(ref.V))
+			} else {
+				tokens = append(tokens, hclwrite.TokensForValue(v))
 			}
 		}
-		body.SetAttributeRaw(attr.K, hclList(lst))
+		body.SetAttributeRaw(attr.K, hclList(tokens))
 	default:
-		return fmt.Errorf("schemacl: unknown literal type %T", v)
+		body.SetAttributeValue(attr.K, attr.V)
 	}
 	return nil
 }
 
-// normalizeLiterals transforms attributes with LiteralValue that cannot be
-// written as correct HCL into RawExpr.
-func normalizeLiterals(attr *Attr) *Attr {
-	lv, ok := attr.V.(*LiteralValue)
-	if !ok {
-		return attr
-	}
-	exp := "x = " + lv.V
-	p := hclparse.NewParser()
-	if _, diag := p.ParseHCL([]byte(exp), ""); diag != nil {
-		return &Attr{K: attr.K, V: &RawExpr{X: lv.V}}
-	}
-	return attr
-}
-
 func (s *State) findTypeSpec(t string) (*TypeSpec, bool) {
 	for _, v := range s.config.types {
 		if v.T == t {
@@ -572,18 +483,7 @@ func hclType(spec *TypeSpec, typ *Type) (string, error) {
 		if !ok {
 			continue
 		}
-		switch val := arg.V.(type) {
-		case *LiteralValue:
-			args = append(args, val.V)
-		case *ListValue:
-			for _, li := range val.V {
-				lit, ok := li.(*LiteralValue)
-				if !ok {
-					return "", errors.New("expecting literal value")
-				}
-				args = append(args, lit.V)
-			}
-		}
+		args = append(args, valueArgs(param, arg.V)...)
 	}
 	// If no args were chosen and the type can be described without a function.
 	if len(args) == 0 && len(typeFuncReqArgs(spec)) == 0 {
@@ -592,6 +492,28 @@ func hclType(spec *TypeSpec, typ *Type) (string, error) {
 	return fmt.Sprintf("%s(%s)", spec.Name, strings.Join(args, ",")), nil
 }
 
+func valueArgs(spec *TypeAttr, v cty.Value) []string {
+	switch {
+	case v.Type().IsListType(), v.Type().IsTupleType(), v.Type().IsSetType(), v.Type().IsCollectionType():
+		args := make([]string, 0, v.LengthInt())
+		for _, v := range v.AsValueSlice() {
+			args = append(args, valueArgs(spec, v)...)
+		}
+		return args
+	case v.Type() == cty.String:
+		return []string{strconv.Quote(v.AsString())}
+	case v.Type() == cty.Number && spec.Kind == reflect.Int:
+		iv, _ := v.AsBigFloat().Int64()
+		return []string{strconv.FormatInt(iv, 10)}
+	case v.Type() == cty.Number:
+		fv, _ := v.AsBigFloat().Float64()
+		return []string{strconv.FormatFloat(fv, 'f', -1, 64)}
+	case v.Type() == cty.Bool:
+		return []string{strconv.FormatBool(v.True())}
+	}
+	return nil
+}
+
 func findAttr(attrs []*Attr, k string) (*Attr, bool) {
 	for _, attr := range attrs {
 		if attr.K == k {
@@ -661,32 +583,63 @@ func hclList(items []hclwrite.Tokens) hclwrite.Tokens {
 	return t
 }
 
-// Eval implements the Evaluator interface.
-func (f EvalFunc) Eval(p *hclparse.Parser, i any, input map[string]cty.Value) error {
-	return f(p, i, input)
+func forEachBlocks(ctx *hcl.EvalContext, b *hclsyntax.Block) ([]*hclsyntax.Block, error) {
+	forEach, diags := b.Body.Attributes[forEachAttr].Expr.Value(ctx)
+	if diags.HasErrors() {
+		return nil, diags
+	}
+	if t := forEach.Type(); !t.IsSetType() && !t.IsObjectType() {
+		return nil, fmt.Errorf("schemahcl: for_each does not support %s type", t.FriendlyName())
+	}
+	delete(b.Body.Attributes, forEachAttr)
+	blocks := make([]*hclsyntax.Block, 0, forEach.LengthInt())
+	for it := forEach.ElementIterator(); it.Next(); {
+		k, v := it.Element()
+		nctx := ctx.NewChild()
+		nctx.Variables = map[string]cty.Value{
+			eachRef: cty.ObjectVal(map[string]cty.Value{
+				"key":   k,
+				"value": v,
+			}),
+		}
+		nb, err := copyBlock(nctx, b)
+		if err != nil {
+			return nil, fmt.Errorf("schemahcl: evaluate block for value %q: %w", v, err)
+		}
+		blocks = append(blocks, nb)
+	}
+	return blocks, nil
 }
 
-func dynamicSpec(b *hclsyntax.Block) (hcldec.Spec, error) {
-	switch {
-	case len(b.Labels) != 1:
-		return nil, fmt.Errorf("unexpected number of labels for 'dynamic' block: %d", len(b.Labels))
-	case b.Body == nil || len(b.Body.Attributes) != 1 || b.Body.Attributes["for_each"] == nil:
-		return nil, fmt.Errorf("expect a single 'for_each' attribute on 'dynamic' block: %q", b.Labels[0])
-	case len(b.Body.Blocks) != 1 || b.Body.Blocks[0].Type != "content" || b.Body.Blocks[0].Body == nil:
-		return nil, fmt.Errorf("expect a non-empty 'content' block on 'dynamic' block: %q", b.Labels[0])
-	}
-	attrs := make(hcldec.ObjectSpec)
-	for name := range b.Body.Blocks[0].Body.Attributes {
-		attrs[name] = &hcldec.AttrSpec{
-			Name:     name,
-			Required: true,
-			Type:     cty.DynamicPseudoType,
-		}
-	}
-	return &hcldec.ObjectSpec{
-		b.Labels[0]: &hcldec.BlockListSpec{
-			TypeName: b.Labels[0],
-			Nested:   attrs,
+func copyBlock(ctx *hcl.EvalContext, b *hclsyntax.Block) (*hclsyntax.Block, error) {
+	nb := &hclsyntax.Block{
+		Type:   b.Type,
+		Labels: b.Labels,
+		Body: &hclsyntax.Body{
+			Attributes: make(map[string]*hclsyntax.Attribute),
+			Blocks:     make([]*hclsyntax.Block, 0, len(b.Body.Blocks)),
 		},
-	}, nil
+	}
+	for k, v := range b.Body.Attributes {
+		x, diags := v.Expr.Value(ctx)
+		if diags.HasErrors() {
+			return nil, diags
+		}
+		nv := *v
+		nv.Expr = &hclsyntax.LiteralValueExpr{Val: x}
+		nb.Body.Attributes[k] = &nv
+	}
+	for _, v := range b.Body.Blocks {
+		nv, err := copyBlock(ctx, v)
+		if err != nil {
+			return nil, err
+		}
+		nb.Body.Blocks = append(nb.Body.Blocks, nv)
+	}
+	return nb, nil
+}
+
+// Eval implements the Evaluator interface.
+func (f EvalFunc) Eval(p *hclparse.Parser, i any, input map[string]cty.Value) error {
+	return f(p, i, input)
 }
diff --git a/schemahcl/opts.go b/schemahcl/opts.go
index 42de4f0c..63fae765 100644
--- a/schemahcl/opts.go
+++ b/schemahcl/opts.go
@@ -188,28 +188,14 @@ func typeFuncSpecImpl(_ *function.Spec, typeSpec *TypeSpec) function.ImplFunc {
 		for _, attr := range typeFuncArgs(typeSpec) {
 			// If the attribute is a slice, read all remaining args into a list value.
 			if attr.Kind == reflect.Slice {
-				lst := &ListValue{}
-				for _, arg := range args {
-					v, err := extractValue(arg)
-					if err != nil {
-						return cty.NilVal, err
-					}
-					lst.V = append(lst.V, v)
-				}
-				t.Attrs = append(t.Attrs, &Attr{K: attr.Name, V: lst})
+				t.Attrs = append(t.Attrs, &Attr{K: attr.Name, V: cty.ListVal(args)})
 				break
 			}
 			if len(args) == 0 {
 				break
 			}
-			// Pop the first arg and add it as a literal to the type.
-			var arg cty.Value
-			arg, args = args[0], args[1:]
-			v, err := extractValue(arg)
-			if err != nil {
-				return cty.NilVal, err
-			}
-			t.Attrs = append(t.Attrs, &Attr{K: attr.Name, V: v})
+			t.Attrs = append(t.Attrs, &Attr{K: attr.Name, V: args[0]})
+			args = args[1:]
 		}
 		return cty.CapsuleVal(ctyTypeSpec, t), nil
 	}
diff --git a/schemahcl/spec.go b/schemahcl/spec.go
index f16f4df9..27afd651 100644
--- a/schemahcl/spec.go
+++ b/schemahcl/spec.go
@@ -6,10 +6,13 @@ package schemahcl
 
 import (
 	"fmt"
+	"math/big"
 	"reflect"
-	"strconv"
 
 	"ariga.io/atlas/sql/schema"
+
+	"github.com/zclconf/go-cty/cty"
+	"github.com/zclconf/go-cty/cty/gocty"
 )
 
 type (
@@ -25,27 +28,7 @@ type (
 	// Attr is an attribute of a Resource.
 	Attr struct {
 		K string
-		V Value
-	}
-
-	// Value represents the value of an Attr.
-	Value interface {
-		val()
-	}
-
-	// LiteralValue implements Value and represents a literal value (string, number, etc.)
-	LiteralValue struct {
-		V string
-	}
-
-	// RawExpr implements Value and represents any raw expression.
-	RawExpr struct {
-		X string
-	}
-
-	// ListValue implements Value and represents a list of Values.
-	ListValue struct {
-		V []Value
+		V cty.Value
 	}
 
 	// Ref implements Value and represents a reference to another Resource.
@@ -57,6 +40,11 @@ type (
 		V string
 	}
 
+	// RawExpr implements Value and represents any raw expression.
+	RawExpr struct {
+		X string
+	}
+
 	// TypeSpec represents a specification for defining a Type.
 	TypeSpec struct {
 		// Name is the identifier for the type in an Atlas DDL document.
@@ -103,6 +91,33 @@ type (
 	}
 )
 
+// IsRef indicates if the attribute is a reference type.
+func (a *Attr) IsRef() bool {
+	if !a.V.Type().IsCapsuleType() {
+		return false
+	}
+	_, ok := a.V.EncapsulatedValue().(*Ref)
+	return ok
+}
+
+// IsRawExpr indicates if the attribute is a RawExpr type.
+func (a *Attr) IsRawExpr() bool {
+	if !a.V.Type().IsCapsuleType() {
+		return false
+	}
+	_, ok := a.V.EncapsulatedValue().(*RawExpr)
+	return ok
+}
+
+// IsType indicates if the attribute is a type spec.
+func (a *Attr) IsType() bool {
+	if !a.V.Type().IsCapsuleType() {
+		return false
+	}
+	_, ok := a.V.EncapsulatedValue().(*Type)
+	return ok
+}
+
 // Int returns an int from the Value of the Attr. If The value is not a LiteralValue or the value
 // cannot be converted to an integer an error is returned.
 func (a *Attr) Int() (int, error) {
@@ -115,14 +130,9 @@ func (a *Attr) Int() (int, error) {
 
 // Int64 returns an int64 from the Value of the Attr. If The value is not a LiteralValue or the value
 // cannot be converted to an integer an error is returned.
-func (a *Attr) Int64() (int64, error) {
-	lit, ok := a.V.(*LiteralValue)
-	if !ok {
-		return 0, fmt.Errorf("schema: cannot read attribute %q as literal", a.K)
-	}
-	i, err := strconv.ParseInt(lit.V, 10, 64)
-	if err != nil {
-		return 0, fmt.Errorf("schema: cannot read attribute %q as integer: %w", a.K, err)
+func (a *Attr) Int64() (i int64, err error) {
+	if err = gocty.FromCtyValue(a.V, &i); err != nil {
+		return 0, err
 	}
 	return i, nil
 }
@@ -130,85 +140,97 @@ func (a *Attr) Int64() (int64, error) {
 // String returns a string from the Value of the Attr. If The value is not a LiteralValue
 // an error is returned.  String values are expected to be quoted. If the value is not
 // properly quoted an error is returned.
-func (a *Attr) String() (string, error) {
-	return StrVal(a.V)
+func (a *Attr) String() (s string, err error) {
+	if err = gocty.FromCtyValue(a.V, &s); err != nil {
+		return "", err
+	}
+	return s, nil
 }
 
 // Bool returns a boolean from the Value of the Attr. If The value is not a LiteralValue or the value
 // cannot be converted to a boolean an error is returned.
-func (a *Attr) Bool() (bool, error) {
-	lit, ok := a.V.(*LiteralValue)
-	if !ok {
-		return false, fmt.Errorf("schema: cannot read attribute %q as literal", a.K)
-	}
-	b, err := strconv.ParseBool(lit.V)
-	if err != nil {
-		return false, fmt.Errorf("schema: cannot read attribute %q as bool: %w", a.K, err)
+func (a *Attr) Bool() (b bool, err error) {
+	if err = gocty.FromCtyValue(a.V, &b); err != nil {
+		return false, err
 	}
 	return b, nil
 }
 
-// Ref returns the string representation of the Attr. If the value is not a Ref or the value
-// an error is returned.
+// Ref extracts the reference from the Value of the Attr.
 func (a *Attr) Ref() (string, error) {
-	ref, ok := a.V.(*Ref)
+	ref, ok := a.V.EncapsulatedValue().(*Ref)
 	if !ok {
 		return "", fmt.Errorf("schema: cannot read attribute %q as ref", a.K)
 	}
 	return ref.V, nil
 }
 
-// Refs returns a slice of references.
-func (a *Attr) Refs() ([]*Ref, error) {
-	l, ok := a.V.(*ListValue)
+// Type extracts the Type from the Attr.
+func (a *Attr) Type() (*Type, error) {
+	t, ok := a.V.EncapsulatedValue().(*Type)
 	if !ok {
-		return nil, fmt.Errorf("schema: attribute %q is not a list", a.K)
+		return nil, fmt.Errorf("schema: cannot read attribute %q as type", a.K)
+	}
+	return t, nil
+}
+
+// RawExpr extracts the RawExpr from the Attr.
+func (a *Attr) RawExpr() (*RawExpr, error) {
+	if !a.IsRawExpr() {
+		return nil, fmt.Errorf("schema: cannot read attribute %q as raw expression", a.K)
 	}
-	refs := make([]*Ref, 0, len(l.V))
-	for _, v := range l.V {
-		r, ok := v.(*Ref)
+	return a.V.EncapsulatedValue().(*RawExpr), nil
+}
+
+// Refs returns a slice of references.
+func (a *Attr) Refs() ([]*Ref, error) {
+	refs := make([]*Ref, 0, len(a.V.AsValueSlice()))
+	for _, v := range a.V.AsValueSlice() {
+		ref, ok := v.EncapsulatedValue().(*Ref)
 		if !ok {
-			return nil, fmt.Errorf("schemahcl: expected %T to be Ref", v)
+			return nil, fmt.Errorf("schema: cannot read attribute %q as ref", a.K)
 		}
-		refs = append(refs, r)
+		refs = append(refs, ref)
 	}
 	return refs, nil
 }
 
 // Strings returns a slice of strings from the Value of the Attr. If The value is not a ListValue or its
 // values cannot be converted to strings an error is returned.
-func (a *Attr) Strings() ([]string, error) {
-	l, ok := a.V.(*ListValue)
-	if !ok {
-		return nil, fmt.Errorf("schema: attribute %q is not a list", a.K)
-	}
-	out := make([]string, 0, len(l.V))
-	for _, item := range l.V {
-		sv, err := StrVal(item)
-		if err != nil {
-			return nil, fmt.Errorf("schemahcl: failed parsing item %q to string: %w", item, err)
+func (a *Attr) Strings() (vs []string, err error) {
+	if a.V.Type().IsTupleType() {
+		for _, v := range a.V.AsValueSlice() {
+			var s string
+			if err = gocty.FromCtyValue(v, &s); err != nil {
+				return nil, err
+			}
+			vs = append(vs, s)
 		}
-		out = append(out, sv)
+		return vs, nil
+	}
+	if err = gocty.FromCtyValue(a.V, &vs); err != nil {
+		return nil, err
 	}
-	return out, nil
+	return vs, nil
 }
 
 // Bools returns a slice of bools from the Value of the Attr. If The value is not a ListValue or its
 // values cannot be converted to bools an error is returned.
-func (a *Attr) Bools() ([]bool, error) {
-	lst, ok := a.V.(*ListValue)
-	if !ok {
-		return nil, fmt.Errorf("schemahcl: attribute %q is not a list", a.K)
-	}
-	out := make([]bool, 0, len(lst.V))
-	for _, item := range lst.V {
-		b, err := BoolVal(item)
-		if err != nil {
-			return nil, err
+func (a *Attr) Bools() (vs []bool, err error) {
+	if a.V.Type().IsTupleType() {
+		for _, v := range a.V.AsValueSlice() {
+			var b bool
+			if err = gocty.FromCtyValue(v, &b); err != nil {
+				return nil, err
+			}
+			vs = append(vs, b)
 		}
-		out = append(out, b)
+		return vs, nil
+	}
+	if err = gocty.FromCtyValue(a.V, &vs); err != nil {
+		return nil, err
 	}
-	return out, nil
+	return vs, nil
 }
 
 // Resource returns the first child Resource by its type and reports whether it was found.
@@ -262,32 +284,6 @@ func replaceOrAppendAttr(attrs []*Attr, attr *Attr) []*Attr {
 	return append(attrs, attr)
 }
 
-// StrVal returns the raw string representation of v. If v is not a *LiteralValue
-// it returns an error. If the raw string representation of v cannot be read as
-// a string by unquoting it, an error is returned as well.
-func StrVal(v Value) (string, error) {
-	lit, ok := v.(*LiteralValue)
-	if !ok {
-		return "", fmt.Errorf("schemahcl: expected %T to be LiteralValue", v)
-	}
-	return strconv.Unquote(lit.V)
-}
-
-// BoolVal returns the bool representation of v. If v is not a *LiteralValue
-// it returns an error. If the raw string representation of v cannot be read as
-// a bool, an error is returned as well.
-func BoolVal(v Value) (bool, error) {
-	lit, ok := v.(*LiteralValue)
-	if !ok {
-		return false, fmt.Errorf("schemahcl: expected %T to be LiteralValue", v)
-	}
-	b, err := strconv.ParseBool(lit.V)
-	if err != nil {
-		return false, fmt.Errorf("schemahcl: failed parsing %q as bool: %w", lit.V, err)
-	}
-	return b, nil
-}
-
 // Attr returns a TypeAttr by name and reports if one was found.
 func (s *TypeSpec) Attr(name string) (*TypeAttr, bool) {
 	for _, ta := range s.Attributes {
@@ -298,38 +294,78 @@ func (s *TypeSpec) Attr(name string) (*TypeAttr, bool) {
 	return nil, false
 }
 
-func (*LiteralValue) val() {}
-func (*RawExpr) val()      {}
-func (*ListValue) val()    {}
-func (*Ref) val()          {}
-func (*Type) val()         {}
+var _ Marshaler = MarshalerFunc(nil)
 
-var (
-	_ Marshaler = MarshalerFunc(nil)
-)
+// StringAttr is a helper method for constructing *schemahcl.Attr instances that contain string value.
+func StringAttr(k string, v string) *Attr {
+	return &Attr{
+		K: k,
+		V: cty.StringVal(v),
+	}
+}
+
+// IntAttr is a helper method for constructing *schemahcl.Attr instances that contain int64 value.
+func IntAttr(k string, v int) *Attr {
+	return Int64Attr(k, int64(v))
+}
+
+// Int64Attr is a helper method for constructing *schemahcl.Attr instances that contain int64 value.
+func Int64Attr(k string, v int64) *Attr {
+	return &Attr{
+		K: k,
+		V: cty.NumberVal(new(big.Float).SetInt64(v).SetPrec(512)),
+	}
+}
+
+// BoolAttr is a helper method for constructing *schemahcl.Attr instances that contain a boolean value.
+func BoolAttr(k string, v bool) *Attr {
+	return &Attr{
+		K: k,
+		V: cty.BoolVal(v),
+	}
+}
 
-// LitAttr is a helper method for constructing *schemahcl.Attr instances that contain literal values.
-func LitAttr(k, v string) *Attr {
+// RefAttr is a helper method for constructing *schemahcl.Attr instances that contain a Ref value.
+func RefAttr(k string, v *Ref) *Attr {
 	return &Attr{
 		K: k,
-		V: &LiteralValue{V: v},
+		V: cty.CapsuleVal(ctyRefType, v),
 	}
 }
 
-// StrLitAttr is a helper method for constructing *schemahcl.Attr instances that contain literal values
-// representing string literals.
-func StrLitAttr(k, v string) *Attr {
-	return LitAttr(k, strconv.Quote(v))
+// StringsAttr is a helper method for constructing *schemahcl.Attr instances that contain list strings.
+func StringsAttr(k string, vs ...string) *Attr {
+	vv := make([]cty.Value, len(vs))
+	for i, v := range vs {
+		vv[i] = cty.StringVal(v)
+	}
+	return &Attr{
+		K: k,
+		V: cty.ListVal(vv),
+	}
 }
 
-// ListAttr is a helper method for constructing *schemahcl.Attr instances that contain list values.
-func ListAttr(k string, litValues ...string) *Attr {
-	lv := &ListValue{}
-	for _, v := range litValues {
-		lv.V = append(lv.V, &LiteralValue{V: v})
+// RefsAttr is a helper method for constructing *schemahcl.Attr instances that contain list references.
+func RefsAttr(k string, refs ...*Ref) *Attr {
+	vv := make([]cty.Value, len(refs))
+	for i, v := range refs {
+		vv[i] = cty.CapsuleVal(ctyRefType, v)
 	}
 	return &Attr{
 		K: k,
-		V: lv,
+		V: cty.ListVal(vv),
 	}
 }
+
+// RawAttr is a helper method for constructing *schemahcl.Attr instances that contain RawExpr value.
+func RawAttr(k string, x string) *Attr {
+	return &Attr{
+		K: k,
+		V: RawExprValue(&RawExpr{X: x}),
+	}
+}
+
+// RawExprValue is a helper method for constructing a cty.Value that capsules a raw expression.
+func RawExprValue(x *RawExpr) cty.Value {
+	return cty.CapsuleVal(ctyRawExpr, x)
+}
diff --git a/schemahcl/types.go b/schemahcl/types.go
index 737fd4bb..c4c5272e 100644
--- a/schemahcl/types.go
+++ b/schemahcl/types.go
@@ -9,7 +9,6 @@ import (
 	"fmt"
 	"log"
 	"reflect"
-	"strconv"
 	"strings"
 
 	"ariga.io/atlas/sql/schema"
@@ -43,24 +42,11 @@ func (r *TypeRegistry) PrintType(typ *Type) (string, error) {
 			}
 			continue
 		}
-		switch v := arg.V.(type) {
-		case *LiteralValue:
-			args = append(args, v.V)
-		case *ListValue:
-			for _, li := range v.V {
-				lit, ok := li.(*LiteralValue)
-				if !ok {
-					return "", fmt.Errorf("expecting literal value. got: %T", li)
-				}
-				uq, err := strconv.Unquote(lit.V)
-				if err != nil {
-					return "", fmt.Errorf("expecting list items to be quoted strings: %w", err)
-				}
-				args = append(args, "'"+uq+"'")
-			}
-		default:
-			return "", fmt.Errorf("unsupported type %T for PrintType", v)
+		attr, ok := spec.Attr(arg.K)
+		if !ok {
+			return "", fmt.Errorf("specutil: attribute %q not found in typespec %q", arg.K, typ.T)
 		}
+		args = append(args, valueArgs(attr, arg.V)...)
 	}
 	if len(args) > 0 {
 		mid = "(" + strings.Join(args, ",") + ")"
@@ -224,25 +210,25 @@ func (r *TypeRegistry) Convert(typ schema.Type) (*Type, error) {
 			if v == 0 && len(s.Attrs) == 0 {
 				break
 			}
-			i := strconv.Itoa(v)
-			s.Attrs = append([]*Attr{LitAttr(attr.Name, i)}, s.Attrs...)
+			s.Attrs = append([]*Attr{IntAttr(attr.Name, v)}, s.Attrs...)
 		case reflect.Bool:
 			v := field.Bool()
 			if !v && len(s.Attrs) == 0 {
 				break
 			}
-			b := strconv.FormatBool(v)
-			s.Attrs = append([]*Attr{LitAttr(attr.Name, b)}, s.Attrs...)
+			s.Attrs = append([]*Attr{BoolAttr(attr.Name, v)}, s.Attrs...)
+		case reflect.String:
+			v := field.String()
+			if v == "" && len(s.Attrs) == 0 {
+				break
+			}
+			s.Attrs = append([]*Attr{StringAttr(attr.Name, v)}, s.Attrs...)
 		case reflect.Slice:
-			lits := make([]string, 0, field.Len())
-			for i := 0; i < field.Len(); i++ {
-				fi := field.Index(i)
-				if fi.Kind() != reflect.String {
-					return nil, errors.New("specutil: only string slices currently supported")
-				}
-				lits = append(lits, strconv.Quote(fi.String()))
+			vs, ok := field.Interface().([]string)
+			if !ok {
+				return nil, fmt.Errorf("specutil: unsupported slice type %T", field.Interface())
 			}
-			s.Attrs = append([]*Attr{ListAttr(attr.Name, lits...)}, s.Attrs...)
+			s.Attrs = append([]*Attr{StringsAttr(attr.Name, vs...)}, s.Attrs...)
 		default:
 			return nil, fmt.Errorf("specutil: unsupported attr kind %s for attribute %q of %q", attr.Kind, attr.Name, typeSpec.Name)
 		}
diff --git a/sql/internal/specutil/convert.go b/sql/internal/specutil/convert.go
index 538cd522..be5b9826 100644
--- a/sql/internal/specutil/convert.go
+++ b/sql/internal/specutil/convert.go
@@ -14,6 +14,8 @@ import (
 	"ariga.io/atlas/sql/internal/sqlx"
 	"ariga.io/atlas/sql/schema"
 	"ariga.io/atlas/sql/sqlspec"
+
+	"github.com/zclconf/go-cty/cty"
 )
 
 // List of convert function types.
@@ -133,12 +135,20 @@ func Column(spec *sqlspec.Column, conv ConvertTypeFunc) (*schema.Column, error)
 			Null: spec.Null,
 		},
 	}
-	if spec.Default != nil {
-		switch d := spec.Default.(type) {
-		case *schemahcl.LiteralValue:
-			out.Default = &schema.Literal{V: d.V}
-		case *schemahcl.RawExpr:
-			out.Default = &schema.RawExpr{X: d.X}
+	if d := spec.Default; !d.IsNull() {
+		switch {
+		case d.Type() == cty.String:
+			out.Default = &schema.Literal{V: d.AsString()}
+		case d.Type() == cty.Number:
+			out.Default = &schema.Literal{V: d.AsBigFloat().String()}
+		case d.Type() == cty.Bool:
+			out.Default = &schema.Literal{V: strconv.FormatBool(d.True())}
+		case d.Type().IsCapsuleType():
+			x, ok := d.EncapsulatedValue().(*schemahcl.RawExpr)
+			if !ok {
+				return nil, fmt.Errorf("invalid default value %q", d.Type().FriendlyName())
+			}
+			out.Default = &schema.RawExpr{X: x.X}
 		default:
 			return nil, fmt.Errorf("unsupported value type for default: %T", d)
 		}
@@ -368,7 +378,7 @@ func FromColumn(col *schema.Column, columnTypeSpec ColumnTypeSpecFunc) (*sqlspec
 		},
 	}
 	if col.Default != nil {
-		lv, err := toValue(col.Default)
+		lv, err := ExprValue(col.Default)
 		if err != nil {
 			return nil, err
 		}
@@ -383,7 +393,7 @@ func FromGenExpr(x schema.GeneratedExpr, t func(string) string) *schemahcl.Resou
 	return &schemahcl.Resource{
 		Type: "as",
 		Attrs: []*schemahcl.Attr{
-			StrAttr("expr", x.Expr),
+			schemahcl.StringAttr("expr", x.Expr),
 			VarAttr("type", t(x.Type)),
 		},
 	}
@@ -421,37 +431,44 @@ func ConvertGenExpr(r *schemahcl.Resource, c *schema.Column, t func(string) stri
 	return nil
 }
 
-func toValue(expr schema.Expr) (schemahcl.Value, error) {
-	var (
-		v   string
-		err error
-	)
-	switch expr := expr.(type) {
+// ExprValue converts a schema.Expr to a cty.Value.
+func ExprValue(expr schema.Expr) (cty.Value, error) {
+	switch x := expr.(type) {
 	case *schema.RawExpr:
-		return &schemahcl.RawExpr{X: expr.X}, nil
+		return schemahcl.RawExprValue(&schemahcl.RawExpr{X: x.X}), nil
 	case *schema.Literal:
-		v, err = normalizeQuotes(expr.V)
-		if err != nil {
-			return nil, err
+		switch {
+		case oneOfPrefix(x.V, "0x", "0X", "0b", "0B", "b'", "B'", "x'", "X'"):
+			return schemahcl.RawExprValue(&schemahcl.RawExpr{X: x.V}), nil
+		case sqlx.IsQuoted(x.V, '\'', '"'):
+			// Normalize single quotes to double quotes.
+			s, err := sqlx.Unquote(x.V)
+			if err != nil {
+				return cty.NilVal, err
+			}
+			return cty.StringVal(s), nil
+		case x.V == "true", x.V == "false":
+			return cty.BoolVal(x.V == "true"), nil
+		case strings.Contains(x.V, "."):
+			f, err := strconv.ParseFloat(x.V, 64)
+			if err != nil {
+				return cty.NilVal, err
+			}
+			return cty.NumberFloatVal(f), nil
+		case sqlx.IsLiteralNumber(x.V):
+			i, err := strconv.ParseInt(x.V, 10, 64)
+			if err != nil {
+				return cty.NilVal, err
+			}
+			return cty.NumberIntVal(i), nil
+		default:
+			return cty.NilVal, fmt.Errorf("unsupported literal value %q", x.V)
 		}
-		return &schemahcl.LiteralValue{V: v}, nil
 	default:
-		return nil, fmt.Errorf("converting expr %T to literal value", expr)
+		return cty.NilVal, fmt.Errorf("converting expr %T to literal value", expr)
 	}
 }
 
-func normalizeQuotes(s string) (string, error) {
-	if len(s) < 2 {
-		return s, nil
-	}
-	// If string is quoted with single quotes:
-	if strings.HasPrefix(s, `'`) && strings.HasSuffix(s, `'`) {
-		uq := strings.ReplaceAll(s[1:len(s)-1], "''", "'")
-		return strconv.Quote(uq), nil
-	}
-	return s, nil
-}
-
 // FromIndex converts schema.Index to sqlspec.Index.
 func FromIndex(idx *schema.Index, partFns ...func(*schema.IndexPart, *sqlspec.IndexPart)) (*sqlspec.Index, error) {
 	spec := &sqlspec.Index{Name: idx.Name, Unique: idx.Unique}
@@ -662,7 +679,7 @@ func convertCommentFromSpec(spec Attrer, attrs *[]schema.Attr) error {
 func convertCommentFromSchema(src []schema.Attr, trgt *[]*schemahcl.Attr) {
 	var c schema.Comment
 	if sqlx.Has(src, &c) {
-		*trgt = append(*trgt, StrAttr("comment", c.Text))
+		*trgt = append(*trgt, schemahcl.StringAttr("comment", c.Text))
 	}
 }
 
@@ -682,3 +699,12 @@ func Var(s string) string { return strings.ReplaceAll(s, " ", "_") }
 
 // FromVar is the inverse function of Var.
 func FromVar(s string) string { return strings.ReplaceAll(s, "_", " ") }
+
+func oneOfPrefix(s string, ps ...string) bool {
+	for _, p := range ps {
+		if strings.HasPrefix(s, p) {
+			return true
+		}
+	}
+	return false
+}
diff --git a/sql/internal/specutil/spec.go b/sql/internal/specutil/spec.go
index d9df06a8..81034064 100644
--- a/sql/internal/specutil/spec.go
+++ b/sql/internal/specutil/spec.go
@@ -6,7 +6,6 @@ package specutil
 
 import (
 	"fmt"
-	"strconv"
 
 	"ariga.io/atlas/schemahcl"
 	"ariga.io/atlas/sql/schema"
@@ -16,77 +15,9 @@ import (
 	"github.com/zclconf/go-cty/cty"
 )
 
-// StrAttr is a helper method for constructing *schemahcl.Attr of type string.
-func StrAttr(k, v string) *schemahcl.Attr {
-	return &schemahcl.Attr{
-		K: k,
-		V: &schemahcl.LiteralValue{V: strconv.Quote(v)},
-	}
-}
-
-// BoolAttr is a helper method for constructing *schemahcl.Attr of type bool.
-func BoolAttr(k string, v bool) *schemahcl.Attr {
-	return &schemahcl.Attr{
-		K: k,
-		V: &schemahcl.LiteralValue{V: strconv.FormatBool(v)},
-	}
-}
-
-// IntAttr is a helper method for constructing *schemahcl.Attr with the numeric value of v.
-func IntAttr(k string, v int) *schemahcl.Attr {
-	return Int64Attr(k, int64(v))
-}
-
-// Int64Attr is a helper method for constructing *schemahcl.Attr with the numeric value of v.
-func Int64Attr(k string, v int64) *schemahcl.Attr {
-	return &schemahcl.Attr{
-		K: k,
-		V: &schemahcl.LiteralValue{V: strconv.FormatInt(v, 10)},
-	}
-}
-
-// LitAttr is a helper method for constructing *schemahcl.Attr instances that contain literal values.
-func LitAttr(k, v string) *schemahcl.Attr {
-	return &schemahcl.Attr{
-		K: k,
-		V: &schemahcl.LiteralValue{V: v},
-	}
-}
-
-// RawAttr is a helper method for constructing *schemahcl.Attr instances that contain sql expressions.
-func RawAttr(k, v string) *schemahcl.Attr {
-	return &schemahcl.Attr{
-		K: k,
-		V: &schemahcl.RawExpr{X: v},
-	}
-}
-
 // VarAttr is a helper method for constructing *schemahcl.Attr instances that contain a variable reference.
 func VarAttr(k, v string) *schemahcl.Attr {
-	return &schemahcl.Attr{
-		K: k,
-		V: &schemahcl.Ref{V: v},
-	}
-}
-
-// RefAttr is a helper method for constructing *schemahcl.Attr instances that contain a reference.
-func RefAttr(k string, r *schemahcl.Ref) *schemahcl.Attr {
-	return &schemahcl.Attr{
-		K: k,
-		V: r,
-	}
-}
-
-// ListAttr is a helper method for constructing *schemahcl.Attr instances that contain list values.
-func ListAttr(k string, litValues ...string) *schemahcl.Attr {
-	lv := &schemahcl.ListValue{}
-	for _, v := range litValues {
-		lv.V = append(lv.V, &schemahcl.LiteralValue{V: v})
-	}
-	return &schemahcl.Attr{
-		K: k,
-		V: lv,
-	}
+	return schemahcl.RefAttr(k, &schemahcl.Ref{V: v})
 }
 
 type doc struct {
diff --git a/sql/mysql/convert.go b/sql/mysql/convert.go
index 1be90417..c05e4ef0 100644
--- a/sql/mysql/convert.go
+++ b/sql/mysql/convert.go
@@ -119,7 +119,7 @@ func ParseType(raw string) (schema.Type, error) {
 	case TypeBit:
 		return &BitType{
 			T:    t,
-			Size: int(size),
+			Size: size,
 		}, nil
 	// bool and booleans are synonyms for
 	// tinyint with display-width set to 1.
diff --git a/sql/mysql/sqlspec.go b/sql/mysql/sqlspec.go
index a566c0c4..7ef0a75f 100644
--- a/sql/mysql/sqlspec.go
+++ b/sql/mysql/sqlspec.go
@@ -169,11 +169,11 @@ func convertColumn(spec *sqlspec.Column, _ *schema.Table) (*schema.Column, error
 		return nil, err
 	}
 	if attr, ok := spec.Attr("on_update"); ok {
-		exp, ok := attr.V.(*schemahcl.RawExpr)
-		if !ok {
-			return nil, fmt.Errorf(`unexpected type %T for atrribute "on_update"`, attr.V)
+		x, err := attr.RawExpr()
+		if err != nil {
+			return nil, fmt.Errorf(`unexpected type %T for atrribute "on_update"`, attr.V.Type())
 		}
-		c.AddAttrs(&OnUpdate{A: exp.X})
+		c.AddAttrs(&OnUpdate{A: x.X})
 	}
 	if attr, ok := spec.Attr("auto_increment"); ok {
 		b, err := attr.Bool()
@@ -202,10 +202,10 @@ func schemaSpec(s *schema.Schema) (*sqlspec.Schema, []*sqlspec.Table, error) {
 		return nil, nil, err
 	}
 	if c, ok := hasCharset(s.Attrs, nil); ok {
-		sc.Extra.Attrs = append(sc.Extra.Attrs, specutil.StrAttr("charset", c))
+		sc.Extra.Attrs = append(sc.Extra.Attrs, schemahcl.StringAttr("charset", c))
 	}
 	if c, ok := hasCollate(s.Attrs, nil); ok {
-		sc.Extra.Attrs = append(sc.Extra.Attrs, specutil.StrAttr("collate", c))
+		sc.Extra.Attrs = append(sc.Extra.Attrs, schemahcl.StringAttr("collate", c))
 	}
 	return sc, t, nil
 }
@@ -224,10 +224,10 @@ func tableSpec(t *schema.Table) (*sqlspec.Table, error) {
 		return nil, err
 	}
 	if c, ok := hasCharset(t.Attrs, t.Schema.Attrs); ok {
-		ts.Extra.Attrs = append(ts.Extra.Attrs, specutil.StrAttr("charset", c))
+		ts.Extra.Attrs = append(ts.Extra.Attrs, schemahcl.StringAttr("charset", c))
 	}
 	if c, ok := hasCollate(t.Attrs, t.Schema.Attrs); ok {
-		ts.Extra.Attrs = append(ts.Extra.Attrs, specutil.StrAttr("collate", c))
+		ts.Extra.Attrs = append(ts.Extra.Attrs, schemahcl.StringAttr("collate", c))
 	}
 	return ts, nil
 }
@@ -246,7 +246,7 @@ func indexSpec(idx *schema.Index) (*sqlspec.Index, error) {
 
 func partAttr(part *schema.IndexPart, spec *sqlspec.IndexPart) {
 	if p := (SubPart{}); sqlx.Has(part.Attrs, &p) && p.Len > 0 {
-		spec.Extra.Attrs = append(spec.Extra.Attrs, specutil.IntAttr("prefix", p.Len))
+		spec.Extra.Attrs = append(spec.Extra.Attrs, schemahcl.IntAttr("prefix", p.Len))
 	}
 }
 
@@ -257,16 +257,16 @@ func columnSpec(c *schema.Column, t *schema.Table) (*sqlspec.Column, error) {
 		return nil, err
 	}
 	if c, ok := hasCharset(c.Attrs, t.Attrs); ok {
-		spec.Extra.Attrs = append(spec.Extra.Attrs, specutil.StrAttr("charset", c))
+		spec.Extra.Attrs = append(spec.Extra.Attrs, schemahcl.StringAttr("charset", c))
 	}
 	if c, ok := hasCollate(c.Attrs, t.Attrs); ok {
-		spec.Extra.Attrs = append(spec.Extra.Attrs, specutil.StrAttr("collate", c))
+		spec.Extra.Attrs = append(spec.Extra.Attrs, schemahcl.StringAttr("collate", c))
 	}
 	if o := (OnUpdate{}); sqlx.Has(c.Attrs, &o) {
-		spec.Extra.Attrs = append(spec.Extra.Attrs, specutil.RawAttr("on_update", o.A))
+		spec.Extra.Attrs = append(spec.Extra.Attrs, schemahcl.RawAttr("on_update", o.A))
 	}
 	if sqlx.Has(c.Attrs, &AutoIncrement{}) {
-		spec.Extra.Attrs = append(spec.Extra.Attrs, specutil.BoolAttr("auto_increment", true))
+		spec.Extra.Attrs = append(spec.Extra.Attrs, schemahcl.BoolAttr("auto_increment", true))
 	}
 	if x := (schema.GeneratedExpr{}); sqlx.Has(c.Attrs, &x) {
 		spec.Extra.Children = append(spec.Extra.Children, specutil.FromGenExpr(x, storedOrVirtual))
@@ -292,7 +292,7 @@ func storedOrVirtual(s string) string {
 func checkSpec(s *schema.Check) *sqlspec.Check {
 	c := specutil.FromCheck(s)
 	if e := (Enforced{}); sqlx.Has(s.Attrs, &e) {
-		c.Extra.Attrs = append(c.Extra.Attrs, specutil.BoolAttr("enforced", true))
+		c.Extra.Attrs = append(c.Extra.Attrs, schemahcl.BoolAttr("enforced", true))
 	}
 	return c
 }
@@ -372,6 +372,16 @@ var TypeRegistry = schemahcl.NewRegistry(
 				{Name: "values", Kind: reflect.Slice, Required: true},
 			},
 			RType: reflect.TypeOf(schema.EnumType{}),
+			FromSpec: func(t *schemahcl.Type) (schema.Type, error) {
+				if len(t.Attrs) != 1 || t.Attrs[0].K != "values" {
+					return nil, fmt.Errorf("invalid enum type spec: %v", t)
+				}
+				v, err := t.Attrs[0].Strings()
+				if err != nil {
+					return nil, err
+				}
+				return &schema.EnumType{T: "enum", Values: v}, nil
+			},
 		},
 		&schemahcl.TypeSpec{
 			Name: TypeSet,
@@ -380,6 +390,16 @@ var TypeRegistry = schemahcl.NewRegistry(
 				{Name: "values", Kind: reflect.Slice, Required: true},
 			},
 			RType: reflect.TypeOf(SetType{}),
+			FromSpec: func(t *schemahcl.Type) (schema.Type, error) {
+				if len(t.Attrs) != 1 || t.Attrs[0].K != "values" {
+					return nil, fmt.Errorf("invalid set type spec: %v", t)
+				}
+				v, err := t.Attrs[0].Strings()
+				if err != nil {
+					return nil, err
+				}
+				return &SetType{Values: v}, nil
+			},
 		},
 		schemahcl.NewTypeSpec(TypeBool),
 		schemahcl.NewTypeSpec(TypeBoolean),
diff --git a/sql/postgres/diff.go b/sql/postgres/diff.go
index 422e513c..e5fbdf3c 100644
--- a/sql/postgres/diff.go
+++ b/sql/postgres/diff.go
@@ -78,7 +78,7 @@ func (d *diff) defaultChanged(from, to *schema.Column) (bool, error) {
 	if ok1 != ok2 {
 		return true, nil
 	}
-	if trimCast(d1) == trimCast(d2) {
+	if trimCast(d1) == trimCast(d2) || quote(d1) == quote(d2) {
 		return false, nil
 	}
 	// Use database comparison in case of mismatch (e.g. `SELECT ARRAY[1] = '{1}'::int[]`).
diff --git a/sql/postgres/sqlspec.go b/sql/postgres/sqlspec.go
index 0b6d68f1..0b09b92c 100644
--- a/sql/postgres/sqlspec.go
+++ b/sql/postgres/sqlspec.go
@@ -208,27 +208,27 @@ func fromPartition(p Partition) *schemahcl.Resource {
 			specutil.VarAttr("type", strings.ToUpper(specutil.Var(p.T))),
 		},
 	}
-	columns, ok := func() (*schemahcl.ListValue, bool) {
-		parts := make([]schemahcl.Value, 0, len(p.Parts))
+	columns, ok := func() ([]*schemahcl.Ref, bool) {
+		parts := make([]*schemahcl.Ref, 0, len(p.Parts))
 		for _, p := range p.Parts {
 			if p.C == nil {
 				return nil, false
 			}
 			parts = append(parts, specutil.ColumnRef(p.C.Name))
 		}
-		return &schemahcl.ListValue{V: parts}, true
+		return parts, true
 	}()
 	if ok {
-		key.Attrs = append(key.Attrs, &schemahcl.Attr{K: "columns", V: columns})
+		key.Attrs = append(key.Attrs, schemahcl.RefsAttr("columns", columns...))
 		return key
 	}
 	for _, p := range p.Parts {
 		part := &schemahcl.Resource{Type: "by"}
 		switch {
 		case p.C != nil:
-			part.Attrs = append(part.Attrs, specutil.RefAttr("column", specutil.ColumnRef(p.C.Name)))
+			part.Attrs = append(part.Attrs, schemahcl.RefAttr("column", specutil.ColumnRef(p.C.Name)))
 		case p.X != nil:
-			part.Attrs = append(part.Attrs, specutil.StrAttr("expr", p.X.(*schema.RawExpr).X))
+			part.Attrs = append(part.Attrs, schemahcl.StringAttr("expr", p.X.(*schema.RawExpr).X))
 		}
 		key.Children = append(key.Children, part)
 	}
@@ -237,7 +237,7 @@ func fromPartition(p Partition) *schemahcl.Resource {
 
 // convertColumn converts a sqlspec.Column into a schema.Column.
 func convertColumn(spec *sqlspec.Column, _ *schema.Table) (*schema.Column, error) {
-	if err := fixDefaultQuotes(spec.Default); err != nil {
+	if err := fixDefaultQuotes(spec); err != nil {
 		return nil, err
 	}
 	c, err := specutil.Column(spec, convertColumnType)
@@ -278,17 +278,17 @@ func convertIdentity(r *schemahcl.Resource) (*Identity, error) {
 
 // fixDefaultQuotes fixes the quotes on the Default field to be single quotes
 // instead of double quotes.
-func fixDefaultQuotes(value schemahcl.Value) error {
-	lv, ok := value.(*schemahcl.LiteralValue)
-	if !ok {
+func fixDefaultQuotes(spec *sqlspec.Column) error {
+	if spec.Default.Type() != cty.String {
 		return nil
 	}
-	if sqlx.IsQuoted(lv.V, '"') {
-		uq, err := strconv.Unquote(lv.V)
+	if s := spec.Default.AsString(); sqlx.IsQuoted(s, '"') {
+		uq, err := strconv.Unquote(s)
 		if err != nil {
 			return err
 		}
-		lv.V = "'" + uq + "'"
+		s = "'" + uq + "'"
+		spec.Default = cty.StringVal(s)
 	}
 	return nil
 }
@@ -501,20 +501,17 @@ func indexSpec(idx *schema.Index) (*sqlspec.Index, error) {
 		spec.Extra.Attrs = append(spec.Extra.Attrs, specutil.VarAttr("type", strings.ToUpper(i.T)))
 	}
 	if i := (IndexInclude{}); sqlx.Has(idx.Attrs, &i) && len(i.Columns) > 0 {
-		attr := &schemahcl.ListValue{}
+		refs := make([]*schemahcl.Ref, 0, len(i.Columns))
 		for _, c := range i.Columns {
-			attr.V = append(attr.V, specutil.ColumnRef(c.Name))
+			refs = append(refs, specutil.ColumnRef(c.Name))
 		}
-		spec.Extra.Attrs = append(spec.Extra.Attrs, &schemahcl.Attr{
-			K: "include",
-			V: attr,
-		})
+		spec.Extra.Attrs = append(spec.Extra.Attrs, schemahcl.RefsAttr("include", refs...))
 	}
 	if i := (IndexPredicate{}); sqlx.Has(idx.Attrs, &i) && i.P != "" {
 		spec.Extra.Attrs = append(spec.Extra.Attrs, specutil.VarAttr("where", strconv.Quote(i.P)))
 	}
 	if p, ok := indexStorageParams(idx.Attrs); ok {
-		spec.Extra.Attrs = append(spec.Extra.Attrs, specutil.Int64Attr("page_per_range", p.PagesPerRange))
+		spec.Extra.Attrs = append(spec.Extra.Attrs, schemahcl.Int64Attr("page_per_range", p.PagesPerRange))
 	}
 	return spec, nil
 }
@@ -544,10 +541,10 @@ func fromIdentity(i *Identity) *schemahcl.Resource {
 	}
 	if s := i.Sequence; s != nil {
 		if s.Start != 1 {
-			id.Attrs = append(id.Attrs, specutil.Int64Attr("start", s.Start))
+			id.Attrs = append(id.Attrs, schemahcl.Int64Attr("start", s.Start))
 		}
 		if s.Increment != 1 {
-			id.Attrs = append(id.Attrs, specutil.Int64Attr("increment", s.Increment))
+			id.Attrs = append(id.Attrs, schemahcl.Int64Attr("increment", s.Increment))
 		}
 	}
 	return id
@@ -640,7 +637,7 @@ var TypeRegistry = schemahcl.NewRegistry(
 					spec.T = specutil.Var(strings.ToLower(i.F))
 				}
 				if p := i.Precision; p != nil && *p != defaultTimePrecision {
-					spec.Attrs = []*schemahcl.Attr{specutil.IntAttr("precision", *p)}
+					spec.Attrs = []*schemahcl.Attr{schemahcl.IntAttr("precision", *p)}
 				}
 				return spec, nil
 			}),
@@ -692,7 +689,7 @@ func typeSpec(t schema.Type) (*schemahcl.Type, error) {
 	if t, ok := t.(*schema.TimeType); ok && t.T != TypeDate {
 		spec := &schemahcl.Type{T: timeAlias(t.T)}
 		if p := t.Precision; p != nil && *p != defaultTimePrecision {
-			spec.Attrs = []*schemahcl.Attr{specutil.IntAttr("precision", *p)}
+			spec.Attrs = []*schemahcl.Attr{schemahcl.IntAttr("precision", *p)}
 		}
 		return spec, nil
 	}
diff --git a/sql/sqlite/sqlspec.go b/sql/sqlite/sqlspec.go
index c95b6fbf..fa3b8fa5 100644
--- a/sql/sqlite/sqlspec.go
+++ b/sql/sqlite/sqlspec.go
@@ -137,7 +137,7 @@ func columnSpec(c *schema.Column, _ *schema.Table) (*sqlspec.Column, error) {
 		return nil, err
 	}
 	if sqlx.Has(c.Attrs, &AutoIncrement{}) {
-		s.Extra.Attrs = append(s.Extra.Attrs, specutil.BoolAttr("auto_increment", true))
+		s.Extra.Attrs = append(s.Extra.Attrs, schemahcl.BoolAttr("auto_increment", true))
 	}
 	if x := (schema.GeneratedExpr{}); sqlx.Has(c.Attrs, &x) {
 		s.Extra.Children = append(s.Extra.Children, specutil.FromGenExpr(x, storedOrVirtual))
diff --git a/sql/sqlspec/sqlspec.go b/sql/sqlspec/sqlspec.go
index 21625713..580d4e06 100644
--- a/sql/sqlspec/sqlspec.go
+++ b/sql/sqlspec/sqlspec.go
@@ -6,6 +6,8 @@ package sqlspec
 
 import (
 	"ariga.io/atlas/schemahcl"
+
+	"github.com/zclconf/go-cty/cty"
 )
 
 type (
@@ -33,7 +35,7 @@ type (
 		Name    string          `spec:",name"`
 		Null    bool            `spec:"null"`
 		Type    *schemahcl.Type `spec:"type"`
-		Default schemahcl.Value `spec:"default"`
+		Default cty.Value       `spec:"default"`
 		schemahcl.DefaultExtension
 	}
 


Test patch:
diff --git a/cmd/atlas/internal/cmdapi/migrate_test.go b/cmd/atlas/internal/cmdapi/migrate_test.go
index d4da0428..3f1efc33 100644
--- a/cmd/atlas/internal/cmdapi/migrate_test.go
+++ b/cmd/atlas/internal/cmdapi/migrate_test.go
@@ -436,7 +436,12 @@ variable "url" {
 
 data "sql" "tenants" {
   url   = var.url
-  query = "SELECT name FROM tenants"
+  query = <<EOS
+SELECT name FROM tenants
+	WHERE mode LIKE ? AND active = ? AND created = ?
+EOS
+  # Pass all types of arguments.
+  args  = ["%test", true, 1]
 }
 
 env "local" {
@@ -466,7 +471,7 @@ env "local" {
 		require.NoError(t, err)
 		db, err := sql.Open("sqlite3", fmt.Sprintf("file:%s?cache=shared&_fk=1", filepath.Join(p, "tenants.db")))
 		require.NoError(t, err)
-		_, err = db.Exec("CREATE TABLE `tenants` (`name` TEXT)")
+		_, err = db.Exec("CREATE TABLE `tenants` (`name` TEXT, `mode` TEXT DEFAULT 'test', `active` BOOL DEFAULT TRUE, `created` INT DEFAULT 1);")
 		require.NoError(t, err)
 		_, err = db.Exec("INSERT INTO `tenants` (`name`) VALUES (?), (?), (?)", dbs[0], dbs[1], dbs[2])
 		require.NoError(t, err)
@@ -583,25 +588,81 @@ func TestMigrate_ApplyTxMode(t *testing.T) {
 }
 
 func TestMigrate_ApplyBaseline(t *testing.T) {
-	p := t.TempDir()
-	// Run migration with baseline should store this revision in the database.
-	s, err := runCmd(
-		migrateApplyCmd(),
-		"--dir", "file://testdata/baseline1",
-		"--baseline", "1",
-		"--url", fmt.Sprintf("sqlite://file:%s?cache=shared&_fk=1", filepath.Join(p, "test.db")),
-	)
-	require.NoError(t, err)
-	require.Contains(t, s, "No migration files to execute")
+	t.Run("FromFlags", func(t *testing.T) {
+		p := t.TempDir()
+		// Run migration with baseline should store this revision in the database.
+		s, err := runCmd(
+			migrateApplyCmd(),
+			"--dir", "file://testdata/baseline1",
+			"--baseline", "1",
+			"--url", fmt.Sprintf("sqlite://file:%s?cache=shared&_fk=1", filepath.Join(p, "test1.db")),
+		)
+		require.NoError(t, err)
+		require.Contains(t, s, "No migration files to execute")
+		// Next run without baseline should run the migration from the baseline.
+		s, err = runCmd(
+			migrateApplyCmd(),
+			"--dir", "file://testdata/baseline1",
+			"--url", fmt.Sprintf("sqlite://file:%s?cache=shared&_fk=1", filepath.Join(p, "test1.db")),
+		)
+		require.NoError(t, err)
+		require.Contains(t, s, "No migration files to execute")
 
-	// Next run without baseline should run the migration from the baseline.
-	s, err = runCmd(
-		migrateApplyCmd(),
-		"--dir", "file://testdata/baseline2",
-		"--url", fmt.Sprintf("sqlite://file:%s?cache=shared&_fk=1", filepath.Join(p, "test.db")),
-	)
-	require.NoError(t, err)
-	require.Contains(t, s, "Migrating to version 20220318104615 from 1 (2 migrations in total)")
+		// Multiple migration files with baseline.
+		s, err = runCmd(
+			migrateApplyCmd(),
+			"--dir", "file://testdata/baseline2",
+			"--baseline", "1",
+			"--url", fmt.Sprintf("sqlite://file:%s?cache=shared&_fk=1", filepath.Join(p, "test2.db")),
+		)
+		require.NoError(t, err)
+		require.Contains(t, s, "Migrating to version 20220318104615 from 1 (2 migrations in total)")
+
+		// Run all migration files and skip baseline.
+		s, err = runCmd(
+			migrateApplyCmd(),
+			"--dir", "file://testdata/baseline2",
+			"--url", fmt.Sprintf("sqlite://file:%s?cache=shared&_fk=1", filepath.Join(p, "test3.db")),
+		)
+		require.NoError(t, err)
+		require.Contains(t, s, "Migrating to version 20220318104615 (3 migrations in total)")
+	})
+
+	t.Run("FromConfig", func(t *testing.T) {
+		const h = `
+env "local" {
+  migration {
+    baseline = "1"
+  }
+}`
+		p := t.TempDir()
+		path := filepath.Join(p, "atlas.hcl")
+		err := os.WriteFile(path, []byte(h), 0600)
+		require.NoError(t, err)
+		cmd := migrateCmd()
+		cmd.AddCommand(migrateApplyCmd())
+		s, err := runCmd(
+			cmd, "apply",
+			"-c", "file://"+path,
+			"--env", "local",
+			"--dir", "file://testdata/baseline1",
+			"--url", fmt.Sprintf("sqlite://file:%s?cache=shared&_fk=1", filepath.Join(p, "test1.db")),
+		)
+		require.NoError(t, err)
+		require.Contains(t, s, "No migration files to execute")
+
+		cmd = migrateCmd()
+		cmd.AddCommand(migrateApplyCmd())
+		s, err = runCmd(
+			cmd, "apply",
+			"-c", "file://"+path,
+			"--env", "local",
+			"--dir", "file://testdata/baseline2",
+			"--url", fmt.Sprintf("sqlite://file:%s?cache=shared&_fk=1", filepath.Join(p, "test2.db")),
+		)
+		require.NoError(t, err)
+		require.Contains(t, s, "Migrating to version 20220318104615 from 1 (2 migrations in total)")
+	})
 }
 
 func TestMigrate_Diff(t *testing.T) {
@@ -668,6 +729,18 @@ func TestMigrate_Diff(t *testing.T) {
 	require.ErrorIs(t, err, errLock)
 }
 
+func TestMigrate_StatusJSON(t *testing.T) {
+	p := t.TempDir()
+	s, err := runCmd(
+		migrateStatusCmd(),
+		"--dir", "file://"+p,
+		"-u", openSQLite(t, ""),
+		"--log", "{{ json .Env.Driver }}",
+	)
+	require.NoError(t, err)
+	require.Equal(t, `"sqlite3"`, s)
+}
+
 func TestMigrate_New(t *testing.T) {
 	var (
 		p = t.TempDir()
diff --git a/cmd/atlas/internal/cmdapi/project_test.go b/cmd/atlas/internal/cmdapi/project_test.go
index 99daef49..fd728f31 100644
--- a/cmd/atlas/internal/cmdapi/project_test.go
+++ b/cmd/atlas/internal/cmdapi/project_test.go
@@ -99,7 +99,7 @@ env "multi" {
 							{
 								Type: "destructive",
 								Attrs: []*schemahcl.Attr{
-									{K: "error", V: &schemahcl.LiteralValue{V: "true"}},
+									schemahcl.BoolAttr("error", true),
 								},
 							},
 						},
@@ -109,10 +109,10 @@ env "multi" {
 			DefaultExtension: schemahcl.DefaultExtension{
 				Extra: schemahcl.Resource{
 					Attrs: []*schemahcl.Attr{
-						{K: "bool", V: &schemahcl.LiteralValue{V: "true"}},
-						{K: "integer", V: &schemahcl.LiteralValue{V: "42"}},
-						{K: "src", V: &schemahcl.LiteralValue{V: `"./app.hcl"`}},
-						{K: "str", V: &schemahcl.LiteralValue{V: `"hello"`}},
+						schemahcl.BoolAttr("bool", true),
+						schemahcl.IntAttr("integer", 42),
+						schemahcl.StringAttr("src", "./app.hcl"),
+						schemahcl.StringAttr("str", "hello"),
 					},
 				},
 			},
diff --git a/cmd/atlas/internal/cmdapi/schema_test.go b/cmd/atlas/internal/cmdapi/schema_test.go
index 5e096ef8..74618491 100644
--- a/cmd/atlas/internal/cmdapi/schema_test.go
+++ b/cmd/atlas/internal/cmdapi/schema_test.go
@@ -7,12 +7,14 @@ package cmdapi
 import (
 	"context"
 	"fmt"
+	"net/url"
 	"os"
 	"path"
 	"path/filepath"
 	"testing"
 
 	"ariga.io/atlas/sql/migrate"
+	"ariga.io/atlas/sql/schema"
 	"ariga.io/atlas/sql/sqlclient"
 	"github.com/stretchr/testify/require"
 )
@@ -192,6 +194,31 @@ table "tbl" {
 	)
 }
 
+func TestCmdSchemaApply(t *testing.T) {
+	const drvName = "checknormalizer"
+	// If no dev-database is given, there must not be a call to Driver.Normalize.
+	sqlclient.Register(
+		drvName,
+		sqlclient.OpenerFunc(func(ctx context.Context, url *url.URL) (*sqlclient.Client, error) {
+			url.Scheme = "sqlite"
+			c, err := sqlclient.OpenURL(ctx, url)
+			if err != nil {
+				return nil, err
+			}
+			c.Driver = &assertNormalizerDriver{t: t, Driver: c.Driver}
+			return c, nil
+		}),
+	)
+
+	p := filepath.Join(t.TempDir(), "schema.hcl")
+	require.NoError(t, os.WriteFile(p, []byte(`schema "my_schema" {}`), 0644))
+	_, _ = runCmd(
+		schemaApplyCmd(),
+		"--url", drvName+"://?mode=memory",
+		"-f", p,
+	)
+}
+
 func TestFmt(t *testing.T) {
 	for _, tt := range []struct {
 		name          string
@@ -321,3 +348,20 @@ func setupFmtTest(t *testing.T, inputDir map[string]string) string {
 	require.NoError(t, err)
 	return dir
 }
+
+type assertNormalizerDriver struct {
+	migrate.Driver
+	t *testing.T
+}
+
+// NormalizeSchema returns the normal representation of a schema.
+func (d *assertNormalizerDriver) NormalizeSchema(context.Context, *schema.Schema) (*schema.Schema, error) {
+	d.t.Fatal("did not expect a call to NormalizeSchema")
+	return nil, nil
+}
+
+// NormalizeRealm returns the normal representation of a database.
+func (d *assertNormalizerDriver) NormalizeRealm(context.Context, *schema.Realm) (*schema.Realm, error) {
+	d.t.Fatal("did not expect a call to NormalizeRealm")
+	return nil, nil
+}
diff --git a/internal/integration/hclsqlspec/hclsqlspec_test.go b/internal/integration/hclsqlspec/hclsqlspec_test.go
index e00af199..8a76605c 100644
--- a/internal/integration/hclsqlspec/hclsqlspec_test.go
+++ b/internal/integration/hclsqlspec/hclsqlspec_test.go
@@ -5,6 +5,7 @@
 package hclsqlspec
 
 import (
+	"math/big"
 	"testing"
 
 	"github.com/zclconf/go-cty/cty"
@@ -148,25 +149,25 @@ table "accounts" {
 						Name:    "id",
 						Type:    &schemahcl.Type{T: "int"},
 						Null:    false,
-						Default: &schemahcl.LiteralValue{V: "123"},
+						Default: cty.NumberVal(big.NewFloat(123).SetPrec(512)),
 					},
 					{
 						Name:    "age",
 						Type:    &schemahcl.Type{T: "int"},
 						Null:    false,
-						Default: &schemahcl.LiteralValue{V: "10"},
+						Default: cty.NumberVal(big.NewFloat(10).SetPrec(512)),
 					},
 					{
 						Name:    "active",
 						Type:    &schemahcl.Type{T: "boolean"},
 						Null:    false,
-						Default: &schemahcl.LiteralValue{V: "true"},
+						Default: cty.BoolVal(true),
 					},
 					{
 						Name:    "account_active",
 						Type:    &schemahcl.Type{T: "boolean"},
 						Null:    false,
-						Default: &schemahcl.LiteralValue{V: "true"},
+						Default: cty.BoolVal(true),
 					},
 				},
 				PrimaryKey: &sqlspec.PrimaryKey{
@@ -224,25 +225,25 @@ table "accounts" {
 						Name:    "id",
 						Type:    &schemahcl.Type{T: "int"},
 						Null:    false,
-						Default: &schemahcl.LiteralValue{V: "123"},
+						Default: cty.NumberVal(big.NewFloat(123).SetPrec(512)),
 					},
 					{
 						Name:    "age",
 						Type:    &schemahcl.Type{T: "int"},
 						Null:    false,
-						Default: &schemahcl.LiteralValue{V: "10"},
+						Default: cty.NumberVal(big.NewFloat(10).SetPrec(512)),
 					},
 					{
 						Name:    "active",
 						Type:    &schemahcl.Type{T: "boolean"},
 						Null:    false,
-						Default: &schemahcl.LiteralValue{V: "true"},
+						Default: cty.BoolVal(true),
 					},
 					{
 						Name:    "user_active",
 						Type:    &schemahcl.Type{T: "boolean"},
 						Null:    false,
-						Default: &schemahcl.LiteralValue{V: "true"},
+						Default: cty.BoolVal(true),
 					},
 				},
 				PrimaryKey: &sqlspec.PrimaryKey{
@@ -307,7 +308,7 @@ schema "hi" {
 				DefaultExtension: schemahcl.DefaultExtension{
 					Extra: schemahcl.Resource{
 						Attrs: []*schemahcl.Attr{
-							{K: "x", V: &schemahcl.LiteralValue{V: "1"}},
+							schemahcl.IntAttr("x", 1),
 						},
 					},
 				},
diff --git a/schemahcl/context_test.go b/schemahcl/context_test.go
index d5cac59b..decf66f7 100644
--- a/schemahcl/context_test.go
+++ b/schemahcl/context_test.go
@@ -381,33 +381,25 @@ variadic = enum("a","b","c")
 	require.NoError(t, err)
 	require.EqualValues(t, "int", test.First.T)
 	require.EqualValues(t, "bool", test.Second.T)
-	require.EqualValues(t, &Type{
-		T: "varchar",
-		Attrs: []*Attr{
-			{K: "size", V: &LiteralValue{V: "255"}},
-		},
-	}, test.Varchar)
-	require.EqualValues(t, &Type{
-		T: "enum",
-		Attrs: []*Attr{
-			{
-				K: "values",
-				V: &ListValue{
-					V: []Value{
-						&LiteralValue{V: `"a"`},
-						&LiteralValue{V: `"b"`},
-						&LiteralValue{V: `"c"`},
-					},
-				},
-			},
-		},
-	}, test.Variadic)
-	require.EqualValues(t, &Type{
-		T: "int",
-		Attrs: []*Attr{
-			{K: "size", V: &LiteralValue{V: "10"}},
-		},
-	}, test.Third)
+
+	require.EqualValues(t, "varchar", test.Varchar.T)
+	require.Len(t, test.Varchar.Attrs, 1)
+	i, err := test.Varchar.Attrs[0].Int()
+	require.NoError(t, err)
+	require.EqualValues(t, 255, i)
+
+	require.EqualValues(t, "enum", test.Variadic.T)
+	require.Len(t, test.Variadic.Attrs, 1)
+	vs, err := test.Variadic.Attrs[0].Strings()
+	require.NoError(t, err)
+	require.EqualValues(t, []string{"a", "b", "c"}, vs)
+
+	require.EqualValues(t, "int", test.Third.T)
+	require.Len(t, test.Third.Attrs, 1)
+	i, err = test.Third.Attrs[0].Int()
+	require.NoError(t, err)
+	require.EqualValues(t, 10, i)
+
 	after, err := s.MarshalSpec(&test)
 	require.NoError(t, err)
 	require.EqualValues(t, f, string(after))
@@ -445,13 +437,22 @@ arg_2 = float(10,2)
 	err := s.EvalBytes([]byte(f), &test, nil)
 	require.NoError(t, err)
 	require.Nil(t, test.Arg0.Attrs)
-	require.EqualValues(t, []*Attr{
-		LitAttr("precision", "10"),
-	}, test.Arg1.Attrs)
-	require.EqualValues(t, []*Attr{
-		LitAttr("precision", "10"),
-		LitAttr("scale", "2"),
-	}, test.Arg2.Attrs)
+
+	require.Len(t, test.Arg1.Attrs, 1)
+	require.Equal(t, "precision", test.Arg1.Attrs[0].K)
+	i, err := test.Arg1.Attrs[0].Int()
+	require.NoError(t, err)
+	require.EqualValues(t, 10, i)
+
+	require.Len(t, test.Arg2.Attrs, 2)
+	require.Equal(t, "precision", test.Arg2.Attrs[0].K)
+	i, err = test.Arg2.Attrs[0].Int()
+	require.NoError(t, err)
+	require.EqualValues(t, 10, i)
+	require.Equal(t, "scale", test.Arg2.Attrs[1].K)
+	i, err = test.Arg2.Attrs[1].Int()
+	require.NoError(t, err)
+	require.EqualValues(t, 2, i)
 }
 
 func TestQualifiedRefs(t *testing.T) {
diff --git a/schemahcl/extension_test.go b/schemahcl/extension_test.go
index b6fa5fdb..6d59f662 100644
--- a/schemahcl/extension_test.go
+++ b/schemahcl/extension_test.go
@@ -8,19 +8,21 @@ import (
 	"testing"
 
 	"ariga.io/atlas/schemahcl"
+
 	"github.com/stretchr/testify/require"
+	"github.com/zclconf/go-cty/cty"
 )
 
 type OwnerBlock struct {
 	schemahcl.DefaultExtension
-	ID        string                  `spec:",name"`
-	FirstName string                  `spec:"first_name"`
-	Born      int                     `spec:"born"`
-	Active    bool                    `spec:"active"`
-	BoolPtr   *bool                   `spec:"bool_ptr"`
-	OmitBool1 bool                    `spec:"omit_bool1,omitempty"`
-	OmitBool2 bool                    `spec:"omit_bool2,omitempty"`
-	Lit       *schemahcl.LiteralValue `spec:"lit"`
+	ID        string    `spec:",name"`
+	FirstName string    `spec:"first_name"`
+	Born      int       `spec:"born"`
+	Active    bool      `spec:"active"`
+	BoolPtr   *bool     `spec:"bool_ptr"`
+	OmitBool1 bool      `spec:"omit_bool1,omitempty"`
+	OmitBool2 bool      `spec:"omit_bool2,omitempty"`
+	Lit       cty.Value `spec:"lit"`
 }
 
 type PetBlock struct {
@@ -47,13 +49,13 @@ func TestExtension(t *testing.T) {
 		Name: "name",
 		Type: "owner",
 		Attrs: []*schemahcl.Attr{
-			schemahcl.StrLitAttr("first_name", "tzuri"),
-			schemahcl.LitAttr("born", "2019"),
-			schemahcl.LitAttr("active", "true"),
-			schemahcl.LitAttr("bool_ptr", "true"),
-			schemahcl.LitAttr("omit_bool1", "true"),
-			schemahcl.LitAttr("lit", "1000"),
-			schemahcl.LitAttr("extra", "true"),
+			schemahcl.StringAttr("first_name", "tzuri"),
+			schemahcl.IntAttr("born", 2019),
+			schemahcl.BoolAttr("active", true),
+			schemahcl.BoolAttr("bool_ptr", true),
+			schemahcl.BoolAttr("omit_bool1", true),
+			schemahcl.StringAttr("lit", "1000"),
+			schemahcl.BoolAttr("extra", true),
 		},
 		Children: []*schemahcl.Resource{
 			{
@@ -71,7 +73,7 @@ func TestExtension(t *testing.T) {
 	require.EqualValues(t, true, owner.Active)
 	require.NotNil(t, owner.BoolPtr)
 	require.EqualValues(t, true, *owner.BoolPtr)
-	require.EqualValues(t, schemahcl.LitAttr("lit", "1000").V, owner.Lit)
+	require.EqualValues(t, cty.StringVal("1000"), owner.Lit)
 	attr, ok := owner.Remain().Attr("extra")
 	require.True(t, ok)
 	eb, err := attr.Bool()
@@ -90,25 +92,25 @@ func TestNested(t *testing.T) {
 		Name: "donut",
 		Type: "pet",
 		Attrs: []*schemahcl.Attr{
-			schemahcl.StrLitAttr("breed", "golden retriever"),
-			schemahcl.LitAttr("born", "2002"),
+			schemahcl.StringAttr("breed", "golden retriever"),
+			schemahcl.IntAttr("born", 2002),
 		},
 		Children: []*schemahcl.Resource{
 			{
 				Name: "rotemtam",
 				Type: "owner",
 				Attrs: []*schemahcl.Attr{
-					schemahcl.StrLitAttr("first_name", "rotem"),
-					schemahcl.LitAttr("born", "1985"),
-					schemahcl.LitAttr("active", "true"),
+					schemahcl.StringAttr("first_name", "rotem"),
+					schemahcl.IntAttr("born", 1985),
+					schemahcl.BoolAttr("active", true),
 				},
 			},
 			{
 				Name: "gonnie",
 				Type: "role_model",
 				Attrs: []*schemahcl.Attr{
-					schemahcl.StrLitAttr("breed", "golden retriever"),
-					schemahcl.LitAttr("born", "1998"),
+					schemahcl.StringAttr("breed", "golden retriever"),
+					schemahcl.IntAttr("born", 1998),
 				},
 			},
 		},
@@ -148,10 +150,7 @@ func TestRef(t *testing.T) {
 		Name: "x",
 		Type: "a",
 		Attrs: []*schemahcl.Attr{
-			{
-				K: "user",
-				V: &schemahcl.Ref{V: "$user.rotemtam"},
-			},
+			schemahcl.RefAttr("user", &schemahcl.Ref{V: "$user.rotemtam"}),
 		},
 	}
 	var a A
@@ -174,15 +173,11 @@ func TestListRef(t *testing.T) {
 		Name: "x",
 		Type: "b",
 		Attrs: []*schemahcl.Attr{
-			{
-				K: "users",
-				V: &schemahcl.ListValue{
-					V: []schemahcl.Value{
-						&schemahcl.Ref{V: "$user.a8m"},
-						&schemahcl.Ref{V: "$user.rotemtam"},
-					},
-				},
-			},
+			schemahcl.RefsAttr(
+				"users",
+				&schemahcl.Ref{V: "$user.a8m"},
+				&schemahcl.Ref{V: "$user.rotemtam"},
+			),
 		},
 	}
 
@@ -209,7 +204,7 @@ func TestNameAttr(t *testing.T) {
 		Name: "id",
 		Type: "named",
 		Attrs: []*schemahcl.Attr{
-			schemahcl.StrLitAttr("name", "atlas"),
+			schemahcl.StringAttr("name", "atlas"),
 		},
 	}
 	var tgt Named
diff --git a/schemahcl/hcl_test.go b/schemahcl/hcl_test.go
index 56d9fa95..d98ab405 100644
--- a/schemahcl/hcl_test.go
+++ b/schemahcl/hcl_test.go
@@ -361,83 +361,6 @@ func TestMultiFile(t *testing.T) {
 	}, test.People[1])
 }
 
-func TestDynamicBlocks(t *testing.T) {
-	type (
-		URL struct {
-			Value string `spec:"value"`
-		}
-		Env struct {
-			Name string `spec:",name"`
-			URLs []*URL `spec:"url"`
-		}
-	)
-	var (
-		doc struct {
-			Envs []*Env `spec:"env"`
-		}
-		b = []byte(`
-variable "tenants" {
-  type    = list(string)
-  default = ["atlas", "ent"]
-}
-
-variable "domains" {
-  type = list(string)
-}
-
-env "prod" {
-  dynamic "url" {
-    for_each = var.tenants
-    content {
-      value = "mysql://root:pass@:3306/${url.value}"
-    }
-  }
-  migration {
-    dir = "file://migrations"
-  }
-}
-
-env "staging" {
-  dynamic "url" {
-    for_each = [for t in var.tenants: "mysql://root:pass@:3306/${t}"]
-    content {
-      value = "${url.value}"
-    }
-  }
-  migration {
-    dir = "file://migrations"
-  }
-}
-`)
-	)
-	require.NoError(t, New().EvalBytes(b, &doc, map[string]cty.Value{
-		"domains": cty.ListVal([]cty.Value{
-			cty.StringVal("a"),
-			cty.StringVal("b"),
-		}),
-	}))
-	require.Len(t, doc.Envs, 2)
-	require.Equal(t, "prod", doc.Envs[0].Name)
-	require.Equal(t, "staging", doc.Envs[1].Name)
-	require.Len(t, doc.Envs[0].URLs, 2)
-	require.Len(t, doc.Envs[1].URLs, 2)
-	require.Equal(t, "mysql://root:pass@:3306/atlas", doc.Envs[0].URLs[0].Value)
-	require.Equal(t, "mysql://root:pass@:3306/atlas", doc.Envs[1].URLs[0].Value)
-	require.Equal(t, "mysql://root:pass@:3306/ent", doc.Envs[0].URLs[1].Value)
-	require.Equal(t, "mysql://root:pass@:3306/ent", doc.Envs[1].URLs[1].Value)
-
-	// A one-element is allowed for list types.
-	require.NoError(t, New().EvalBytes(b, &doc, map[string]cty.Value{
-		"domains": cty.StringVal("a"),
-	}))
-
-	// Mismatched element types.
-	err := New().EvalBytes(b, &doc, map[string]cty.Value{
-		"domains": cty.BoolVal(false),
-	})
-	require.EqualError(t, err, `variable "domains": list of string required`)
-}
-
 func TestForEachResources(t *testing.T) {
 	type (
 		Env struct {
diff --git a/schemahcl/spec_test.go b/schemahcl/spec_test.go
deleted file mode 100644
index d92cc73e..00000000
--- a/schemahcl/spec_test.go
+++ /dev/null
@@ -1,33 +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.
-
-package schemahcl
-
-import (
-	"testing"
-
-	"github.com/stretchr/testify/require"
-)
-
-func TestBoolVal(t *testing.T) {
-	b, err := BoolVal(&LiteralValue{V: "true"})
-	require.NoError(t, err)
-	require.True(t, b)
-}
-
-func TestBools(t *testing.T) {
-	a := Attr{
-		K: "b",
-		V: &ListValue{
-			V: []Value{
-				&LiteralValue{V: "true"},
-				&LiteralValue{V: "false"},
-				&LiteralValue{V: "true"},
-			},
-		},
-	}
-	bls, err := a.Bools()
-	require.NoError(t, err)
-	require.EqualValues(t, []bool{true, false, true}, bls)
-}
diff --git a/schemahcl/types_test.go b/schemahcl/types_test.go
index 1e6d473f..d714c28f 100644
--- a/schemahcl/types_test.go
+++ b/schemahcl/types_test.go
@@ -33,7 +33,7 @@ func TestTypePrint(t *testing.T) {
 		},
 		{
 			spec:     intSpec,
-			typ:      &Type{T: "int", Attrs: []*Attr{LitAttr("unsigned", "true")}},
+			typ:      &Type{T: "int", Attrs: []*Attr{BoolAttr("unsigned", true)}},
 			expected: "int unsigned",
 		},
 		{
@@ -42,7 +42,7 @@ func TestTypePrint(t *testing.T) {
 				T:          "float",
 				Attributes: []*TypeAttr{unsignedTypeAttr()},
 			},
-			typ:      &Type{T: "float", Attrs: []*Attr{LitAttr("unsigned", "true")}},
+			typ:      &Type{T: "float", Attrs: []*Attr{BoolAttr("unsigned", true)}},
 			expected: "float unsigned",
 		},
 		{
@@ -53,7 +53,7 @@ func TestTypePrint(t *testing.T) {
 					{Name: "size", Kind: reflect.Int, Required: true},
 				},
 			},
-			typ:      &Type{T: "varchar", Attrs: []*Attr{LitAttr("size", "255")}},
+			typ:      &Type{T: "varchar", Attrs: []*Attr{IntAttr("size", 255)}},
 			expected: "varchar(255)",
 		},
 	} {
@@ -171,34 +171,34 @@ func TestRegistryConvert(t *testing.T) {
 	}{
 		{
 			typ:      &schema.StringType{T: "varchar", Size: 255},
-			expected: &Type{T: "varchar", Attrs: []*Attr{LitAttr("size", "255")}},
+			expected: &Type{T: "varchar", Attrs: []*Attr{IntAttr("size", 255)}},
 		},
 		{
 			typ:      &schema.IntegerType{T: "int", Unsigned: true},
-			expected: &Type{T: "int", Attrs: []*Attr{LitAttr("unsigned", "true")}},
+			expected: &Type{T: "int", Attrs: []*Attr{BoolAttr("unsigned", true)}},
 		},
 		{
 			typ:      &schema.IntegerType{T: "int", Unsigned: true},
-			expected: &Type{T: "int", Attrs: []*Attr{LitAttr("unsigned", "true")}},
+			expected: &Type{T: "int", Attrs: []*Attr{BoolAttr("unsigned", true)}},
 		},
 		{
 			typ: &schema.DecimalType{T: "decimal", Precision: 10, Scale: 2}, // decimal(10,2)
 			expected: &Type{T: "decimal", Attrs: []*Attr{
-				LitAttr("precision", "10"),
-				LitAttr("scale", "2"),
+				IntAttr("precision", 10),
+				IntAttr("scale", 2),
 			}},
 		},
 		{
 			typ: &schema.DecimalType{T: "decimal", Precision: 10}, // decimal(10)
 			expected: &Type{T: "decimal", Attrs: []*Attr{
-				LitAttr("precision", "10"),
+				IntAttr("precision", 10),
 			}},
 		},
 		{
 			typ: &schema.DecimalType{T: "decimal", Scale: 2}, // decimal(0,2)
 			expected: &Type{T: "decimal", Attrs: []*Attr{
-				LitAttr("precision", "0"),
-				LitAttr("scale", "2"),
+				IntAttr("precision", 0),
+				IntAttr("scale", 2),
 			}},
 		},
 		{
@@ -208,7 +208,7 @@ func TestRegistryConvert(t *testing.T) {
 		{
 			typ: &schema.EnumType{T: "enum", Values: []string{"on", "off"}},
 			expected: &Type{T: "enum", Attrs: []*Attr{
-				ListAttr("values", `"on"`, `"off"`),
+				StringsAttr("values", "on", "off"),
 			}},
 		},
 		{
diff --git a/sql/internal/spectest/spectest.go b/sql/internal/spectest/spectest.go
index 034223c9..0cb27ca8 100644
--- a/sql/internal/spectest/spectest.go
+++ b/sql/internal/spectest/spectest.go
@@ -9,7 +9,6 @@ import (
 	"testing"
 
 	"ariga.io/atlas/schemahcl"
-	"ariga.io/atlas/sql/internal/specutil"
 	"ariga.io/atlas/sql/schema"
 
 	"github.com/hashicorp/hcl/v2/hclparse"
@@ -87,13 +86,13 @@ func dummyType(t *testing.T, ts *schemahcl.TypeSpec) *schemahcl.Type {
 		var a *schemahcl.Attr
 		switch attr.Kind {
 		case reflect.Int, reflect.Int64:
-			a = specutil.LitAttr(attr.Name, "2")
+			a = schemahcl.IntAttr(attr.Name, 2)
 		case reflect.String:
-			a = specutil.LitAttr(attr.Name, `"a"`)
+			a = schemahcl.StringAttr(attr.Name, "a")
 		case reflect.Slice:
-			a = specutil.ListAttr(attr.Name, `"a"`, `"b"`)
+			a = schemahcl.StringsAttr(attr.Name, "a", "b")
 		case reflect.Bool:
-			a = specutil.LitAttr(attr.Name, "false")
+			a = schemahcl.BoolAttr(attr.Name, false)
 		default:
 			t.Fatalf("unsupported kind: %s", attr.Kind)
 		}
diff --git a/sql/sqlcheck/datadepend/datadepend_test.go b/sql/sqlcheck/datadepend/datadepend_test.go
index d2f83907..fefad1fb 100644
--- a/sql/sqlcheck/datadepend/datadepend_test.go
+++ b/sql/sqlcheck/datadepend/datadepend_test.go
@@ -9,7 +9,6 @@ import (
 	"testing"
 
 	"ariga.io/atlas/schemahcl"
-	"ariga.io/atlas/sql/internal/specutil"
 
 	"ariga.io/atlas/sql/migrate"
 	"ariga.io/atlas/sql/schema"
@@ -226,7 +225,7 @@ func TestAnalyzer_Options(t *testing.T) {
 			{
 				Type: "data_depend",
 				Attrs: []*schemahcl.Attr{
-					specutil.BoolAttr("error", true),
+					schemahcl.BoolAttr("error", true),
 				},
 			},
 		},
diff --git a/sql/sqlcheck/destructive/destructive_test.go b/sql/sqlcheck/destructive/destructive_test.go
index e8cb5ad5..063a27b3 100644
--- a/sql/sqlcheck/destructive/destructive_test.go
+++ b/sql/sqlcheck/destructive/destructive_test.go
@@ -9,7 +9,6 @@ import (
 	"testing"
 
 	"ariga.io/atlas/schemahcl"
-	"ariga.io/atlas/sql/internal/specutil"
 	"ariga.io/atlas/sql/migrate"
 	"ariga.io/atlas/sql/schema"
 	"ariga.io/atlas/sql/sqlcheck"
@@ -168,7 +167,7 @@ func TestAnalyzer_DropSchema(t *testing.T) {
 			{
 				Type: "destructive",
 				Attrs: []*schemahcl.Attr{
-					specutil.BoolAttr("error", false),
+					schemahcl.BoolAttr("error", false),
 				},
 			},
 		},
@@ -235,7 +234,7 @@ func TestAnalyzer_DropColumn(t *testing.T) {
 			{
 				Type: "destructive",
 				Attrs: []*schemahcl.Attr{
-					specutil.BoolAttr("error", false),
+					schemahcl.BoolAttr("error", false),
 				},
 			},
 		},
diff --git a/sql/sqlite/sqlspec_test.go b/sql/sqlite/sqlspec_test.go
index 665eb4aa..9e021eb4 100644
--- a/sql/sqlite/sqlspec_test.go
+++ b/sql/sqlite/sqlspec_test.go
@@ -172,7 +172,6 @@ table "accounts" {
 			{SeqNo: 0, C: exp.Tables[1].Columns[0]},
 		},
 	}
-
 	var s schema.Schema
 	err := EvalHCLBytes([]byte(f), &s, nil)
 	require.NoError(t, err)