atlas-best-practices
Atlas Best Practices
Atlas is a language-independent tool for managing database schemas using declarative or versioned workflows.
Two Workflows
Declarative (Terraform-like): Atlas compares current vs desired state and generates migrations automatically.
atlas schema apply --url "postgres://..." --to "file://schema.hcl" --dev-url "docker://postgres/15"
Versioned: Atlas generates migration files from schema changes, stored in version control.
atlas migrate diff add_users --dir "file://migrations" --to "file://schema.sql" --dev-url "docker://postgres/15"
atlas migrate apply --dir "file://migrations" --url "postgres://..."
Dev Database
Atlas requires a dev database for schema validation, diffing, and linting. Use the docker driver for ephemeral containers:
# PostgreSQL
--dev-url "docker://postgres/15/dev?search_path=public"
# MySQL
--dev-url "docker://mysql/8/dev"
# SQLite
--dev-url "sqlite://dev?mode=memory"
Schema-as-Code
HCL Schema (Recommended)
Use database-specific file extensions for editor support: .pg.hcl (PostgreSQL), .my.hcl (MySQL), .lt.hcl (SQLite).
schema "public" {
comment = "Application schema"
}
table "users" {
schema = schema.public
column "id" {
type = bigint
}
column "email" {
type = varchar(255)
null = false
}
column "created_at" {
type = timestamptz
default = sql("now()")
}
primary_key {
columns = [column.id]
}
index "idx_users_email" {
columns = [column.email]
unique = true
}
}
table "orders" {
schema = schema.public
column "id" {
type = bigint
}
column "user_id" {
type = bigint
null = false
}
column "total" {
type = numeric
null = false
}
foreign_key "fk_user" {
columns = [column.user_id]
ref_columns = [table.users.column.id]
on_delete = CASCADE
}
check "positive_total" {
expr = "total > 0"
}
}
SQL Schema
Use standard SQL DDL files:
CREATE TABLE "users" (
"id" bigint PRIMARY KEY,
"email" varchar(255) NOT NULL UNIQUE,
"created_at" timestamptz DEFAULT now()
);
Project Configuration
Create atlas.hcl for environment configuration:
variable "db_url" {
type = string
}
env "local" {
src = "file://schema.pg.hcl"
url = var.db_url
dev = "docker://postgres/15/dev?search_path=public"
migration {
dir = "file://migrations"
}
format {
migrate {
diff = "{{ sql . \" \" }}"
}
}
}
env "prod" {
src = "file://schema.pg.hcl"
url = var.db_url
migration {
dir = "atlas://myapp" # Atlas Registry
}
}
Run with environment:
atlas schema apply --env local --var "db_url=postgres://..."
Migration Linting
Atlas analyzes migrations for safety. Configure in atlas.hcl:
lint {
destructive {
error = true # Fail on DROP TABLE/COLUMN
}
data_depend {
error = true # Fail on data-dependent changes
}
naming {
match = "^[a-z_]+$"
message = "must be lowercase with underscores"
index {
match = "^idx_"
message = "indexes must start with idx_"
}
}
# PostgreSQL: require CONCURRENTLY for indexes (Pro)
concurrent_index {
error = true
}
}
Key analyzers:
- DS: Destructive changes (DROP SCHEMA/TABLE/COLUMN)
- MF: Data-dependent changes (ADD UNIQUE, NOT NULL)
- BC: Backward incompatible (rename table/column)
- PG (Pro): Concurrent index, blocking DDL
Lint migrations:
atlas migrate lint --env local --latest 1
Suppress specific checks in migration files:
-- atlas:nolint destructive
DROP TABLE old_users;
Schema Testing
Write tests in .test.hcl files:
test "schema" "user_constraints" {
parallel = true
exec {
sql = "INSERT INTO users (id, email) VALUES (1, 'test@example.com')"
}
# Test unique constraint
catch {
sql = "INSERT INTO users (id, email) VALUES (2, 'test@example.com')"
error = "duplicate key"
}
assert {
sql = "SELECT COUNT(*) = 1 FROM users"
error_message = "expected exactly one user"
}
cleanup {
sql = "DELETE FROM users"
}
}
# Table-driven tests
test "schema" "email_validation" {
for_each = [
{input: "valid@test.com", valid: true},
{input: "invalid", valid: false},
]
exec {
sql = "SELECT validate_email('${each.value.input}')"
output = each.value.valid ? "t" : "f"
}
}
Run tests:
atlas schema test --env local schema.test.hcl
Transaction Modes
Control transaction behavior per-file with directives:
-- atlas:txmode none
CREATE INDEX CONCURRENTLY idx_users_email ON users (email);
Modes: file (default, one tx per file), all (one tx for all), none (no tx).
Pre-Execution Checks (Pro)
Block dangerous operations in atlas.hcl (requires Atlas Pro):
env "prod" {
check "migrate_apply" {
deny "too_many_files" {
condition = length(self.planned_migration.files) > 3
message = "Cannot apply more than 3 migrations at once"
}
}
}
Common Commands
# Generate migration from schema diff
atlas migrate diff migration_name --env local
# Apply pending migrations
atlas migrate apply --env local
# Validate migration directory integrity
atlas migrate validate --env local
# View migration status
atlas migrate status --env local
# Push to Atlas Registry
atlas migrate push myapp --env local
# Declarative apply (no migration files)
atlas schema apply --env local --auto-approve
# Inspect current database schema
atlas schema inspect --url "postgres://..." --format "{{ sql . }}"
# Compare schemas
atlas schema diff --from "postgres://..." --to "file://schema.hcl"
CI/CD Integration
GitHub Actions setup:
- uses: ariga/setup-atlas@v0
with:
cloud-token: ${{ secrets.ATLAS_CLOUD_TOKEN }}
- name: Lint migrations
run: atlas migrate lint --env ci --git-base origin/main
Baseline for Existing Databases
When adopting Atlas on existing databases:
# Create baseline migration reflecting current schema
atlas migrate diff baseline --env local --to "file://schema.hcl"
# Mark baseline as applied (skip execution)
atlas migrate apply --env prod --baseline "20240101000000"
ORM Integration
Atlas supports loading schemas from ORMs via external providers:
data "external_schema" "gorm" {
program = [
"go", "run", "-mod=mod",
"ariga.io/atlas-provider-gorm",
"load", "--path", "./models",
"--dialect", "postgres",
]
}
env "local" {
src = data.external_schema.gorm.url
}
Supported: GORM, Sequelize, TypeORM, Django, SQLAlchemy, Prisma, and more.
Instructions
- Always use a dev database for
migrate diffandschema apply; it validates schemas safely. - Enable strict linting in CI to catch destructive and data-dependent changes early.
- Use versioned migrations for production; declarative workflow suits development/testing.
- Test schemas with
.test.hclfiles; validate constraints, triggers, and functions. - Push migrations to Atlas Registry for deployment; avoid copying files manually.
- Use
-- atlas:txmode nonefor PostgreSQL concurrent index operations. - Configure naming conventions in lint rules; consistency prevents errors.