LINUX.ORG.RU

SQL Builder для Python: нужны советы

 ,


0

1

У меня давно была идея написать свою ORM для Python. Подтолкнуло на нее меня отсутствие асинхронных ORM. Это задача очень сложная и для ее реализации я ее решил разбить на подзадачи. Первая – это написать простенький SQL Builder, который станет основой для ORM. Я изучил исходники подобных продуктов на Ruby, Python, JavaScript, C# и Scala. На это у меня ушло пару дней. Сегодня я решил набросать основу:


from abc import abstractmethod
from functools import wraps
from typing import Any

__version__ = '0.1.0'


class Expr:

  @abstractmethod
  def to_sql(self):
    raise NotImplementedError


class Table(Expr):

  def __init__(
    self,
    name: str,
    *,
    schema: str = None,
    alias: str = None
  ) -> None:
    self.name = name
    self.schema = schema
    self.alias = alias

  def __getitem__(self, name) -> Expr:
    if name == '*':
      return Star(self)
    return Field(self, name)

  __getattr__ = __getitem__


class Star(Expr):

  def __init__(self, table: Table) -> None:
    self.table = table


# TODO: нужно придумать другое имя
def to_expr(f):
  @wraps(f)
  def wrapper(self, other):
    if isinstance(other, Expr):
      other = Value(other)
    return f(self, other)
  return wrapper


class Field(Expr):

  def __init__(self, table: Table, name: str) -> None:
    self.table = table
    self.name = name

  @to_expr
  def __eq__(self, other: Any) -> 'EQ':
    return EQ(self, other)

  @to_expr
  def __ne__(self, other: Any) -> 'NE':
    return NE(self, other)

  @to_expr
  def __lte__(self, other: Any) -> 'LTE':
    return LTE(self, other)

  @to_expr
  def __gte__(self, other: Any) -> 'GTE':
    return GTE(self, other)

  @to_expr
  def __lt__(self, other: Any) -> 'LT':
    return LT(self, other)

  @to_expr
  def __gt__(self, other: Any) -> 'GT':
    return GT(self, other)

  @to_expr
  def __or__(self, other: Any) -> 'OR':
    return OR(self, other)

  @to_expr
  def __and__(self, other: Any) -> 'AND':
    return AND(self, other)


class Value(Expr):

  def __init__(self, value: Any) -> None:
    self.value = value


class BinOp(Expr):

  def __init__(self, left: Expr, right: Expr) -> None:
    self.left = left
    self.right = right


class EQ(BinOp):
  ...


class NE(BinOp):
  ...


class LTE(BinOp):
  ...


class GTE(BinOp):
  ...


class LT(BinOp):
  ...


class GT(BinOp):
  ...


class OR(BinOp):
  ...


class AND(BinOp):
  ...


class Query:

  def __init__(self, dsn: str) -> None:
    self.dsn = dsn

  def select(self, *args, **kw) -> 'Select':
    return Select(self, *args, **kw)


class Statement(Expr):
  ...


class Select(Statement):

  def __init__(self, db, *args, **kw) -> None:
    self.db = db

  def from_(self, *args, **kw):
    return self

  def where(self, *args, **kw):
    return self

  def offset(self, *args, **kw):
    return self

  def limit(self, *args, **kw):
    return self

  def paginate(self, page: int = 1, per_page: int = 10):
    return self.offset((page - 1) * per_page).limit(per_page)

  def join(self, *args, **kw):
    return self

  def order_by(self, *args, **kw):
    return self

  def fetch(self):
    sql = self.to_sql()
    ...

  def fetchall(self):
    ...

  def single(self):
    ...


class Order(Expr):

  def __init__(self, field: Field) -> None:
    self.field = Field


class ASC(Order):
  ...


class DESC(Order):
  ...


q = Query('postgresql:///test')
p = Table('posts')
u = Table('users')
posts = q.select(p['*'], author=u.username) \
  .from_(p) \
  .join(u, p.author_id == u.id) \
  .where(p.deleted == False) \
  .order_by(DESC(p.published_at)) \
  .paginate() \
  .fetchall()

Примерно такая архитектура классов должна быть в библиотеке. Мне интересны ваши мнения. Может я что-то упускаю из виду. Я смутно представляю как все должно работать. Макет я набросал за 2 часа. Я не ставлю целью создать универсальный SQL Builder. Мне нужны только select с подзапросами, insert, update и delete.

Подтолкнуло на нее меня отсутствие асинхронных ORM.

Поскольку они отсутствуют, то даже не понял, что такое асинхронная ORM. Выражение SQL Builder странное и непонятное, но если имеется ввиду конструктор запросов, то для Python он есть в составе библиотеки SQL Alchemy, где также есть ORM.

Может я что-то упускаю из виду.

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

Partisan ★★ ()

