Migrations
Migrations
Заголовок раздела «Migrations»Модуль wippy/migration предоставляет фреймворк миграций базы данных с компактным DSL для описания изменений схемы, раннер, который обнаруживает и выполняет их, и загрузчик, запускающий ожидающие миграции для каждого target_db, зарегистрированного в проекте.
Миграции поддерживают SQLite, PostgreSQL и MySQL, с реализациями up/down для каждого драйвера, определёнными рядом.
Настройка
Заголовок раздела «Настройка»Добавьте модуль в проект:
wippy add wippy/migrationwippy 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:migrationreturn 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 rowslocal 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) endend)API раннера
Заголовок раздела «API раннера»Раннер экспонируется как библиотека для программного использования:
imports: runner: wippy.migration:runnerlocal runner = require("runner").setup("app:app_db")
local result = runner:run() -- apply all pending migrationslocal result = runner:run_next() -- apply the next pending migrationlocal result = runner:rollback({ id = "app:01_create_users_table" })local status = runner:status() -- list applied + pending migrationsrunner:run(options)
Заголовок раздела «runner:run(options)»Применяет все ожидающие миграции для настроенной базы данных. Возвращает сводку:
{ 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(options)
Заголовок раздела «runner:rollback(options)»Откатывает одну миграцию по идентификатору (обязательный):
runner:rollback({ id = "app:01_create_users_table" })runner:status(options)
Заголовок раздела «runner:status(options)»Возвращает { applied = {...}, pending = {...} }, отсортированные по applied_at и meta.timestamp соответственно.
API реестра
Заголовок раздела «API реестра»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 — по возможности не засевайте данные в той же миграции, где создаётся таблица.
- Тестируйте оба направления — примените миграцию, откатите её и убедитесь, что схема соответствует начальному состоянию.
См. также
Заголовок раздела «См. также»- SQL-драйвер — Конфигурация ресурса базы данных
- Загрузчик — Порядок и хуки загрузчика
- Обзор фреймворка — Использование модулей фреймворка