Дрейф схемы
Переименовали поле, перенесли его на другой уровень, сделали из значения карту — а версию не тронули. Миграция тогда не выполнится: её никто не просил. Но 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], открытый так, не мигрирует ничего, а потом сообщает о дрейфе: именно так эта ошибка и выглядит.