跳转到内容

迁移

wippy/migration 模块提供了数据库迁移框架,包含用于定义模式变更的小型 DSL、用于发现和执行迁移的运行器,以及一个为项目中每个已注册 target_db 运行待执行迁移的引导加载器。

迁移支持 SQLite、PostgreSQL 和 MySQL,每个驱动的 up/down 实现可并列定义。

将模块添加到项目:

Terminal window
wippy add wippy/migration
wippy install

声明依赖以及迁移所针对的应用数据库:

version: "1.0"
namespace: app
entries:
- name: app_db
kind: db.sql.sqlite
path: ./data/app.db
- name: dep.migration
kind: ns.dependency
component: wippy/migration
version: "*"

迁移引导加载器以顺序 20 注册到 wippy/bootloader。当应用启动时,它会发现注册表中的每个迁移条目,按 meta.target_db 分组,并针对每个数据库运行待执行迁移。

迁移是带有 meta.type: migrationfunction.lua 条目。该条目返回由 migration.define(...) 生成的函数。

entries:
- name: 01_create_users_table
kind: function.lua
meta:
type: migration
target_db: app:app_db
timestamp: "2025-01-15T10:00:00Z"
source: file://01_create_users_table.lua
imports:
migration: wippy.migration:migration
return require("migration").define(function()
migration("Create users table", function()
database("sqlite", function()
up(function(db)
local ok, err = db:execute([[
CREATE TABLE users (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
email TEXT NOT NULL UNIQUE
)
]])
if err then error(err) end
end)
down(function(db)
db:execute("DROP TABLE IF EXISTS users")
end)
end)
database("postgres", function()
up(function(db)
db:execute([[
CREATE TABLE users (
id SERIAL PRIMARY KEY,
name TEXT NOT NULL,
email TEXT NOT NULL UNIQUE
)
]])
end)
down(function(db)
db:execute("DROP TABLE IF EXISTS users")
end)
end)
end)
end)
字段必填说明
meta.type必须为 "migration" 以便被发现
meta.target_db要执行的数据库的注册表 ID
meta.timestampISO-8601 时间戳,用于在多个迁移针对同一数据库时排序
meta.tags标签数组;运行器可按标签过滤迁移

某个数据库的迁移按 meta.timestamp 升序运行。

在传递给 migration.define 的函数内部,可使用三个嵌套函数:

函数说明
migration(description, fn)打开一个新的迁移,附带人类可读的描述
database(type, fn)"sqlite""postgres""mysql" 声明实现
up(fn) / down(fn)定义正向和回滚函数
after(fn)可选的迁移后钩子(同一事务)

每个 up/down/after 函数接收的是事务对象,而非原始连接。三个操作都在单一事务中运行,出错时会回滚。

local rows, err = db:query(sql, params) -- SELECT, returns array of rows
local result, err = db:execute(sql, params) -- INSERT/UPDATE/DDL, returns { rows_affected, last_insert_id }
local stmt, err = db:prepare(sql) -- prepared statement

始终使用参数化查询:

db:execute("INSERT INTO users (name, email) VALUES (?, ?)", { "Alice", "alice@example.com" })

调用 error(...) 将中止迁移并回滚事务。对每条可能失败的语句进行包装:

up(function(db)
local _, err = db:execute("CREATE TABLE ...")
if err then error(err) end
end)

运行器以库的形式暴露,供程序化使用:

imports:
runner: wippy.migration:runner
local runner = require("runner").setup("app:app_db")
local result = runner:run() -- apply all pending migrations
local result = runner:run_next() -- apply the next pending migration
local result = runner:rollback({ id = "app:01_create_users_table" })
local status = runner:status() -- list applied + pending migrations

对所配置数据库应用每个待执行迁移。返回摘要:

{
status = "complete", -- "complete" or "error"
migrations_found = 3,
migrations_applied = 2,
migrations_skipped = 1,
migrations_failed = 0,
duration = 0.123,
migrations = { ... }, -- per-migration status
skipped_details = { ... },
}

选项:

选项说明
tags标签数组;仅考虑 meta.tags 与之有交集的迁移

按 id 回滚单个迁移(必填):

runner:rollback({ id = "app:01_create_users_table" })

返回 { applied = {...}, pending = {...} },分别按 applied_atmeta.timestamp 排序。

wippy.migration:registry 提供直接的注册表查询:

函数说明
registry.find({ target_db, tags })返回所有匹配条件的迁移条目
registry.get(id)按 id 返回单个迁移条目
registry.get_target_dbs()返回迁移中出现的每个唯一 meta.target_db
registry.get_tags()返回迁移上出现的每个唯一标签

引导加载器使用这些函数在启动时发现目标数据库的完整集合。

运行器首次执行时会在每个目标数据库中创建 wippy_migrations 表。已应用的迁移按 id 记录,后续运行会跳过它们。该跟踪表会自动创建;请勿编写自己的迁移来创建它。

  • 每次迁移做一项逻辑变更 - 创建一张表、添加一列、创建一个索引。
  • 写一个真实的 down - 如果回滚不可能(数据丢失),请将其记录下来并抛出错误,而不是无声地成功。
  • 优先考虑幂等性 - CREATE TABLE IF NOT EXISTSDROP TABLE IF EXISTS 可在重复执行时保持稳定,无需特殊处理。
  • 将 DDL 与 DML 分开 - 当可以避免时,不要在创建表的同一迁移中播种数据。
  • 双向测试 - 应用迁移,回滚它,并验证模式与初始状态一致。