Разработка собственных расширений
Шесть прошлых уроков были про готовые расширения. Этот — про то, как написать своё. Вы не станете писать расширение каждый день, но понимать, как они устроены изнутри, важно по двум причинам. Во-первых, это закрывает картину: вы видели, что расширение «добавляет функции», теперь увидите, как именно. Во-вторых, наступит задача, которую core- и community-расширения не покрывают — нестандартный источник данных, доменная функция, специфичный формат, — и тогда написать расширение окажется правильным решением. Урок даёт карту: что такое extension template, как устроен C++ API, каков жизненный цикл расширения.
Что физически представляет собой расширение
Расширение DuckDB — это файл .duckdb_extension. По сути это динамически линкуемая библиотека (на разных ОС — .so, .dylib, .dll), переименованная и снабжённая метаданными. Когда вы делаете LOAD, DuckDB подгружает этот файл в адресное пространство своего процесса и вызывает в нём специальную точку входа — init-функцию.
Init-функция получает ссылку на работающий экземпляр DuckDB и регистрирует в нём новые объекты: скалярные функции, table functions, типы, file systems, оптимизационные правила, replacement scan callbacks. Регистрация — это запись в каталог и системные таблицы экземпляра. После того как init-функция отработала, зарегистрированные объекты неотличимы от встроенных: парсер, binder, оптимизатор и исполнитель работают с ними одинаково.
Extension template: точка старта
Писать расширение с нуля не нужно — у DuckDB есть официальный extension template (репозиторий-шаблон на GitHub). Это готовый каркас проекта: структура папок, система сборки на CMake, привязка к исходникам DuckDB как подмодулю, заготовка CI, который собирает расширение под все поддерживаемые платформы, и пример простой функции внутри.
Работа начинается так: создаёте репозиторий из шаблона, и у вас сразу есть собирающееся расширение с демонстрационной функцией. Дальше вы заменяете демо-функцию на свою логику. Шаблон берёт на себя всю обвязку — сборку, линковку с DuckDB, упаковку в .duckdb_extension, кросс-платформенный CI, — оставляя вам только содержательную часть.
Расширения собираются под конкретную пару «версия DuckDB + платформа». Расширение, собранное под DuckDB 1.4 и linux_amd64, не загрузится в DuckDB 1.5 или на osx_arm64. Поэтому в extension template и встроен матричный CI: он собирает ваше расширение сразу под весь набор платформ. При самостоятельной сборке всегда сверяйте версию DuckDB, под которую линкуетесь, с версией, в которой собираетесь расширение загружать.
C++ API: основной язык расширений
Ядро DuckDB написано на C++, и основной язык разработки расширений — тоже C++. Расширение линкуется с исходниками DuckDB и использует их внутренние классы напрямую: DataChunk, Vector, LogicalType, ClientContext и прочие — те самые сущности, которые мы разбирали в модулях про векторизованный движок и систему типов.
Посмотрим на минимальную скалярную функцию — функцию, которая для каждой строки возвращает одно значение. Её суть — обработать входной DataChunk (колоночный батч на ~2048 значений) и заполнить выходной Vector:
// Скалярная функция, удваивающая целое число.
// Работает не построчно, а сразу над вектором значений.
static void DoubleFunction(DataChunk &args, ExpressionState &state, Vector &result) {
// args.data[0] — входной вектор; result — выходной вектор
UnaryExecutor::Execute<int64_t, int64_t>(
args.data[0], result, args.size(),
[](int64_t input) { return input * 2; } // логика для одного значения
);
}
Ключевой момент здесь — векторизация. Функция получает не одну строку, а целый DataChunk. Хелпер UnaryExecutor::Execute применяет вашу поэлементную лямбду ко всему вектору сразу, корректно обрабатывая validity mask (NULL-значения) и физический тип вектора. Так расширение автоматически встраивается в векторизованную модель исполнения DuckDB — ваша функция исполняется так же эффективно, как встроенные.
Регистрация функции происходит в init-функции расширения:
// В init-функции расширения: регистрируем функцию в каталоге
ScalarFunction double_fn("double_it", // имя в SQL
{LogicalType::BIGINT}, // типы аргументов
LogicalType::BIGINT, // тип результата
DoubleFunction); // реализация
ExtensionUtil::RegisterFunction(instance, double_fn);
После загрузки такого расширения в SQL появляется функция double_it:
LOAD my_extension;
SELECT double_it(21) AS answer;
-- answer = 42
Что ещё можно регистрировать
Скалярная функция — простейший случай. C++ API позволяет добавлять и более сложные вещи:
- Table functions — функции, возвращающие таблицу. Именно так устроены
read_parquet,read_csvи сканеры баз: table function реализует интерфейс «выдавай потокDataChunk-ов». Через table function пишут собственные сканеры новых форматов и источников. - Aggregate functions — агрегатные функции со своим состоянием, шагами накопления и слияния (последнее нужно для параллельной агрегации).
- Новые типы — пользовательские
LogicalTypeсо своим физическим представлением и кастами. - File systems — реализации файловой системы для новых протоколов; так устроен httpfs.
- Replacement scan callbacks — те самые callbacks из первого модуля, которые превращают имя в источник данных.
- Optimizer rules — правила, которые оптимизатор применяет к плану запроса.
То есть расширение может дотянуться почти до любого слоя DuckDB. Это и делает архитектуру «маленькое ядро плюс расширения» настолько гибкой: внешний код подключается на тех же интерфейсах, что использует само ядро.
Подпись и распространение
Готовое расширение надо распространять. DuckDB проверяет подпись расширения при загрузке — это защита от подмены и запуска чужого кода. Расширения из официального и community-репозиториев подписаны через инфраструктуру DuckDB.
Своё, локально собранное расширение не подписано. Чтобы DuckDB его загрузил, нужен флаг allow_unsigned_extensions при старте. Это нормально для разработки и внутреннего использования: собрали, запустили DuckDB с этим флагом, загрузили расширение с локального пути.
-- DuckDB запущен с allow_unsigned_extensions
-- Загрузка расширения по прямому пути к файлу
LOAD '/path/to/my_extension.duckdb_extension';
Для широкого распространения путь — подать расширение в community-репозиторий. Тогда оно собирается инфраструктурой DuckDB под все платформы, подписывается, и любой пользователь сможет поставить его через INSTALL my_extension FROM community — без флага unsigned.
allow_unsigned_extensions отключает проверку подписи для ВСЕХ расширений в этой сессии, а не только для вашего. Загрузка неподписанного расширения означает исполнение произвольного нативного кода прямо в процессе DuckDB — с полным доступом к памяти, файлам и сети. На своей машине при разработке это приемлемо. На общем или продакшен-сервере включать этот флаг и загружать расширения из непроверенных источников нельзя — это прямой вектор атаки.
Когда писать расширение, а когда нет
Расширение — не первый инструмент, к которому стоит тянуться. Прежде чем писать C++, проверьте более лёгкие пути:
- Нужна доменная функция — возможно, хватит обычной SQL-функции (
CREATE MACRO) или Python UDF (об этом — в модуле про Python-экосистему). Это пишется за минуты и не требует сборки. - Нужен нестандартный источник — возможно, его уже покрывает существующее community-расширение.
Писать собственное расширение оправдано, когда: нужна высокая производительность нативного кода (Python UDF медленнее); нужен доступ к внутренним слоям движка (свой сканер формата, своя file system, optimizer rule); функциональность должна распространяться как переиспользуемый модуль для многих пользователей. В этих случаях extension template и C++ API — правильный путь, и теперь вы знаете, из чего он состоит.
Попробуй сам
Это задание на чтение и эксперимент — компилятор C++ не обязателен.
- Откройте на GitHub официальный extension template DuckDB. Изучите структуру: где лежит код расширения, где CMake-файлы, где находится init-функция, как DuckDB подключён подмодулем.
- Найдите в шаблоне демонстрационную скалярную функцию. Разберите её: что она получает на вход, как использует executor-хелпер, как регистрируется в init-функции.
- В работающем DuckDB выполните
SELECT function_name, function_type FROM duckdb_functions() LIMIT 20. Обратите внимание на колонку с типом функции (scalar, aggregate, table) — это те самые категории, которые регистрирует C++ API. - Сформулируйте для себя: какую конкретную задачу из вашей практики решило бы собственное расширение, и можно ли её закрыть более лёгким средством — CREATE MACRO или Python UDF — без написания C++.