amaslyaev / noorm

NoORM (Not only ORM) - Python library that makes your database operations convenient and natural
MIT License
16 stars 0 forks source link

"WHERE field IN" statement #3

Closed LecronRu closed 2 months ago

LecronRu commented 2 months ago

Нечасто, но встречается задача subj. Когда доступ через execute(select), запрос модифицируется до нужного количества плейсхолдеров, в соответсвии с количеством элементов последовательности (списка). Как это решить с помощью вашей библиотеки? Ведь запрос уже является частью декоратора.

Хотелось бы прозрачности, похожей на ORM. Список является одиночным параметром, а в запросе используется мета-плейсхолдер. Например:

@nm.sql_fetch_all(
    DbUser, "SELECT rowid AS id, username, email FROM users WHERE salary < :salary_limit AND name IN (:*names)"
)
def get_user_by_condition(salary_limit: int, names: list[str]):
    return nm.params(salary_limit=salary_limit, names=names)
amaslyaev commented 2 months ago

Как раз для таких случаев сделаны функции nm.query_only и nm.query_and_params. Через них можно выкрутиться как-то так:

@nm.sql_fetch_all(DbUser)
def get_some_users(salary_limit: int, names: typing.Collection[str]):
    if not names:
        raise nm.CancelExecException
    return nm.query_and_params(
        f"""
            SELECT rowid AS id, username, email
            FROM users
            WHERE salary < :salary_limit
                AND username IN ({", ".join(":n_"+str(i) for i in range(len(names)))})
        """,
        salary_limit=salary_limit,
        **{f"n_{i}": v for i, v in enumerate(names)},
    )

Если выписывание таких конструкций начёт напрягать, можно приделать пару удобняшек:

def list_placeholders(prefix: str, vals: typing.Collection[typing.Any]) -> str:
    return ", ".join(f":{prefix}{i}" for i in range(len(vals)))

def list_vals(
    prefix: str, vals: typing.Collection[typing.Any]
) -> dict[str, typing.Any]:
    return {f"{prefix}{i}": v for i, v in enumerate(vals)}

после чего оно станет поприятнее:

@nm.sql_fetch_all(DbUser)
def get_some_users(salary_limit: int, names: typing.Collection[str]):
    if not names:
        raise nm.CancelExecException
    return nm.query_and_params(
        f"""
            SELECT rowid AS id, username, email
            FROM users
            WHERE salary < :salary_limit
                and username in ({list_placeholders("n_", names)})
        """,
        salary_limit=salary_limit,
        **list_vals("n_", names),
    )

Конкретно с WHERE ... IN у нас проблем нет, поскольку Постгрес понимает параметры, являющиеся tuple. Но для гибкой фильтрации или для гибких UPDATE-ов манипулирование текстом запроса очень пригождается. Тем не менее, какого-то повторяющегося паттерна, чтобы намутить даже хотя бы project-specific удобняшек, пока что не обнаружили.

Боюсь, что если начать прикручивать или самодельный шаблонизатор для SQL-ек, или подключить какой-то из уже имеющихся (например, jinja), можно открыть портал в ад, и через сравнительно небольшое количество итераций обнаружить, что маленькая, простая и элегантная штучка превратилась в монстра с кучей неочевидно как работающих фич и документацией на 500 страниц. Может быть, пока есть смысл сделать паузу и поднабрать статистику, что конкретно было бы наиболее востребовано, и тогда, возможно, придумать элегантное решение. Мета-плейсхолдеры – весьма неплохая идея. Есть смысл взять на заметку. Спасибо.

amaslyaev commented 2 months ago

А вообще мне подумалось, что адаптеры для SQLite (noorm.sqlite3 и noorm.aiosqlite) уже и так имеют доп. фичу, которая без лишних телодвижений интерпретирует строки как date, datetime и Decimal.

Так что мне нравится идея приделать прозрачный, беспроблемный и понятно как работающий parameters propagation чисто для SQLite. Сделаю в релизе 0.1.3.

amaslyaev commented 2 months ago

Сделал. Теперь (в версии 0.1.3) можно передавать коллекции в параметры. Как в именованные (в стиле :ids), так и позиционные (в стиле ?). Работает прозрачно, как будто движок SQLite научился такое понимать:

@nm.sql_fetch_all(
    DbUser, "SELECT rowid AS id, username, email FROM users WHERE salary < :salary_limit AND name IN (:names)"
)
def get_user_by_condition(salary_limit: int, names: list[str]):
    return nm.params(salary_limit=salary_limit, names=names)

(т.е. звёздочки не нужно)

Попробуйте пожалуйста, работает ли оно как ожидалось.

amaslyaev commented 2 months ago

Done in v0.1.3