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

Дрейф схемы

Переименовали поле, перенесли его на другой уровень, сделали из значения карту — а версию не тронули. Миграция тогда не выполнится: её никто не просил. Но amethystate заметит, что структура изменилась, и скажет об этом.

Не типы, а расположение. Рядом с данными каждая структура записывает список мест, которые ей принадлежат. На следующем запуске, если сохранённая версия совпала с объявленной, этот список сличают с тем, что структура объявляет сейчас.

Место — это путь, по которому лежит значение или уровень карты. Вложенная структура местом не считается: по её пути ничего не хранится. Поэтому если обёртка получила или потеряла свой сегмент, в отчёт попадёт не она, а всё, что под ней: старые пути освободились, новые заняты.

объявлено принадлежит
------------------------------------------------
ui вложенная - сама она местом не является
theme поле ui.theme
ui flattened - и сегмента детям не даёт
theme поле theme

Сравнивают пути, а не имена полей. Одну и ту же раскладку можно записать по-разному: поле theme внутри вложенной ui и поле с path = "ui.theme" лежат в одном месте, хоть и названы по-разному. И наоборот: два поля с именем theme в разных структурах друг к другу отношения не имеют. Значение имеет только путь, на который поле легло.

Что случилось Приговор Почему
место было объявлено, а теперь нет ломает то, что там лежит, больше никому не принадлежит, и кто-то должен решить, что с ним делать
на том же месте теперь другое: значение вместо уровня карты или наоборот ломает на диске это разные вещи
появилось место, которого раньше никто не объявлял посмотрите, что там если там пусто, ничего и не произошло; если нет — вы забрали чужое
место стало необязательным или перестало им быть безобидно владелец у места прежний

О дрейфе сообщают, если хотя бы одно расхождение ломает. Остальные показывают рядом: изменение понятнее целиком, чем по одной сломавшейся части.

Переименование выглядит как освобождение и захват сразу, и отличить его от двух независимых изменений нельзя. Затем и нужен #[rename(old => new)] — им вы говорите, что поле одно и то же.

Запуск он не блокирует, но и сам не проходит

Заголовок раздела «Запуск он не блокирует, но и сам не проходит»

Store откроется, приложение заработает. Но то же самое вам скажут и на следующем запуске, и на всех следующих: пока дрейф не разобрали, записанная форма остаётся прежней, и сравнивать по-прежнему есть с чем.

Прекратить это можно двумя способами. Поднять версию и написать шаг миграции — если изменение было задуманным. Или вернуть освобождённые места на место — если не было. У store, который ещё в разработке, есть и третий способ — о нём ниже.

Структура в разгаре разработки меняется каждый день, и версия с шагом на каждое изменение — история миграций, которую никто никогда не запустит. Вместо этого store можно попросить отпускать то, чего никто не объявляет:

let (store, report) = StoreBuilder::new(path)
.rules(|r| r.on_undeclared(OnUndeclared::Drop))
.migrate()?;
assert!(!report.has_drift());

На каждом открытии каждый ключ под объявленным префиксом, которого не объявляет ни одна структура этой сборки, удаляется, а на место старой формы записывается та, что объявлена сейчас. Версия не двигается, шаги не бегут, и сообщать о дрейфе нечего — ни на этом открытии, ни на следующем. Переименованное поле начинает со своего значения по умолчанию: то, что лежало под старым именем, удалено, а не перенесено, — этим Drop и отличается от шага.

Подметают только префиксы, которые объявляет какая-нибудь структура, и только когда все шаги этого открытия уже отработали: шаг читает старые ключи — своей линии или чужой, — и к его запуску они должны быть на месте. Если хоть одна миграция упала, не удаляется ничего. Ключ вне всех объявленных префиксов остаётся где был, а корень не подметают никогда: структура с as_root занимает там свои поля, а не всё, что лежит на верхнем уровне.

Drop удаляет данные в ту же минуту, как поле переименовали, поэтому место ему в отладочной сборке, а не в той, что открывает чужой store. OnUndeclared::Keep, значение по умолчанию, ничего не удаляет и сообщает о дрейфе.

MigrationReport::has_drift говорит, был ли дрейф. Подробности — в component.nagging: по одной NaggingRecord на линию, а в ней префикс и id, все сдвинувшиеся места с приговорами и список полей, которые появились и исчезли, по именам.

let (store, report) = StoreBuilder::new("./app.redb").migrate()?;
for record in report.components.iter().flat_map(|c| &c.nagging) {
for moved in &record.moved {
println!("{:?} {moved}", moved.verdict());
}
}

Место в отчёте названо относительно префикса — token, а не app.token. Сам префикс лежит рядом, в той же записи.

И build, и migrate и так пишут отчёт через tracing, печатать руками ничего не надо. А если включить фичу diagnostics, вместо строки на каждое поле дрейф покажут так, как компилятор показывает предупреждение:

amethystate = { version = "0.26", features = ["diagnostics"] }

Ниже — одна структура, которую поправили, а version не тронули. Сборка, записавшая данные, объявляла так:

#[amethystate(prefix = "app", version = 2)]
pub struct Settings {
#[amestate(default = None)]
pub host: Option<String>,
#[amestate(default = {})]
pub port: ReactiveMap<String, u16>,
#[amestate(default = String::new())]
pub token: String,
}

а сборка, которая их читает, объявляет так:

#[amethystate(prefix = "app", version = 2)]
pub struct Settings {
#[amestate(default = "localhost".to_string())]
pub host: String,
#[amestate(default = 8080u16)]
pub port: u16,
}

token исчез, port перестал быть картой, host перестал быть необязательным, а version как был 2, так и остался:

amethystate::drift
⚠ `app` holds a shape this build does not declare
help: raise the struct's `version` and write a step for it, or declare the
released places again
Warning:
⚠ `app.token` was declared before and is not now
help: what an earlier build wrote there is under no declaration now
Warning:
⚠ `app.port` was written as Map and is declared as Field
help: one value and a level of entries are not the same thing on disk
Advice:
☞ `app.host` may no longer hold nothing
help: what was written there as nothing reads back as the declared default

Сначала идёт то, что ломает, потом то, на что стоит взглянуть, потом остальное. В самой записи порядок другой — тот, в котором шло сравнение: коду он соответствует, а тому, кто решает, делать что-то или нет, не помогает.

Фича выключена по умолчанию и отвечает только за вид. Сравнение идёт в любом случае, и has_drift отвечает в любом случае. Включайте там, где вывод читает человек, — в отладочной сборке, в профиле для разработки. А если рендерить хочется самому, MigrationReport::drift отдаст те же самые диагностики.

Типы. Тип листа — ваш, и значение, которое перестало декодироваться, разбирают там, где его читают: этим занимается on_unreadable, а список таких значений даёт disagreements(). См. Ошибки.

Поэтому смена u16 на u32 дрейфом не считается: место то же, и что там лежит, решает кодек. Если новый тип не прочитает старое значение, вы узнаете об этом при чтении, а не при запуске.

Ничего. Дрейф смотрят на каждом открытии — и через build, и через migrate, — и бинарник, в котором объявлены структуры, а #[migrate] нет вовсе, проверяют точно так же.

migrate нужен, чтобы выполнялись сами шаги. build не выполняет ни одного, поэтому бинарник, полный #[migrate], открытый так, не мигрирует ничего, а потом сообщает о дрейфе: именно так эта ошибка и выглядит.