Repository: ariga/atlas
Pull request #2996 (2024-07-24 08:29:14)

doc/md: testing triggers guide


Patch:
diff --git a/README.md b/README.md
index ac28d9ff..f177b78a 100644
--- a/README.md
+++ b/README.md
@@ -299,6 +299,18 @@ Atlas offers additional commands to assist users managing their database migrati
 ### Supported databases
 MySQL, MariaDB, PostgresSQL, SQLite, TiDB, CockroachDB, SQL Server, ClickHouse, Redshift.
 
+## Supported Version Policy
+
+To ensure the best performance, security and compatibility with the Atlas Cloud service, the Atlas team
+will only support the two most recent minor versions of the CLI. For example, if the latest version is
+`v0.25`, the supported versions will be `v0.24` and `v0.25` (in addition to any patch releases and the
+"canary" release which is built twice a day).
+
+### Old Binaries
+
+As part of the *Supported Version Policy* mentioned above, binaries for versions that were published
+more than 6 months ago will be removed from the CDN and Docker Hub.
+
 [HCL]: https://atlasgo.io/atlas-schema/hcl
 [SQL]: https://atlasgo.io/atlas-schema/sql
 [ORM]: https://atlasgo.io/atlas-schema/external
diff --git a/cmd/atlas/doc.tmpl b/cmd/atlas/doc.tmpl
index 6691d0b8..caa34394 100644
--- a/cmd/atlas/doc.tmpl
+++ b/cmd/atlas/doc.tmpl
@@ -16,8 +16,22 @@ flags.
 For a more detailed introduction to the CLI capabilities, head over to the
 [Getting Started](/getting-started/) page.
 
+## Supported Version Policy
+
+To ensure the best performance, security and compatibility with the Atlas Cloud service, the Atlas team
+will only support the two most recent minor versions of the CLI. For example, if the latest version is
+`v0.25`, the supported versions will be `v0.24` and `v0.25` (in addition to any patch releases and the
+"canary" release which is built twice a day).
+
 ## Distributed Binaries
 
+:::info Old Versions
+
+As part of our **Supported Version Policy** mentioned above, binaries for versions that were published
+more than 6 months ago will be removed from the CDN and Docker Hub.
+
+:::
+
 The binaries and Docker images distributed in official releases are released in two flavors:
 
 * Atlas - A binary built from the OSS repository in addition to proprietary code maintained by
