Если вы работаете с базами данных и используете ORM, вы, вероятно, сталкивались с той же проблемой, что и я. ORM отлично подходят для отображения таблиц на объекты. Но они начинают мешать, когда запрос становится сложным: агрегации, тщательно продуманные JOIN’ы, формы отчетов, которые не соответствуют одной модели на таблицу. Вы боретесь с ORM, переходите на сырой SQL, а затем вручную пишете связующий код (маппинг).

Не каждый SELECT возвращает то, что подходит под одну ORM-модель. SQL - это лучший язык для доступа к данным. Лучшие ORM, которые я использовал, такие как Drizzle, побеждают, потому что они остаются близки к SQL. Я хотел пойти дальше: хранить SQL в системе контроля версий и генерировать из него типизированный Python.

Именно поэтому я создал nORM (no ORM - не ORM) и выпустил версию v0.1.0 на этой неделе (мой первый опенсорс проект).

nORM - это альтернатива использованию ORM для всего. Пока генератор работает только с Python. Позже я планирую миграции и больше языковых бэкендов; после этого он мог бы полностью заменить ORM, если вы этого захотите. На сегодняшний день это рабочий процесс в стиле sqlc плюс динамические возможности для запросов, которые в противном случае вы бы писали на Python.

Этот рабочий процесс вдохновлен sqlc. Если вы уже используете sqlc, и его вам достаточно - продолжайте.

SQL на входе, Python на выходе

Вы пишете схему и запросы на SQL. norm generate создает модели и классы репозиториев. Откройте сгенерированный метод, и SQL будет прямо там. Никакого скрытого слоя запросов.

Схема (norm_in/schema.sql):

CREATE TABLE users (
    id SERIAL PRIMARY KEY,
    name text NOT NULL,
    blocked bool DEFAULT false
);

Запросы (norm_in/repositories/users_repo.sql):

-- repo_name: UsersRepo

-- name: get_user :one
SELECT * FROM users WHERE id = :id;

-- name: list_users :many
SELECT * FROM users ORDER BY name;

Сгенерированный код (сокращенно из гайда по Python):

class UsersRepo:
    async def get_user(self, id: int) -> User | None:
        query = """
            SELECT
              users.id AS id,
              users.name AS name,
              users.blocked AS blocked
            FROM users
            WHERE users.id = %(id)s
        """
        params = {"id": id}
        async with self.db.cursor() as cur:
            await cur.execute(query, params)
            result = await cur.fetchone()
            ...
        return User(id=result[0], name=result[1], blocked=result[2])

Ваше приложение:

from norm_out.users_repo import UsersRepo

async with get_db() as db:
    repo = UsersRepo(db)
    user = await repo.get_user(id=42)

Три шага

  1. Напишите SQL (схема + запросы репозитория).

  2. Запустите norm generate.

  3. Импортируйте сгенерированный пакет и вызывайте методы репозитория из вашего приложения.

norm init создает шаблоны norm.yaml и папки. norm check полезен в CI: если запрос ссылается на отсутствующую колонку или тип параметра не совпадает, генерация завершится ошибкой.

Чем nORM превосходит простую кодогенерацию

sqlc останавливается там, где приложению все еще нужна runtime-композиция: опциональные фильтры для конечных точек списков, выбранные пользователем колонки для сортировки, частичные обновления, nullable объединения. nORM добавляет макросы, чтобы логика оставалась в SQL, а генератор ее разворачивал.

Динамическая фильтрация. Добавьте префикс _ к параметру, чтобы сделать предикат опциональным. Один запрос может покрыть множество комбинаций фильтров вместо построения строк в Python.

-- name: search_authors :many
SELECT * FROM authors
WHERE name = :_name AND rating > :_min_rating;

Руководство по динамической фильтрации описывает сгенерированный API и то, как nORM обрезает дерево WHERE во время выполнения.

Динамическая сортировка. Используйте n.ord() в ORDER BY, когда клиент выбирает колонку и направление сортировки.

-- name: list_authors_sorted :many
SELECT * FROM authors a
ORDER BY n.ord(a, :order_by, :desc), a.id ASC;

Сгенерированные методы принимают Literal[...] для разрешенных колонок и проверяют их перед выполнением запроса. Подробности: руководство по динамической сортировке.

