DDL (Data Definition Language) в Tarantool - это не отдельный язык, а набор операций над схемой: создание спейса, описание format, создание и изменение индексов, удаление объектов. Любое такое изменение в терминологии Tarantool называется миграцией - даже первое создание спейса. Понять урок - значит понять, что DDL это не "файлы со схемой", а транзакционные записи в системные спейсы, которые попадают в WAL и реплицируются, как обычные данные.
Ключевая идея: в Tarantool схема опциональна. Можно создать спейс без format вообще, и тогда в кортежах будут лежать произвольные данные (кроме индексируемых полей - они обязаны иметь одинаковый тип). Схема (format) - это валидатор поверх MsgPack, а не способ хранения. Хранится кортеж всегда как MsgPack-массив; format лишь даёт полям имена и проверяет типы перед каждым insert/update.
Механика: где живёт схема
Вся метаинформация хранится в служебных спейсах с отрицательными/маленькими id, доступных через box.space:
- _space - список всех спейсов, их id, engine, format
- _index - описания всех индексов (по какому спейсу, parts, тип, уникальность)
- _sequence - генераторы значений
- _func, _user, _priv - функции, пользователи, права
Запомните формулу: DDL = транзакционная запись в _space/_index -> WAL -> репликация. Никаких отдельных файлов схемы у движка нет.

DDL-поток: схема, format, индексы и миграции
Два трека: классический box.cfg и декларативный 3.x
Классический трек (1.x/2.x/3.x). Схему описывают императивным Lua-кодом, обычно в init.lua: сначала box.cfg{}, затем create_space, format, create_index. Этот способ работает во всех версиях и остаётся основным языком самих миграций.
Декларативный трек 3.x. В Tarantool 3 конфигурация инстанса задаётся YAML (config.yaml / etcd), но - важная тонкость - сама бизнес-схема спейсов в стандартный YAML-конфиг не входит. Конфиг 3.x управляет топологией, ролями, репликацией. Схему спейсов в 3.x применяют либо тем же Lua (в app-роли через скрипт старта), либо декларативно через официальный модуль ddl, который принимает YAML-описание спейсов и сам раскладывает его в create_space/format/create_index. То есть "декларативность 3.x" для схемы - это модуль ddl, а не box.cfg.
Ключевые команды и короткие примеры
Создание спейса, format и индекса в классическом стиле:
Код: Выделить всё
box.cfg{}
local users = box.schema.create_space('users', { if_not_exists = true })
users:format({
{ name = 'id', type = 'unsigned' },
{ name = 'email', type = 'string' },
{ name = 'age', type = 'unsigned', is_nullable = true },
})
-- первый индекс = первичный ключ, обязан быть уникальным
users:create_index('primary', {
parts = { { field = 'id', type = 'unsigned' } },
})
-- вторичный индекс по имени поля (имя доступно благодаря format)
users:create_index('by_email', {
parts = { 'email' },
unique = true,
})
Декларативное описание тем же модулем ddl (YAML):
Код: Выделить всё
spaces:
users:
engine: memtx
is_local: false
temporary: false
format:
- { name: id, type: unsigned, is_nullable: false }
- { name: email, type: string, is_nullable: false }
indexes:
- name: primary
unique: true
type: TREE
parts: [ { path: id, type: unsigned, is_nullable: false } ]
- name: by_email
unique: true
type: TREE
parts: [ { path: email, type: string, is_nullable: false } ]
Код: Выделить всё
local yaml = require('yaml')
local ddl = require('ddl')
box.cfg{}
local schema = yaml.decode(io.open('ddl.yml'):read('*all'))
local ok, err = ddl.check_schema(schema) -- сухая проверка
assert(ok, err)
ok, err = ddl.set_schema(schema) -- применение
assert(ok, err)
Миграции: простые и сложные
Tarantool делит миграции на два класса.
Простые - не трогают существующие данные:
- Создание нового индекса - можно в любой момент.
- Добавление поля В КОНЕЦ спейса через расширение format. Новое поле обязано быть is_nullable = true, иначе старые кортежи не пройдут валидацию.
Код: Выделить всё
local s = box.space.users
local fmt = s:format()
table.insert(fmt, { name = 'created_at', type = 'unsigned', is_nullable = true })
s:format(fmt) -- старые кортежи остаются валидными: новое поле = nil
Сами миграции - это просто Lua-скрипты, которые запускают на живом инстансе: через hot-reload в коде приложения, либо tt connect ... -f 0001-migration.lua. В Enterprise есть централизованный механизм через etcd (tt и TCM), раскатывающий миграции по всему кластеру с историей.
Частые заблуждения и грабли
- "Схема обязательна". Нет, format опционален; но индексируемые поля всё равно типизированы.
- "DDL мгновенный и бесплатный". create_index на большом спейсе строит индекс синхронно и может надолго занять инстанс.
- "Поле можно добавить куда угодно". Простая миграция - только добавление В КОНЕЦ и только nullable. Вставка в середину ломает позиции полей.
- "unsigned и integer - одно и то же". Для индекса нет: unsigned отвергает отрицательные значения.
- "DDL-схему модуля ddl можно потом менять set_schema". Нет: после применения ddl-схему менять запрещено, для изменений - механизм миграций.
- "box.schema.upgrade() = space:upgrade()". Разные вещи: первое обновляет СИСТЕМНЫЕ спейсы под новую версию Tarantool, второе - формат пользовательского спейса.
- "Никакого первичного индекса не нужно". До создания первого (первичного, уникального) индекса в спейс нельзя ни вставлять, ни читать.
- Грабли nullable-индекса: exclude_null = true автоматически ставит is_nullable = true и заставляет индекс пропускать кортежи с null в этом парте.
Запустите tarantool, выполните box.cfg{}. Создайте спейс accounts с format из полей id (unsigned) и login (string). Добавьте первичный TREE-индекс по id и вторичный уникальный индекс по login. Вставьте 2-3 кортежа. Затем выполните простую миграцию: добавьте в конец nullable-поле balance (unsigned) и убедитесь, что старые кортежи читаются (balance = nil). В конце посмотрите box.space._index:select{box.space.accounts.id} и найдите там оба своих индекса - это докажет, что схема живёт в системном спейсе.
Контрольные вопросы
- В какие системные спейсы Tarantool записывает создание спейса и индекса, и почему благодаря этому DDL реплицируется автоматически?
- Почему добавление поля в конец спейса считается простой миграцией, а смена типа существующего поля - сложной? Какое требование к новому полю обязательно?
- Чем декларативный трек схемы в 3.x (модуль ddl) отличается от YAML-конфига инстанса box.cfg/config.yaml?
- В чём разница между box.schema.upgrade() и space:upgrade(), и какие требования предъявляются к функции апгрейда во втором случае?