Меня зовут Олег, и в Яндексе мы с командой занимаемся Python-обвязкой вокруг нашей базы данных YDB. Python знаменит «батарейками в комплекте», широким ассортиментом библиотек на все случаи жизни, включая богатую экосистему для работы с базами данных. Есть свой интерфейс DBAPI (PEP-249), несколько конкурирующих ORM и многочисленные уровни абстракции между софтом и базами. В этой статье — о том, как мы делали полноценную интеграцию нашей базы данных с Apache Superset: чтобы достаточно было выбрать YDB из выпадающего меню и начать визуализировать аналитические данные.
Что нужно сделать, чтобы какая-то программа стала поддерживать вашу базу данных? Посмотреть, как в этой программе организована поддержка других СУБД, и реализовать поддержку своей по аналогии. Apache Superset написана на Python, и для каждой поддерживаемой базы данных нужно написать класс-спецификацию, реализующую нужные платформе атрибуты. Но такой класс не полностью абстрагирует базу данных, а только предоставляет информацию о драйвере и о том, как работать с time-series-данными. Основную работу берёт на себя SQLAlchemy, которую Apache Superset использует для работы с базой. Поэтому главная задача — сделать новую базу совместимой с SQLAlchemy.
SQLAlchemy ожидает, что разработчики баз данных реализуют диалект, который по историческим причинам строится поверх родного для Python-протокола доступа к базе данных DBAPI. Более того, если вначале реализовать DBAPI, то его можно использовать как фундамент и для диалекта SQLAlchemy, и для других проектов, использующих DBAPI. Поэтому мы начали разработку снизу вверх: решили сначала написать для YDB пакет, реализующий протокол DBAPI, затем на базе этого пакета создать диалект для SQLAlchemy. Ну а финалом нашей разработки должна была стать интеграция с Apache Superset.
DBAPI: старый, но не бесполезный
Python-библиотеку для YDB мы сделали ещё в 2017 году. Она использует gRPC, чтобы установить подключение к базе, после чего оборачивает всю функциональность в идиоматичный Python код. Например, вот так выглядит запрос на YQL (наше расширение для SQL) внутри транзакции:
config = {
"endpoint": "grpc://localhost:2136",
"database": "/local"}
with ydb.Driver(**config) as driver:
with ydb.QuerySessionPool(driver) as pool:
query = "DECLARE $v AS Int64; SELECT $v"
pool.execute_with_retries(query, {"$v": 1})
Библиотека ydb — низкоуровневая. Ожидается, что программисты будут использовать её для автоматизации работы с базой и в качестве фундамента для интеграции с другими проектами. Например, чтобы сделать модуль, предоставляющий интерфейс DBAPI.
Как и многие другие решения в мире Python, интерфейс для работы с базами данных стандартизирован в виде PEP (Python Enhancement Proposal). PEP-249 был принят в 2001 году и с тех пор не менялся. В нём нет таких современных фич, как prepared queries или поддержка асинхронности, но он хорошо фиксирует самое важное для работы с базой данных: подключения, транзакции, работу с курсором, набор поддерживаемых типов и список возможных ошибок.
Работая над этим проектом, мы аккуратно имплементировали все требуемые классы, вызывая под капотом код нашей Python-библиотеки. После чего сделали трансляцию типов данных и ошибок. В результате код из предыдущего примера превратился вот в такой DBAPI-совместимый вариант:
connection = ydb_dbapi.connect(
host="localhost", port="2136", database="/local"
)
with connection.cursor() as cursor:
cursor.execute("DECLARE $v AS Int64; SELECT $v", {"$v": 1})
cursor.fetchall()
Диалект для SQLAlchemy
Apache Superset и pandas используют много возможностей SQLAlchemy, и для поддержки новой базы данных нужно реализовать диалект — так разработчики SQLAlchemy называют модули для работы с БД. Эти модули должны предоставить более сложный интерфейс, чем скромный стандарт DBAPI 2001 года.
Инструкция по реализации диалекта фокусирует внимание разработчиков на классе SuiteRequirements модуля sqlalchemy.testing.requirements, который описывает все требования к новому диалекту. Автору нового диалекта достаточно сделать класс-заглушку, передать его в тест и последовательно реализовывать всё, на чём тест упадёт. Такой вот test-driven development.
Что требует SQLAlchemy от диалекта сверх того, что можно найти в DBAPI?
Во-первых, нужен доступ к структуре базы данных. SQLAlchemy — это не только удобное ORM для SQL. Это ещё и стандартизированный интерфейс к большинству возможностей БД, которые могут понадобиться разработчику. Например, Inspector.get_table_names возвращает список таблиц в базе данных. Такой возможности нет в стандарте SQL, и её нужно реализовать, используя интерфейс конкретной базы данных. В нашем случае — с помощью низкоуровневой библиотеки ydb. Которая, как уже говорилось выше, использует gRPC для общения с базой.
Во-вторых, SQLAlchemy использует набор независимых от БД типов, например Integer или String. В терминологии библиотеки они называются CamelCase datatypes, и для их работы авторам диалекта нужно было реализовать конвертеры для парных UPPERCASE-типов: INTEGER, VARCHAR и так далее. Здесь снова помогают тесты, которые показывают, что нужно реализовать и какое поведение должно быть у тех или иных типов.
И, наконец, самое главное: правила формирования запросов. Они определяют, как SQLAlchemy будет формировать запросы с такими конструкциями, как, например, LIMIT или GROUP BY. В коде правил сосредоточена специфика работы с той или иной базой данных: как формировать корректные и оптимальные строки SQL-запросов.
Нюансом стала обратная совместимость. 2-я версия SQLAlchemy несовместима с 1-й, при этом многие проекты, включая Apache Superset, до сих пор используют версию 1.4. Мы решили использовать один Python-пакет для поддержки обеих версий: во время импортирования пакета мы проверяем версию SQLAlchemy и выбираем одну из двух реализаций.
Финальный босс: интеграция с Apache Superset
Когда диалект SQLAlchemy был написан и протестирован, мы приступили к интеграции с Apache Superset. Все интеграции хранятся в репозитории проекта. Чтобы добавить новую, нужно внести изменения в разные части кодовой базы: обновить README.md, написать документацию в markdown-формате, добавить зависимость в pyproject.toml и написать класс интеграции, унаследованный от BaseEngineSpec.
Apache Superset ожидает, что класс интеграции предоставит всё необходимое для работы с time-series. Это несколько функций для преобразования даты и времени, а ещё — таблица _timegrain_expressions, в которой содержатся правила для выборок по временному интервалу, от секунды до года.
После того как весь код был написан, мы предложили мейнтейнерам пул-реквест, и они довольно быстро взялись за ревью, которое было детальным и педантичным — что хорошо говорит о сообществе Apache Superset. За несколько итераций мы добавили ещё больше тестов, и в декабре 2024 года наш пул-реквест был принят! Он будет опубликован вместе с готовящейся версией Apache Superset 5, для которой уже сейчас можно попробовать второй релиз-кандидат:

Дальнейшие планы
Диалект SQLAlchemy позволяет использовать YDB и с другими популярными решениями. Например, из коробки можно использовать pandas или JupySQL. Для каких-то программ нужно так же, как для Apache Superset, писать адаптеры и коннекторы. Мы сделали такой коннектор для Yandex DataLens — нашей альтернативы Apache Superset.
Напишите в комментариях, если в вашем любимом Python-проекте ещё нет поддержки YDB. Мы постоянно собираем обратную связь от коллег-питонистов и на её основе планируем развитие наших продуктов.
Нельзя не упомянуть Django — одно из старейших решений в мире Python для веб-разработки. Так же, как с SQLAlchemy, для Django нужно будет реализовать бэкенд поверх протокола DBAPI. Подписывайтесь на наш хаб на Хабре, и мы расскажем, когда YDB можно будет использовать с вашими проектами на Django.
А еще мы сделали бесплатный курс «Разработчик YDB» — пишите в комментариях, если прошли его и есть вопросы или предложения.