Перейти к содержанию

Миграции

Миграции версионируют декларации в вашем проекте: схему датасета, гиперпараметры модели, конфигурацию агента. Это обычные Python-файлы, которые вы читаете и правите, генерируемые сравнением того, что говорит код сейчас, с тем, что говорит история миграций.

python manage.py makemigrations
python manage.py migrate

Зачем это ML-проекту

Версия датасета, записанная полгода назад, несёт отпечаток схемы. Без истории этот отпечаток — непрозрачный хеш. С историей вы можете точно сказать, какие поля существовали, какие у них были границы и какие классы принимал LabelField — а именно это делает старый ран интерпретируемым, а не просто записанным.

Как выглядит миграция

reviews/migrations/0001_initial.py
"""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, сделала бы каждую историческую версию датасета неверной, и заметить это было бы нечем. Если вы имели в виду переименование, скажите это:

operations = [
    migrations.RenameObject("Reviews", "Feedback"),
]

Миграции данных

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="Пометить версии языком"),
    ]

Начать такую миграцию:

python manage.py makemigrations reviews --empty -n backfill_language

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. Это теряет историю, так что делайте это осознанно.