Версионирование storage-формата
В первом уроке модуля мы прочитали из заголовка файла поле storage version — целое число вроде 68. Тогда мы отложили разговор о том, что это число значит и какие гарантии за ним стоят. Пришло время разобрать. Этот урок про то, как формат файла DuckDB эволюционирует от версии к версии, почему новый DuckDB читает старые файлы, почему старый DuckDB обычно не читает новые, и как параметром STORAGE_VERSION намеренно записать файл в более старом, совместимом формате.
Storage version — это не версия DuckDB
Первое, что нужно твёрдо развести: версия DuckDB и storage version — это разные числа про разные вещи.
Версия DuckDB — это 1.5.2, 1.4.4, 1.3.0. Она меняется с каждым релизом движка, включая патчи и фиксы багов, которые формата файла вообще не касаются.
Storage version — это целое число: 68, 67, 66. Оно описывает не движок, а структуру файла на диске. Оно меняется только тогда, когда меняется сам бинарный формат — добавляется новая схема сжатия, новый тип данных, новая структура метаданных. Несколько релизов DuckDB подряд могут писать один и тот же storage version, если в этих релизах раскладку файла не трогали.
Соответствие версий формата и линеек DuckDB:
| Storage version | Линейка DuckDB |
|---|---|
| v68 | DuckDB 1.5.x |
| v67 | DuckDB 1.4.x |
| v66 | DuckDB 1.3.x |
| v65 | DuckDB 1.2.x |
| v64 | DuckDB 0.9.x - 1.1.x |
Обратите внимание на последнюю строку: один storage version 64 покрывает целый диапазон релизов — от 0.9.x до 1.1.x включительно. Это и есть иллюстрация принципа: на отрезке от 0.9 до 1.1 формат файла не менялся, поэтому все эти релизы пишут и читают версию 64. А вот с 1.2 формат сдвинулся — версия стала 65, дальше 66 в 1.3, 67 в 1.4, 68 в 1.5.
Backward-совместимость: новый движок читает старые файлы
Backward-совместимость — это способность нового DuckDB открыть файл, записанный старым DuckDB. Версия 1.5 открывает файл формата 64, записанный когда-то DuckDB 1.0. Это гарантировано.
Точнее: backward-совместимость storage-формата гарантируется начиная с DuckDB v0.10, то есть начиная со storage version 64. Любой файл формата 64 и новее гарантированно читается любым DuckDB версии 0.10 и новее. Файл, созданный сегодня в DuckDB 1.0, без проблем откроется в DuckDB 1.5 и в будущих версиях.
Почему это можно гарантировать. Новый движок знает всё про старые форматы — он содержит код чтения версий 64, 65, 66, 67, 68. Когда он открывает файл, он читает storage version из заголовка и выбирает подходящий путь чтения. Старый формат — это подмножество того, что движок умеет: добавить в новый движок код для разбора старого формата технически несложно, его просто нужно не выбрасывать. Поэтому обещание «новый читает старое» поддерживается долго и устойчиво.
Практический смысл огромен. Файл .duckdb — это надёжный долговременный контейнер данных. Записали данные сегодня — через годы, на новой мажорной версии DuckDB, вы их откроете. Не нужно бояться обновлять движок: апгрейд DuckDB не превращает накопленные файлы в нечитаемые. Это та же гарантия, что делает SQLite основой для долговременного хранения, — и DuckDB её для своей ниши воспроизводит.
Forward-совместимость: best-effort, без гарантий
Обратное направление — forward-совместимость, способность старого DuckDB открыть файл, записанный новым DuckDB. Версия 1.3 пытается открыть файл формата 68, записанный DuckDB 1.5. Вот это уже не гарантировано — это best-effort, «по возможности».
Почему гарантировать нельзя, понятно из логики. Новый формат может содержать структуры, которых на момент выпуска старого движка просто не существовало: новую схему сжатия, новый физический тип, новую раскладку метаданных. Старый движок физически не содержит кода для их разбора — этот код написали позже. Заставить программу прошлого понимать формат будущего невозможно: нельзя добавить в уже выпущенный бинарник поддержку того, что ещё не придумано.
Что произойдёт на практике, когда старый DuckDB встретит файл с более новым storage version. Сценарий зависит от того, насколько форматы разошлись. Часто старый движок прочитает storage version из заголовка, увидит незнакомо большое число и откажется открывать файл с явным сообщением — мол, файл записан более новой версией, обновите DuckDB. Это аккуратный отказ, и именно для него storage version лежит в заголовке: чтобы несовместимость обнаружилась сразу, на третьем поле файла, а не глубоко внутри разбора структуры. Иногда, если изменения формата невелики, старый движок может файл и прочитать — но рассчитывать на это нельзя.
Backward-совместимость — твёрдая гарантия: новый DuckDB читает старые файлы. Forward-совместимость — лишь best-effort: старый DuckDB может и не открыть файл нового формата. Практический вывод для команд: если файлом базы обмениваются несколько человек или сервисов, ориентируйтесь на самую старую версию DuckDB в этом наборе. Тот, кто записывает файл, должен писать в формате не новее, чем понимает самый старый читатель.
STORAGE_VERSION: намеренно писать в старом формате
Раз forward-совместимость не гарантирована, возникает практическая задача: я работаю на свежем DuckDB 1.5, но файл нужно отдать коллеге, у которого ещё 1.2. Если я запишу файл в формате 68, его 1.2 файл не откроет. Решение — записать файл в более старом формате намеренно.
Для этого служит параметр STORAGE_VERSION, задаваемый при ATTACH:
-- Создать/присоединить базу в формате, совместимом с DuckDB 1.2.0
ATTACH 'shared.duckdb' (STORAGE_VERSION 'v1.2.0');
-- Все таблицы, созданные в этой базе, лягут в формат 65,
-- и файл откроется любым DuckDB начиная с 1.2.0
CREATE TABLE report AS SELECT range AS id FROM range(1000) USING shared;
STORAGE_VERSION 'v1.2.0' говорит движку: пиши этот файл в формате той версии. DuckDB 1.5 умеет записывать не только свой родной формат 68, но и более старые — он сознательно ограничивает себя, не использует структуры, которых в формате 65 не было, и файл получается читаемым на DuckDB 1.2 и новее. Можно указать и 'latest' — это явно затребовать самый свежий формат, который движок умеет.
Есть и эквивалент на уровне CLI — флаг -storage-version при запуске (доступен начиная с DuckDB 1.2.0):
# Записать базу в формате, совместимом с заданной версией
duckdb -storage-version v1.2.0 shared.duckdb \
"CREATE TABLE report AS SELECT range AS id FROM range(1000);"
Компромисс очевиден: записывая файл в старом формате, вы отказываетесь от возможностей, появившихся в новых версиях формата, — новых типов, новых схем сжатия, более компактных метаданных. Файл может оказаться чуть менее эффективным. Но взамен он гарантированно открывается на старом движке. Это осознанный обмен «новые фичи формата» на «совместимость с конкретной старой версией», и STORAGE_VERSION делает этот обмен явным и управляемым.
Когда совместимости storage-формата мало
Storage version решает совместимость на уровне байтового формата файла. Но иногда нужна переносимость более радикальная — поверх версий, поверх движков, даже на другие СУБД. Storage version тут не поможет: даже файл формата 64 — это всё равно специфичный для DuckDB бинарный формат, и Postgres его не прочитает.
Для такой переносимости есть другой инструмент — логический дамп через EXPORT DATABASE и IMPORT DATABASE. Он выгружает базу не как бинарный файл, а как набор Parquet/CSV-файлов с данными плюс SQL-скрипт со схемой. Это переносимо несравнимо шире, чем любой бинарный формат, — и этому посвящён следующий, последний урок модуля. Здесь же зафиксируем границу: storage version — про совместимость бинарного формата между версиями DuckDB; логический экспорт — про переносимость данных за пределы этой бинарной совместимости вообще.
Попробуй сам
Поэкспериментируйте с версиями формата.
- Узнайте свою версию DuckDB:
duckdb --version. По таблице из урока определите, какой storage version она пишет по умолчанию. - Создайте обычную базу:
duckdb v_default.duckdb "CREATE TABLE t AS SELECT range AS n FROM range(1000);". Прочитайте storage version прямо из файла:hexdump -C v_default.duckdb | head -1и переведите байт по смещению0x0Cв десятичную систему. Совпало с ожидаемым по таблице? - Создайте базу в намеренно старом формате:
duckdb -storage-version v1.2.0 v_old.duckdb "CREATE TABLE t AS SELECT range AS n FROM range(1000);". Снова прочитайте storage version из заголовка черезhexdump. Какое теперь число? Должно быть 65. - Откройте обе базы своим текущим DuckDB — обе должны открыться без проблем (backward-совместимость: новый движок читает оба формата).
- Поразмышляйте над сценарием: у вас 1.5, у коллеги 1.3. В каком формате нужно записать файл, чтобы коллега его открыл? А если бы у коллеги было 1.5, а у вас 1.3 — смогли бы вы открыть его файл, и почему это уже не гарантировано?
Этот эксперимент показывает, что storage version — это видимое в байтах файла число, которым можно управлять, и что выбор формата — это выбор между новыми возможностями и совместимостью со старым движком.
Эволюция ORC-формата: схема и добавление новых колонок