Migration Analyzers
The database is often the most critical component in software architectures. Being a stateful component, it cannot be easily rebuilt, scaled-out or fixed by a restart. Outages that involve damage to data or simply unavailability of the database are notoriously hard to manage and recover from, often taking long hours of careful work by a team's most senior engineers.
As most outages happen directly as a result of a change to a system, Atlas provides users with means to verify the
safety of planned changes before they happen. The sqlcheck
package provides interfaces for analyzing the contents of SQL files to generate insights on the safety of many kinds of
changes to database schemas. With this package developers may define an Analyzer that can be used to diagnose the impact
of SQL statements on the target database.
Using these interfaces, Atlas provides different Analyzer implementations that are useful for determining the
safety of migration scripts.
Analyzers
Below are the Analyzer implementations currently supported by Atlas.
Non-Linear Changes
Non-additive changes, often referred to as non-linear changes, are changes to the migration directory that are not added in a sequential order. This is a bit like the linear history in version control systems, where migration files are commits and the migration directory is the repository. Let's explain with three examples why ensuring the linearity of the migration directory is important:
- When a developer introduces a new migration file without having the latest state of the directory, there is a risk of generating an incorrect file that might conflict with the actual schema and cause a failure during deployment.
- When a developer merges a feature branch to the main branch with a new migration file that is not positioned at the end of the directory, there is a risk that Atlas will skip this migration file, as there might be some database that already contains a higher version of the migration directory. This scenario can cause unexpected and surprising behavior during deployments.
- Having non-linear history can make it challenging to roll back (or revert changes) to a specific version of the migration directory, as the state of the database might be different than the state of the migration directory. Applying the migration directory in consistent order promises deterministic behavior.
Luckily, Atlas detects non-linear and non-additive changes made to a migration directory. To enable this behavior in your project, integrate Atlas into your GitHub Actions or GitLab CI pipelines, and Atlas will automatically detect and report non-linear changes during the CI run.
By default, non-linear changes are reported but not cause migration linting to fail. Users can change this by
configuring the non_linear changes detector in the atlas.hcl file:
lint {
non_linear {
error = true
}
}
Relax Non-Linear Errors on Edits
Users who configured the non_linear change detector to fail migration linting (error = true) can relax or ignore
these errors when editing existing migration files by setting the on_edit option to WARN or IGNORE.
WARN: file edits that would trigger a non-linear error are downgraded to warnings.IGNORE: file edits are ignored and do not produce any diagnostic.
lint {
non_linear {
error = true
on_edit = WARN // IGNORE | ERROR
}
}
This is useful when developers need to update the latest migration files right after they land on the main branch and want to keep CI pipelines unblocked while non-linear checks still apply to older files.
Destructive Changes
Destructive changes are changes to a database schema that result in loss of data. For instance, consider a statement such as:
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. There are definitely situations where this type
of change is desired, but they are relatively rare. Using the destructive (GoDoc)
Analyzer, teams can detect this type of change and design workflows that prevent it from happening accidentally.
Running migration linting locally on in CI fails with exit code 1 in case destructive changes are detected. However,
users can disable this by configuring the destructive analyzer in the atlas.hcl
file:
lint {
destructive {
error = false
}
}
Besides failing CI, the analyzer also gates the declarative flow at apply time: with a
review policy of ERROR, atlas schema apply auto-approves
changes only when linting passes and requires manual approval when destructive changes are detected.
Enforce Destructive Change Checks Atlas Pro
In some teams, destructive changes are considered high-risk and should never be skipped, regardless of whether a developer
adds an -- atlas:nolint directive. In these cases, the force option can be set on the destructive analyzer. This guarantees
the check always runs and reports diagnostics, even if explicitly excluded.
lint {
destructive {
force = true
}
}
Allowlist Drop Rules Atlas Pro
The destructive analyzer can be configured to allow specific destructive changes, such as dropping a table or column
whose name matches a defined pattern. This allows teams following a deprecation workflow, like renaming objects with
a drop_ prefix before deletion, to continue using the analyzer while avoiding false positives from intentional drops.
lint {
destructive {
// Allow dropping tables or columns
// that their name start with "drop_".
allow_table {
match = "drop_.+"
}
allow_column {
match = "drop_.+"
}
}
}
Data-dependent Changes
Data-dependent changes are changes to a database schema that may succeed or fail, depending on the data that is stored in the database. For instance, consider a statement such as:
ALTER TABLE `example`.`orders` ADD UNIQUE INDEX `idx_name` (`name`);
This statement is considered data-dependent because if the orders table
contains duplicate values on the name column we will not be able to add a uniqueness
constraint. Consider we added two records with the name atlas to the table:
mysql> create table orders ( name varchar(100) );
Query OK, 0 rows affected (0.11 sec)
mysql> insert into orders (name) values ("atlas");
Query OK, 1 row affected (0.06 sec)
mysql> insert into orders (name) values ("atlas");
Query OK, 1 row affected (0.01 sec)
Attempting to add a uniqueness constraint on the name column, will fail:
mysql> ALTER TABLE `example`.`orders` ADD UNIQUE INDEX `idx_name` (`name`);
This type of change is tricky because a developer trying to simulate it locally
might succeed in performing it only to be surprised that their migration script
fails in production, breaking a deployment sequence or causing other unexpected
behavior. Using the data_depend (GoDoc)
Analyzer, teams can detect this risk early and account for it in pre-deployment checks to a database.
By default, data-dependent changes are reported but not cause migration linting to fail. Users can change this by
configuring the data_depend analyzer in the atlas.hcl file:
lint {
data_depend {
error = true
}
}
Backward Incompatible Changes
Backward-incompatible changes, also known as breaking changes, are schema changes that have the potential to break the contract with applications that rely on
the old schema. For instance, renaming a column from email_address to email can cause errors during deployment
(migration) phase if applications running the previous version of the schema reference the old column name in their
queries.
The incompatible analyzer detects renames (BC101, BC102) and drops (BC103,
BC104) of tables and columns. Drops are also reported by the destructive analyzer, and the two are
complementary: destructive reports the data loss, incompatible reports that clients still referencing the object
will fail, even if it holds no data. This is common with rolling deployments, where the previous application version
keeps running while the migration applies.
By default, detected breaking changes are reported but do not cause migration linting to fail. Users can change this by
configuring the incompatible analyzer in the atlas.hcl file:
lint {
incompatible {
error = true
}
}
-- atlas:nolint destructive silences only the data-loss diagnostic; use -- atlas:nolint destructive incompatible
to skip both.
Custom Messages for Drop Checks Atlas Pro
The drop_table and drop_column blocks append a policy message to BC103 and BC104 diagnostics.
The message is free-form, so teams can tailor it to their stack, such as point authors at the ORM's schema path and
deprecation syntax, name the owning team to contact, or link an internal runbook. It is evaluated per dropped object,
with the object bound to self:
lint {
incompatible {
drop_column {
message = "deprecate ${self.table.name}.${self.name} in the application schema (ent: Deprecated, GraphQL: @deprecated) and release it before dropping"
}
drop_table {
message = "contact the owners of ${self.schema.name}.${self.name} before dropping it"
}
}
}
Restrictive Changes Atlas Pro
The restrictive analyzer reports changes that reject writes or reads from clients still running against the
previous schema, such as application instances that are not deployed yet. It is off by default. To enable it, add a
restrictive block:
lint {
restrictive {}
}
Ownership Policy Atlas Pro
GitHub CODEOWNERS enforces who can modify files, but cannot enforce who can modify schema objects. A developer
with access to the inventory/ schema folder could alter the schema files to include changes to
public.users, a core table owned by another team, causing a migration that crosses schema boundaries.
The ownership policy validates that migration changes only affect resources the author is authorized to modify.
It resolves the current user from the GITHUB_ACTOR environment variable and checks membership against the
configured rules.
lint {
ownership "github" {
allow "core-tables" {
match = "public.*[type=table]"
teams = ["backend"]
users = ["a8m"]
}
allow "inventory" {
match = "inventory.*"
teams = ["data-eng"]
}
deny "contractors" {
match = "*"
users = ["contractor-x"]
}
}
}
Rules are evaluated in order. The first matching rule determines access. deny rules take precedence over
allow rules for the same resource. Changes that don't match any rule are denied by default. Set
default = ALLOW on the ownership block to invert this behavior.
When you specify teams, Atlas uses the GITHUB_TOKEN environment variable to call the GitHub Teams API and
check membership. Make sure the token has the correct permissions. See GitHub's
fine-grained personal access tokens
documentation for details.
Naming Conventions Policy
In database schema design, maintaining consistency and readability through naming conventions is a widely common practice. Atlas provides an analyzer that can help enforce naming conventions on a variety of schema resources, including tables, columns, and indexes.
Users can enable this by configuring the naming analyzer in their
atlas.hcl file:
- Global policy
- Resource-specific policy
lint {
naming {
match = "^[a-z]+$"
message = "must be lowercase"
}
}
lint {
naming {
match = "^[a-z]+$"
message = "must be lowercase"
index {
match = "^[a-z]+_idx$"
message = "must be lowercase and end with _idx"
}
// schema, table, column, foreign_key and check are also supported.
}
}
By default, detected naming violations are reported but do not cause migration linting to fail. Users can change this by
configuring the naming analyzer in the atlas.hcl file:
lint {
naming {
error = true
match = "^[a-z]+$"
message = "must be lowercase"
}
}
Concurrent Index Policy (PostgreSQL) Atlas Pro
Schema changes like CREATE INDEX or DROP INDEX can cause the database to lock the table against write operations.
Luckily, PostgreSQL provides the
CONCURRENTLY option that
may be more resource-intensive, but allows normal database operations to continue while the index is built or dropped.
Atlas provides an analyzer that identifies non-concurrent index creation or deletion for tables not created within the same file, and recommends executing them concurrently.
Additionally, since indexes cannot be created or deleted concurrently within a transaction, Atlas ensures the
atlas:txmode none directive exists in the file header to prevent this file from running in a transaction. This check
can be disabled along with the other ones as follows:
lint {
concurrent_index {
check_create = false // `true` by default.
check_drop = false // `true` by default.
check_txmode = false // `true` by default.
}
}
By default, detected concurrent index violations are reported but do not cause migration linting to fail. Users can
change this by configuring the concurrent_index analyzer in the
atlas.hcl file:
lint {
concurrent_index {
error = true
}
}
Blocking Changes Atlas Pro
The blocking analyzer reports changes that block or rewrite tables: the PostgreSQL PG3xx checks and the MySQL
MY110 to MY148 checks listed under Checks. Use the blocking block to configure them
together, for example, to fail migration linting on any of them:
lint {
blocking {
error = true
}
}
Set force = true to report them even when an -- atlas:nolint directive excludes them. To configure a single check,
use a check block.
Nested Transactions Atlas Pro
By default, Atlas wraps each migration file in its own transaction (see transaction configuration for more details). If a file includes a statement that starts a transaction (for example, BEGIN or START TRANSACTION), the database will return an error (or fail later when using a connection-pooler like PgBouncer) such as cannot start a transaction within a transaction (SQLite) or pq: unexpected transaction status idle (PostgreSQL).
The nestedtx analyzer flags such statements. You can fix them by either removing the manual transaction control or adding -- atlas:txmode none to the file header to indicate that this migration file should not be wrapped in a transaction by Atlas. When Atlas detects this issue, it automatically adds a code suggestion with this directive to the pull request.
By default, violations are reported as warnings. You can make them fail the lint step by configuring the analyzer in your atlas.hcl file:
lint {
nestedtx {
error = true
}
}
Statement Rules Atlas Pro
The statement analyzer lets you define regular expression rules to permit or reject specific SQL statements in your migration files.
It processes each statement sequentially against an ordered list of rules. The supported rule types are allow and deny:
allow: When a statement matches anallowrule, the analyzer lets it pass.deny: When a statement matches adenyrule, the analyzer rejects it and reports a diagnostic.- If no rules match, the statement is allowed by default.
A rule can also set a condition: an expression that must be true for the rule to apply to a statement it matches.
The expression can read self.text, the statement itself, and self.before and self.after, the statements before
and after it in the file.
The statement analyzer reports violations as warnings by default. You can make them fail the linting step by
setting error = true in the statement block.
To ensure the analyzer cannot be skipped with the -- atlas:nolint directive, set force = true.
- Block DELETE/TRUNCATE
- Block DELETE without WHERE
- Require lock_timeout before DDL
lint {
statement {
error = true
deny "delete-truncate" {
match = "(?i)(DELETE|TRUNCATE)"
}
}
}
lint {
statement {
allow "delete-with-where" {
match = "(?i)DELETE .+ WHERE"
}
deny "delete-truncate" {
match = "(?i)DELETE"
message = "DELETE statements must have a WHERE clause"
}
}
}
lint {
statement {
deny "ddl-without-lock-timeout" {
match = "(?i)^(ALTER|CREATE|DROP)\\b"
condition = !anytrue([for s in self.before : regexmatch("(?i)^SET\\s+lock_timeout", s)])
message = "set lock_timeout before running DDL"
}
}
}
SQL Injection
The SQL Injection analyzer examines SQL statements in migration files and declarative schemas to detect constructs that could allow untrusted input to be executed as part of an SQL command. It identifies unsafe dynamic SQL patterns, such as string concatenation or variable interpolation, that may bypass proper escaping or identifier quoting.
As more schema code and migrations are edited automatically, including by AI-assisted tools, developers may introduce such patterns unintentionally. This analyzer provides a deterministic way to surface these risks early in the development process, helping teams maintain secure, reviewable, and policy-compliant database changes.
This analyzer performs static analysis on parsed statements and reports potential vulnerabilities based on the following checks:
- Use of
EXECorEXECUTEstatements that concatenate strings or interpolate variables into SQL text. - Construction of dynamic SQL using string concatenation or format functions without proper sanitization or identifier quoting.
-- Unsafe: building a query through concatenation
EXECUTE 'ALTER TABLE ' || tbl_name || ' DROP COLUMN ' || col;
-- Safe: using format() with identifier sanitization
EXECUTE format('ALTER TABLE %I DROP COLUMN %I', tbl_name, col);
Static analysis cannot always determine whether a value is derived from a trusted source at runtime. As a result, the analyzer takes a conservative approach and may report false positives, particularly in complex and well-audited dynamic SQL code.
Vulnerable Extensions Atlas Pro
Database extensions ship their own code and their own vulnerabilities, and the version that is installed is rarely
reviewed. The cve analyzer resolves the extensions in a schema against the
Atlas Security Graph and reports the CVEs published for them, each with its
severity and the suggested fix.
Enable it by adding a cve block to the atlas.hcl file:
lint {
cve {}
}
atlas migrate lint reports the extensions installed or updated by the analyzed files, and attributes each
diagnostic to the statement that left the extension in that version. Extensions dropped by the file are not
reported, as they are no longer installed after it runs:
-- analyzing version 1
-- vulnerable extensions detected:
-- L1: Extension "pgcrypto" version "1.3" is vulnerable to CVE-2026-2005 (HIGH):
PostgreSQL pgcrypto heap buffer overflow executes arbitrary code. Upgrade the
database engine to version 14.21 or later to resolve CVE-2026-2005: the fix for
extension "pgcrypto" ships in engine releases
atlas schema lint reports every extension in the schema, not only the ones a change touches, as new CVEs are
published for versions that are already installed.
The analyzer supports the following options:
| Option | Description |
|---|---|
min_severity | The minimum severity to report: LOW, MEDIUM, HIGH or CRITICAL. All reported vulnerabilities are included when unset. |
ignore | CVE identifiers that are not reported. |
error | Report the diagnostics as errors and fail the command. false by default. |
timeout | Time limit for querying the Security Graph, 5s by default. Reporting is skipped if Atlas Cloud does not answer in time. |
lint {
cve {
min_severity = HIGH
ignore = ["CVE-2024-10977"]
error = true
timeout = "10s"
}
}
Findings are matched against the installed extension version and the version of the dev database, so built-in extensions, whose fixes ship in engine releases rather than in the extension itself, are reported against the engine you run.
To report the extensions installed in a running database rather than those declared bt a schema or a change,
use atlas security scan. This grades findings on the level the Security Graph assigns them,
NORMAL to CRITICAL, rather than on the CVSS rating this analyzer filters by, and is configured by its own
security block.