Миграции¶
Миграции версионируют декларации в вашем проекте: схему датасета, гиперпараметры модели, конфигурацию агента. Это обычные Python-файлы, которые вы читаете и правите, генерируемые сравнением того, что говорит код сейчас, с тем, что говорит история миграций.
Зачем это ML-проекту¶
Версия датасета, записанная полгода назад, несёт отпечаток схемы. Без истории
этот отпечаток — непрозрачный хеш. С историей вы можете точно сказать, какие поля
существовали, какие у них были границы и какие классы принимал LabelField — а
именно это делает старый ран интерпретируемым, а не просто записанным.
Как выглядит миграция¶
"""Generated by mlango 0.2.0 on 2026-07-31 12:00."""
from mlango import migrations
from mlango.core import fields
class Migration(migrations.Migration):
initial = True
dependencies = []
operations = [
migrations.CreateObject(
name="Reviews",
kind="dataset",
fields=[
("id", fields.IntegerField()),
("text", fields.TextField()),
("label", fields.LabelField(["negative", "positive"])),
],
options={
"description": "Отзывы покупателей о товарах.",
"primary_key": "id",
},
),
]
Операции¶
| Операция | Значение |
|---|---|
CreateObject(name, kind, fields, options) |
Новая декларация |
DeleteObject(name) |
Декларация удалена |
RenameObject(old_name, new_name) |
Переименование — пишется руками |
AddField(object_name, name, field) |
Поле добавлено |
RemoveField(object_name, name) |
Поле удалено |
AlterField(object_name, name, field) |
Определение поля изменилось |
AlterOptions(object_name, options) |
Изменились опции Meta |
RunPython(code, reverse_code=None) |
Произвольный код — бэкфиллы, перекодирование |
Переименования никогда не угадываются¶
Переименуйте поле — и автодетектор запишет RemoveField и AddField, а не
RenameObject.
Это намеренно. Ошибочная догадка о переименовании молча меняет то, что означают
сохранённые данные — колонка body, тихо принятая за старую text, сделала бы
каждую историческую версию датасета неверной, и заметить это было бы нечем. Если
вы имели в виду переименование, скажите это:
Миграции данных¶
RunPython получает состояние проекта и контекст с сессией метастора, поэтому
бэкфилл может читать и писать версии, не открывая своё соединение:
from mlango import migrations
def backfill_language(state, context):
"""Пометить каждую материализованную версию определённым языком."""
from mlango.metastore import DatasetVersion
session = context["session"]
for version in session.query(DatasetVersion).filter_by(label="reviews.Reviews"):
version.notes = f"{version.notes} language=ru".strip()
class Migration(migrations.Migration):
dependencies = [("reviews", "0003_reviews_language")]
operations = [
migrations.RunPython(backfill_language, description="Пометить версии языком"),
]
Начать такую миграцию:
RunPython выполняется при migrate
Проверяйте миграции от внешних контрибьюторов так же, как любой другой код.
Команды¶
python manage.py makemigrations # все приложения
python manage.py makemigrations reviews # одно приложение
python manage.py makemigrations -n add_language # своё имя
python manage.py makemigrations --dry-run # показать, не записывать
python manage.py makemigrations reviews --empty # заготовку под ручную правку
python manage.py migrate # применить всё ожидающее
python manage.py migrate reviews # одно приложение
python manage.py migrate --plan # что будет применено
python manage.py migrate --fake # отметить применённым без запуска
python manage.py showmigrations # что есть и что применено
python manage.py showmigrations -v 2 # с перечислением операций
Собственные таблицы метастора¶
Десять таблиц метастора принадлежат фреймворку и никогда не меняют форму по
вашему запросу, поэтому они создаются по требованию — вам не нужно вызывать
migrate перед первым materialize(). migrate тоже их создаёт, а затем
применяет ваши декларативные миграции.
Это разделение намеренное: служебные таблицы не должны быть тем, о чём новичку приходится думать, а вот история его схемы должна быть явной.
Обновление самого mlango обрабатывается здесь же. Когда релиз добавляет колонку в таблицу метастора, следующее подключение добавит её и в вашу базу — со значением по умолчанию, чтобы строки, записанные старой версией, продолжали работать. Годятся только добавления: переименование или смена типа — это уже настоящая миграция, и молча переписывать историю ранов при импорте было бы плохой идеей. Если новую колонку нельзя заполнить безопасно, в лог попадает её имя, а база остаётся нетронутой — вместо угадывания.
Порядок и зависимости¶
Внутри приложения порядок задаёт числовой префикс. Между приложениями — объявите:
class Migration(migrations.Migration):
dependencies = [
("reviews", "0002_reviews_language"),
("shared", "0001_initial"),
]
Цикл — жёсткая ошибка, а не то, что нужно обходить.
Не каждая опция Meta записывается¶
Опция, которая не переживает круговорот через файл, не входит в состояние
миграций — например, source, указывающий на живой Python-генератор. Обратное
породило бы миграции, падающие при импорте. Простые значения (строки, числа,
булевы, плоские списки и словари из них) записываются; вызываемые объекты и живые
объекты — нет.
Схлопывание¶
Пока не реализовано. Пока проект, которому нужен чистый лист, может удалить свои
файлы миграций, сгенерировать 0001_initial заново и вызвать migrate --fake.
Это теряет историю, так что делайте это осознанно.