Обзор SQLGlot: разбор и перевод SQL на Python — это разбор библиотеки, которая превращает текст запроса в дерево выражений и генерирует SQL целевого диалекта. В документации проекта на sqlglot.com она описана как парсер, транспайлер, оптимизатор и движок без внешних зависимостей, написанный на чистом Python.
Что делает SQLGlot
Документация проекта на sqlglot.com описывает SQLGlot как парсер, транспайлер, оптимизатор и движок без внешних зависимостей. Библиотека читает SQL разных диалектов и генерирует SQL целевого диалекта в пределах поддерживаемых преобразований. В том же описании указано, что она умеет форматировать запросы, анализировать их, обходить деревья выражений и собирать SQL программно.
Практический смысл для читателя: одна библиотека закрывает три задачи — разобрать чужой SQL, привести его к нужному диалекту, собрать новый запрос кодом. Разбор идёт через дерево выражений, поэтому вы работаете с узлами. Документация проекта на sqlglot.com перечисляет среди поддерживаемых диалектов DuckDB, Presto/Trino, Spark/Databricks, Snowflake и BigQuery.
Установка описана там же. Чистая версия ставится командой pip3 install sqlglot. Вариант с C-расширениями, скомпилированными через mypyc, ставится как pip3 install "sqlglot[c]". В документации проекта отмечено, что на Python 3.9 этот вариант устанавливает чистую Python-версию.
Отдельно стоит сказать про движок. Документация проекта на sqlglot.com описывает executor, который интерпретирует запросы над таблицами, представленными словарями Python. Авторы прямо пишут, что движок не рассчитан на скорость, зато полезен для юнит-тестов и запуска SQL над объектами Python. Это учебный исполнитель над объектами Python.
| Задача | Что использует читатель | Где описано |
|---|---|---|
| Разобрать SQL в дерево | parse_one, parse |
документация проекта на sqlglot.com |
| Перевести диалект | transpile, read, write |
документация проекта на sqlglot.com |
| Отформатировать вывод | pretty=True, identify=True |
документация проекта на sqlglot.com |
| Выполнить над словарями | sqlglot.executor.execute |
документация проекта на sqlglot.com |
| Собрать запрос кодом | select, condition, exp |
документация проекта на sqlglot.com |
Parse и AST
Разбор начинается с parse_one. Документация проекта на sqlglot.com описывает parse_one как функцию, которая принимает строку SQL и возвращает дерево синтаксиса. Если в строке несколько инструкций, возвращается выражение Block со всеми деревьями. Функция parse возвращает список деревьев, по одному на инструкцию.
Дерево состоит из узлов — экземпляров sqlglot.Expression. Официальный AST primer в репозитории проекта на GitHub объясняет устройство узла: дети лежат в args, ссылка на родителя — в parent. Класс узла объявляет arg_types — словарь возможных детей, где значение True помечает обязательный аргумент. В примере из primer у Column ключ this хранит идентификатор имени колонки.
Разберём учебный пример. Возьмём запрос SELECT a FROM t WHERE a > 1 и разберём его без указания диалекта. Ожидаемое дерево: один узел Select, внутри колонка Column(a), источник Table(t) и условие GT(a, 1). Это учебная схема дерева; библиотеку в редакции не запускали.
Посмотреть дерево можно через repr. Пример из AST primer: repr(parse_one("SELECT a + 1 AS z")) печатает Select с Alias, внутри которого Add, Column и Literal. Такой вывод показывает структуру узлов и помогает понять, какие типы искать при обходе.
Обход дерева описан в том же primer. Простой путь — методы find, find_all и walk у Expression. find возвращает первый узел нужного типа, find_all — все совпадения, walk даёт пошаговый обход. Для точного доступа к известной структуре используют args напрямую.
В AST primer есть предупреждение о ловушке. Поиск find_all(exp.Table) находит узлы Table, среди которых оказывается имя common table expression. В примере с WITH x AS (SELECT a FROM y) SELECT a FROM x такой поиск печатает x и y, где x — это CTE. Для семантически верного обхода нужен модуль Scope.
import sqlglot
from sqlglot import exp
tree = sqlglot.parse_one("SELECT a FROM t WHERE a > 1")
print(repr(tree))
for column in tree.find_all(exp.Column):
print(column.alias_or_name)
for table in tree.find_all(exp.Table):
print(table.name)
Этот код показывает дерево и печатает имена колонок и таблиц. Запускать его стоит на своём окружении: вывод зависит от версии библиотеки.
read/write dialect
Перевод между диалектами строится на двух параметрах. Документация проекта на sqlglot.com описывает transpile так: read задаёт исходный диалект для разбора, write — целевой диалект для генерации. Функция возвращает список строк, по одной на инструкцию. Параметр identity при значении True и отсутствии write использует исходный диалект как целевой.
Ключевое правило из FAQ документации: диалект нужно указывать явно. Без read функция parse_one применяет «диалект SQLGlot», задуманный как надмножество всех поддерживаемых. Для запроса на Spark документация советует писать parse_one(sql, dialect="spark"). Для перевода из Spark в DuckDB — parse_one(sql, dialect="spark").sql(dialect="duckdb") либо transpile(sql, read="spark", write="duckdb").
Учебный пример из принятого артефакта: пара диалектов read="duckdb", write="postgres", входной SQL SELECT a FROM t WHERE a > 1. Копируемый вызов по API первички — sqlglot.transpile("SELECT a FROM t WHERE a > 1", read="duckdb", write="postgres"). Ожидаемый результат преобразования — ["SELECT a FROM t WHERE a > 1"]. Это ожидаемый текст для простого общего подмножества; локальный API в редакции не запускался.
import sqlglot
result = sqlglot.transpile(
"SELECT a FROM t WHERE a > 1",
read="duckdb",
write="postgres",
)
print(result)
Что менять под себя: строку запроса, значение read под исходную систему и write под целевую. Имена таблиц и колонок сверяйте со своей схемой.
Документация проекта на sqlglot.com приводит примеры перевода функций. Запрос SELECT EPOCH_MS(1618088028295) при read="duckdb", write="hive" даёт SELECT FROM_UNIXTIME(1618088028295 / POW(10, 3)). Формат времени тоже переводится: STRFTIME(x, '%y-%-m-%S') из DuckDB превращается в DATE_FORMAT(x, 'yy-M-ss') для Hive.
Идентификаторы и типы данных переводятся вместе с запросом. В примере из документации запрос с CAST("b"."a" AS REAL) при write="spark", identify=True, pretty=True даёт Spark SQL с обратными кавычками и типом FLOAT. Параметр identify заключает все идентификаторы в кавычки, pretty форматирует вывод.
| Параметр | Роль | Значение по умолчанию |
|---|---|---|
read |
исходный диалект для разбора | не задан |
write |
целевой диалект для генерации | не задан |
identity |
подстановка read в write |
True |
pretty |
форматирование вывода | False |
identify |
кавычки вокруг идентификаторов | не задан |
unsupported_level |
реакция на неподдерживаемое | best-effort |
Контроль перевода
Разбор в SQLGlot намеренно мягкий. Документация проекта на sqlglot.com формулирует это прямо: парсер принимает запросы, которые реальный движок отклонил бы. SQLGlot работает как транспайлер, и запрос, успешно разобранный, может упасть при исполнении. Успешный разбор показывает, что SQLGlot построил AST; валидность для конкретного движка проверяется отдельно.
Ошибки синтаксиса библиотека находит. Документация проекта перечисляет среди них несбалансированные скобки и неверное использование зарезервированных слов. При обнаружении ошибки поднимается ParseError. Пример из документации: transpile("SELECT foo FROM (SELECT baz FROM t") даёт sqlglot.errors.ParseError: Expecting ). Line 1, Col: 34.
Структурированные ошибки доступны программно. В примере из документации перехват sqlglot.errors.ParseError даёт список словарей с полями description, line, col, start_context, highlight, end_context. Поле description содержит текст Expecting ), поле col — позицию 34. Это позволяет строить отчёт по нескольким запросам сразу.
Неподдерживаемые конструкции обрабатываются отдельно. Документация проекта на sqlglot.com описывает поведение по умолчанию: SQLGlot выдаёт предупреждение и делает перевод best-effort. Пример: transpile("SELECT APPROX_DISTINCT(a, 0.1) FROM foo", read="presto", write="hive") печатает предупреждение о том, что аргумент accuracy не поддерживается для выражения ApproxDistinct при цели Hive, и возвращает SELECT APPROX_COUNT_DISTINCT(a) FROM foo.
Строгий режим включается атрибутом unsupported_level. Документация проекта показывает значение sqlglot.ErrorLevel.RAISE, при котором вместо предупреждения поднимается sqlglot.errors.UnsupportedError с тем же текстом про accuracy. Для контроля перевода это главный рычаг: предупреждение оставляют для чернового прогона, исключение — для проверки в конвейере.
import sqlglot
try:
sqlglot.transpile(
"SELECT APPROX_DISTINCT(a, 0.1) FROM foo",
read="presto",
write="hive",
unsupported_level=sqlglot.ErrorLevel.RAISE,
)
except sqlglot.errors.UnsupportedError as error:
print(error)
Этот вызов переводит неподдерживаемую конструкцию в исключение. Под себя меняйте запрос и пару диалектов; уровень ошибки оставляйте строгим там, где нужен отказ.
Форматирование и кавычки тоже относятся к контролю. Документация проекта на sqlglot.com объясняет, что SQLGlot разбирает запрос в AST и генерирует SQL обратно. Форматирование может меняться. Косметика меняется: регистр, кавычки, переносы. Параметр pretty=True форматирует вывод, identify=True заключает идентификаторы в кавычки. Комментарии сохраняются по мере возможности: в примере из документации комментарии MySQL с # переводятся в стандартный синтаксис.
Границы схемы и функций
Часть переводов требует знания схемы. Документация проекта на sqlglot.com пишет, что некоторым запросам для точного перевода нужна дополнительная информация — схемы упомянутых таблиц. Причина в том, что отдельные преобразования зависят от типов и требуют вывода типов. Правила оптимизатора qualify и annotate_types дают эту информацию, но по умолчанию не применяются, поскольку добавляют накладные расходы и сложность.
Официальный AST primer в репозитории проекта на GitHub показывает, как это выглядит на практике. Функция qualify из sqlglot.optimizer.qualify дописывает к каждой колонке имя её таблицы. В примере с подзапросами SELECT a, c FROM (SELECT a, b FROM x) AS x JOIN (SELECT b, c FROM y) AS y ON x.b = y.b после qualify колонки получают префиксы x.a, y.c, x.b, y.b. Только после этого колонку можно проследить до источника.
Для неоднозначных случаев схема обязательна. Primer приводит запрос SELECT a FROM x CROSS JOIN y, где колонка a может прийти из x или из y. В таких случаях схему нужно передать в qualify. Это же касается lineage: модуль lineage строит родословную колонок, опираясь на квалифицированное дерево.
Разбор с Scope даёт семантический контекст. Primer описывает build_scope из sqlglot.optimizer.scope: функция возвращает объект Scope, у которого есть метод traverse для обхода дерева областей. Поиск таблиц через scope.selected_sources отличает реальную таблицу от CTE: в примере с WITH x AS (SELECT a FROM y) SELECT a FROM x такой обход печатает только y.
Ограничения диалектов описаны в документации проекта на sqlglot.com. Диалекты делятся на официальные, которые поддерживает основная команда с высоким приоритетом исправлений, и community, которые развиваются в основном силами сообщества. Среди официальных в списке Athena, BigQuery, ClickHouse, Databricks, DuckDB, Hive, MySQL, Oracle, Postgres, Presto, Redshift, Snowflake, Spark, SQLite, StarRocks, Tableau, Trino, TSQL. Среди community — DAX, Doris, Dremio, Drill, Druid, Dune, Exasol, Fabric, Materialize, RisingWave, SingleStore, Solr, Teradata.
Плагинные диалекты появились с версии v28.6.0. Документация проекта на sqlglot.com описывает их как сторонние диалекты, которые разрабатываются и поддерживаются в отдельных репозиториях независимыми авторами. Они распространяются отдельными пакетами, и команда SQLGlot не предоставляет по ним поддержку. Свой диалект можно оформить как плагин: класс диалекта регистрируется через entry points в setup.py под ключом sqlglot.dialects, после чего используется как встроенный.
Собственный диалект можно получить и наследованием. Документация проекта на sqlglot.com приводит пример класса Custom(Dialect) с вложенными Tokenizer и Generator, где задаются кавычки, идентификаторы, ключевые слова и отображение типов. Там же есть предупреждение: при установленном sqlglot[c] наследование может работать некорректно, поскольку рантайм-наследование mypyc-классов отключено ради производительности. Для кастомных диалектов документация советует чистую Python-версию.
Чек-лист безопасной интеграции
Порядок действий для контролируемой интеграции выглядит так. Сначала зафиксируйте пару диалектов и учебный запрос. Затем разберите его и посмотрите дерево. После этого переведите запрос и сравните результат с ожидаемым текстом. Далее включите строгий уровень ошибок и прогоните набор запросов. В конце проверьте семантику на реальных данных отдельными эталонами.
Первый шаг — установка и версия. Документация проекта на sqlglot.com описывает схему версий: PATCH растёт при обратно совместимых изменениях, MINOR — при обратно несовместимых, MAJOR — при значительных обратно несовместимых. Для конвейера это значит, что обновление MINOR требует прогона тестов на ваших запросах.
Второй шаг — явные диалекты. В parse_one и parse указывайте исходный диалект через read или dialect. Целевой диалект задавайте в transpile(write=...) или при генерации через Expression.sql(dialect=...). Отсутствие диалекта включает надмножество SQLGlot, и разбор пройдёт там, где реальный движок запрос отклонит.
Третий шаг — строгий уровень. Для проверки в конвейере ставьте unsupported_level=sqlglot.ErrorLevel.RAISE. Предупреждение в логе легко пропустить, исключение останавливает прогон.
Четвёртый шаг — схема. Передавайте схему в qualify там, где есть неоднозначные колонки или нужна родословная. Без схемы часть переводов зависит от вывода типов и остаётся неточной.
Пятый шаг — эталоны данных. Синтаксический успех не доказывает семантическую эквивалентность сложного запроса. Отдельные эталоны нужны для DATEDIFF, часовых поясов, целочисленного деления и quoted identifiers. Учебный эталон из принятого артефакта: при данных t.a = [NULL, 1, 2] условие a > 1 оставляет строку a = 2. Этот результат не вычисляет transpile; его проверяют на реальном движке.
| Шаг | Действие | Что проверяет |
|---|---|---|
| 1 | зафиксировать пару диалектов | исходную и целевую систему |
| 2 | разобрать запрос и вывести дерево | структуру узлов |
| 3 | перевести и сравнить с ожидаемым текстом | синтаксис целевого диалекта |
| 4 | включить ErrorLevel.RAISE |
неподдерживаемые конструкции |
| 5 | передать схему в qualify |
неоднозначные колонки |
| 6 | прогнать эталоны данных | семантику на реальных значениях |
Шестой шаг — границы инструмента. Документация проекта на sqlglot.com описывает executor над словарями Python для юнит-тестов. Подключение к production-базе и выполнение запросов на ней остаются задачей вашего приложения. SQLGlot готовит и проверяет текст запроса.
Частые вопросы
Что такое SQLGlot и для чего он нужен?
SQLGlot — библиотека на Python для разбора SQL, перевода между диалектами, оптимизации и сборки запросов кодом. Документация проекта на sqlglot.com описывает её как парсер, транспайлер, оптимизатор и движок без внешних зависимостей. Типовые задачи: миграция запросов между системами, форматирование, анализ дерева выражений. Разбор создаёт дерево Expression.
Как установить SQLGlot через pip?
Чистая версия ставится командой pip3 install sqlglot. Вариант с C-расширениями, скомпилированными через mypyc, ставится как pip3 install "sqlglot[c]". Документация проекта на sqlglot.com отмечает, что на Python 3.9 второй вариант устанавливает чистую Python-версию. Для кастомных диалектов документация советует чистую версию, поскольку при sqlglot[c] наследование может работать некорректно.
Как перевести запрос из одного диалекта в другой?
Используйте transpile с параметрами read и write. Документация проекта на sqlglot.com приводит пример transpile(sql, read="spark", write="duckdb"). Альтернатива — parse_one(sql, dialect="spark").sql(dialect="duckdb"). Диалекты указывайте явно: без read применяется надмножество SQLGlot. Результат — список строк, по одной на инструкцию.
Как SQLGlot обрабатывает неподдерживаемые конструкции?
По умолчанию библиотека выдаёт предупреждение и делает перевод best-effort. Документация проекта на sqlglot.com показывает пример с APPROX_DISTINCT при переводе из Presto в Hive: выводится предупреждение про аргумент accuracy, а запрос переводится в APPROX_COUNT_DISTINCT. Атрибут unsupported_level со значением sqlglot.ErrorLevel.RAISE поднимает UnsupportedError вместо предупреждения.
Чем SQLGlot отличается от SQLFluff?
Обе библиотеки написаны на Python и работают с SQL. Документация проекта на sqlglot.com описывает SQLGlot как парсер, транспайлер, оптимизатор и движок с обходом деревьев и сборкой запросов. SQLFluff в бенчмарке из той же документации показан в разделе сравнения производительности разбора. Для перевода между диалектами и работы с AST подходит SQLGlot; для линтинга и стиля запросов смотрите возможности SQLFluff.