Если прикидывать задачу вроде «сделать асинхронный запрос из SQL базы и превратить полученные данные в объекты питона», то я бы сделал запрос в преобразование в отдельном потоке, а потом использовал бы в основном результаты.
Наблюдать процесс загрузки/превращения большого числа данных? Я бы не использовал для этого ORM, прежде всего.

byko3y ()
Ответ на: комментарий от tz4678

Ща придумал такое:

# QueryBuilder должен формировать что-то подобное:

parts, binds = ['SELECT "t1".* FROM "public"."users" AS "t1" WHERE "t1"."age" >= ', ' AND "t1"."city" = ', ' ORDER BY "t1"."ranking" DESC LIMIT ', ' OFFSET ', ';'], [18, 'SPb', 10, 0]

# На эту идею меня натолкнули шаблонные строки в JavaScript

# В библиотеках для работы с SQL-базами применяются разные плейсхолдеры:

# %s – самый распространненный плейсхолдер
print(parts[0] + ''.join('%s' + i for i in parts[1:]))

# так же довольно часто встречается «?»
print(parts[0] + ''.join('?' + i for i in parts[1:]))
SELECT "t1".* FROM "public"."users" AS "t1" WHERE "t1"."age" >= %s AND "t1"."city" = %s ORDER BY "t1"."ranking" DESC LIMIT %s OFFSET %s;
SELECT "t1".* FROM "public"."users" AS "t1" WHERE "t1"."age" >= ? AND "t1"."city" = ? ORDER BY "t1"."ranking" DESC LIMIT ? OFFSET ?;
SELECT "t1".* FROM "public"."users" AS "t1" WHERE "t1"."age" >= $1 AND "t1"."city" = $2 ORDER BY "t1"."ranking" DESC LIMIT $3 OFFSET $4;

Даже хз как метод обозвать и как это формировать.

tz4678 ()
Ответ на: комментарий от tz4678

алхимия – это набор говна, ее переделывать проблематично проще новое написать

Скорее «набор говна, которое я не осилил». Вот пример с потоками, как я описывал, к слову:
https://bitbucket.org/zzzeek/green_sqla/overview
а вот пояснения:
https://www.jasonamyers.com/2016/gevent-postgres-sqlalchemy/

byko3y ()
Ответ на: комментарий от tz4678

https://github.com/fantix/gino

Не нужно ничего переделывать. Выше асинхронная обёртка вокруг алхимии.

Автор просто них-синдромщик, либо не умеет гуглить.

Примечание: по ссылке пока postgres-only. Забыл, но у меня как раз оно.

WitcherGeralt ★★ ()
Последнее исправление: WitcherGeralt (всего исправлений: 1)
Ответ на: комментарий от WitcherGeralt

там нет связей многие ко многим. когда объявляешь enum и gino его пытается создать, все падает… это кастрат какой-то: взяли полнтценную orm и отрезали яйца по самые гланды.

tz4678 ()
Ответ на: комментарий от tz4678

Ну так ещё не допилено, вот если в 1.0 такая херня будет, тогда уже можно ругать. Я как раз с 2.6 на 3.6 резко пересел, мне Gino сейчас понадобится, мб получится что-то допилить. А вообще, половина библиотек, заточенных под asyncio, такие, либо кастрированные обёртки, либо просто не допилены, так что в данном случае особенно не на что жаловаться, уж что есть, и то хорошо.

WitcherGeralt ★★ ()

В чем проблема использовать асинхронную стратегию в алхимии? Полной и чистой асинхронности без замены конектора на самописный с использованием асинхронных сокетов все равно приведет тебя к тому, что нужно держать поток для запросов и обработки ответа.

anonymous ()

Потому что async/await в питоне - чистый каргокульт, притащенный с JS-помойки без понимания, зачем оно ваще сдалось. Покайся и юзай gevent.

anonymous ()

Тут все в кучу!

class Expr:

  @abstractmethod
  def to_sql(self):
    raise NotImplementedError

Надо либо так:

class Expr:
  def to_sql(self):
    raise NotImplementedError

либо так

from abc import ABC, abstractmethod

class Expr(ABC):
  @abstractmethod
  def to_sql(self):
    ...

qaqa ()
Ответ на: комментарий от qaqa

нет. тут лучше всего подходит патерн Visitor:

class Node:
  def accept(self, v):
     v.visit(self)

...


class Visitor:
  ...
  
  def visit(self, node):
    method = 'visit_' + node.__class__.__name__
    getattr(self, method)(node)

  ... 

  def visit_Select(self, node):
    self.sql.write('SELECT')
    for item in (node.select_list, node.from_clause, ...):
      if item is None:
        continue
      self.sql.write(' ')
      item.accept(self)

  def compile_sql(self, node):
    self.bindings = []
    self.sql = StringIO()
    node.accept(self)
    return self.sql.getvalue(), self.bindings.copy()
tz4678 ()