Перейти к содержимому

Migrations

Модуль wippy/migration предоставляет фреймворк миграций базы данных с компактным DSL для описания изменений схемы, раннер, который обнаруживает и выполняет их, и загрузчик, запускающий ожидающие миграции для каждого target_db, зарегистрированного в проекте.

Миграции поддерживают SQLite, PostgreSQL и MySQL, с реализациями up/down для каждого драйвера, определёнными рядом.

Добавьте модуль в проект:

Окно терминала
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: "*"

Загрузчик миграций регистрируется в wippy/bootloader с порядком 20. При запуске приложения он обнаруживает каждую запись миграции в реестре, группирует их по meta.target_db и выполняет ожидающие миграции для каждой базы данных.

Миграция — это запись function.lua с meta.type: migration. Запись возвращает функцию, созданную 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даИдентификатор реестра базы данных для выполнения
meta.timestampнетМетка времени ISO-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 пересекаются

Откатывает одну миграцию по идентификатору (обязательный):

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

Возвращает { applied = {...}, pending = {...} }, отсортированные по applied_at и meta.timestamp соответственно.

wippy.migration:registry предоставляет прямые запросы к реестру:

ФункцияОписание
registry.find({ target_db, tags })Вернуть все записи миграций, соответствующие критериям
registry.get(id)Вернуть одну запись миграции по идентификатору
registry.get_target_dbs()Вернуть все уникальные meta.target_db, присутствующие в миграциях
registry.get_tags()Вернуть все уникальные теги, присутствующие в миграциях

Загрузчик использует их для обнаружения полного набора целевых баз данных при запуске.

Раннер создаёт таблицу wippy_migrations в каждой целевой базе данных при первом запуске. Применённые миграции записываются по идентификатору, чтобы последующие запуски пропускали их. Таблица отслеживания создаётся автоматически; не пишите собственную миграцию для её создания.

  • Одно логическое изменение на миграцию — создать одну таблицу, добавить один столбец, создать один индекс.
  • Пишите настоящий down — если откат невозможен (потеря данных), задокументируйте это и вызовите ошибку, а не молчаливо завершайте успехом.
  • Предпочитайте идемпотентностьCREATE TABLE IF NOT EXISTS и DROP TABLE IF EXISTS переживают повторные запуски без специальной обработки.
  • Разделяйте DDL и DML — по возможности не засевайте данные в той же миграции, где создаётся таблица.
  • Тестируйте оба направления — примените миграцию, откатите её и убедитесь, что схема соответствует начальному состоянию.