把数据库 schema 当代码管:Atlas 的声明式与版本化迁移
手写的 ALTER TABLE 散在发布脚本里,预发和生产悄悄漂移,删列丢数据没人拦——这是多数团队管 schema 的现状。Atlas 把这套流程改成代码化:定义期望状态,引擎计算迁移计划,破坏性变更在 apply 之前被拦下。
- GitHub:
- 官网:
Atlas 的定位是 "Declarative database schema management — define your desired state, Atlas computes the plan",类比的话就是「数据库界的 Terraform」。
声明式:删一列会被直接拒绝
编辑 schema/categories.sql,删掉 category_description 列,运行:
atlas schema apply --env local
输出:
Planning migration statements (1 in total):
-- modify "categories" table:
-> ALTER TABLE "categories" DROP COLUMN "category_description";
Analyzing planned statements (1 in total):
-- destructive change detected: data loss possible
DS102: column "category_description" contains data
Destructive change blocked.
Data loss detected. Migration rejected.
计划生成出来,但因为有数据丢失风险,apply 被拒绝。对应的老做法是手工写 ALTER TABLE、没有破坏性变更审查、环境之间 schema 漂移。
Schema 可以用 SQL 或 HCL 描述。比如 users.sql 里直接引用 functions.sql:
-- atlas:import functions.sql
CREATE TABLE users (
id serial NOT NULL,
name varchar(255) NOT NULL,
email varchar(255) UNIQUE NOT NULL,
phone varchar(20),
bio text,
PRIMARY KEY (id)
);
CREATE TABLE user_logs (
id serial NOT NULL,
user_id int NOT NULL,
body text NOT NULL,
PRIMARY KEY (id),
CONSTRAINT user_fk FOREIGN KEY (user_id) REFERENCES users(id)
);
Atlas 生成的迁移文件 20261009173911_add_user_logs.sql:
ALTER TABLE "users" ADD COLUMN "phone" varchar(20) NULL, ADD COLUMN "bio" text NULL;
CREATE TABLE "user_logs" (...);
Schema 定义支持 SQL、HCL、Python、Node.js、Java、Go、C#、PHP,也可以从 ORM(GORM、Drizzle、Django、SQLAlchemy)反向读取。目标数据库覆盖 PostgreSQL、MySQL、SQL Server、MariaDB、ClickHouse、Redshift、SQLite、Oracle、Snowflake、Spanner、Databricks、CockroachDB、Aurora DSQL、YugabyteDB、Azure Fabric。
SQLite 改列默认值必须重建表
users 表结构是 id INTEGER, greeting TEXT,现在想给 greeting 加默认值 "shalom"。SQLite 不支持修改已有列的默认值,常见绕法是建新表、拷旧行、drop 旧表。
声明式写法只改 HCL:
table "users" {
schema = schema.main
column "id" {
type = int
}
column "greeting" {
type = text
default = "shalom"
}
}
Atlas 引擎生成四步计划:
-- Create "new_users" table
CREATE TABLE `new_users` (`id` int NOT NULL, `greeting` text NOT NULL DEFAULT 'shalom')
-- Copy rows from old table "users" to new temporary table "new_users"
INSERT INTO `new_users` (`id`,`greeting`) SELECT `id`, IFNULL(`greeting`,'shalom') AS `greeting` FROM `users`
-- Drop "users" table after copying rows
DROP TABLE `users`
-- Rename temporary table "new_users" to "users"
ALTER TABLE `new_users` RENAME TO `users`
两个命令的区别:
atlas schema plan:审阅并批准迁移计划,apply 阶段跳过审批流。atlas schema apply:应用变更,默认执行前需要审批,可以是人工批准,也可以是基于 review policy 的 lint 诊断。被 plan 预先批准的变更会自动应用。
两者都支持 Atlas Actions(GitHub Actions、GitLab CI、Azure DevOps)。
版本化:每次变更签入一个迁移文件
当应用同时部署到多个远程环境、开发团队无法访问甚至无法控制目标库时,声明式那套「连目标库 + 人工实时批准计划」跑不起来。这时用版本化迁移,SQL 文件描述 how,每个文件带唯一版本号和描述,Flyway、Liquibase、golang-migrate 都是这个路子。
三个命令:
atlas migrate diff:对比迁移目录当前状态和期望 schema,自动生成迁移文件。atlas migrate lint:分析迁移文件里的潜在问题。atlas migrate apply:应用待执行的迁移文件。
版本化的代价是规划负担落到开发者身上,写安全迁移需要专业度,比如上面 SQLite 那种重建表的情况。Atlas 推荐第三种组合——Versioned Migration Authoring:仍然声明期望状态,用引擎规划安全迁移,但不把规划和执行耦合,把计划写成普通迁移文件,签入源码控制,允许手工微调,走 code review。
两条路怎么混用
本地开发走声明式:改 HCL 或 SQL 文件,对本地库 atlas schema apply 快速迭代,不用写迁移文件。
需要共享时切版本化:对更新后的期望状态跑 atlas migrate diff,生成迁移文件写进 migrations 目录,commit 进 PR,走 code review、迁移 lint 和 CI/CD 里 atlas migrate apply。
什么时候必须用版本化
- On-premise / 打包软件部署:客户自己安装管理,apply 阶段没人能审阅批准计划。版本化保证经创建、审阅、合并到主分支的迁移文件按原样在客户库执行,基于 git 历史可重复。
- CI/CD 数据库访问受限:runner 因网络隔离、安全策略或合规要求连不上生产/预发,无法动态生成计划。版本化基于迁移目录前一状态预生成,apply 前不需要检查目标库 schema。
- 大团队并发改 schema:迁移目录完整性文件
atlas.sum防止冲突,通过 merge conflict 检测保证线性历史,需要 rebase。 - 跨环境一致:多个环境、客户、实例应用完全相同的变更。
- 严格变更控制与审计:每次变更需要显式的版本记录,供合规和安全审计。
CI 里 lint 能拦下什么
atlas migrate lint 逐个分析迁移的破坏性变更、表锁、数据丢失风险和并发索引违规。内建 analyzers 包括:Destructive changes、Data-dependent changes、Backward incompatible、Blocking table locks、Concurrent index、Table copy detection、Constraint deletion、Naming conventions、SQL injection、Transaction safety。
PR 里检查输出的典型例子:
Destructive changes detected (Dropping non-virtual column "email")
Data dependent changes detected (Adding a non-nullable "varchar" column "phone" without a DEFAULT value)
Blocking table change detected (Changing column type from "int" to "bigint" requires table rewrite)
Concurrent index violation (Creating index non-concurrently acquires SHARE lock, blocking writes)
Security、Data、DataOps 也可以声明
Security as Code 用 HCL 定义 role、user 和 permission:
role "app_readonly" {
comment = "Read-only access"
}
role "app_writer" {
member_of = [role.app_readonly]
}
user "api_service" {
member_of = [role.app_writer, role.rds_iam]
}
permission {
for_each = [table.users, table.orders]
for = each.value
to = role.app_readonly
privileges = [SELECT]
}
policy.hcl 里可以写策略规则,例如 rule "schema" "no-superuser",用 predicate.not_superuser 断言并在违反时给出消息;还有 rule "schema" "no-grantable"。
流程是 Define → Plan(atlas schema plan)→ Review(CI 里跑 policy 检查)→ Deploy(Kubernetes operator 应用)。
Data as Code 管理声明式数据:Seed 查表、比较 desired 与 live、生成 DML,三种同步模式分别是 INSERT(仅添加)、UPSERT(添加 + 更新)、SYNC(全同步)。
data {
table = table.countries
rows = [
{ id = 1, code = "US", name = "United States" },
]
}
DataOps as Code 把 backfill、GDPR 删除、报表都声明成代码,用 atlas script exec 运行,事务模式 ONE_UNIT,出错策略 on_error ROLLBACK:
script "exec" "archive_dormant" {
condition "have_dormant" { ... }
query "dormant" { rows { id = int } }
exec "purge" { expect_rows = length(query.dormant.rows) }
}
Cloud Native 与 Agents
Terraform provider 是 ariga/atlas ~> 0.9.7:
provider "atlas" {
dev_url = "docker://postgres/15/myapp"
}
data "atlas_schema" "sql" {
src = "file://${path.module}/schema.sql"
}
resource "atlas_schema" "postgres" {
url = "..."
hcl = data.atlas_schema.sql.hcl
}
集成面覆盖 Terraform、Kubernetes、Argo CD、Crossplane、GitHub Actions、GitLab CI、Azure DevOps。
Agents 侧,Atlas 为 Claude Code、Codex、Cursor、Copilot 提供了 skills:
claude plugin marketplace add ariga/atlas
claude plugin install atlas@ariga
npx skills add ariga/atlas -a codex -y
装好后用 /atlas:atlas-onboard 起步。
项目地址:GitHub ariga/atlas · atlasgo.io