Repository: ariga/atlas
Pull request #2483 (2024-01-26 11:06:43)

doc/md: add django provider guide


Patch:
diff --git a/doc/md/components/card.tsx b/doc/md/components/card.tsx
index fcb51e51..e4e75199 100644
--- a/doc/md/components/card.tsx
+++ b/doc/md/components/card.tsx
@@ -24,6 +24,7 @@ export enum CardImage {
     Postgres = "postgres",
     Schema = "schema",
     SQLite = "sqlite",
+    SQLServer = "sqlserver",
     Terraform = "terraform",
     Testing = "testing",
     Tools = "tools",
diff --git a/doc/md/guides/clickhouse.mdx b/doc/md/guides/clickhouse.mdx
index e55b2a92..8a2d2771 100644
--- a/doc/md/guides/clickhouse.mdx
+++ b/doc/md/guides/clickhouse.mdx
@@ -1,6 +1,6 @@
 ---
 id: getting-started-clickhouse
-title: Manage ClickHouse with Atlas
+title: Automatic Migrations for ClickHouse with Atlas
 slug: /guides/clickhouse
 tags: [clickhouse]
 ---
@@ -27,9 +27,8 @@ In this guide, we will dive into setting up Atlas for ClickHouse, and introduce
 
 ## Prerequisites
 
-1. A ClickHouse database
-2. Docker
-3. Atlas installed on your machine:
+1. Docker
+2. Atlas installed on your machine:
 <InstallationInstructions />
 
 ## Logging in to Atlas
@@ -167,8 +166,9 @@ atlas schema inspect -u "clickhouse://localhost:9000/example" -w
 
 ## Declarative Migrations
 
-The declarative approach allows users to manage their database schemas by simply defining the desired end schema, and
-Atlas provides a safe way to alter the database to get there. Let's see this in action.
+The declarative approach lets users manage schemas by defining the desired state of the database as code.
+Atlas then inspects the target database and calculates an execution plan to reconcile the difference between the desired and actual states.
+Let's see this in action.
 
 We will start off by making a change to our schema file, such as adding a `repos` table:
 
@@ -267,7 +267,7 @@ atlas schema apply \
 --to file://schema.sql \
 --dev-url "docker://clickhouse/23.11/demo"
 ```
-  
+
 </TabItem>
 </Tabs>
 
@@ -527,7 +527,7 @@ CREATE TABLE `commits` (
   `author_id` Bigint NOT NULL,
 )  ENGINE = MergeTree
    PRIMARY KEY (`id`)
-   SETTINGS index_granularity = 8192;;
+   SETTINGS index_granularity = 8192;
 // highlight-end
 ```
 </TabItem>
@@ -557,7 +557,7 @@ atlas migrate diff add_commits \
 </TabItem>
 </Tabs>
 
-Run `ls migrations`, and you'll notice that a new migration has been generated.
+Run `ls migrations`, and you'll notice that a new migration file has been generated.
 
 ```sql title="20240130141055.sql"
 ALTER TABLE `repos` ADD COLUMN `description` Nullable(String);
diff --git a/doc/md/guides/deploying/k8s-operator-certs.mdx b/doc/md/guides/deploying/k8s-operator-certs.mdx
new file mode 100644
index 00000000..59fe593e
--- /dev/null
+++ b/doc/md/guides/deploying/k8s-operator-certs.mdx
@@ -0,0 +1,63 @@
+---
+id: k8s-operator-certs
+title: Using SSL Certs with the Atlas Operator
+slug: /guides/deploying/k8s-operator-certs
+---
+Many modern databases support SSL/TLS encryption for secure communication between clients and the database.
+In this document we provide some basic guidance on how to use SSL/TLS certificates with the
+[Atlas Operator](/integrations/kubernetes/operator) on Kubernetes.
+
+## Step 1: Create a Secret for the SSL/TLS Certificates
+
+The first step is to create a Kubernetes Secret that contains the SSL/TLS certificates. If you are using
+a Kubernetes Operator that supports automatically creating certificates such as the
+[CockroachDB Operator](https://github.com/cockroachdb/cockroach-operator), you can use the certificates
+created by that Operator.
+
+Here is an example of how to create a Secret with SSL/TLS certificates:
+
+```shell
+kubectl create secret generic my-secret \
+  --from-file=ca.crt=./path/to/ca.crt \
+  --from-file=tls.key=./path/to/tls.key \
+  --from-file=tls.crt=./path/to/tls.crt
+```
+
+This will create a Secret named `my-secret` with the SSL/TLS certificates.
+
+## Step 2: Mount the Certificates into the Atlas Operator
+
+The next step is to mount the SSL/TLS certificates into the Atlas Operator. To do this, by create
+a file named `values.yaml` with the following content:
+
+```yaml
+extraVolumes:
+   - name: certs
+     secret:
+       secretName: my-secret
+       defaultMode: 0640
+extraVolumeMounts:
+   - name: certs
+     mountPath: /certs
+     readOnly: true
+```
+
+Now, install the operator using this `values.yaml` file:
+
+```shell
+helm install atlas-operator oci://ghcr.io/ariga/charts/atlas-operator -f values.yaml
+```
+
+This will install the Atlas Operator, overriding the `extraVolumes` and `extraVolumeMounts` values to mount the
+SSL/TLS certificates into the Operator.
+
+## Step 3: Use the Certificates in the Database URL
+
+The final step is to use the SSL/TLS certificates in the database [URL](/concepts/url). For example, if you are using the
+PostgreSQL or CockroachDB databases, you can use the following database URL:
+
+```shell
+postgresql://username@hostname:port/database?sslmode=verify-full&sslcert=/certs/tls.crt&sslkey=/certs/tls.key&sslrootcert=/certs/ca.crt
+```
+
+To learn more about how to securely provide the database URL to the operator, see the [docs](/integrations/kubernetes/operator#providing-credentials).
\ No newline at end of file
diff --git a/doc/md/guides/guides.mdx b/doc/md/guides/guides.mdx
index e8008a4b..d7ca3105 100644
--- a/doc/md/guides/guides.mdx
+++ b/doc/md/guides/guides.mdx
@@ -12,7 +12,7 @@ import { Card, CardImage } from "../components/card";
 
 <div className="row">
   <Card
-    name="Get Started with ClickHouse"
+    name="Automatic Migrations for ClickHouse"
     description="This guide is for getting started with ClickHouse using Atlas"
     url="/guides/clickhouse"
     image={CardImage.ClickHouse}
@@ -280,6 +280,23 @@ import { Card, CardImage } from "../components/card";
     url={"/guides/orms/sqlalchemy"}
     image={CardImage.Tools}
   />
+  <Card
+    name="Django"
+    description="Learn about automatic migration planning for Django using Atlas"
+    url="/guides/orms/django"
+    image={CardImage.Tools}
+  />
+</div>
+
+## SQL Server
+
+<div className="row">
+  <Card
+    name="Automatic Migrations for SQL Server"
+    description="This guide is for getting started with SQL Server using Atlas"
+    url="/guides/mssql"
+    image={CardImage.SQLServer}
+  />
 </div>
 
 ## Terraform
diff --git a/doc/md/guides/mssql.mdx b/doc/md/guides/mssql.mdx
new file mode 100644
index 00000000..8cc4ce14
--- /dev/null
+++ b/doc/md/guides/mssql.mdx
@@ -0,0 +1,617 @@
+---
+id: getting-started-mssql
+title: Automatic Migrations for SQL Server with Atlas
+slug: /guides/mssql
+tags: [mssql]
+---
+
+import InstallationInstructions from '../components/_installation_instructions.mdx';
+import Tabs from '@theme/Tabs';
+import TabItem from '@theme/TabItem';
+
+[Microsoft SQL Server](https://www.microsoft.com/en-us/sql-server/) is a powerful relational database management
+system that has one of the prominent enterprise-grade data solutions for decades. Commonly used by enterprises, SQL Server
+can efficiently handle growing amounts of data and users, making it easy to scale.
+
+However, managing a large database schema in SQL Server can be challenging due to the complexity of related data
+structures and the need for coordinated schema changes across multiple teams and applications.
+
+#### Enter: Atlas
+
+Atlas helps developers manage their database schema as code - abstracting away the intricacies of database schema
+management. With Atlas, users provide the desired state of the database schema and Atlas automatically plans the
+required migrations.
+
+In this guide, we will dive into setting up Atlas for SQL Server, and introduce the different workflows available.
+
+## Prerequisites
+
+1. Docker
+2. Atlas installed on your machine:
+<InstallationInstructions />
+
+## Logging in to Atlas
+
+To use SQL Server with Atlas, you'll need to [log in to Atlas](https://auth.atlasgo.cloud/signup). If it's your first time,
+you will be prompted to create both an account and a workspace (organization):
+
+<Tabs>
+<TabItem value="web" label="Via Web">
+
+```shell
+atlas login
+```
+
+</TabItem>
+<TabItem value="token" label="Via Token">
+
+```shell
+atlas login --token "ATLAS_TOKEN"
+```
+
+</TabItem>
+<TabItem value="env-var" label="Via Environment Variable">
+
+```shell
+ATLAS_TOKEN="ATLAS_TOKEN" atlas login
+```
+
+</TabItem>
+</Tabs>
+
+## Inspecting our Database
+
+Let's start off by spinning up a database using Docker:
+
+```bash
+docker run --rm -e 'ACCEPT_EULA=Y' -e 'MSSQL_SA_PASSWORD=P@ssw0rd0995' -p 1433:1433 --name atlas-demo -d mcr.microsoft.com/mssql/server:latest
+```
+
+For this example we will begin with a minimal database with a `users` table and an `id` as the primary key.
+
+```sql
+CREATE TABLE users (
+  id bigint PRIMARY KEY,
+  name varchar(255) NOT NULL
+)
+```
+
+To create this on our SQL Server database, run the following command:
+
+```bash
+docker exec -it atlas-demo /opt/mssql-tools/bin/sqlcmd -S localhost -U SA -P 'P@ssw0rd0995' -Q "CREATE TABLE users (ID bigint PRIMARY KEY, name varchar(255) NOT NULL);"
+```
+
+The `atlas schema inspect` command supports reading the database description provided by a [URL](/concepts/url) and outputting it in
+different formats, including [Atlas DDL](/atlas-schema/hcl.mdx) (default), SQL, and JSON. In this guide, we will
+demonstrate the flow using both the Atlas DDL and SQL formats, as the JSON format is often used for processing the
+output using `jq`.
+
+
+<Tabs>
+<TabItem value="hcl" label="Atlas DDL (HCL)" default>
+
+To inspect our locally-running SQL Server instance, use the `-u` flag and write the output to a file named `schema.hcl`:
+
+```shell
+atlas schema inspect -u "sqlserver://SA:P@ssw0rd0995@localhost:1433?database=master&mode=database > schema.hcl
+```
+
+Open the `schema.hcl` file to view the Atlas schema that describes our database.
+
+```hcl title="schema.hcl"
+table "users" {
+  schema = schema.dbo
+  column "ID" {
+    type = bigint
+    null = false
+  }
+  column "name" {
+    type = varchar(255)
+    null = false
+  }
+  primary_key {
+    columns = [column.ID]
+  }
+}
+schema "dbo" {
+}
+```
+
+This first block represents a [table](/atlas-schema/hcl.mdx#table) resource with `id`, and `name`
+columns. The `schema` field references the `dbo` schema that is defined in the block below. In addition, the `primary_key`
+sub-block defines the `id` column as the primary key for the table. 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 `bigint`, and `varchar(255)` for the `name` column.
+
+</TabItem>
+<TabItem value="sql" label="SQL">
+
+To inspect our locally-running SQL Server instance, use the `-u` flag and write the output to a file named `schema.sql`:
+
+```shell
+atlas schema inspect -u "sqlserver://SA:P@ssw0rd0995@localhost:1433?database=master&mode=database" --format '{{ sql . }}' > schema.sql
+```
+
+Open the `schema.sql` file to view the inspected SQL schema that describes our database.
+
+```sql title="schema.sql"
+CREATE TABLE [dbo].[users] (
+    [ID] bigint NOT NULL,
+    [name] varchar(255) COLLATE SQL_Latin1_General_CP1_CI_AS NOT NULL,
+    CONSTRAINT [PK_users] PRIMARY KEY CLUSTERED ([ID] ASC)
+);
+```
+
+</TabItem>
+</Tabs>
+
+
+:::info
+For in-depth details on the `atlas schema inspect` command, covering aspects like inspecting specific schemas,
+handling multiple schemas concurrently, excluding tables, and more, refer to our documentation
+[here](/declarative/inspect).
+:::
+
+To generate an Entity Relationship Diagram (ERD), or a visual representation of our schema, we can add the `-w` flag
+to the inspect command:
+
+```shell
+atlas schema inspect -u "sqlserver://SA:P@ssw0rd0995@localhost:1433?database=master&mode=database" -w
+```
+
+![mssql-inspect](https://atlasgo.io/uploads/mssql/images/schema-inspect-hcl.png)
+
+## Declarative Migrations
+
+The declarative approach lets users manage schemas by defining the desired state of the database as code.
+Atlas then inspects the target database and calculates an execution plan to reconcile the difference between the desired and actual states.  Let's see this in action.
+
+We will start off by making a change to our schema file, such as adding a `repos` table:
+
+<Tabs>
+<TabItem value="hcl" label="Atlas DDL (HCL)" default>
+
+```hcl title=schema.hcl
+table "users" {
+  schema = schema.dbo
+  column "ID" {
+    type = bigint
+    null = false
+  }
+  column "name" {
+    type = varchar(255)
+    null = false
+  }
+  primary_key {
+    columns = [column.ID]
+  }
+}
+
+// highlight-start
+table "repos" {
+  schema = schema.dbo
+  column "ID" {
+    type = bigint
+    null = false
+  }
+  column "name" {
+    type = varchar(255)
+    null = false
+  }
+  column "owner_id" {
+    type = bigint
+    null = false
+  }
+  primary_key {
+    columns = [column.ID]
+  }
+  foreign_key "fk_repo_owner" {
+    columns     = [column.owner_id]
+    ref_columns = [table.users.column.ID]
+  }
+}
+schema "dbo" {
+}
+// highlight-end
+```
+
+</TabItem>
+<TabItem value="sql" label="SQL">
+
+```sql title="schema.sql"
+-- Create "users" table
+CREATE TABLE [dbo].[users] (
+    [ID] bigint NOT NULL,
+    [name] varchar(255) NOT NULL,
+    CONSTRAINT [PK_users] PRIMARY KEY CLUSTERED ([ID] ASC)
+)
+
+// highlight-start
+-- Create "repos" table
+CREATE TABLE [dbo].[repos] (
+    [ID] bigint NOT NULL,
+    [name] varchar(255) NOT NULL,
+    [owner_id] bigint,
+    CONSTRAINT [PK_repos] PRIMARY KEY CLUSTERED ([ID] ASC),
+    CONSTRAINT [fk_repo_owner] FOREIGN KEY ([owner_id]) REFERENCES [dbo].[users]([ID])
+);
+ //highlight-end
+```
+
+</TabItem>
+</Tabs>
+
+Now that our _desired state_ has changed, to apply these changes to our database, Atlas will plan a migration for us
+by running the `atlas schema apply` command:
+
+<Tabs>
+<TabItem value="hcl" label="Atlas DDL (HCL)" default>
+
+```shell
+atlas schema apply \
+-u "sqlserver://SA:P@ssw0rd0995@localhost:1433?database=master&mode=database" \
+--to file://schema.hcl \
+--dev-url "docker://sqlserver/2022-latest/dev?mode=database"
+```
+
+</TabItem>
+<TabItem value="sql" label="SQL">
+
+```shell
+atlas schema apply \
+-u "sqlserver://SA:P@ssw0rd0995@localhost:1433?database=master&mode=database" \
+--to file://schema.sql \
+--dev-url "docker://sqlserver/2022-latest/dev?mode=database"
+```
+
+</TabItem>
+</Tabs>
+
+Apply the changes, and that's it! You have successfully run a declarative migration.
+
+:::info
+For a more detailed description of the `atlas schema apply` command refer to our documentation
+[here](/declarative/apply).
+:::
+
+To ensure that the changes have been made to the schema, let's run the `inspect` command with the `-w` flag once more
+and view the ERD:
+
+![atlas-schema](https://atlasgo.io/uploads/mssql/images/schema-inspect-repos.png)
+
+## Versioned Migrations
+
+Alternatively, the versioned migration workflow, sometimes called "change-based migrations", allows each change to the
+database schema to be checked-in to source control and reviewed during code-review. Users can still benefit from Atlas
+intelligently planning migrations for them, however they are not automatically applied.
+
+### Creating the first migration
+
+In the versioned migration workflow, our database state is managed by a _migration directory_. The migration directory
+holds all of the migration files created by Atlas, and the sum of all files in lexicographical order represents the current
+state of the database.
+
+To create our first migration file, we will run the `atlas migrate diff` command, and we will provide the necessary parameters:
+
+* `--dir` the URL to the migration directory, by default it is file://migrations.
+* `--to` the URL of the desired state. A state can be specified using a database URL, HCL or SQL schema, or another migration directory.
+* `--dev-url` a URL to a [Dev Database](/concepts/dev-database) that will be used to compute the diff.
+
+<Tabs>
+<TabItem value="hcl" label="Atlas DDL (HCL)" default>
+
+```shell
+atlas migrate diff initial \
+  --to file://schema.hcl \
+  --dev-url "docker://sqlserver/2022-latest/dev?mode=database"
+```
+
+</TabItem>
+<TabItem value="sql" label="SQL">
+
+```shell
+atlas migrate diff initial \
+  --to file://schema.sql \
+  --dev-url "docker://sqlserver/2022-latest/dev?mode=database"
+```
+
+</TabItem>
+</Tabs>
+
+Run `ls migrations`, and you'll notice that Atlas has automatically created a migration directory for us, as well as
+two files:
+
+<Tabs>
+<TabItem value="file" label="20240208092238_initial.sql" default>
+
+```sql
+-- Create "users" table
+CREATE TABLE [dbo].[users] (
+    [ID] bigint NOT NULL,
+    [name] varchar(255) COLLATE SQL_Latin1_General_CP1_CI_AS NOT NULL,
+    CONSTRAINT [PK_users] PRIMARY KEY CLUSTERED ([ID] ASC)
+);
+
+-- Create "repos" table
+CREATE TABLE [dbo].[repos] (
+    [ID] bigint NOT NULL,
+    [name] varchar(255) COLLATE SQL_Latin1_General_CP1_CI_AS NOT NULL,
+    [owner_id] bigint NOT NULL,
+    CONSTRAINT [PK_repos] PRIMARY KEY CLUSTERED ([ID] ASC),
+    CONSTRAINT [repo_owner] FOREIGN KEY ([owner_id]) REFERENCES [dbo].[users] ([ID]) ON UPDATE NO ACTION ON DELETE NO ACTION
+);
+```
+
+</TabItem>
+<TabItem value="atlas.sum" label="atlas.sum">
+
+```shell
+h1:rjaBcqArMcMRxbSJh/xykNO0DXqnwBFNkHpkaeuoZaM=
+20240208092238_initial.sql h1:y2p3yVltYcwc7mbsvPf5JfeZkzubYEBrhra71/ILVWc=
+```
+</TabItem>
+</Tabs>
+
+The migration file represents the current state of our database, and the sum file is used by Atlas to maintain the integrity
+of the migration directory. To learn more about the sum file, read the [documentation](/concepts/migration-directory-integrity).
+
+### Pushing migration directories to Atlas
+
+Now that we have our first migration, we can apply it to a database. There are multiple ways to accomplish this, with
+most methods covered in the [guides](/guides) section. In this example, we'll demonstrate how to push migrations to
+[Atlas Cloud](https://atlasgo.cloud), much like how Docker images are pushed to Docker Hub.
+
+<div style={{textAlign: 'center'}}>
+    <img src="https://atlasgo.io/uploads/mssql/images/first-dir-push.png" alt="mssql migrate push" width="100%"/>
+    <p style={{fontSize: 12}}>Migration Directory created with <code>atlas migrate push</code></p>
+</div>
+
+Let's name our new migration project `app` and run `atlas migrate push`:
+
+```shell
+atlas migrate push app \
+  --dev-url "docker://sqlserver/2022-latest/dev?mode=database"
+```
+
+Once the migration directory is pushed, Atlas prints a URL to the created directory, similar to the once shown in the
+image above.
+
+### Applying migrations
+
+Once our `app` migration directory has been pushed, we can apply it to a database from any CD platform without
+necessarily having our directory there.
+
+Let's create another database using Docker to resemble a local environment, this time on port `1434`:
+
+```bash
+docker run --platform linux/amd64 -e 'ACCEPT_EULA=Y' -e 'MSSQL_SA_PASSWORD=P@ssw0rd0995' -p 1434:1433 --name atlas-local -d mcr.microsoft.com/mssql/server:latest
+```
+
+Next, we'll create a simple Atlas configuration file (`atlas.hcl`) to store the settings for our local environment:
+
+```hcl title="atlas.hcl" {1}
+# The "dev" environment represents our local testings.
+env "local" {
+  url = "sqlserver://SA:P@ssw0rd0995@localhost:1434?database=master&mode=database"
+  migration {
+    dir = "atlas://app"
+  }
+}
+```
+
+The final step is to apply the migrations to the database. Let's run `atlas migrate apply` with the `--env` flag
+to instruct Atlas to select the environment configuration from the `atlas.hcl` file:
+
+```shell
+atlas migrate apply --env local
+```
+
+Boom! After applying the migration, you should receive a link to the deployment and the database where the migration
+was applied. Here's an example of what it should look like:
+
+<div style={{textAlign: 'center'}}>
+    <img src="https://atlasgo.io/uploads/mssql/images/mssql-deployment.png" alt="first deployment" width="100%"/>
+    <p style={{fontSize: 12}}>Migration deployment reported created with <code>atlas migrate apply</code></p>
+</div>
+
+### Generating another migration
+
+After applying the first migration, it's time to update our schema defined in the schema file and tell Atlas to generate
+another migration. This will bring the migration directory (and the database) in line with the new state defined by the
+desired schema (schema file).
+
+Let's make two changes to our schema:
+
+* Add a new `description` column to our repos table
+* Add a new `commits` table
+
+
+<Tabs>
+<TabItem value="hcl" label="Atlas DDL (HCL)" default>
+
+```hcl title="schema.hcl"
+table "users" {
+  schema = schema.dbo
+  column "ID" {
+    type = bigint
+    null = false
+  }
+  column "name" {
+    type = varchar(255)
+    null = false
+  }
+  primary_key {
+    columns = [column.ID]
+  }
+}
+
+table "repos" {
+  schema = schema.dbo
+  column "ID" {
+    type = bigint
+    null = false
+  }
+  column "name" {
+    type = varchar(255)
+    null = false
+  }
+  column "owner_id" {
+    type = bigint
+    null = false
+  }
+// highlight-start
+   column "description" {
+     type = varchar(max)
+     null = true
+   }
+// highlight-end
+  primary_key {
+    columns = [column.ID]
+  }
+  foreign_key "fk_repo_owner" {
+    columns     = [column.owner_id]
+    ref_columns = [table.users.column.ID]
+  }
+}
+// highlight-start
+table "commits" {
+  schema = schema.dbo
+  column "ID" {
+    type = bigint
+    null = false
+  }
+  column "message" {
+    type = varchar(255)
+    null = false
+  }
+  column "repo_id" {
+    type = bigint
+    null = false
+  }
+  column "author_id" {
+    type = bigint
+    null = false
+  }
+  primary_key {
+    columns = [column.id]
+  }
+  foreign_key "fk_repo_commit" {
+    columns     = [column.repo_id]
+    ref_columns = [table.repos.column.ID]
+  }
+  foreign_key "fk_commit_author" {
+    columns     = [column.author_id]
+    ref_columns = [table.users.column.ID]
+  }
+}
+// highlight-end
+schema "dbo" {
+}
+```
+
+</TabItem>
+<TabItem value="sql" label="SQL">
+
+```sql title="schema.sql"
+-- Create "users" table
+CREATE TABLE [dbo].[users] (
+    [ID] bigint NOT NULL,
+    [name] varchar(255) NOT NULL,
+    CONSTRAINT [PK_users] PRIMARY KEY CLUSTERED ([ID] ASC)
+)
+
+-- Create "repos" table
+CREATE TABLE [dbo].[repos] (
+    [ID] bigint NOT NULL,
+    [name] varchar(255) NOT NULL,
+    [owner_id] bigint,
+    // highlight-next-line
+    [description] varchar(max) NULL,
+    CONSTRAINT [PK_repos] PRIMARY KEY CLUSTERED ([ID] ASC),
+    CONSTRAINT [fk_repo_owner] FOREIGN KEY ([owner_id]) REFERENCES [dbo].[users]([ID])
+)
+
+// highlight-start
+CREATE TABLE `commits` (
+  [ID] bigint  NOT NULL,
+  [message] varchar(255) NOT NULL,
+  [repo_id] bigint NOT NULL,
+  [author_id] bigint NOT NULL,
+  CONSTRAINT [PK_commits] PRIMARY KEY CLUSTERED ([ID] ASC),
+  CONSTRAINT [fk_repo_commit] FOREIGN KEY ([repo_id]) REFERENCES [dbo].[repos]([ID]),
+  CONSTRAINT [fk_commit_author] FOREIGN KEY ([author_id]) REFERENCES [dbo].[users]([ID])
+)
+// highlight-end
+```
+</TabItem>
+</Tabs>
+
+Next, let's run the `atlas migrate diff` command once more:
+
+<Tabs>
+<TabItem value="hcl" label="Atlas DDL (HCL)" default>
+
+```shell
+atlas migrate diff add_commits \
+  --to file://schema.hcl \
+  --dev-url "docker://sqlserver/2022-latest/dev?mode=database"
+```
+
+</TabItem>
+<TabItem value="sql" label="SQL">
+
+```shell
+atlas migrate diff add_commits \
+  --to file://schema.sql \
+  --dev-url "docker://sqlserver/2022-latest/dev?mode=database"
+```
+
+</TabItem>
+</Tabs>
+
+Run `ls migrations`, and you'll notice that a new migration file has been generated.
+
+```sql title="20240208115924_add_commits.sql"
+ALTER TABLE [dbo].[repos] ADD [description] varchar(MAX) COLLATE SQL_Latin1_General_CP1_CI_AS NULL;
+-- Modify "repos" table
+ALTER TABLE [dbo].[repos] ADD CONSTRAINT [fk_repo_owner] FOREIGN KEY ([owner_id]) REFERENCES [dbo].[users] ([ID]) ON UPDATE NO ACTION ON DELETE NO ACTION;
+-- Create "commits" table
+CREATE TABLE [dbo].[commits] (
+    [ID] bigint NOT NULL,
+    [message] varchar(255) COLLATE SQL_Latin1_General_CP1_CI_AS NOT NULL,
+    [repo_id] bigint NOT NULL,
+    [author_id] bigint NOT NULL,
+    CONSTRAINT [PK_commits] PRIMARY KEY CLUSTERED ([ID] ASC),
+    CONSTRAINT [FK_commits_repos] FOREIGN KEY ([repo_id]) REFERENCES [dbo].[repos] ([ID]) ON UPDATE NO ACTION ON DELETE NO ACTION,
+    CONSTRAINT [FK_commits_users] FOREIGN KEY ([author_id]) REFERENCES [dbo].[users] ([ID]) ON UPDATE NO ACTION ON DELETE NO ACTION
+);
+```
+
+Let's run `atlas migrate push` again and
+observe the new file on the migration directory page.
+
+```shell
+atlas migrate push app \
+  --dev-url "docker://sqlserver/2022-latest/dev?mode=database"
+```
+
+<div style={{textAlign: 'center'}}>
+    <img src="https://atlasgo.io/uploads/mssql/images/mssql-second-push.png" alt="mssql migrate push" width="100%"/>
+    <p style={{fontSize: 12}}>Migration Directory created with <code>atlas migrate push</code></p>
+</div>
+
+## Next Steps
+
+In this guide we learned about the declarative and versioned workflows, and how to use Atlas to generate migrations,
+push them to an Atlas workspace and apply them to databases.
+
+Next steps:
+* Read the [full docs](/atlas-schema/hcl) to learn HCL schema syntax or about specific SQL Server [column types](/atlas-schema/hcl-types#sql-server)
+* Learn how to [set up CI](/cloud/setup-ci) for your migration directory
+* Deploy schema changes with [Terraform](/integrations/terraform-provider) or [Kubernetes](/integrations/kubernetes/operator)
+* Learn about [modern CI/CD principles](/guides/modern-database-ci-cd) for databases
+
+For more in-depth guides, check out the other pages in [this section](/guides) or visit our [Docs](/getting-started) section.
+
+Have questions? Feedback? Find our team on our [Discord server](https://discord.com/invite/zZ6sWVg6NT).
+
diff --git a/doc/md/guides/orms/django.mdx b/doc/md/guides/orms/django.mdx
new file mode 100644
index 00000000..cbf63dd0
--- /dev/null
+++ b/doc/md/guides/orms/django.mdx
@@ -0,0 +1,221 @@
+---
+id: django
+title: Automatic migration planning for Django
+slug: /guides/orms/django
+---
+
+# Import the installation instructions component found elsewhere
+
+import InstallationInstructions from '../../components/_installation_instructions.mdx'
+
+## TL;DR
+* [Django](https://www.djangoproject.com) is a Python web framework, with a built-in ORM.
+* [Atlas](https://atlasgo.io) is an open-source tool for inspecting, planning, linting and
+  executing schema changes to your database.
+* Developers using Django can use Atlas to automatically plan schema migrations
+  for them.
+
+## Automatic migration planning for Django
+
+Django is the most popular web framework in the Python community. It includes a [built-in ORM](https://docs.djangoproject.com/en/5.0/#the-model-layer)
+which allows users to describe their data model using Python classes. Migrations are then [created](https://docs.djangoproject.com/en/5.0/topics/migrations)
+using the `python manage.py makemigrations` command, which can be applied to the database using `python manage.py migrate`.
+
+Among the many ORMs available in our industry, Django's automatic migration is one of the most powerful and robust. 
+It can handle a wide range of schema changes, including adding new tables, adding new columns, changing column types, and more.
+However, having been created in 2014, a very different era in software engineering, it naturally has some limitations.
+
+Some of the limitations of Django's migration system include:
+
+* **Database Features.** Because it was created to provide interoperability across database engines, Django's 
+  migration system is centered around the "lowest common denominator" of database features. More advanced 
+  features such as Triggers, Views, and Stored Procedures have very limited support and require developers to jump through [all kinds of hoops](https://docs.djangoproject.com/en/5.0/ref/migration-operations/#django.db.migrations.operations.SeparateDatabaseAndState)
+  to use them.
+* **Ensuring Migration Safety.** Migrations are a risky business. If you're not careful, you can easily cause data loss or 
+  a production outage. Django's migration system does not provide a native way to ensure that a migration is safe to apply.
+* **Modern Deployments.**  Django does not provide native integration with modern deployment practices such as [GitOps](/blog/2023/12/06/gitops-for-databases-part-1)
+  or [Infrastructure-as-Code](/blog/2023/07/19/bridging-the-gap-between-iac-and-schema-management).
+
+Atlas, on the other hand, lets you manage your Django applications using the [Database Schema-as-Code](/blog/2024/01/24/why-schema-as-code)
+paradigm. This means that you can use Atlas to automatically plan schema migrations for your Django project, and then apply them to your database.
+Using Atlas, you can enjoy automatic migration planning, automatic code review and integrations with your favorite CI/CD tools.
+
+### How it works
+
+Atlas works by planning migrations from the database's current state to the desired state.
+
+In the context of [versioned migrations](/concepts/declarative-vs-versioned#versioned-migrations),
+the current state can be thought of as the database schema that would have
+been created by applying all previous migration scripts.
+
+The desired schema of your application can be provided to Atlas via an [External Schema Datasource](/atlas-schema/projects#data-source-external_schema),
+which is any program that can output a SQL schema definition to stdout.
+
+To use Atlas with Django, users can utilize the [Django Atlas Provider](https://github.com/ariga/atlas-provider-django)
+which is a small program that can be used to load the schema of a Django project into Atlas.
+
+In this guide, we will show how Atlas can be used to automatically plan schema migrations for Django users.
+
+
+## Prerequisites
+
+* A local [Django](https://www.djangoproject.com) project.
+
+If you don't have a Django project handy, check out the [Django getting started page](https://docs.djangoproject.com/en/5.0/intro/tutorial01/)
+
+## Using the Atlas Django Provider
+
+In this guide, we will use the [Django Atlas Provider](https://github.com/ariga/atlas-provider-django)
+to automatically plan schema migrations for a Django project.
+
+### Installation
+
+<InstallationInstructions />
+
+Start Python virtual environment if you haven't already:
+```bash
+python3 -m venv venv
+source venv/bin/activate
+```
+
+Install the provider by running:
+```bash
+pip install atlas-provider-django
+``` 
+
+### Configuration
+
+Add the provider to your Django project's `INSTALLED_APPS` in [`settings.py`](https://docs.djangoproject.com/en/5.0/topics/settings/):
+
+```python
+INSTALLED_APPS = [
+    ...,
+    'atlas_provider_django',
+    ...
+]
+```
+
+In your project directory, where [`manage.py`](https://docs.djangoproject.com/en/5.0/ref/django-admin/) file is located, 
+create a new file named `atlas.hcl` with the following contents:
+
+```hcl
+data "external_schema" "django" {
+  program = [
+    "python",
+    "manage.py",
+    "atlas-provider-django",
+    "--dialect", "mysql" // mariadb | postgresql | sqlite | mssql
+    
+    // if you want to only load a subset of your app models, you can specify the apps by adding
+    // "--apps", "app1", "app2", "app3"
+  ]
+}
+
+env "django" {
+  src = data.external_schema.django.url
+  dev = "docker://mysql/8/dev"
+  migration {
+    dir = "file://migrations"
+  }
+  format {
+    migrate {
+      diff = "{{ sql . \"  \" }}"
+    }
+  }
+}
+```
+
+## Usage
+
+Atlas supports a [versioned migrations](/concepts/declarative-vs-versioned#versioned-migrations)
+workflow, where each change to the database is versioned and recorded in a migration file. You can use the
+`atlas migrate diff` command to automatically generate a migration file that will migrate the database
+from its latest revision to the current Django schema.
+
+Suppose we have installed [app](https://docs.djangoproject.com/en/5.0/ref/applications/) named `polls`, 
+with the following models in our `polls/models.py` file:
+
+```python
+from django.db import models
+
+
+class Question(models.Model):
+    question_text = models.CharField(max_length=200)
+    pub_date = models.DateTimeField("date published")
+
+
+class Choice(models.Model):
+    question = models.ForeignKey(Question, on_delete=models.CASCADE)
+    choice_text = models.CharField(max_length=200)
+    votes = models.IntegerField(default=0)
+```
+
+We can generate a migration file by running this command:
+
+```bash
+atlas migrate diff --env django
+```
+
+Running this command will generate files similar to this in the `migrations` directory:
+
+```
+migrations
+|-- 20240126104629.sql
+`-- atlas.sum
+
+0 directories, 2 files
+```
+
+Examining the contents of `20240126104629.sql`:
+
+```sql
+-- Create "polls_question" table
+CREATE TABLE `polls_question` (
+  `id` bigint NOT NULL AUTO_INCREMENT,
+  `question_text` varchar(200) NOT NULL,
+  `pub_date` datetime(6) NOT NULL,
+  PRIMARY KEY (`id`)
+) CHARSET utf8mb4 COLLATE utf8mb4_0900_ai_ci;
+-- Create "polls_choice" table
+CREATE TABLE `polls_choice` (
+  `id` bigint NOT NULL AUTO_INCREMENT,
+  `choice_text` varchar(200) NOT NULL,
+  `votes` int NOT NULL,
+  `question_id` bigint NOT NULL,
+  PRIMARY KEY (`id`),
+  INDEX `polls_choice_question_id_c5b4b260_fk_polls_question_id` (`question_id`),
+  CONSTRAINT `polls_choice_question_id_c5b4b260_fk_polls_question_id` FOREIGN KEY (`question_id`) REFERENCES `polls_question` (`id`) ON UPDATE NO ACTION ON DELETE NO ACTION
+) CHARSET utf8mb4 COLLATE utf8mb4_0900_ai_ci;
+
+```
+
+Amazing! Atlas automatically generated a migration file that will create the `polls_question` and `polls_choice` tables in our database.
+Next, alter the `Question` class to add a new `question_type` field:
+
+```diff
+class Question(models.Model):
+    question_text = models.CharField(max_length=200)
++   question_type = models.CharField(max_length=20, null=True)
+    pub_date = models.DateTimeField("date published")
+```
+Re-run this command:
+
+```bash
+atlas migrate diff --env django
+```
+
+Observe a new migration file is generated:
+
+```sql
+-- Modify "polls_question" table
+ALTER TABLE `polls_question` ADD COLUMN `question_type` varchar(20) NULL;
+```
+
+## Conclusion
+
+In this guide we demonstrated how projects using Django can use Atlas to automatically
+plan schema migrations based only on their data model. To learn more about executing
+migrations against your production database, read the documentation for the
+[`migrate apply`](/versioned/apply) command.
+
+Have questions? Feedback? Find our team [on our Discord server](https://discord.gg/zZ6sWVg6NT)
diff --git a/doc/md/integrations/github-actions.mdx b/doc/md/integrations/github-actions.mdx
index 41cdffe7..1102e719 100644
--- a/doc/md/integrations/github-actions.mdx
+++ b/doc/md/integrations/github-actions.mdx
@@ -607,6 +607,7 @@ All inputs are optional as they may be specified in the Atlas configuration file
   named `atlas.hcl` in the current directory. For example, `file://config/atlas.hcl`.
   Learn more about [Atlas configuration files](https://atlasgo.io/atlas-schema/projects).
 * `env` - The environment to use from the Atlas configuration file.  For example, `dev`.
+* `dry-run` - Print SQL without executing it. Defaults to `false`.
 
 ### Outputs
 
diff --git a/doc/md/integrations/go-sdk.mdx b/doc/md/integrations/go-sdk.mdx
index 958f8b58..e03eee72 100644
--- a/doc/md/integrations/go-sdk.mdx
+++ b/doc/md/integrations/go-sdk.mdx
@@ -94,7 +94,7 @@ func main() {
 	defer workdir.Close()
 
 	// Initialize the client.
-	client, err := atlasexec.NewClientWithDir(workdir.Path(), "atlas")
+	client, err := atlasexec.NewClient(workdir.Path(), "atlas")
 	if err != nil {
 		log.Fatalf("failed to initialize client: %v", err)
 	}
@@ -132,4 +132,3 @@ Applied 0 migrations
 In this tutorial, we have seen how to use the `atlasexec` package to drive the Atlas CLI from Go code.
 We focused on the `Apply` method, but the SDK also provides methods for `SchemaApply`, `Status`, `Inspect`, and more.
 For more information, please refer to the [package documentation](https://pkg.go.dev/ariga.io/atlas-go-sdk@master/atlasexec).
-
diff --git a/doc/md/lint/analyzers.mdx b/doc/md/lint/analyzers.mdx
index 000524f3..78e335e8 100644
--- a/doc/md/lint/analyzers.mdx
+++ b/doc/md/lint/analyzers.mdx
@@ -283,20 +283,46 @@ DROP SCHEMA test;
 
 #### DS102 {#DS102}
 
-Destructive change that is reported when a table schema was dropped. For example:
+A destructive change is reported when a table is dropped, posing a risk of data loss. For example:
 
 ```sql
-DROP TABLE test.t;
+DROP TABLE t;
+```
+
+The suggested solution is to back up the data before dropping the table, or add a [pre-migration check](/versioned/checks)
+to ensure that the table is empty before dropping it:
+
+```diff
++ -- atlas:txtar
++
++ -- checks/destructive.sql --
++ SELECT NOT EXISTS (SELECT 1 FROM t1) AS t1_empty;
++
++ -- migration.sql --
+DROP TABLE t1;
 ```
 
 #### DS103 {#DS103}
 
-Destructive change that is reported when a non-virtual column was dropped. For example:
+A destructive change is reported when a non-virtual column is dropped, posing a risk of data loss. For example:
 
 ```sql
 ALTER TABLE t DROP COLUMN c;
 ```
 
+The suggest solution to resolve these errors is to add a [pre-migration check](/versioned/checks) that ensures the
+column is empty before dropping it:
+
+```diff
++ -- atlas:txtar
++
++ -- checks/destructive.sql --
++ SELECT NOT EXISTS (SELECT 1 FROM t WHERE c IS NOT NULL) AS t_c_empty;
++
++ -- migration.sql --
+ALTER TABLE t DROP COLUMN c;
+```
+
 #### MF101 {#MF101}
 
 Adding a unique index to a table might fail in case one of the indexed columns contain duplicate entries. For example:
diff --git a/doc/website/sidebars.js b/doc/website/sidebars.js
index 7a35c172..898a3f55 100644
--- a/doc/website/sidebars.js
+++ b/doc/website/sidebars.js
@@ -228,7 +228,12 @@ module.exports = {
                     type: 'doc',
                     id: 'guides/deploying/secrets',
                     label: 'Working with secrets'
-                }
+                },
+                {
+                    type: 'doc',
+                    id: 'guides/deploying/k8s-operator-certs',
+                    label: 'SSL Certs (Kubernetes)'
+                },
             ]
         },
         {
@@ -336,6 +341,11 @@ module.exports = {
                     id: 'guides/orms/sqlalchemy',
                     label: 'SQLAlchemy'
                 },
+                {
+                    type: 'doc',
+                    id: 'guides/orms/django',
+                    label: 'Django'
+                },
                 {
                     type: 'doc',
                     id: 'guides/frameworks/sqlc-declarative',
@@ -474,6 +484,11 @@ module.exports = {
             id: 'guides/getting-started-clickhouse',
             label: 'ClickHouse'
         },
+        {
+            type: 'doc',
+            id: 'guides/getting-started-mssql',
+            label: 'SQL Server'
+        },
         {
             type: 'doc',
             id: 'guides/ddl'
diff --git a/schemahcl/schemahcl.go b/schemahcl/schemahcl.go
index 8635be04..d7be1dfe 100644
--- a/schemahcl/schemahcl.go
+++ b/schemahcl/schemahcl.go
@@ -545,6 +545,9 @@ func (s *State) toAttrs(ctx *hcl.EvalContext, vr SchemaValidator, hclAttrs hclsy
 			for it := value.ElementIterator(); it.Next(); {
 				_, v := it.Element()
 				if isRef(v) {
+					if !v.Type().HasAttribute("__ref") {
+						return nil, fmt.Errorf("%s: invalid reference used in %s", hclAttr.SrcRange, hclAttr.Name)
+					}
 					v = cty.CapsuleVal(ctyRefType, &Ref{V: v.GetAttr("__ref").AsString()})
 				}
 				if vt != cty.NilType && vt != v.Type() {
@@ -559,7 +562,6 @@ func (s *State) toAttrs(ctx *hcl.EvalContext, vr SchemaValidator, hclAttrs hclsy
 		}
 		attrs = append(attrs, at)
 	}
-	// hclsyntax.Attrs is an alias for map[string]*Attribute
 	sort.Slice(attrs, func(i, j int) bool {
 		return attrs[i].K < attrs[j].K
 	})
@@ -1031,7 +1033,7 @@ func typeFuncSpec(typeSpec *TypeSpec) function.Function {
 
 // typeFuncSpecImpl returns the function implementation for the HCL function spec.
 func typeFuncSpecImpl(_ *function.Spec, typeSpec *TypeSpec) function.ImplFunc {
-	return func(args []cty.Value, retType cty.Type) (cty.Value, error) {
+	return func(args []cty.Value, _ cty.Type) (cty.Value, error) {
 		t := &Type{
 			T: typeSpec.T,
 		}
diff --git a/schemahcl/stdlib.go b/schemahcl/stdlib.go
index 3d6bec8a..e247d4e3 100644
--- a/schemahcl/stdlib.go
+++ b/schemahcl/stdlib.go
@@ -48,7 +48,7 @@ func stdTypes(ctx *hcl.EvalContext) *hcl.EvalContext {
 				{Name: "elem_type", Type: ctyNilType},
 			},
 			Type: function.StaticReturnType(ctyNilType),
-			Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) {
+			Impl: func(args []cty.Value, _ cty.Type) (cty.Value, error) {
 				argT := args[0].EncapsulatedValue().(*cty.Type)
 				setT := cty.Set(*argT)
 				return cty.CapsuleVal(ctyNilType, &setT), nil
@@ -59,7 +59,7 @@ func stdTypes(ctx *hcl.EvalContext) *hcl.EvalContext {
 				{Name: "elem_type", Type: ctyNilType},
 			},
 			Type: function.StaticReturnType(ctyNilType),
-			Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) {
+			Impl: func(args []cty.Value, _ cty.Type) (cty.Value, error) {
 				argT := args[0].EncapsulatedValue().(*cty.Type)
 				mapT := cty.Map(*argT)
 				return cty.CapsuleVal(ctyNilType, &mapT), nil
@@ -70,7 +70,7 @@ func stdTypes(ctx *hcl.EvalContext) *hcl.EvalContext {
 				{Name: "elem_type", Type: cty.List(ctyNilType)},
 			},
 			Type: function.StaticReturnType(ctyNilType),
-			Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) {
+			Impl: func(args []cty.Value, _ cty.Type) (cty.Value, error) {
 				argV := args[0]
 				argsT := make([]cty.Type, 0, argV.LengthInt())
 				for it := argV.ElementIterator(); it.Next(); {
@@ -86,7 +86,7 @@ func stdTypes(ctx *hcl.EvalContext) *hcl.EvalContext {
 				{Name: "attr_type", Type: cty.Map(ctyNilType)},
 			},
 			Type: function.StaticReturnType(ctyNilType),
-			Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) {
+			Impl: func(args []cty.Value, _ cty.Type) (cty.Value, error) {
 				argV := args[0]
 				argsT := make(map[string]cty.Type)
 				for it := argV.ElementIterator(); it.Next(); {
@@ -262,7 +262,7 @@ var (
 			{Name: "def", Type: cty.String, AllowNull: false},
 		},
 		Type: function.StaticReturnType(ctyRawExpr),
-		Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) {
+		Impl: func(args []cty.Value, _ cty.Type) (cty.Value, error) {
 			x := args[0].AsString()
 			if len(x) == 0 {
 				return cty.NilVal, errors.New("empty expression")
@@ -288,7 +288,7 @@ var (
 			},
 		},
 		Type: function.StaticReturnType(cty.String),
-		Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) {
+		Impl: func(args []cty.Value, _ cty.Type) (cty.Value, error) {
 			u, err := url.Parse(args[0].AsString())
 			if err != nil {
 				return cty.NilVal, err
@@ -317,7 +317,7 @@ var (
 			AllowNull: true,
 		},
 		Type: function.StaticReturnType(cty.String),
-		Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) {
+		Impl: func(args []cty.Value, _ cty.Type) (cty.Value, error) {
 			u, err := url.Parse(args[0].AsString())
 			if err != nil {
 				return cty.NilVal, err
@@ -344,7 +344,7 @@ var (
 			},
 		},
 		Type: function.StaticReturnType(cty.String),
-		Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) {
+		Impl: func(args []cty.Value, _ cty.Type) (cty.Value, error) {
 			u, err := url.Parse(args[0].AsString())
 			if err != nil {
 				return cty.NilVal, err
@@ -362,7 +362,7 @@ var (
 			},
 		},
 		Type: function.StaticReturnType(cty.String),
-		Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) {
+		Impl: func(args []cty.Value, _ cty.Type) (cty.Value, error) {
 			u := url.QueryEscape(args[0].AsString())
 			return cty.StringVal(u), nil
 		},
@@ -378,7 +378,7 @@ var (
 		Type: func(args []cty.Value) (cty.Type, error) {
 			return args[0].Type(), nil
 		},
-		Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) {
+		Impl: func(args []cty.Value, _ cty.Type) (cty.Value, error) {
 			switch args[0].Type() {
 			case cty.String:
 				fmt.Println(args[0].AsString())
@@ -409,7 +409,7 @@ var (
 			},
 		},
 		Type: function.StaticReturnType(cty.Bool),
-		Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) {
+		Impl: func(args []cty.Value, _ cty.Type) (cty.Value, error) {
 			if s1, s2 := args[0].AsString(), args[1].AsString(); strings.HasPrefix(s1, s2) {
 				return cty.True, nil
 			}
@@ -429,7 +429,7 @@ var (
 			},
 		},
 		Type: function.StaticReturnType(cty.Bool),
-		Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) {
+		Impl: func(args []cty.Value, _ cty.Type) (cty.Value, error) {
 			if s1, s2 := args[0].AsString(), args[1].AsString(); strings.HasSuffix(s1, s2) {
 				return cty.True, nil
 			}
@@ -449,7 +449,7 @@ func MakeFileFunc(base string) function.Function {
 			},
 		},
 		Type: function.StaticReturnType(cty.String),
-		Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) {
+		Impl: func(args []cty.Value, _ cty.Type) (cty.Value, error) {
 			if !filepath.IsAbs(base) {
 				return cty.NilVal, fmt.Errorf("base directory must be an absolute path. got: %s", base)
 			}
diff --git a/sql/mysql/driver.go b/sql/mysql/driver.go
index 0035c2c7..23d6070e 100644
--- a/sql/mysql/driver.go
+++ b/sql/mysql/driver.go
@@ -255,6 +255,16 @@ func (*Driver) ParseType(s string) (schema.Type, error) {
 	return ParseType(s)
 }
 
+// StmtBuilder is a helper method used to build statements with MySQL formatting.
+func (*Driver) StmtBuilder(opts migrate.PlanOptions) *sqlx.Builder {
+	return &sqlx.Builder{
+		QuoteOpening: '`',
+		QuoteClosing: '`',
+		Schema:       opts.SchemaQualifier,
+		Indent:       opts.Indent,
+	}
+}
+
 // ScanStmts implements migrate.StmtScanner.
 func (*Driver) ScanStmts(input string) ([]*migrate.Stmt, error) {
 	return (&migrate.Scanner{
diff --git a/sql/mysql/migrate.go b/sql/mysql/migrate.go
index 13dc8b3f..f347df92 100644
--- a/sql/mysql/migrate.go
+++ b/sql/mysql/migrate.go
@@ -763,8 +763,8 @@ func (s *state) columnDefault(b *sqlx.Builder, c *schema.Column) {
 
 // Build instantiates a new builder and writes the given phrase to it.
 func (s *state) Build(phrases ...string) *sqlx.Builder {
-	b := &sqlx.Builder{QuoteOpening: '`', QuoteClosing: '`', Schema: s.SchemaQualifier, Indent: s.Indent}
-	return b.P(phrases...)
+	return (*Driver).StmtBuilder(nil, s.PlanOptions).
+		P(phrases...)
 }
 
 // skipAutoChanges filters unnecessary changes that are automatically
diff --git a/sql/postgres/driver.go b/sql/postgres/driver.go
index 9565d564..805d4dd2 100644
--- a/sql/postgres/driver.go
+++ b/sql/postgres/driver.go
@@ -316,6 +316,16 @@ func (*Driver) ParseType(s string) (schema.Type, error) {
 	return ParseType(s)
 }
 
+// StmtBuilder is a helper method used to build statements with PostgreSQL formatting.
+func (*Driver) StmtBuilder(opts migrate.PlanOptions) *sqlx.Builder {
+	return &sqlx.Builder{
+		QuoteOpening: '"',
+		QuoteClosing: '"',
+		Schema:       opts.SchemaQualifier,
+		Indent:       opts.Indent,
+	}
+}
+
 // ScanStmts implements migrate.StmtScanner.
 func (*Driver) ScanStmts(input string) ([]*migrate.Stmt, error) {
 	return (&migrate.Scanner{
diff --git a/sql/postgres/migrate.go b/sql/postgres/migrate.go
index c7856008..9ab51c19 100644
--- a/sql/postgres/migrate.go
+++ b/sql/postgres/migrate.go
@@ -1089,8 +1089,8 @@ func (s *state) append(c ...*migrate.Change) {
 
 // Build instantiates a new builder and writes the given phrase to it.
 func (s *state) Build(phrases ...string) *sqlx.Builder {
-	b := &sqlx.Builder{QuoteOpening: '"', QuoteClosing: '"', Schema: s.SchemaQualifier, Indent: s.Indent}
-	return b.P(phrases...)
+	return (*Driver).StmtBuilder(nil, s.PlanOptions).
+		P(phrases...)
 }
 
 // skipAutoChanges filters unnecessary changes that are automatically
diff --git a/sql/sqlcheck/destructive/destructive.go b/sql/sqlcheck/destructive/destructive.go
index 84c00b2c..01b96990 100644
--- a/sql/sqlcheck/destructive/destructive.go
+++ b/sql/sqlcheck/destructive/destructive.go
@@ -8,10 +8,12 @@ import (
 	"context"
 	"errors"
 	"fmt"
+	"strconv"
 	"strings"
 
 	"ariga.io/atlas/schemahcl"
 	"ariga.io/atlas/sql/internal/sqlx"
+	"ariga.io/atlas/sql/migrate"
 	"ariga.io/atlas/sql/schema"
 	"ariga.io/atlas/sql/sqlcheck"
 )
@@ -47,7 +49,10 @@ func (*Analyzer) Name() string {
 
 // Analyze implements sqlcheck.Analyzer.
 func (a *Analyzer) Analyze(_ context.Context, p *sqlcheck.Pass) error {
-	var diags []sqlcheck.Diagnostic
+	var (
+		edits []*migrate.Stmt
+		diags []sqlcheck.Diagnostic
+	)
 	for _, sc := range p.File.Changes {
 		for _, c := range sc.Changes {
 			switch c := c.(type) {
@@ -69,33 +74,66 @@ func (a *Analyzer) Analyze(_ context.Context, p *sqlcheck.Pass) error {
 					})
 				}
 			case *schema.DropTable:
-				if p.File.SchemaSpan(c.T.Schema) != sqlcheck.SpanDropped && p.File.TableSpan(c.T) != sqlcheck.SpanTemporary {
+				if p.File.SchemaSpan(c.T.Schema) != sqlcheck.SpanDropped && p.File.TableSpan(c.T) != sqlcheck.SpanTemporary && !a.hasEmptyTableCheck(p, c.T) {
 					diags = append(diags, sqlcheck.Diagnostic{
 						Code: codeDropT,
 						Pos:  sc.Stmt.Pos,
 						Text: fmt.Sprintf("Dropping table %q", c.T.Name),
+						SuggestedFixes: []sqlcheck.SuggestedFix{
+							{
+								Message: fmt.Sprintf("Add a pre-migration check to ensure table %q is empty before dropping it", c.T.Name),
+							},
+						},
 					})
+					if stmt, err := a.emptyTableCheckStmt(p, c.T); err == nil {
+						edits = append(edits, stmt)
+					}
 				}
 			case *schema.ModifyTable:
+				var names []string
 				for i := range c.Changes {
 					d, ok := c.Changes[i].(*schema.DropColumn)
 					if !ok || p.File.ColumnSpan(c.T, d.C) == sqlcheck.SpanTemporary {
 						continue
 					}
-					if g := (schema.GeneratedExpr{}); !sqlx.Has(d.C.Attrs, &g) || strings.ToUpper(g.Type) != "VIRTUAL" {
-						diags = append(diags, sqlcheck.Diagnostic{
-							Code: codeDropC,
-							Pos:  sc.Stmt.Pos,
-							Text: fmt.Sprintf("Dropping non-virtual column %q", d.C.Name),
-						})
+					if g := (schema.GeneratedExpr{}); (!sqlx.Has(d.C.Attrs, &g) || strings.ToUpper(g.Type) != "VIRTUAL") && !a.hasEmptyColumnCheck(p, c.T, d.C) {
+						names = append(names, strconv.Quote(d.C.Name))
+						if stmt, err := a.emptyColumnCheckStmt(p, c.T, d.C.Name); err == nil {
+							edits = append(edits, stmt)
+						}
 					}
 				}
+				switch n := len(names); {
+				case n == 1:
+					diags = append(diags, sqlcheck.Diagnostic{
+						Code: codeDropC,
+						Pos:  sc.Stmt.Pos,
+						Text: fmt.Sprintf("Dropping non-virtual column %s", names[0]),
+						SuggestedFixes: []sqlcheck.SuggestedFix{
+							{
+								Message: fmt.Sprintf("Add a pre-migration check to ensure column %q is NULL before dropping it", c.T.Name),
+							},
+						},
+					})
+				case n > 1:
+					// All changes generated by the same statement (same position).
+					diags = append(diags, sqlcheck.Diagnostic{
+						Code: codeDropC,
+						Pos:  sc.Stmt.Pos,
+						Text: fmt.Sprintf("Dropping non-virtual columns %s and %s", strings.Join(names[:n-1], ", "), names[n-1]),
+						SuggestedFixes: []sqlcheck.SuggestedFix{
+							{
+								Message: fmt.Sprintf("Add pre-migration checks to ensure columns %s and %s are NULL before dropping them", strings.Join(names[:n-1], ", "), names[n-1]),
+							},
+						},
+					})
+				}
 			}
 		}
 	}
 	if len(diags) > 0 {
 		const reportText = "destructive changes detected"
-		p.Reporter.WriteReport(sqlcheck.Report{Text: reportText, Diagnostics: diags})
+		p.Reporter.WriteReport(sqlcheck.Report{Text: reportText, Diagnostics: diags, SuggestedFixes: suggestFix(p, edits)})
 		if sqlx.V(a.Error) {
 			return errors.New(reportText)
 		}
diff --git a/sql/sqlcheck/destructive/destructive_oss.go b/sql/sqlcheck/destructive/destructive_oss.go
new file mode 100644
index 00000000..d8e95681
--- /dev/null
+++ b/sql/sqlcheck/destructive/destructive_oss.go
@@ -0,0 +1,35 @@
+// 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.
+
+//go:build !ent
+
+package destructive
+
+import (
+	"errors"
+
+	"ariga.io/atlas/sql/migrate"
+	"ariga.io/atlas/sql/schema"
+	"ariga.io/atlas/sql/sqlcheck"
+)
+
+func (*Analyzer) hasEmptyTableCheck(*sqlcheck.Pass, *schema.Table) bool {
+	return false // unimplemented.
+}
+
+func (*Analyzer) hasEmptyColumnCheck(*sqlcheck.Pass, *schema.Table, *schema.Column) bool {
+	return false // unimplemented.
+}
+
+func (*Analyzer) emptyTableCheckStmt(*sqlcheck.Pass, *schema.Table) (*migrate.Stmt, error) {
+	return nil, errors.New("unimplemented")
+}
+
+func (*Analyzer) emptyColumnCheckStmt(*sqlcheck.Pass, *schema.Table, string) (*migrate.Stmt, error) {
+	return nil, errors.New("unimplemented")
+}
+
+func suggestFix(*sqlcheck.Pass, []*migrate.Stmt) []sqlcheck.SuggestedFix {
+	return nil // unimplemented.
+}
diff --git a/sql/sqlcheck/sqlcheck.go b/sql/sqlcheck/sqlcheck.go
index b9eaf6c3..5e7a7abc 100644
--- a/sql/sqlcheck/sqlcheck.go
+++ b/sql/sqlcheck/sqlcheck.go
@@ -77,15 +77,34 @@ type (
 
 	// A Report describes an analysis report with an optional specific diagnostic.
 	Report struct {
-		Text        string       // Report text.
-		Diagnostics []Diagnostic // Report diagnostics.
+		Text           string         `json:"Text"`                     // Report text.
+		Diagnostics    []Diagnostic   `json:"Diagnostics,omitempty"`    // Report diagnostics.
+		SuggestedFixes []SuggestedFix `json:"SuggestedFixes,omitempty"` // Report-level suggested fixes.
 	}
 
 	// A Diagnostic is a text associated with a specific position of a statement in a file.
 	Diagnostic struct {
-		Pos  int    // Diagnostic position.
-		Text string // Diagnostic text.
-		Code string // Code describes the check. For example, DS101
+		Pos            int            `json:"Pos"`                      // Diagnostic position.
+		Text           string         `json:"Text"`                     // Diagnostic text.
+		Code           string         `json:"Code"`                     // Code describes the check. For example, DS101
+		SuggestedFixes []SuggestedFix `json:"SuggestedFixes,omitempty"` // Fixes to this specific diagnostics (statement-level).
+	}
+
+	// A SuggestedFix is a change associated with a diagnostic that can
+	// be applied to fix the issue. Both the message and the text edits
+	// are optional.
+	SuggestedFix struct {
+		Message   string     `json:"Message"`
+		TextEdits []TextEdit `json:"TextEdits,omitempty"`
+	}
+
+	// A TextEdit represents a code changes in a file. If End is set to Pos or 0,
+	// it means that the change is an insertion at the given position. Otherwise,
+	// it represents a replacement of the text between Pos and End with NewText.
+	TextEdit struct {
+		Pos     int    `json:"Pos"`
+		End     int    `json:"End"`
+		NewText string `json:"NewText"`
 	}
 
 	// ReportWriter represents a writer for analysis reports.
diff --git a/sql/sqlite/driver.go b/sql/sqlite/driver.go
index 5c77cff5..0a9d6858 100644
--- a/sql/sqlite/driver.go
+++ b/sql/sqlite/driver.go
@@ -189,6 +189,16 @@ func (*Driver) ParseType(s string) (schema.Type, error) {
 	return ParseType(s)
 }
 
+// StmtBuilder is a helper method used to build statements with SQLite formatting.
+func (*Driver) StmtBuilder(opts migrate.PlanOptions) *sqlx.Builder {
+	return &sqlx.Builder{
+		QuoteOpening: '`',
+		QuoteClosing: '`',
+		Schema:       opts.SchemaQualifier,
+		Indent:       opts.Indent,
+	}
+}
+
 // ScanStmts implements migrate.StmtScanner.
 func (*Driver) ScanStmts(input string) ([]*migrate.Stmt, error) {
 	return (&migrate.Scanner{
diff --git a/sql/sqlite/migrate.go b/sql/sqlite/migrate.go
index 0abbd8c2..db9ab878 100644
--- a/sql/sqlite/migrate.go
+++ b/sql/sqlite/migrate.go
@@ -579,8 +579,8 @@ func autoincPK(pk *schema.Index) bool {
 
 // Build instantiates a new builder and writes the given phrase to it.
 func (s *state) Build(phrases ...string) *sqlx.Builder {
-	b := &sqlx.Builder{QuoteOpening: '`', QuoteClosing: '`', Schema: s.SchemaQualifier, Indent: s.Indent}
-	return b.P(phrases...)
+	return (*Driver).StmtBuilder(nil, s.PlanOptions).
+		P(phrases...)
 }
 
 func defaultValue(c *schema.Column) (string, error) {
diff --git a/sql/sqlite/sqlitecheck/sqlitecheck.go b/sql/sqlite/sqlitecheck/sqlitecheck.go
index 3c2389d9..1055ebbe 100644
--- a/sql/sqlite/sqlitecheck/sqlitecheck.go
+++ b/sql/sqlite/sqlitecheck/sqlitecheck.go
@@ -79,7 +79,7 @@ func init() {
 			return nil, err
 		}
 		return []sqlcheck.Analyzer{
-			sqlcheck.AnalyzerFunc(func(ctx context.Context, p *sqlcheck.Pass) error {
+			sqlcheck.AnalyzerFunc(func(_ context.Context, p *sqlcheck.Pass) error {
 				var changes []*sqlcheck.Change
 				// Detect sequence of changes using temporary table and transform them to one ModifyTable change.
 				// See: https://www.sqlite.org/lang_altertable.html#making_other_kinds_of_table_schema_changes.


Test patch:
diff --git a/schemahcl/schemahcl_test.go b/schemahcl/schemahcl_test.go
index b06eadb1..5119a795 100644
--- a/schemahcl/schemahcl_test.go
+++ b/schemahcl/schemahcl_test.go
@@ -110,6 +110,21 @@ func TestResource(t *testing.T) {
 	require.EqualValues(t, f, string(buf))
 }
 
+func TestInvalidRefs(t *testing.T) {
+	var doc struct {
+		Tables []struct {
+			Name string `spec:",name"`
+			Refs []*Ref `spec:"ref"`
+		}
+	}
+	err := New().EvalBytes([]byte(`
+table "bar" {
+  refs = [table]
+}
+`), &doc, nil)
+	require.EqualError(t, err, ":3,3-17: invalid reference used in refs")
+}
+
 func ExampleUnmarshal() {
 	f := `
 show "seinfeld" {
@@ -865,7 +880,7 @@ func TestTypeLabelBlock(t *testing.T) {
 				callT++
 				return cty.ObjectVal(map[string]cty.Value{"url": cty.StringVal("driver://" + v.AsString())}), nil
 			}),
-			WithTypeLabelBlock("driver", "not_called", func(ctx *hcl.EvalContext, b *hclsyntax.Block) (cty.Value, error) {
+			WithTypeLabelBlock("driver", "not_called", func(*hcl.EvalContext, *hclsyntax.Block) (cty.Value, error) {
 				t.Fatal("should not be called")
 				return cty.NilVal, nil
 			}),
diff --git a/sql/mysql/inspect_test.go b/sql/mysql/inspect_test.go
index d9dbb0bf..bd776b31 100644
--- a/sql/mysql/inspect_test.go
+++ b/sql/mysql/inspect_test.go
@@ -629,7 +629,7 @@ func TestDriver_InspectTable(t *testing.T) {
 `))
 				m.noFKs()
 			},
-			expect: func(require *require.Assertions, t *schema.Table, err error) {
+			expect: func(require *require.Assertions, _ *schema.Table, err error) {
 				// nothing to expect, ExpectQuery is enough for this test
 				require.NoError(err)
 			},
diff --git a/sql/sqlclient/client_test.go b/sql/sqlclient/client_test.go
index c783532c..353ead77 100644
--- a/sql/sqlclient/client_test.go
+++ b/sql/sqlclient/client_test.go
@@ -22,7 +22,7 @@ func TestRegisterOpen(t *testing.T) {
 	c := &sqlclient.Client{}
 	sqlclient.Register(
 		"mysql",
-		sqlclient.OpenerFunc(func(ctx context.Context, url *url.URL) (*sqlclient.Client, error) {
+		sqlclient.OpenerFunc(func(context.Context, *url.URL) (*sqlclient.Client, error) {
 			return c, nil
 		}),
 		sqlclient.RegisterFlavours("maria"),
@@ -39,7 +39,7 @@ func TestRegisterOpen(t *testing.T) {
 		t,
 		"sql/sqlclient: Register called twice for mysql",
 		func() {
-			sqlclient.Register("mysql", sqlclient.OpenerFunc(func(ctx context.Context, url *url.URL) (*sqlclient.Client, error) {
+			sqlclient.Register("mysql", sqlclient.OpenerFunc(func(context.Context, *url.URL) (*sqlclient.Client, error) {
 				return c, nil
 			}))
 		},