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

Чего store не примет

Запись отклоняют две разные вещи, и задать вам дано только одну.

Насколько глубоко может вкладываться значение

Заголовок раздела «Насколько глубоко может вкладываться значение»

Предел здесь — на форму значения. Структура, которая держит структуру, которая держит Vec структур, — это уже три уровня, а больше ничего и не считали. А у типа, который ссылается на себя, — дерево, меню, выражение, — уровней столько, сколько их окажется у данных в день запуска.

Кодек любого движка читает мельче, чем пишет. serde_json на входе останавливается на 128, на выходе не ограничен ничем; ron останавливается на 64; у rmp_serde предела нет вовсе — вместо него кончается стек, примерно на трёх тысячах (мерили на Windows 11 при размере стека по умолчанию; где именно он кончится, зависит от системы и от того, с каким стеком запущен поток). Это убивает процесс, а не возвращает ошибку, и повторяется на каждом следующем запуске, потому что значение уже записано.

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

Считают уровни по ходу записи, а не до неё. Осмотреть значение заранее нельзя: до store оно доходит как &dyn Serialize, и пятиуровневая структура там неотличима от пятиуровневого дерева. Построить его, чтобы измерить, тоже нельзя — как раз построить и опасно: на redb именно это переполняет стек. Serde проталкивает, store принимает, поэтому уровни и считают по мере прохождения, внутри собственного прохода кодека.

let engine = default_backend();
println!("{}: {} levels", engine.extension(), engine.depth_ceiling());

Числа не взяты с потолка, а измерены: tests/probe_*.rs доводит каждый движок до его границы.

движок уровней
redb 512
JSON 127
SQLite 127
TOML 80
RON 64

Своего предела у redb нет. rmp_serde рекурсирует, пока не кончится стек, — уровне на трёхтысячном на Windows 11 при стеке по умолчанию, — и процесс умирает вместо того, чтобы вернуть ошибку, и так на каждом следующем запуске. Это число — замер, а не константа: оно поедет вместе с системой и с тем, какой стек достался потоку. 512 поставлены ровно поэтому: намного выше любых данных, которые кто-нибудь собирается хранить, и намного ниже места, где кончается стек на любой из них.

На текстовых движках уровни пути становятся уровнями документа: ui.panels.left — это три уровня вложенности в файле ещё до того, как начнётся значение. Поэтому их и считают вместе, и мелкое значение по глубокому пути нечитаемо ровно так же, как глубокое по мелкому.

SQLite тут исключение: путь у него ключ TEXT, а не вложенность. Но платить там за путь — несколько уровней из 127, и второго правила это не стоит.

Отклонив запись, отчёт скажет, куда ушёл бюджет: сколько уровней забрал путь, сколько осталось значению и сколько этот store читает всего.

Оба идут через limits, и поднять потолок ни один из них не может — только опустить.

let store = StoreBuilder::new(settings)
.limits(|l| l.key_depth(4))
.build()?;
let deep = StorePath::from_segments(["a", "b", "c", "d", "e"]);
if let Err(refused) = store.set(&deep, &1u32) {
println!("{refused:?}");
}

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

let store = StoreBuilder::new(settings)
.limits(|l| l.portable_across([default_backend()]))
.build()?;

portable_across называет движки, на которых содержимое должно остаться читаемым, — помимо того, что работает на самом деле, — и опускает потолок до самого низкого из названных. Store на redb, назвавший RON, читает 64 уровня, а не 512: значение, слишком глубокое для файла, куда его потом выгрузят, отклонят сейчас, а не после выгрузки.

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

свойство кто его теряет
NaN и бесконечности JSON и SQLite: у формата нет для них написания, и записанное читается обратно как null
enum-варианты RON: его дерево значений их не знает, см. ron#122. Тип, который для человека пишет себя строкой, — например, IpAddr, — проверяют в этом виде
Some(None) все, кроме RON: вложенный Option схлопывается в None
u128 и i128 с любым значением TOML и RON: у них нет 128-битного целого
целые шире i64 TOML: там для них нет места
целые, которых не вмещает ни i64, ни u64 JSON: число читается обратно как i64, u64 или f64

Работающий движок входит в расчёт наравне с названными, и по той же причине, что и его потолок: значение, которого его кодек не прочитает обратно, потеряно при любых настройках. Поэтому NaN не примет store на JSON, даже если не называть никого.

Движки называют поимённо, а не пишут все, и это намеренно. «Все» — цель подвижная: добавили движок, и она уехала под ногами. А настоящее требование обычно уже — приложению, которое ставит JSON на десктопе и SQLite на телефоне, нужны эти два, а про RON у него мнения нет.

Store следит за глубиной и за шестью свойствами выше: запись, которая нарушает одно из них, он отклоняет. Что формат способен выразить — вопрос отдельный, и настройками его не сдвинуть: Выбор движка.