2025, Oct 04 09:19
Сбой парсинга MySQL PARTITION BY RANGE в sqlglot 27.8.0
Почему sqlglot 27.8.0 падает на MySQL DDL с PARTITION BY RANGE и VALUES LESS THAN: пример Python‑кода, типичное сообщение об ошибке, диалект doris и что делать.
Разбор MySQL DDL с секционированием таблиц может поставить в тупик универсальные SQL‑парсеры. Если ваш скрипт использует PARTITION BY RANGE с VALUES LESS THAN, sqlglot 27.8.0 выдаст ошибку разбора, даже если DDL корректен для MySQL.
Как воспроизвести проблему
Задача — разобрать файл со схемой и извлечь имена таблиц. Следующий фрагмент на Python читает файл, просит sqlglot распарсить его в диалекте MySQL и собирает узлы exp.Table.
import logging
import sqlglot
from sqlglot import exp
# настройка логирования
logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s [%(levelname)s] %(message)s"
)
log = logging.getLogger(__name__)
def collect_tables(path_to_sql, sql_flavor="mysql"):
    """
    Разобрать SQL‑файл и вернуть множество уникальных найденных имен таблиц.
    Логировать ошибки, если файл не найден или разбор завершился неудачей.
    """
    try:
        with open(path_to_sql, "r") as fh:
            ddl_blob = fh.read()
        ast_list = sqlglot.parse(ddl_blob, dialect=sql_flavor)
        seen_tables = set()
        for node in ast_list:
            seen_tables.update([t.name for t in node.find_all(exp.Table)])
        return seen_tables
    except FileNotFoundError:
        log.error(f"File not found: {path_to_sql}")
        return set()
    except Exception as err:
        log.error(f"Error parsing `{path_to_sql}`: {err}")
        return set()
if __name__ == "__main__":
    input_sql = "changeLogs/health-service/create_db.sql"
    names = collect_tables(input_sql)
    log.info(f"Total unique tables found: {len(names)}")
    log.info(f"Table names: {sorted(list(names))}")
