Если вы работаете с базами данных и используете 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

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


  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 не скучать.