Больше макросов (частичные обновления, встраивание JOIN и другие) доступны в обзоре и руководствах.

Как это работает

Разбор и работа с диалектами SQL выполняются через sqlglot. nORM читает DDL, SQL репозиториев и макросы, затем анализирует типы и структуру SQL перед кодогенерацией. Postgres, SQLite, MySQL, ClickHouse и DuckDB используют один и тот же путь. CLI, генераторы, макросы и конфигурация norm.yaml — это сам nORM.

Проект имеет широкое тестовое покрытие. v0.1.0 - это первый публичный релиз, но основная часть генерации закалялась в течение нескольких месяцев.

Что входит в версию 0.1.0

Включено: Генератор Python (асинхронный или синхронный, Pydantic или dataclasses через norm.yaml). CLI: norm init, norm generate, norm check, norm schema pull для интроспекции Postgres.

Пока нет: Генераторы для Rust, Go и TypeScript. Нет команды для миграций в версии 0.1.0; я планирую добавить миграции позже. До этого используйте свой обычный инструмент для изменений схемы.

Попробуйте

pipx install norm-cli
norm init
norm generate

Репозиторий: https://github.com/devfros/nORM

Комментарии (56)


  1. wango_pama
    05.06.2026 10:15

    Прежде чем браться за миграции: а как оно будет работать с аггрегацией данных из трёх таблиц? Какая-нибудь нормализация, например, если применена к данным


    1. AfrosRajabov Автор
      05.06.2026 10:15

      Я думаю сделать schema.sql источником правды с автогенерацией миграций там, где это возможно, плюс возможность дописывать миграции там, где изменения сложнее, чем обычные добавления/удаления колонок. Так работает Alembic, например. У меня уже есть наработки и успехи с PostgreSQL, но пока далеко от идеала.


      1. wango_pama
        05.06.2026 10:15

        Ок, ещё раз: а как nORM будет работать с аггрегацией данных из трёх таблиц?


        1. AfrosRajabov Автор
          05.06.2026 10:15

          Уже сейчас отлично работает с запросами любой сложности, и не важно, сколько таблиц в них участвует. Успешно справляется с определением того, какой набор колонок в итоге возвращается, и генерирует соответствующий код. Если запрос возвращает не строго какую-то одну таблицу, то генерируется новая модель, и результат маппится на неё.


        1. AfrosRajabov Автор
          05.06.2026 10:15

          Вся задача этого инструмента - это определение того, что принимает запрос, что возвращает и генерирование типизированного кода на основе этого. Плюс дополнительные макросы, чтобы по ORM не скучать.


  1. Tishka17
    05.06.2026 10:15

    Почему бы не использовать sqlachemy с (императивным) маппингом на запрос? Она умеет маппить модели/датаклассы на резульатат селекта любой сложности.


    1. AfrosRajabov Автор
      05.06.2026 10:15

      Вся концепция заключается в том, чтобы избавиться от ORM как от зависимости вовсе.


      1. Tishka17
        05.06.2026 10:15

        Я не понимаю эту ценность. Мы поменяли одну (очень мощную) библиотеку на другую (надо изучать, хватит ли возможностей).


        1. AfrosRajabov Автор
          05.06.2026 10:15

          nORM — это просто SQL с макросами и генератором кода. Он может всё, что может SQL, а это гораздо больше, чем любой ORM. Возможно, это непонятно из статьи, рекомендую пройтись по документации - я постарался там всё по полочкам разложить. Здесь же просто обзор.

          И это не библиотека, которая идет в ваш проект, сгенерированный код идет.


          1. Tishka17
            05.06.2026 10:15

            Он может всё, что может SQL, а это гораздо больше, чем любой ORM. 

            Охотно верю, но можно пример что не может та же sqlachemy зато легко решается с вашим проектом?

            И я не очень понял как состоит дело с динамическим sql? Условно когда содержимое where зависит от входных параметров и может содержать сабквери и доп. джойны и union. Как ваш проект это решает?


            1. AfrosRajabov Автор
              05.06.2026 10:15

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

              WITH customer_metrics AS (
                  SELECT 
                      c.id,
                      c.name,
                      c.plan_type,
                      COUNT(DISTINCT p.id) FILTER (WHERE p.status = 'failed') AS failed_payments,
                      COUNT(DISTINCT p.id) FILTER (WHERE p.status = 'refunded') AS refunded_payments,
                      COUNT(DISTINCT d.id) AS open_disputes,
                      SUM(p.amount) FILTER (WHERE p.status = 'failed') AS total_failed_amount,
                      MAX(p.created_at) FILTER (WHERE p.status = 'success') AS last_successful_payment,
                      MIN(p.created_at) FILTER (WHERE p.status = 'failed') AS first_failed_since_success
                  FROM customers c
                  LEFT JOIN payments p ON p.customer_id = c.id 
                      AND p.created_at > NOW() - INTERVAL '90 days'
                  LEFT JOIN disputes d ON d.payment_id = p.id 
                      AND d.status IN ('pending', 'investigation')
                  WHERE c.status = 'active'
                      AND c.deleted_at IS NULL
                  GROUP BY c.id, c.name, c.plan_type
                  HAVING COUNT(p.id) > 5  -- Has payment history
              )
              SELECT 
                  cm.id,
                  cm.name,
                  cm.plan_type,
                  cm.failed_payments,
                  cm.refunded_payments,
                  cm.open_disputes,
                  cm.total_failed_amount,
                  CASE 
                      WHEN cm.open_disputes >= 2 THEN 'immediate_suspend'
                      WHEN cm.failed_payments >= 3 AND cm.refunded_payments * 1.0 / NULLIF(cm.failed_payments, 0) > 0.5 THEN 'suspend'
                      WHEN cm.total_failed_amount > 1000 THEN 'review_required'
                      ELSE 'safe'
                  END AS action,
                  DENSE_RANK() OVER (ORDER BY cm.open_disputes DESC, cm.failed_payments DESC) AS risk_rank
              FROM customer_metrics cm
              WHERE cm.last_successful_payment > NOW() - INTERVAL '30 days'
                  AND cm.plan_type NOT IN ('enterprise', 'annual_enterprise')
                  AND (
                      cm.failed_payments >= 2 
                      OR cm.open_disputes >= 1
                  )
              ORDER BY risk_rank
              LIMIT 100;
              


              1. TheDigitalMadness
                05.06.2026 10:15

                Не буду говорить, что мне не понравился ваше решение (напротив, оно очень интересное). Просто накину тезис.

                Может я что-то неправильно понимаю, но в sqlalchemy (как и практически во все орм) есть возможность напрямую исполнить такой код. Предвижу справедливое замечание: при исполнении обычного sql запроса в орм, типизация отсутствует. И, фактически, это именно то, в чем выигрывает ваше решение: оно автоматически мапит типы. Но тут возникает вопрос резонности использования вашей nORM заместо sqlalchemy, теряя при этом функционал, который дает сама sqlalchemy


                1. AfrosRajabov Автор
                  05.06.2026 10:15

                  Да, text() / session.execute() выполнят любой SQL. Но это не тот же workflow: запрос в строке внутри Python, типов на входе/выходе нет, Row мапится вручную, схема и SQL живут отдельно, ошибки всплывают в рантайме. nORM - SQL в .sql-файлах, типизированные методы из генерации, плюс макросы для динамики без сборки строк. Разница не в том, можно ли выполнить запрос, а в том, сколько ручной работы остаётся вокруг него.

                  Про “теряем функционал SA” - частично согласен, но только если SA реально используется как ORM. На сложных запросах многие и так уходят в raw SQL и платят за ORM-слой, который для этих путей не работает. nORM это альтернативный слой доступа к данным, который работает для запросов любой сложности, а не замена (пока что).


                  1. wango_pama
                    05.06.2026 10:15

                    ошибки всплывают в рантайме

                    В большинстве случаев чтобы этого избежать достаточно для всех запросов при старте проделать процедуру PREPARE

                    Полностью избежать не получится потому что схема БД может меняться прямо на лету (когда к БД подключены "захардкоженные" клиенты)


                    1. Tishka17
                      05.06.2026 10:15

                      prepare сделать для всего не получится, если есть динамические запросы. А они почти всегда есть в каком-то количестве


                      1. wango_pama
                        05.06.2026 10:15

                        А для динамических и типы результатов не определены - следовательно, это не решаемая анализом синтаксиса проблема.

                        Такие запросы зависят от переданных аргументов. Именно поэтому для них не сделали возможность PREPARE.

                        (В таком случае мы можем запускать запрос в транзакции, получать данные о типах и откатывать её. Нужно только где-то раздобыть валидные аргументы для запуска транзакции.)


                      1. Tishka17
                        05.06.2026 10:15

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


                      1. wango_pama
                        05.06.2026 10:15

                        Мы не можем сохранить состав (и численность!) колонок потому что он может зависеть от аргументов этого запроса - всё зависит только от того как написан сам этот EXECUTE-запрос.

                        Сколько колонок вернёт вызов такой функции, например?

                        CREATE FUNCTION func1(query_text text)
                        RETURNS SETOF record AS $$
                        BEGIN
                            RETURN QUERY EXECUTE query_text;
                        END;
                        $$ LANGUAGE plpgsql;
                        


                      1. Tishka17
                        05.06.2026 10:15

                        Код, который выдает разный состав колонок в зависимости от параметров более редкий, так как с такими данными работать, так скажем, сложнее. А вот сохраняя состав колонок менять условия фильтрации, в том числе меняя джойны - это совершенно обычная ситуация.


                      1. wango_pama
                        05.06.2026 10:15

                        Так ведь теория нам говорит о том что мы не знаем что вернёт код пока не выполним его. И в этом деле нам не поможет даже анализ его AST.

                        То есть, мы не знаем когда на эту редкость напоремся (а напоремся мы обязательно)


              1. Tishka17
                05.06.2026 10:15

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

                PAYMENT_HISTORY_SINCE = now - timedelta(days=90)
                RECENT_SUCCESS_SINCE = now - timedelta(days=30)
                
                DISPUTE_OPEN_STATUSES = [
                    DisputeStatus.PENDING,
                    DisputeStatus.INVESTIGATION,
                ]
                
                EXCLUDED_PLAN_TYPES = [
                    "enterprise",
                    "annual_enterprise",
                ]
                
                cm = (
                    select(
                        c.c.id,
                        c.c.name,
                        c.c.plan_type,
                        func.count(distinct(p.c.id)).filter(p.c.status == PaymentStatus.FAILED).label("failed_payments"),
                        func.count(distinct(p.c.id)).filter(p.c.status == PaymentStatus.REFUNDED).label("refunded_payments"),
                        func.count(distinct(d.c.id)).label("open_disputes"),
                        func.sum(p.c.amount).filter(p.c.status == PaymentStatus.FAILED).label("total_failed_amount"),
                        func.max(p.c.created_at).filter(p.c.status == PaymentStatus.SUCCESS).label("last_successful_payment"),
                    )
                    .select_from(c)
                    .outerjoin(
                        p,
                        and_(
                            p.c.customer_id == c.c.id,
                            p.c.created_at > PAYMENT_HISTORY_SINCE,
                        ),
                    )
                    .outerjoin(
                        d,
                        and_(
                            d.c.payment_id == p.c.id,
                            d.c.status.in_(DISPUTE_OPEN_STATUSES),
                        ),
                    )
                    .where(
                        c.c.status == CustomerStatus.ACTIVE,
                        c.c.deleted_at.is_(None),
                    )
                    .group_by(c.c.id, c.c.name, c.c.plan_type)
                    .having(func.count(p.c.id) > 5)
                    .cte("customer_metrics")
                )
                
                query = (
                    select(
                        cm.c.id,
                        cm.c.name,
                        cm.c.plan_type,
                        cm.c.failed_payments,
                        cm.c.refunded_payments,
                        cm.c.open_disputes,
                        cm.c.total_failed_amount,
                        case(
                            (cm.c.open_disputes >= 2, "immediate_suspend"),
                            (
                                and_(
                                    cm.c.failed_payments >= 3,
                                    cm.c.refunded_payments / func.nullif(cm.c.failed_payments, 0) > 0.5,
                                ),
                                "suspend",
                            ),
                            (cm.c.total_failed_amount > 1000, "review_required"),
                            else_="safe",
                        ).label("action"),
                        func.dense_rank()
                        .over(order_by=[cm.c.open_disputes.desc(), cm.c.failed_payments.desc()])
                        .label("risk_rank"),
                    )
                    .select_from(cm)
                    .where(
                        cm.c.last_successful_payment > RECENT_SUCCESS_SINCE,
                        cm.c.plan_type.not_in(EXCLUDED_PLAN_TYPES),
                        or_(
                            cm.c.failed_payments >= 2,
                            cm.c.open_disputes >= 1,
                        ),
                    )
                    .order_by("risk_rank")
                    .limit(100)
                )
                


                1. AfrosRajabov Автор
                  05.06.2026 10:15

                  Если это удобно, читабельно и легко поддерживать для вас, nORM вам точно не нужен. Как я уже сказал, это вопрос количества ручной работы вокруг всего этого.


                  1. Tishka17
                    05.06.2026 10:15

                    Я просто попросил пример, что можно сделать на nORM и нельзя на sqlalchemy. Оба кода выше выглядят очень похоже. Да, sql можно сразу скопировать и выполнить, зато у алхимии можно выносить переиспользуемые части и добавлять динамику в конструирование. Получается всё таки вопрос вкуса, а не то что у ORM есть какие-то принципиальные ограничения, которые вы решили своим проектом?


                    1. AfrosRajabov Автор
                      05.06.2026 10:15

                      Мне кажется все, что нужно в повседневной разработке, можно сделать и тем и другим. Вопрос не в том, что nORM умеет такого, что нельзя сделать ОРМ-ом, а в том КАК это делается. В nORM все сводится к написанию SQL запроса в SQL файле (это все что нужно сделать), в то время как с ОРМ этого недостаточно и при сложных запросах ответ нужно маппить руками. Ещё раз: nORM - это альтернативная имплементация слоя доступа к данным.


                      1. Tishka17
                        05.06.2026 10:15

                        Я же правильно понял, что nORM не решает задачу сохранения данных, только чтение?

                        Кстати, а у вас стратегии загрузки связанных данных? Например, иногда дешевле сделать второй запрос по списку айди чем пытаться заджойнить.


                      1. AfrosRajabov Автор
                        05.06.2026 10:15

                        Нет, не только чтение, работает с любым SQL запросом (SELECT, UPDATE, INSERT, DELETE). Даже есть частичные обновления таблиц, где вы пишите запрос на обновление один раз и обновляете только те колонки, которые подаете в функцию.


                      1. Tishka17
                        05.06.2026 10:15

                        Давайте представим что у нас есть что-то такое

                        class Child:
                           id: int
                           name: str
                          
                        class Parent:
                           id: int
                           children: list[Child]

                        Даьше, я с помощью вашего инструмента заселектил Parent с id=1 и всеми связанными children. После этого я поменял добавил туда ещё один Child, а один из существующий отредактировал (поменял name). Мне теперь что надо сделать чтобы это сохранить? Писать руками запрос на insert, ещё один на update и следить какой вызывать? Или тут есть аналоги подхода data mapper и unit of work?


                      1. AfrosRajabov Автор
                        05.06.2026 10:15

                        Нет, такого функицонала как у ОРМ нету. Явно добавляете или обновляете.


                      1. AfrosRajabov Автор
                        05.06.2026 10:15

                        Разработчик сам решает какие запросы ему писать и в какой композиции использовать. Задача nORM сгенерировать модели БД и репозитории с методами под каждый запрос.


                      1. Tishka17
                        05.06.2026 10:15

                        Тогда я вообще не понимаю чем это лучше чем взять какой-нибудь pydantic/adaptix и сделать что-то максимально тупое вроде load(cursor.fetchall(), list[Data])

                        UoW нет, стратегий загрузки нет, квери билдера нет


                      1. AfrosRajabov Автор
                        05.06.2026 10:15

                        Потому что это не ОРМ. Тут вы пишите запросы напрямую, поэтому и нет квери билдера. И текущая ранняя версия ни на что не претендует. Хотя nORM заменил мне ОРМ на несольких микросервисах вполне себе успешно.


            1. AfrosRajabov Автор
              05.06.2026 10:15

              Что касается динамических запросов, я их описал подробно в документации тут: https://devfros.github.io/nORM/guides/dynamic_filtering


              1. Tishka17
                05.06.2026 10:15

                Вв про такое? Не думаю, что это удобно хоть кому-то

                "/*FILTER1*/": {
                    'clause': 'WHERE', 
                    'tree': {
                       't': 'OR', 
                       'l': {'p': ['id'], 
                       'v': 'authors.id = %(id)s'},
                       'r': {'v': 'authors.rating > %(rating)s'}
                    }
                },


                1. AfrosRajabov Автор
                  05.06.2026 10:15

                  Это кусок генерируемого кода внутри функции, которую вы просто вызываете. Какое неудобство это может вызвать? Вы же не инспектируете код каждой библиотеки, которую устанавливаете.


                1. AfrosRajabov Автор
                  05.06.2026 10:15

                  Это дерево предикатов фильтра. В зависимости от поданных вами параметров, в запрос включаются только определенные. Легче было так сделать, чем генерировать тонну if-ов.


                  1. Tishka17
                    05.06.2026 10:15

                    В алхимии я могу написать что-то типа

                    query = select(table1)
                    if arg1:
                      query = query.join(table2, cond)
                    if arg2 is not NULL:
                      query = query.where(table1.c.field2 == arg2)
                    
                    session.execute(query).all()

                    То есть в завимсимости от параметров будет сгенерировано либо (минимальная версия)

                    select * from table1

                    либо (максимальная версия, есть промежуточные)

                    select * from table1
                    join table2 on cond
                    where table1.fiedl2 = %s

                    Как в вашем проекте делать похожие вещи?


                    1. AfrosRajabov Автор
                      05.06.2026 10:15

                      В текущей (0.2.0) версии динамические запросы затрагивают только фильтры и сортировку, JOIN-ов пока нет. В данном случае вы бы имели 2 разных запроса, сгенерировали бы 2 функции и вызывали бы эти функции также, как собираете ваш запрос:

                      if arg1:
                        result = table_repo.get_minimal()
                      elif arg2 is not None:
                        result = table_repo.get_maxified(arg2)
                      


                      1. Tishka17
                        05.06.2026 10:15

                        ну это я написал два, в реальности этот код генерирует 4 варианта. Если фильтров допустим 10 штук (маленькая такая форма, бывает и 20), то это уже 1024 варианта

                        Естественно я не хочу дублировать общую часть, я хочу её переиспользовать


                      1. AfrosRajabov Автор
                        05.06.2026 10:15

                        -- name: optional_id :many
                        SELECT * FROM authors
                        WHERE id = :_id or rating > :_rating;
                        

                        Функцию, которая генерируется на основе этого запроса nORM-ом, можно использовать как 4 разных вариаций в зависимости поданных параметров. 2^n ( где n - количество опциональных параметров). При n = 10 получаем 1024


                      1. Tishka17
                        05.06.2026 10:15

                        я что-то не понял. Если у меня фильтрация по рейтингу не требуется, она это как-то вырежет из запроса?


                      1. AfrosRajabov Автор
                        05.06.2026 10:15

                        Да, в итоге в БД отравится:

                        SELECT * FROM authors
                        WHERE id = :id;
                        


                      1. AfrosRajabov Автор
                        05.06.2026 10:15

                        Не включит предикат (rating > :rating) в запрос, а не вырежет, если быть точнее.


                      1. Tishka17
                        05.06.2026 10:15

                        я не очень понял по каком принципу оно вырезает кусок условия (например там может быть сложный подзапрос, где :_ratingзапрятан глубоко в дереве), но допустим. Подожду что вы придумаете с джойнами


                      1. AfrosRajabov Автор
                        05.06.2026 10:15

                        WHERE или HAVING - это всегда набор предикатов объединенных c помощью логических операторов (OR, AND). Опциональный параметер делает опциональным весь предикат. не важно на сколько сложен и вложен.


                      1. Tishka17
                        05.06.2026 10:15

                        -- name: optional_id :many
                        SELECT * FROM authors
                        WHERE id = :_id or exists(
                          select 1 from books 
                          where books.author_id=authors.id 
                            and books.rating > :_rating;
                        )

                        что будет вырезано? Как мне сказать что в одном случае надо вырезать весь exists, а во втором случае - только проверку на rating внутри?


                      1. AfrosRajabov Автор
                        05.06.2026 10:15

                        Здесь будет контороллироваться только включение всего exist на основе :_rating. Это специальное ограничение пока что, я работаю над вложенными фильтрами.


                      1. Tishka17
                        05.06.2026 10:15

                        Хорошо, работайте, интересно что получится. Пока мне кажется что этот подход тупиковый и вы родите какое-то подобие query builder в конце концов


                      1. AfrosRajabov Автор
                        05.06.2026 10:15

                        Я вам рекомендую установить и попробовать просто, это супер быстро. И генерируется все моментально. Не могу на каждый вопрос ответить так же хорошо, как изложено в документации либо в генерируемом коде. Спасибо за обсуждение!


                      1. AfrosRajabov Автор
                        05.06.2026 10:15

                        Это никогда не будет квери билдером. Я паршу чистый SQL, анализирую и генерирую код. В sql есть вся нужная информация, чтобы генерировать нужный код. В эту сторону я и двигаюсь.


                      1. wango_pama
                        05.06.2026 10:15

                        Я паршу чистый SQL

                        Это очень плохая идея: корректно распарсить SQL-запрос чертовски сложно. SQLAlchemy делает это неправильно, например.

                        А главное - не нужно (по крайней мере для PostgreSQL): информация о типах аргументов в запросе и типах результата ответа всегда доступна (в моём софте я всё это беру через libpq)


                      1. AfrosRajabov Автор
                        05.06.2026 10:15

                        Я использую хороший парсер, который отлично с этим справляется: https://github.com/tobymao/sqlglot

                        Когда он не справлялся, я пару раз открывал issue и 2 раза даже PR отправлял. Вопрос 1-2 дней буквально.

                        Весь проект покрыт e2e и runtime тестами. Все работает.

                        Сложно - никогда не аргумент.


                      1. wango_pama
                        05.06.2026 10:15

                        "Сложно" здесь это обозначает что в таком разобре всегда будут проблемы

                        По какой-то (непонятной мне) причине проект PostgreSQL не предоставляет библиотеку синтаксического анализатора для своего диалекта SQL. (Хотя, естественно, внутри сервера этот код имеется - нужно только начать собирать его в отдельную библиотеку, на манер libpq)

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


                      1. AfrosRajabov Автор
                        05.06.2026 10:15

                        Ну, к счастью, в случае с парсером, который я использую, “глюковая чепуха” не получилась. Отлично парсит 30+ диалектов и дает возможность расширять возможности, если нужно где-то закрыть пробелы.


                      1. wango_pama
                        05.06.2026 10:15

                        Ну, к счастью, в случае с парсером, который я использую, “глюковая чепуха” не получилась.

                        Таким образом, к концу года в программе будут исправлены все ошибки (c)


  1. LernardDranrel
    05.06.2026 10:15

    Как по мне - идея неплохая в своей сути. Можно конечно подескутировать относительно того не создание ли это очередного велосипеда, но от себя вижу вполне полезную нишу: обмен запросами с аналитиками BI - там может приятно лечь на общий pipeline проекта или по крайней мере упростить этот обмен.

    Но есть три "no":)

    1. Я бы порекомендовал где возможность сделать runtime компиляцию без необходимости вызывать команду каждый раз. Что-то вроде простенького nOrm.actualize(). Где в рантайм не получится - набрать готовых встроек на этап компиляции.

    2. Учитывая сколько проектов УЖЕ на ORM, неплохо бы подсластить пелюлю и добавить коннекторы на основные ORMки языков, чтобы итоговый результат запроса аккуратно упаковывался в уже сущесвующие типы объектов строк или коллекций. Тогда выше шанс, что на nORM обратят внимание.

    3. Скажу немного крамольную фразу для Хабра, но: "В эпоху ИИ ценность утилиты уже не столь очевидна". Я бы порекомендовал развивать ее во что-то более комплексное, но направление дать не решусь. Оставлю на ваших плечах.

    Вот 3 моих маленьких совета. Надеюсь они вам помогут, даже если будут отброшены)


    1. AfrosRajabov Автор
      05.06.2026 10:15

      Благодарю!