Схема включает секционированные таблицы с использованием VALUES LESS THAN. Попытка распарсить её приводит к ошибкам вида:
An error occurred during parsing: Expecting ). Line 19, Col: 26.
  created_at`) USING BTREE
    ) PARTITION BY RANGE ( UNIX_TIMESTAMP(audit_ts)) (
    PARTITION p2401 VALUES LESS THAN (UNIX_TIMESTAMP('2024-02-01 00:00:00')),
    PARTITION p2402 VALUES LESS THAN (UNIX_TIMES
Вот SQL, который вызывает сбой:
-- SQL в формате Liquibase
-- набор изменений debraj.manna@nexla.com:NEX-18235
CREATE TABLE IF NOT EXISTS `audit_control`
(
   `id`            BIGINT auto_increment NOT NULL,
   `message_id`    VARCHAR(100) DEFAULT NULL,
    `resource_type` VARCHAR(30) NOT NULL,
    `event_type`    VARCHAR(30) NOT NULL,
    `resource_id`   INT NOT NULL,
    `origin`        VARCHAR(100) NOT NULL,
    `created_at`    TIMESTAMP NOT NULL,
    `body`          mediumtext NOT NULL,
    `audit_ts`      TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
    PRIMARY KEY (id, audit_ts),
    KEY `audit_control_resource_type_resource_id_IDX` (`resource_type`,`resource_id`) USING BTREE,
    KEY `audit_control_created_at_IDX` (`created_at`) USING BTREE
    ) PARTITION BY RANGE ( UNIX_TIMESTAMP(audit_ts)) (
    PARTITION p2401 VALUES LESS THAN (UNIX_TIMESTAMP('2024-02-01 00:00:00')),
    PARTITION p2402 VALUES LESS THAN (UNIX_TIMESTAMP('2024-03-01 00:00:00')),
    PARTITION p2403 VALUES LESS THAN (UNIX_TIMESTAMP('2024-04-01 00:00:00')),
    PARTITION p2404 VALUES LESS THAN (UNIX_TIMESTAMP('2024-05-01 00:00:00')),
    PARTITION p2405 VALUES LESS THAN (UNIX_TIMESTAMP('2024-06-01 00:00:00')),
    PARTITION p2406 VALUES LESS THAN (UNIX_TIMESTAMP('2024-07-01 00:00:00')),
    PARTITION p2407 VALUES LESS THAN (UNIX_TIMESTAMP('2024-08-01 00:00:00')),
    PARTITION p2408 VALUES LESS THAN (UNIX_TIMESTAMP('2024-09-01 00:00:00')),
    PARTITION p2409 VALUES LESS THAN (UNIX_TIMESTAMP('2024-10-01 00:00:00')),
    PARTITION p2410 VALUES LESS THAN (UNIX_TIMESTAMP('2024-11-01 00:00:00')),
    PARTITION p2411 VALUES LESS THAN (UNIX_TIMESTAMP('2024-12-01 00:00:00')),
    PARTITION p2412 VALUES LESS THAN (UNIX_TIMESTAMP('2025-01-01 00:00:00')),
    PARTITION pN VALUES LESS THAN MAXVALUE
);
CREATE TABLE IF NOT EXISTS `audit_coordination`
(
    `id`         BIGINT auto_increment NOT NULL,
    `message_id` VARCHAR(100) DEFAULT NULL,
    `event_type` VARCHAR(30) NOT NULL,
    `created_at` TIMESTAMP NOT NULL,
    `body`       TEXT NOT NULL,
    `audit_ts`   TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
    PRIMARY KEY (id, audit_ts)
    ) PARTITION BY RANGE ( UNIX_TIMESTAMP(audit_ts)) (
    PARTITION p2401 VALUES LESS THAN (UNIX_TIMESTAMP('2024-02-01 00:00:00')),
    PARTITION p2402 VALUES LESS THAN (UNIX_TIMESTAMP('2024-03-01 00:00:00')),
    PARTITION p2403 VALUES LESS THAN (UNIX_TIMESTAMP('2024-04-01 00:00:00')),
    PARTITION p2404 VALUES LESS THAN (UNIX_TIMESTAMP('2024-05-01 00:00:00')),
    PARTITION p2405 VALUES LESS THAN (UNIX_TIMESTAMP('2024-06-01 00:00:00')),
    PARTITION p2406 VALUES LESS THAN (UNIX_TIMESTAMP('2024-07-01 00:00:00')),
    PARTITION p2407 VALUES LESS THAN (UNIX_TIMESTAMP('2024-08-01 00:00:00')),
    PARTITION p2408 VALUES LESS THAN (UNIX_TIMESTAMP('2024-09-01 00:00:00')),
    PARTITION p2409 VALUES LESS THAN (UNIX_TIMESTAMP('2024-10-01 00:00:00')),
    PARTITION p2410 VALUES LESS THAN (UNIX_TIMESTAMP('2024-11-01 00:00:00')),
    PARTITION p2411 VALUES LESS THAN (UNIX_TIMESTAMP('2024-12-01 00:00:00')),
    PARTITION p2412 VALUES LESS THAN (UNIX_TIMESTAMP('2025-01-01 00:00:00')),
    PARTITION pN VALUES LESS THAN MAXVALUE
);
Что на самом деле происходит
Парсер не поддерживает используемый выше синтаксис секционирования MySQL. Это прояснили в обсуждении сообщества для sqlglot 27.8.0, и мейнтейнеры не планируют его реализовывать.
Если входные данные — валидный MySQL, вероятно, это пробел в парсере
похоже, мы не поддерживаем синтаксис
VALUES LESS THANдля MySQLзато Doris уже это умеет
поскольку он наследуется от MySQL, мы, вероятно, можем поднять эту логику выше, чтобы она была доступна и для MySQL, а не держать её только в Doris
Попытка сменить диалект тоже не помогает. При установке диалекта doris разбор всё равно падает:
An error occurred during parsing: Expecting ). Line 18, Col: 42.
   `audit_control_created_at_IDX` (`created_at`) USING BTREE
    ) PARTITION BY RANGE ( UNIX_TIMESTAMP(audit_ts)) (
    PARTITION p2401 VALUES LESS THAN (UNIX_TIMESTAMP('2024-02-01 00:00:00')),
    PARTI
Мы не планируем этим заниматься, так что нет. Но вы можете заняться — с радостью примем хорошо протестированный и задокументированный PR.
Решение и практические шаги
С учётом изложенных ограничений такое поведение ожидаемо в sqlglot 27.8.0. Если ваш DDL зависит от PARTITION BY RANGE с VALUES LESS THAN, библиотека выдаст ошибку разбора. Переключение на диалект doris не устраняет сбой в приведённом примере. Мейнтейнеры открыты к вкладу, который добавит эту функциональность.
Если всё же хотите попробовать другой диалект, единственное изменение в коде — аргумент dialect:
names = collect_tables(input_sql, sql_flavor="doris")
Имейте в виду: для этого входного SQL ошибка разбора сохраняется и с doris.
Почему это важно
Когда вы полагаетесь на инструменты поверх AST для миграций, линейности данных или статического анализа, неподдерживаемые конструкции грамматики могут незаметно блокировать пайплайны. Секционированные таблицы — обычное явление в нагрузках с временными рядами и в аудиторских схемах; знание заранее, что VALUES LESS THAN сейчас вне покрытия sqlglot, помогает не тратить время на разбор последствий.
Выводы
Проверьте, поддерживает ли парсер ваш DDL, прежде чем включать его в автоматизацию. Если наткнулись на пробел в разборе, убедитесь, что фрагмент ошибки и образец SQL совпадают вплоть до указанных строки и столбца — это упростит диагностику. Для PARTITION BY RANGE с VALUES LESS THAN в sqlglot 27.8.0 ожидайте сбой разбора и планируйте это заранее, либо внесите хорошо протестированный PR, добавляющий недостающую грамматику.