Синтаксическая ошибка с рекурсивным запросом SQL и упорядочением по SQLAlchemy - PullRequest
0 голосов
/ 05 июля 2018

Я хочу получить запрос с помощью SQLAlchemy для примера запроса, показанного на странице https://www.sqlite.org/lang_with.html:

WITH RECURSIVE under_alice(name,level) AS (
    VALUES('Alice',0)

    UNION ALL

    SELECT org.name, under_alice.level+1
      FROM org JOIN under_alice ON org.boss=under_alice.name
     ORDER BY 2 DESC
)

SELECT substr('..........',1,level*3) || name FROM under_alice;

Я пытаюсь выполнить свой саморекурсивный запрос, который приводит к синтаксической ошибке для SQLite:

OperationalError: (sqlite3.OperationalError) рядом с "(": синтаксическая ошибка

Исключение возникает, когда SQLAlchemy добавляет круглые скобки в оператор SQL компиляции времени для нижнего запроса в рекурсивном запросе, если он использует orderringring. Как убрать скобки из SQL?

Воспроизведение исключения:

from flask import Flask
from flask_sqlalchemy import SQLAlchemy

app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///:memory:'
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
# app.config['SQLALCHEMY_ECHO'] = True
db = SQLAlchemy(app)


class Category(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    parent_id = db.Column(db.Integer, nullable=True)
    name = db.Column(db.String(100), nullable=False)

    def __repr__(self):
        return f'<Category {self.name}>'



import logging

logging.basicConfig()
logger = logging.getLogger('sqlalchemy.engine')
logger.setLevel(logging.INFO)

db.create_all()

Данные категории:

data = [
    {'parent_id': None, 'name': 'Everything for rehabilitation'},
    {'parent_id': 1,    'name': 'Wheelchairs'},
    {'parent_id': 1,    'name': 'Wheelchairs 2'},
    {'parent_id': 1,    'name': 'Scooters for the disabled'},

    {'parent_id': 2,    'name': 'Active wheelchairs'},
    {'parent_id': 2,    'name': 'Children wheelchairs'},
    {'parent_id': 2,    'name': 'Children wheelchairs 2'},
    {'parent_id': 2,    'name': 'Wheelchair Otto Bock'},
    {'parent_id': 2,    'name': 'Wheelchair Vermeiren'},

    {'parent_id': 3,    'name': 'Folding wheelchairs'},
    {'parent_id': 3,    'name': 'Wheelchair for disabled people'},
    {'parent_id': 3,    'name': 'Wheelchairs for transportation of patients'},

    {'parent_id': 4,    'name': 'Foldable scooters for disabled people'},
    {'parent_id': 4,    'name': 'Three-wheeled scooters for disabled people'}
]

for param in data:
    db.session.add(Category(**param))

db.session.commit()

Запрос:

top_query = db.session.query(Category, db.literal(0).label('level')) \
                    .filter(Category.parent_id == None) \
                    .cte(name='top_query', recursive=True)

top_query = db.aliased(top_query, name='my_category')

bottom_query = db.session.query(Category, (top_query.c.level + 1).label('level')) \
                        .join(top_query, Category.parent_id == top_query.c.id) \
                        .order_by(db.desc(Category.parent_id)) # !!!!!!!!!!!!!!!!!!!!!!

hierarchy_query = top_query.union_all(bottom_query)

db.session.query(hierarchy_query).all()

В нижней части SELECT с круглыми скобками указана ошибка:

WITH RECURSIVE my_category(id, parent_id, name, level) AS (
    SELECT category.id AS id,
           category.parent_id AS parent_id,
           category.name AS name,
           ? AS level 
      FROM category 
     WHERE category.parent_id IS NULL

    UNION ALL

    (SELECT category.id AS category_id,
            category.parent_id AS category_parent_id,
            category.name AS category_name,
            my_category.level + ? AS level 
       FROM category JOIN my_category ON category.parent_id = my_category.id
    ORDER BY category.parent_id DESC)
)

SELECT my_category.id AS my_category_id,
       my_category.parent_id AS my_category_parent_id,
       my_category.name AS my_category_name,
       my_category.level AS my_category_level 
  FROM my_category

Если прокомментировано order_by, исключение не возникает:

top_query = db.session.query(Category, db.literal(0).label('level')) \
                    .filter(Category.parent_id == None) \
                    .cte(name='top_query', recursive=True)

top_query = db.aliased(top_query, name='my_category')

bottom_query = db.session.query(Category, (top_query.c.level + 1).label('level')) \
                        .join(top_query, Category.parent_id == top_query.c.id) \
                        # .order_by(db.desc(Category.parent_id))

hierarchy_query = top_query.union_all(bottom_query)

db.session.query(hierarchy_query).all()

Нижняя часть SELECT без скобок (и без order_by) не имеет ошибки:

WITH RECURSIVE my_category(id, parent_id, name, level) AS (
    SELECT category.id AS id,
           category.parent_id AS parent_id,
           category.name AS name,
           ? AS level 
      FROM category 
     WHERE category.parent_id IS NULL

    UNION ALL

    SELECT category.id AS h_category_id,
           category.parent_id AS category_parent_id,
           category.name AS category_name,
           my_category.level + ? AS level
      FROM category JOIN my_category ON category.parent_id = my_category.id
)

SELECT my_category.id AS my_category_id,
       my_category.parent_id AS my_category_parent_id,
       my_category.name AS my_category_name,
       my_category.level AS my_category_level
  FROM my_category

1 Ответ

0 голосов
/ 06 июля 2018

Причина, по которой SQLAlchemy компилирует запрос так, как он это делает, заключается в том, что

SELECT x FROM foo UNION ALL (SELECT x FROM bar ORDER BY x)

и

SELECT x FROM foo UNION ALL SELECT x FROM bar ORDER BY x;

- это разные запросы (в базах данных, которые поддерживают синтаксис 1-го запроса, например Postgresql):

  • первый представляет собой объединение между двумя выборами, из которых второй упорядочен
  • последний представляет собой объединение двух неупорядоченных выборок. Составной выбор упорядочен целиком.

SQLite не поддерживает синтаксис, использованный в предыдущем запросе, следовательно, синтаксическая ошибка.

В реализации SQLite инструкция WITH ORDER BY при применении к составному select в целом имеет следующее значение:

Если присутствует предложение ORDER BY, оно определяет порядок, в котором строки извлекаются из очереди на шаге 2a. ...

Таким образом, вместо применения порядка ко второму запросу, используемому в объединении, как вы это сделали, его следует применять к составному выбору, используемому в CTE, но, к сожалению, похоже, что SQLAlchemy CTE не поддерживает это из коробки. Для этого вы можете реализовать свою собственную функцию:

from sqlalchemy.sql.selectable import CTE

# Following the implementation of CTE.union_all():
def cte_order_by(self, *clauses):
    return CTE(
        self.original.order_by(*clauses),
        name=self.name,
        recursive=self.recursive,
        _restates=self._restates.union([self]),
        _suffixes=self._suffixes
    )

и затем используйте его по вашему запросу:

hierarchy_query = top_query.union_all(bottom_query)
hierarchy_query = cte_order_by(hierarchy_query, db.desc('2'))
...