diff --git a/cmd/atlas/internal/cloudapi/client.go b/cmd/atlas/internal/cloudapi/client.go
index 8a3c3be6..ce744322 100644
--- a/cmd/atlas/internal/cloudapi/client.go
+++ b/cmd/atlas/internal/cloudapi/client.go
@@ -13,6 +13,7 @@ import (
 	"io"
 	"net/http"
 	"runtime"
+	"slices"
 	"strings"
 	"time"
 
@@ -347,6 +348,13 @@ func SetHeader(req *http.Request, token string) {
 }
 
 // UserAgent is the value the CLI uses in the User-Agent HTTP header.
-func UserAgent() string {
-	return fmt.Sprintf("Atlas/%s (%s/%s)", version, runtime.GOOS, runtime.GOARCH)
+func UserAgent(systems ...string) string {
+	sysInfo := runtime.GOOS + "/" + runtime.GOARCH
+	if len(systems) > 0 {
+		systems = slices.DeleteFunc(systems, func(s string) bool {
+			return strings.TrimSpace(s) == ""
+		})
+		sysInfo = strings.Join(slices.Insert(systems, 0, sysInfo), "; ")
+	}
+	return fmt.Sprintf("Atlas/%s (%s)", version, sysInfo)
 }
diff --git a/doc/md/getting-started/02-inspecting.mdx b/doc/md/getting-started/02-inspecting.mdx
deleted file mode 100644
index 32141b1a..00000000
--- a/doc/md/getting-started/02-inspecting.mdx
+++ /dev/null
@@ -1,264 +0,0 @@
----
-id: getting-started-inspection
-title: Inspecting Schemas
-slug: /cli/getting-started/inspection
----
-
-import Tabs from '@theme/Tabs';
-import TabItem from '@theme/TabItem';
-
-Atlas features a Data Definition Language (DDL) that has an [HCL-syntax](https://github.com/hashicorp/hcl)
-for defining the desired state of database schemas. In this section we will learn how to use the Atlas
-CLI to inspect an existing database and write it's schema in HCL to a file.
-
-Inspection is done via the `atlas schema inspect` command. To learn about its parameters, run:
-```shell
-atlas schema inspect --help
-```
-
-### Inspecting our database
-
-To inspect our locally-running MySQL instance from the [previous](/getting-started/) section,
-use the `-u` flag and write output to a file named `schema.hcl`:
-
-<Tabs
-defaultValue="mysql"
-values={[
-{label: 'MySQL', value: 'mysql'},
-{label: 'MariaDB', value: 'maria'},
-{label: 'PostgreSQL', value: 'postgres'},
-{label: 'SQLite', value: 'sqlite'},
-{label: 'SQL Server', value: 'sqlserver'},
-]}>
-<TabItem value="mysql">
-
-```shell
-atlas schema inspect -u "mysql://root:pass@localhost:3306/example" > schema.hcl
-```
-
-Then, view the contents of the file:
-
-```hcl title="schema.hcl"
-schema "example" {
-  charset = "utf8mb4"
-  collate = "utf8mb4_0900_ai_ci"
-}
-```
-
-</TabItem>
-<TabItem value="maria">
-
-```shell
-atlas schema inspect -u "maria://root:pass@localhost:3306/example" > schema.hcl
-```
-
-Then, view the contents of the file:
-
-```hcl title="schema.hcl"
-schema "example" {
-  charset = "utf8mb4"
-  collate = "utf8mb4_0900_ai_ci"
-}
-```
-
-</TabItem>
-<TabItem value="postgres">
-
-```shell
-atlas schema inspect -u "postgres://postgres:pass@localhost:5432/example?sslmode=disable" > schema.hcl
-```
-
-Then, view the contents of the file:
-
-```hcl title="schema.hcl"
-schema "public" {
-}
-```
-
-</TabItem>
-<TabItem value="sqlite">
-
-```shell
-atlas schema inspect -u "sqlite://file.db" > schema.hcl
-```
-
-Then, view the contents of the file:
-
-```hcl title="schema.hcl"
-schema "main" {
-}
-```
-
-</TabItem>
-<TabItem value="sqlserver">
-
-```shell
-atlas schema inspect -u "sqlserver://sa:Passw0rd@localhost:1433?database=master" > schema.hcl
-```
-
-Then, view the contents of the file:
-
-```hcl title="schema.hcl"
-schema "dbo" {
-}
-```
-
-</TabItem>
-</Tabs>
-
-As you can see, Atlas inspected our (empty) database and wrote an Atlas HCL document
-containing only a [Schema](../atlas-schema/hcl.mdx#schema) resource.
-
-Next, let's create some tables in our SQL database and see how they are reflected
-in the inspected Atlas HCL document.
-
-### Modifying our database schema manually
-
-In your database command-line prompt, create the tables:
-```sql
-CREATE table users (
-    id int PRIMARY KEY,
-    name varchar(100)
-);
-CREATE TABLE blog_posts (
-    id int PRIMARY KEY,
-    title varchar(100),
-    body text,
-    author_id int,
-    FOREIGN KEY (author_id) REFERENCES users(id)
-);
-```
-Observe that the tables are created successfully:
-```text
-Query OK, 0 rows affected (0.02 sec)
-```
-Our schema represents a highly simplified blogging system with a `users` table for the authors and a `blog_posts` table
-for the contents:
-
-![Blog ERD](https://atlasgo.io/uploads/images/blog-erd.png)
-
-### Inspecting table schemas with Atlas CLI
-
-Next, let's re-run our inspection command:
-
-
-<Tabs
-defaultValue="mysql"
-values={[
-{label: 'MySQL', value: 'mysql'},
-{label: 'MariaDB', value: 'maria'},
-{label: 'PostgreSQL', value: 'postgres'},
-{label: 'SQLite', value: 'sqlite'},
-{label: 'SQL Server', value: 'sqlserver'},
-]}>
-<TabItem value="mysql">
-
-```shell
-atlas schema inspect -u "mysql://root:pass@localhost:3306/example" > schema.hcl
-```
-
-</TabItem>
-<TabItem value="maria">
-
-```shell
-atlas schema inspect -u "maria://root:pass@localhost:3306/example" > schema.hcl
-```
-
-</TabItem>
-<TabItem value="postgres">
-
-```shell
-atlas schema inspect -u "postgres://postgres:pass@localhost:5432/example?sslmode=disable" > schema.hcl
-```
-
-</TabItem>
-<TabItem value="sqlite">
-
-```shell
-atlas schema inspect -u "sqlite://file.db" > schema.hcl
-```
-
-</TabItem>
-<TabItem value="sqlserver">
-
-```shell
-atlas schema inspect -u "sqlserver://sa:Passw0rd@localhost:1433?database=master" > schema.hcl
-```
-
-</TabItem>
-</Tabs>
-
-Browse through the updated contents of the file, it contains 3 blocks representing
-our schema as before, and two new blocks representing the `users` and `blog_posts` tables.
-Consider the following block:
-```hcl
-table "users" {
-  schema = schema.example
-  column "id" {
-    null = false
-    type = int
-  }
-  column "name" {
-    null = true
-    type = varchar(100)
-  }
-  primary_key {
-    columns = [column.id]
-  }
-}
-```
-This block represents a [Table](../atlas-schema/hcl.mdx#table) resource with `id`, and `name`
-columns. The `schema` field references the `example` schema that is defined elsewhere
-in this document. In addition, the `primary_key` sub-block defines the `id` column as
-the primary key for the table. In SQL databases, columns usually have a type attribute
-that defines the kind of data that can be stored in that column. Atlas strives to
-mimic the syntax of the database that the user is working against. In this case, the
-type for the `id` column is `int`, and `varchar(100)` for the `name` column. To
-see the full list of supported types, [click here](../atlas-schema/hcl-types.md).
-
-
-Next, consider this block:
-```hcl
-table "blog_posts" {
-  schema = schema.example
-  column "id" {
-    null = false
-    type = int
-  }
-  column "title" {
-    null = true
-    type = varchar(100)
-  }
-  column "body" {
-    null = true
-    type = text
-  }
-  column "author_id" {
-    null = true
-    type = int
-  }
-  primary_key {
-    columns = [column.id]
-  }
-  foreign_key "blog_posts_ibfk_1" {
-    columns     = [column.author_id]
-    ref_columns = [table.users.column.id]
-    on_update   = NO_ACTION
-    on_delete   = NO_ACTION
-  }
-  index "author_id" {
-    unique  = false
-    columns = [column.author_id]
-  }
-}
-```
-This block represents the `blog_posts` table. In addition to the elements
-we saw in the `users` table, here we can find a [Foreign Key](../atlas-schema/hcl.mdx#foreign-key)
-block, declaring that the `author_id` column references the `id` column on the
-`users` table.
-
-To learn more about the resource types that are available to describe SQL schemas,
-read the [SQL Syntax](../atlas-schema/hcl.mdx) documentation.
-
-In the next section, we will see how we can modify our database's schema by
-applying a modified Atlas HCL file using the Atlas CLI.
diff --git a/doc/md/getting-started/03-applying.mdx b/doc/md/getting-started/03-applying.mdx
deleted file mode 100644
index 2206388c..00000000
--- a/doc/md/getting-started/03-applying.mdx
+++ /dev/null
@@ -1,222 +0,0 @@
----
-id: getting-started-apply
-title: Applying Schemas
-slug: /cli/getting-started/applying-schemas
----
-
-import Tabs from '@theme/Tabs';
-import TabItem from '@theme/TabItem';
-
-### Declarative migrations
-
-In the previous section, we learned how to inspect an existing database and write
-its schema as an Atlas DDL HCL file. In this section, we will learn how to use
-the Atlas CLI to modify a database's schema. To do this, we will use Atlas's 
-`atlas schema apply` command which takes a _declarative_ approach, that is,
-we define the _desired_ end schema, and Atlas figures out a safe-way to alter
-the database to get there. 
-
-Let's start by viewing the help text for the `apply` command:
-
-```shell
-atlas schema apply --help
-```
-
-You can see that similar to the `inspect` command, the `-u` flag is used to define the
-URL to connect to the database, and an additional flag `-f` specifies the path to
-the file containing the desired schema. 
-
-### Adding new tables to our database
-
-Let's modify our simplified blogging platform schema from the previous step by adding
-a third table, `categories`. Each table will have an `id` and a `name`. In addition,
-we will create an association table `post_categories` which creates a many-to-many
-relationship between blog posts and categories:
-
-![Blog ERD](https://atlasgo.io/uploads/images/blog-erd-2.png)
-
-First, let's store the existing schema in a file named `schema.hcl`:
-
-<Tabs
-defaultValue="mysql"
-values={[
-{label: 'MySQL', value: 'mysql'},
-{label: 'MariaDB', value: 'maria'},
-{label: 'PostgreSQL', value: 'postgres'},
-{label: 'SQLite', value: 'sqlite'},
-{label: 'SQL Server', value: 'sqlserver'},
-]}>
-<TabItem value="mysql">
-
-```shell
-atlas schema inspect -u "mysql://root:pass@localhost:3306/example" > schema.hcl
-```
-
-</TabItem>
-<TabItem value="maria">
-
-```shell
-atlas schema inspect -u "maria://root:pass@localhost:3306/example" > schema.hcl
-```
-
-</TabItem>
-<TabItem value="postgres">
-
-```shell
-atlas schema inspect -u "postgres://postgres:pass@localhost:5432/example?sslmode=disable" > schema.hcl
-```
-
-</TabItem>
-<TabItem value="sqlite">
-
-```shell
-atlas schema inspect -u "sqlite://file.db" > schema.hcl
-```
-
-</TabItem>
-<TabItem value="sqlserver">
-
-```shell
-atlas schema inspect -u "sqlserver://sa:Passw0rd0995@localhost:1433?database=master" > schema.hcl
-```
-
-</TabItem>
-
-</Tabs>
-
-Next, add the following table definition to the file:
-```hcl
-table "categories" {
-  schema = schema.example
-  column "id" {
-    null = false
-    type = int
-  }
-  column "name" {
-    null = true
-    type = varchar(100)
-  }
-  primary_key {
-    columns = [column.id]
-  }
-}
-```
-
-To add this table to our database, let's use the `atlas schema apply` command:
-
-
-<Tabs
-defaultValue="mysql"
-values={[
-{label: 'MySQL', value: 'mysql'},
-{label: 'MariaDB', value: 'maria'},
-{label: 'PostgreSQL', value: 'postgres'},
-{label: 'SQLite', value: 'sqlite'},
-{label: 'SQL Server', value: 'sqlserver'},
-]}>
-<TabItem value="mysql">
-
-```shell
-atlas schema apply -u "mysql://root:pass@localhost:3306/example" -f schema.hcl
-```
-
-</TabItem>
-<TabItem value="maria">
-
-```shell
-atlas schema apply -u "maria://root:pass@localhost:3306/example" -f schema.hcl
-```
-
-</TabItem>
-<TabItem value="postgres">
-
-```shell
-atlas schema apply -u "postgres://postgres:pass@localhost:5432/example?sslmode=disable" -f schema.hcl
-```
-
-</TabItem>
-<TabItem value="sqlite">
-
-```shell
-atlas schema apply -u "sqlite://file.db" -f schema.hcl
-```
-
-</TabItem>
-<TabItem value="sqlserver">
-
-```shell
-atlas schema apply -u "sqlserver://sa:Passw0rd0995@localhost:1433?database=master" -f schema.hcl
-```
-
-</TabItem>
-</Tabs>
-
-Atlas plans a migration (schema change) for us and prompts us to approve it:
-```text
--- Planned Changes:
--- Create "categories" table
-CREATE TABLE `example`.`categories` (`id` int NOT NULL, `name` varchar(100) NULL, PRIMARY KEY (`id`))
-Use the arrow keys to navigate: ↓ ↑ → ←
-? Are you sure?:
-  ▸ Apply
-    Abort
-```
-To apply the migration, press `ENTER`, and voila!
-```text
-✔ Apply
-```
-
-To verify that our new table was created, open the database command line tool from previous step and run:
-
-```text
-mysql> show create table categories;
-+------------+------------------------------------------------------+
-| Table      | Create Table                                                                                                                                                                 |
-+------------+------------------------------------------------------+
-| categories | CREATE TABLE `categories` (
-  `id` int NOT NULL,
-  `name` varchar(100) DEFAULT NULL,
-  PRIMARY KEY (`id`)
-) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci |
-+------------+------------------------------------------------------
-1 row in set (0.01 sec)
-```
-
-Amazing! Our new table was created. Next, let's define our association table,
-add the following block to our `schema.hcl` file:
-```hcl
-table "post_categories" {
-    schema = schema.example
-    column "post_id" {
-        type = int
-    }
-    column "category_id" {
-        type = int
-    }
-    foreign_key "post_category_post" {
-        columns     = [column.post_id]
-        ref_columns = [table.blog_posts.column.id]
-    }
-    foreign_key "post_category_category" {
-        columns     = [column.category_id]
-        ref_columns = [table.categories.column.id]
-    }
-}
-```
-This block defines the `post_categories` table with two columns `post_id` and `category_id`. 
-In addition, two foreign-keys are created referencing the respective columns on the `blog_posts`
-and `categories` tables. 
-
-Let's try to apply the schema again, this time with the updated schema:
-```text
-atlas schema apply -u "mysql://root:pass@localhost:3306/example" -f schema.hcl
--- Planned Changes:
--- Create "post_categories" table
-CREATE TABLE `example`.`post_categories` (`post_id` int NOT NULL, `category_id` int NOT NULL, CONSTRAINT `post_category_post` FOREIGN KEY (`post_id`) REFERENCES `example`.`blog_posts` (`id`), CONSTRAINT `post_category_category` FOREIGN KEY (`category_id`) REFERENCES `example`.`categories` (`id`))
-✔ Apply
-```
-
-### Conclusion 
-
-In this section, we've seen how to use the `atlas schema apply` command to migrate
-the schema of an existing database to our desired state.
diff --git a/doc/md/reference.md b/doc/md/reference.md
index 32f1019d..ee41f85a 100644
--- a/doc/md/reference.md
+++ b/doc/md/reference.md
@@ -13,8 +13,22 @@ flags.
 For a more detailed introduction to the CLI capabilities, head over to the
 [Getting Started](/getting-started/) page.
 
+## Supported Version Policy
+
+To ensure the best performance, security and compatibility with the Atlas Cloud service, the Atlas team
+will only support the two most recent minor versions of the CLI. For example, if the latest version is
+`v0.25`, the supported versions will be `v0.24` and `v0.25` (in addition to any patch releases and the
+"canary" release which is built twice a day).
+
 ## Distributed Binaries
 
+:::info Old Versions
+
+As part of our **Supported Version Policy** mentioned above, binaries for versions that were published
+more than 6 months ago will be removed from the CDN and Docker Hub.
+
+:::
+
 The binaries and Docker images distributed in official releases are released in two flavors:
 
 * Atlas - A binary built from the OSS repository in addition to proprietary code maintained by
@@ -369,7 +383,7 @@ atlas migrate new [flags] [name]
 
 ### atlas migrate push
 
-Push a migration directory with an optional tag to Atlas
+Push a migration directory with an optional tag to the Atlas registry
 
 #### Usage
 ```
@@ -388,6 +402,7 @@ atlas migrate push [flags] directory[:tag]
       --dir string              select migration directory using URL format (default "file://migrations")
       --dir-format string       select migration file format (default "atlas")
       --lock-timeout duration   set how long to wait for the database lock (default 10s)
+      --tag string              tag to push the directory with
 
 ```
 
diff --git a/doc/website/docusaurus.config.js b/doc/website/docusaurus.config.js
index 1b86be57..36092b08 100644
--- a/doc/website/docusaurus.config.js
+++ b/doc/website/docusaurus.config.js
@@ -182,6 +182,14 @@ module.exports = {
             from: '/ui/intro',
             to: '/cloud/getting-started',
           },
+          {
+            "to": "/declarative/inspect",
+            "from": "/cli/getting-started/inspection",
+          },
+          {
+            "to": "/declarative/apply",
+            "from": "/cli/getting-started/applying-schemas",
+          },
         ],
       },
     ],
diff --git a/doc/website/sidebars.js b/doc/website/sidebars.js
index db1cf9bf..e3573023 100644
--- a/doc/website/sidebars.js
+++ b/doc/website/sidebars.js
@@ -320,6 +320,11 @@ module.exports = {
                     type: 'doc',
                     id: 'guides/testing/testing-procedures',
                     label: 'Testing Stored Procedures'
+                },
+                {
+                    type: 'doc',
+                    id: 'guides/testing/testing-triggers',
+                    label: 'Testing Triggers'
                 }
             ]
         },
diff --git a/sql/internal/specutil/convert.go b/sql/internal/specutil/convert.go
index 5273ac03..852d8211 100644
--- a/sql/internal/specutil/convert.go
+++ b/sql/internal/specutil/convert.go
@@ -940,6 +940,13 @@ func ConvertGenExpr(r *schemahcl.Resource, c *schema.Column, t func(string) stri
 
 // ColumnDefault converts the column default into cty.Value.
 func ColumnDefault(c *schema.Column) (cty.Value, error) {
+	var textlike bool
+	if c.Type != nil {
+		switch c.Type.Type.(type) {
+		case *schema.StringType, *schema.EnumType:
+			textlike = true
+		}
+	}
 	switch x := schema.UnderlyingExpr(c.Default).(type) {
 	case nil:
 		return cty.NilVal, nil
@@ -964,7 +971,7 @@ func ColumnDefault(c *schema.Column) (cty.Value, error) {
 				return cty.NilVal, err
 			}
 			return cty.NumberFloatVal(f), nil
-		case sqlx.IsLiteralNumber(x.V):
+		case sqlx.IsLiteralNumber(x.V) && !textlike:
 			switch i, err := strconv.ParseInt(x.V, 10, 64); {
 			case errors.Is(err, strconv.ErrRange):
 				u, err := strconv.ParseUint(x.V, 10, 64)
@@ -978,14 +985,12 @@ func ColumnDefault(c *schema.Column) (cty.Value, error) {
 				return cty.NumberIntVal(i), nil
 			}
 		default:
-			switch c.Type.Type.(type) {
 			// Literal values (non-expressions) are returned
 			// as strings for text-like types.
-			case *schema.StringType, *schema.EnumType:
+			if textlike {
 				return cty.StringVal(x.V), nil
-			default:
-				return cty.NilVal, fmt.Errorf("unsupported literal value %s for column %s", x.V, c.Name)
 			}
+			return cty.NilVal, fmt.Errorf("unsupported literal value %s for column %s", x.V, c.Name)
 		}
 	default:
 		return cty.NilVal, fmt.Errorf("converting expr %T to literal value for column %s", x, c.Name)


Test patch:
diff --git a/cmd/atlas/internal/cloudapi/client_test.go b/cmd/atlas/internal/cloudapi/client_test.go
index ff219d36..fd3dc9be 100644
--- a/cmd/atlas/internal/cloudapi/client_test.go
+++ b/cmd/atlas/internal/cloudapi/client_test.go
@@ -148,3 +148,10 @@ func TestRedactedURL(t *testing.T) {
 	require.EqualError(t, err, `first path segment in URL cannot contain colon`)
 	require.Empty(t, u)
 }
+
+func TestUserAgent(t *testing.T) {
+	platform := runtime.GOOS + "/" + runtime.GOARCH
+	require.Equal(t, fmt.Sprintf("Atlas/%s (%s)", version, platform), UserAgent())
+	require.Equal(t, fmt.Sprintf("Atlas/%s (%s; foo/bar; bar/baz)", version, platform), UserAgent("foo/bar", "bar/baz"))
+	require.Equal(t, fmt.Sprintf("Atlas/%s (%s; bar/baz)", version, platform), UserAgent("  ", "", "bar/baz"))
+}
diff --git a/doc/md/guides/testing/testing-triggers.mdx b/doc/md/guides/testing/testing-triggers.mdx
new file mode 100644
index 00000000..ec16c28d
--- /dev/null
+++ b/doc/md/guides/testing/testing-triggers.mdx
@@ -0,0 +1,226 @@
+---
+id: testing-triggers
+title: Testing Database Triggers
+slug: /guides/testing/triggers
+---
+
+Testing your database schema and migrations is crucial to ensure code behaves as expected, catch bugs early,
+and prevent regressions. Databases enforce logic, constraints, and complex relationships,
+so testing ensures these elements work correctly and remain intact after changes.
+
+In this guide we will learn how to use Atlas's [`schema test`](/testing/schema) command to test database triggers.
+
+## Triggers
+
+Database triggers are sets of automated instructions that execute in response to specific events, such as INSERT, UPDATE, or
+DELETE operations on a table, to enforce business rules or maintain data integrity.
+
+:::info [Atlas Pro Feature](/features#pro-plan)
+Triggers are currently available only to [Atlas Pro users](/features#pro-plan). To use this feature, run:
+```
+atlas login
+```
+:::
+
+## Project Setup
+
+### Schema File
+For this example, let's assume we have the following schema, including a trigger:
+
+```hcl title="schema.hcl"
+schema "public" {}
+table "products" {
+  schema = schema.public
+  column "id" {
+    null = false
+    type = integer
+  }
+  column "price" {
+    null = true
+    type = numeric
+  }
+  primary_key {
+    columns = [column.id]
+  }
+}
+table "products_audit" {
+  schema = schema.public
+  column "product_id" {
+    null = true
+    type = integer
+  }
+  column "old_price" {
+    null = true
+    type = numeric
+  }
+  column "new_price" {
+    null = true
+    type = numeric
+  }
+  column "changed_at" {
+    null    = true
+    type    = timestamp
+    default = sql("CURRENT_TIMESTAMP")
+  }
+}
+function "log_price_changes" {
+  schema = schema.public
+  lang   = PLpgSQL
+  return = trigger
+  as     = <<-SQL
+  BEGIN
+      IF OLD.price IS DISTINCT FROM NEW.price THEN
+          INSERT INTO products_audit (product_id, old_price, new_price, changed_at)
+          VALUES (OLD.id, OLD.price, NEW.price, NOW());
+  END IF;
+  RETURN NEW;
+  END;
+  SQL
+}
+// highlight-start
+trigger "log_price_changes_trigger" {
+  on = table.products
+  after {
+    update = true
+  }
+  for  = ROW
+  when = "(old.price IS DISTINCT FROM new.price)"
+  execute {
+    function = function.log_price_changes
+  }
+}
+// highlight-end
+```
+
+In the schema above, the `products` table stores product details and prices, while the `products_audit` table logs any changes
+to product prices. The `log_price_changes_trigger` activates the `log_price_changes` function after an update to the price
+column in the products table, recording the old and new prices along with the timestamp of the change in the `products_audit` table.
+
+### Config File
+
+Before we begin testing, create a [config file](/atlas-schema/projects#project-files) named
+`atlas.hcl`.
+
+In this file we will create an environment, specify the source of our schema,
+and a URL for our [dev database](/concepts/dev-database).
+
+We will also create a file named `schema.test.hcl` to write our tests, and
+add it to the `atlas.hcl` file in the test block.
+
+```hcl title="atlas.hcl"
+env "dev" {
+  src = "file://schema.hcl"
+  dev = "docker://postgres/15/dev?search_path=public"
+
+  # Test configuration for local development.
+  test {
+    schema {
+      src = ["schema.test.hcl"]
+    }
+  }
+}
+```
+
+## Writing Tests
+
+### Simple Test
+
+Let's start off with a simple test that will:
+1. Seed data into `products`.
+2. Verify that the `products_audit` table is empty.
+3. Update the price of a product in the `products` table.
+4. Check that `producs_audit` was updated with the new data.
+
+```hcl title="schema.test.hcl"
+test "schema" "trigger" {
+  # Seed data
+  exec {
+    sql = "INSERT INTO products (id, price) VALUES (1, 15.00), (2, 22.99), (3, 13.50);"
+  }
+  # Verify products_audit table empty
+  exec {
+    sql = "SELECT COUNT(*) FROM products_audit"
+    output = "0"
+  }
+  exec {
+    sql = "UPDATE products SET price = 19.99 WHERE id = 1;"
+  }
+  exec {
+    sql = "SELECT product_id, old_price, new_price FROM products_audit"
+    format = table
+    output = <<TAB
+ product_id | old_price | new_price
+------------+-----------+-----------
+ 1          | 15        | 19.99
+TAB
+  }
+}
+```
+
+Run the test by running:
+
+```bash
+atlas schema test --env dev
+```
+
+The output should look similar to:
+```testoutput title="Test Output"
+-- PASS: trigger (4ms)
+PASS
+```
+
+### Table Driven Test
+
+Another alternative is to write a [table driven test](/testing/schema#table-driven-tests). This
+test uses the `for_each` meta-argument, which accepts a map or a set of values and is used to generate
+a test case for each item in the set or map.
+
+Following similar logic to the test above, we will execute the following:
+1. Seed data into `products`.
+2. Verify that the `products_audit` table is empty.
+3. Update the price of two products in the `products` table, using the `for_each` meta-argument.
+4. Check that `producs_audit` was updated with the new data.
+
+```hcl title="schema.test.hcl"
+test "schema" "trigger" {
+  # Seed data
+  exec {
+    sql = "INSERT INTO products (id, price) VALUES (1, 15.00), (2, 22.99), (3, 13.50);"
+  }
+  # Verify products_audit table empty
+  exec {
+    sql = "SELECT COUNT(*) FROM products_audit"
+    output = "0"
+  }
+  for_each = [
+    {product_id: "1", new_price: "19.99"},
+    {product_id: "2", new_price: "20"}
+  ]
+  exec {
+    sql = "UPDATE products SET price = ${each.value.new_price} WHERE id = ${each.value.product_id};"
+  }
+  exec {
+    sql = "SELECT new_price FROM products_audit where product_id = ${each.value.product_id}"
+    output = each.value.new_price
+  }
+  log {
+    message = "Testing for ID:  ${each.value.product_id} - New Price: ${each.value.new_price}"
+  }
+}
+```
+
+Run the test by running:
+
+```bash
+atlas schema test --env dev
+```
+
+The output should look similar to:
+```testoutput title="Test Output"
+-- PASS: trigger/1 (4ms)
+    schema.test.hcl:50: Testing for ID:  1 - New Price: 19.99
+-- PASS: trigger/2 (1ms)
+    schema.test.hcl:50: Testing for ID:  2 - New Price: 20
+PASS
+```
+
diff --git a/sql/internal/specutil/convert_test.go b/sql/internal/specutil/convert_test.go
index c4fb8dea..e9f33454 100644
--- a/sql/internal/specutil/convert_test.go
+++ b/sql/internal/specutil/convert_test.go
@@ -146,6 +146,19 @@ func TestDefault(t *testing.T) {
 	}
 }
 
+func TestColumnDefault_LiteralNumber(t *testing.T) {
+	v, err := ColumnDefault(
+		schema.NewColumn("").SetDefault(&schema.Literal{V: "0"}),
+	)
+	require.NoError(t, err)
+	require.True(t, cty.NumberIntVal(0).Equals(v).True())
+	v, err = ColumnDefault(
+		schema.NewStringColumn("", "text").SetDefault(&schema.Literal{V: "0"}),
+	)
+	require.NoError(t, err)
+	require.True(t, cty.StringVal("0").Equals(v).True())
+}
+
 func TestFromView(t *testing.T) {
 	spec, err := FromView(&schema.View{
 		Name:   "view",