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, добавляющий недостающую грамматику.

Статья основана на вопросе на StackOverflow от tuk и ответе от tuk.