Привет, я Никита Волков - архитектор, консультант и автор hasql, одного из двух основных драйверов PostgreSQL для Haskell, который используется в крупных проектах вроде PostgREST и IHP. После 25 лет в IT и слишком большого числа ночных инцидентов из-за дрейфа схемы я выложил в open source то, чего мне не хватало еще десять лет назад: pGenie.
Дальше - история о том, как я выпустил популярный ORM в 2012 году… потом похоронил его… и в итоге понял, что самой базе данных и должна принадлежать роль единственного источника истины.
2012: я написал ORM
Тогда я был типичным веб-разработчиком своего времени. Единственное, что мне нужно было - это CRUD. Проблемы производительности можно было оставить на “потом”. Деплой сводился к ssh + git pull. Миграций фактически не было.
Я написал SORM - ORM-фреймворк для Scala - и выложил его в open source. Вместо того чтобы рассматривать объекты как изменяемые представления базы данных, он обменивал их с БД как неизменяемые значения. Это казалось свежей идеей. Проект получил отклик. Некоторое время это выглядело как успех.
Потом пришла реальность.
Каждый нетривиальный запрос показывал ограничения DSL.
Синхронизация кодовой модели с реальной схемой базы данных превратилась в неисчерпаемый источник головной боли.
По сути, я изобретал худшую версию SQL только ради того, чтобы сделать интеграцию “безопаснее”.
В 2014 году я остановил разработку. В 2016-м прекратил поддержку. Вывод: абстрагироваться над SQL - обычно ошибка.
Инсайт №1: вместо того чтобы заменять SQL, нужно просто улучшать интеграцию с ним
Вместо того чтобы строить слои абстракции поверх SQL нужно делать работу с сырым SQL максимально безопасной и удобной.
Так в 2014 году появился hasql - драйвер, который принимает SQL как есть и даёт компонуемые абстракции в поддержку этого подхода. Сегодня это один из двух основных драйверов Postgres в экосистеме Haskell и самый эффективный из них.
С этой библиотекой я нашёл оптимальную точку интеграции.
Инсайт №2: оптимальной точкой интеграции с БД является запрос
Не модель из бизнес-логики. Не структура строки таблицы. Сам запрос.
Запрос определяет структуру параметров, которые ему нужны, и то, что он возвращает. Попытки переиспользовать “типы строк” между разными запросами только создают скрытые зависимости и закладывают будущие поломки из разряда “меняем один запрос, ломается другой”. Натягивание моделей из бизнес-логики на сущности БД эскалирует эту проблему на другие слои приложения. А связывание с типами API - это вообще катастрофа, приводящая к абсурдным ситуациям, когда изменение API ломает интеграцию с БД.
Эта идея дала прочную основу, но чего-то всё ещё не хватало.
Каждый запрос приходилось сопровождать самописными кодеками, а SQL-строка никак не валидировалась. Поэтому в 2019 году я выпустил hasql-th - библиотеку проверки SQL-синтаксиса на этапе компиляции, основанную на порте парсера PostgreSQL. Это упростило систему и сделало ее безопаснее, но всё ещё было недостаточно. Не хватало валидации запросов на соответствие фактической схеме.
В течение этих лет я наблюдал проекты, которые пытались решить эту проблему через подключение к базе данных на этапе компиляции. Но это всегда казалось костылем, который делал сборки невоспроизводимыми. Я искал решение, которое могло бы жить целиком в коде, развиваться в репозитории и не зависеть от внешней инфраструктуры, и в итоге нашёл его.
Инсайт №3: миграции + запросы = API базы данных
Вот ментальная модель, которая изменила для меня всё:
Если рассматривать сервер Postgres как микросервис, тогда:
Миграции = контракт (как схема OpenAPI для REST).
Запросы = операции (эндпойнты в OpenAPI).
Ваше приложение - всего лишь клиент этого API. Значит, база данных первична. Значит, код приложения вторичен.
Это и есть подход DB-First (или SQL-First).
И тут всё наконец сложилось. Схема живет в SQL-миграциях. Запросы живут в файлах .sql. Всё это попадает в репозиторий и становится объектом разработки, тестирования и ревью. Всё остальное - типы, кодеки, рекомендации по индексам - можно автоматически выводить и безопасно интегрировать в кодовую базу с помощью компиляторов языков программирования с проверкой типов.
2022–2026: от закрытого SaaS-эксперимента к open source
Впервые я выпустил pGenie как SaaS в 2022 году. Выяснилось, что компании не хотят отправлять свои схемы и запросы третьей стороне. Что ж, пришлось пересмотреть стратегию.
В 2026 году я выложил pGenie полностью в open source и перешёл на бизнес-модель OSS + Consulting:
Это простой CLI. Вы указываете ему папки migrations/ и queries/. Он поднимает реальный контейнер PostgreSQL, воспроизводит схему, выполняя миграции, подготавливает запросы, анализирует information_schema и генерирует на 100% типобезопасный клиентский код для различных языков программирования через плагины. Сейчас Java, Rust и Haskell полностью поддерживаются как такие плагины. Ведётся разработка для C# и TypeScript.
Что на самом деле дает pGenie
SDK без лишнего шаблонного кода и с типобезопасностью
Поддержку продвинутых возможностей Postgres: JSONB, массивов, составных типов, multirange, расширений и т. д.
Децентрализованные генераторы кода, написанные на безопасном и воспроизводимом DSL Dhall
Файлы сигнатур, которые ловят дрейф схемы на этапе сборки
Автоматические рекомендации по индексам и предупреждения о последовательном сканировании
Полное сопоставление исходного SQL и сгенерированного клиентского кода для Java, Rust и Haskell есть в демо-репозитории - попробуйте сами.
Почему это особенно важно сейчас, в эпоху ИИ
LLM отлично пишут SQL. Но они не могут гарантировать, что этот SQL продолжит работать после следующей миграции.
pGenie закрывает этот разрыв: ИИ пишет запрос, pGenie проверяет его по реальной схеме, вы за секунды получаете предсказуемо структурированный типобезопасный SDK.
SQL-First + AI = максимум скорости без сюрпризов.
Готовы перестать бороться с базой данных?
Изучите документацию: pgenie.io/docs
Задавайте вопросы и делитесь идеями: github.com/pgenie-io/pgenie/discussions
Поставьте ⭐ на GitHub: github.com/pgenie-io/pgenie
Комментарии (27)

Kerman
15.04.2026 10:22в итоге понял, что самой базе данных и должна принадлежать роль единственного источника истины
А я примерно с этого же года использовал БД как источник истины и пришёл к тому, что он должен быть не там

amironov
15.04.2026 10:22Непонятно, чем model-first лучше чем code-first: определив типы и их отношения, мы как раз и описываем модель. В том же ef core есть куча точек расширений, в которых можно получить доступ к этой модели и сгенерировать на ее основе всё что угодно.

mojojojo Автор
15.04.2026 10:22Спасибо за ссылку. Интересная статья. Ироничная подача - тоже класс!
Насколько я понял, вы предлагаете отказаться от дихотомии: DB-first/Code-first, - и ввести третье представление между БД и языками. Данное представление будет нейтральным как относительно языков, так и относительно БД. Ценность этой нейтральности ясна: гибкость и устойчивость этой модели к переходам как между языками, так и между БД. Но помимо ценности есть и цена: переход сложности задачи от 2х до 3х тел со всеми вытекающими.
На мой взгляд, предлагаемый вами подход корректен и более гибок, чем DB-First, но и сложнее. Когда гибкость в отношении выбора БД не является требованием к системе, эта сложность будет избыточной. Иными словами, какой подход выбирать, как всегда в инженерии, "зависит".
Kerman
15.04.2026 10:22Не думаю, что сложнее. Этот подход похож на DB-First тем, что правится база данных и потом генерируется то, что вам там надо по схеме. Отличие только в одном. DB-First ломается на нескольких окружениях, когда есть локальная БД и всякие /dev/stage/prod. У каждой БД будет своя истина, а в model-first база данных подгоняется под модель механизмом синхронизации. Code-first решает эту же проблему путём выкладывания в спец. таблицу истории миграций и жёстким запретом на трогание БД руками. Что очень неудобно.
И кстати. Если вы генерите sql запросы по схеме, можете попробовать скооперировать с моим инструментом. Его задача - выдать вашему скрипту актуальное состояние схемы данных и для абсолютной гибкости подхода я и сделал скрипт на стороне пользователя тулзы.

mojojojo Автор
15.04.2026 10:22DB-First ломается на нескольких окружениях, когда есть локальная БД и всякие /dev/stage/prod. У каждой БД будет своя истина, а в model-first база данных подгоняется под модель механизмом синхронизации. Code-first решает эту же проблему путём выкладывания в спец. таблицу истории миграций и жёстким запретом на трогание БД руками.
А как же инструменты миграции Flyway, Liquibase, Sqitch и тп? Там так же накатывание миграций автоматизируется за счёт спец-таблиц. Конкретно это вовсе не прерогатива code-first.
Если вы генерите sql запросы по схеме
Их пишет пользователь.

MishaBucha
15.04.2026 10:22Когда я только начинал и увидел hibernate, думал, что лучше его не будет, но часто именно такая магия, абстракция, становится узким местом и болью, особенно, если БД это основа приложения.
На одном из проектов я познакомился с JOOQ, DSL для работы с базой данных и насколько же удобно и прозрачно все это, теперь я жесткий фатан JOOQ) Понятно, что если у вас маленький проект ORM упростит работу, но с ростом будет сложнее с ним работать
Спасибо за статью)

mojojojo Автор
15.04.2026 10:22Спасибо! Да. jOOQ в том же лагере инструментов. Если интересно, в доках есть сравнение pGenie vs jOOQ.

cmyser
15.04.2026 10:22А смотрели sqlc ? Goorm? Они тоже могут генерить

mojojojo Автор
15.04.2026 10:22Про Gorm не скажу, это похоже не ниша SQL-First.
А sqlc - прямой конкурент.
Принципиальное отличие в том, что sqlc целится на разные БД, из-за чего вынужден приводить внутренние представления к пересечению множества их возможностей, отбрасывая особенности. Поэтому, например, он до сих пор не поддерживает композитные типы Postgres. pGenie сфокусирован на Postgres и уже поддерживает продвинутые типы, включая композиты, многомерные массивы, multirange.
Ещё большое отличие в подходе к анализу. pGenie опирается на сам Postgres, разворачивая временный контейнер, проигрывая в нём миграции и прогоняя каждый запрос с извлечением метаданных из информационных схем. Благодаря этому поддерживаются любые синтаксические ухищрения и запросы любой сложности. Версию Postgres пользователь может выбирать, добиваясь соответствия продовой среде. В sqlc самописный эмулятор, который породил бесконечный поток багов несоответствия поведению Postgres. В последнее время, насколько я понял, они от него из-за этого стали плавно уходить.
Подробное сравнение есть в документации.

jakobz
15.04.2026 10:22У нас .net ef + hot chocolate graphql. И много sql вообще под капотом собирается. Например, фильтры для админок склеиваются с фильтрами для контроля доступа, сверху докидываются сортировки, и т.п. Понятно, что в этом случае контроль над sql вообще утерян, но нам вроде и не надо - для всяких табличек в админах особенно.
В твоем подходе какая-то композиция запросов предполагается? Или только средствами БД (вьюшки и т.п.)?

mojojojo Автор
15.04.2026 10:22Если подразумевается динамическое построение запросов, то пока не предполагается.
В демо-проекте есть пример со статическим запросом, реализующим динамическую выборку фильтров, условий сортировки и выборки. Для примера, вот генерируемый из него код Java.

ionicman
15.04.2026 10:22Вопрос немного из другого разреза разработки - а зачем вообще пытаться что-то рефлектить "из SQL", или "из языка"?
В чем проблема использовать БД как фронт использует бэк, делая запросы к хранимкам (если уж там есть сложная логика)? В таком случае при миграциях приложение вообще трогать не нужно будет, если старый вид данных, конечно, можно как-то получить. Достаточно изменить хранимку в БД - т.е. при bзменении БД вы остаетесь в парадигме лишь этой БД.
Что касается тестов - естественно, они внешние должны быть, тут ничего не сделать, но эндпоинты на хранимках избавляют от необходимости переделывать те тесты, которые уже были для них написаны, и нужно просто написать тесты только для нового или изменившегося функционала.
Вообще, использовать рефлекты при работы с БД - это очень заманчивая, но очень порочная практика, как и ORM, ИМХО. Это ускоряет старт, но жутко мешает потом в жизненном цикле. Обычные запросы и более понятны (не надо гадать, во что они там трансформируются промежуточным слоем) и легче могут быть оптимизированы или вообще заменены. Единственная проблема - смена БД. Но это крайне редкая проблема, можно сделать миграцию и ручками раз в 10 лет.Ну, либо я не понял сути вопроса.

mojojojo Автор
15.04.2026 10:22Возможно, у нас возникло недопонимание. Попытаюсь распутать.
Я не понял, что вы имели в виду под “рефлектами” в контексте статьи.
Обычные запросы и более понятны (не надо гадать, во что они там трансформируются промежуточным слоем) и легче могут быть оптимизированы или вообще заменены.
В статье и предлагается использовать обычные запросы без промежуточных абстракций. Просто они дополняются автоматическим выведением типов, проверкой на корректность и соответствие схеме и генерацией типизированного интеграционного слоя для приложения.
В чем проблема использовать БД как фронт использует бэк, делая запросы к хранимкам (если уж там есть сложная логика)? В таком случае при миграциях приложение вообще трогать не нужно будет, если старый вид данных, конечно, можно как-то получить. Достаточно изменить хранимку в БД - т.е. при bзменении БД вы остаетесь в парадигме лишь этой БД.
Здесь противоречия нет. С помощью хранимок действительно можно создать API БД, скрыв сами таблицы от клиентов как чёрный ящик. Вы действительно получите возможность обеспечить обратную совместимость и избегать изменения в клиентских приложениях при миграциях. У хранимок есть свои проблемы, но это вполне рабочая схема и она применяется много где. Очень хороший доклад в этом году на эту тему был от @mgramin на PGConf.
Однако с хранимками всё так же остаётся открытым вопрос интеграции со стороны языков приложений. Вам всё ещё необходимо писать и поддерживать в приложении код, который вызывает эти хранимки, передаёт в них параметры и разбирает результаты.
Эту часть pGenie может взять на себя и сделать надёжной. С точки зрения pGenie
SELECT call_my_stored_procedure($param1, $param2)- обычный запрос с параметрами и результатом, а потому он всё так же может сгенерировать для него обвязочный код и обеспечить проверки в CI/CD на то, что ваши миграции действительно оставляют ваши процедуры обратно-совместимыми с кодом приложений.Если интересно, уже была близкая по смыслу ветка обсуждения.

ionicman
15.04.2026 10:22Под рефлектами я имел ввиду отражение кого-либо экземпляра класса (его данных) в/из БД.
Да, сейчас стало понятней, что имелось ввиду, благодарю. Т.е. получается этакий линтер по типам и самим параметрам + возможность отслеживать, не нраушится ли флоу вследствии изменений структуры БД? Вот это инетресно, да.
mojojojo Автор
15.04.2026 10:22Скорее, не линтер, а тайпчекер. То есть, он не антипаттерны выявляет, а несоответствия того, как вы БД используете с тем, что у вас в схеме. В этом процессе оно неизбежно выводит типы, и как следствие имеет всё, что нужно для кодогенерации, а потому и генерирует код. Ну а дальше включаются дополнительные фичи вроде файлов сигнатур, которые фиксируют типы ваших запросов в коде и добавляют новые свистелки вроде указания того, является ли запрос идемпотентным, что открывает возможность для авторетраев. Короче говоря, потенциала там много. Гляньте learn-pgenie-in-y-minutes для быстрой экскурсии.
Линтеры SQL есть существующие, поэтому в эту поляну пока лезть не буду.

boldape
15.04.2026 10:22А можно ли подружить ваш пигини с sqlx? Я уже понял что придется писать свой генератор, вопрос на сколько это сложно?
Допустим у меня один сэлект возвращает всю строку целиком. По моим конвенциям названия таблиц множественное число, а в расте я хочу что бы названия типов были единственного числа, значит мне нужно переименовывать пгшные типы по правилам в общем случае и я бы хотел вручную задать имя типа в расте не по правилам когда надо. Генератор такое умеет? Думаю правило не сложно сделать в самом генераторе, а вот как передать метаинформацию в гегератор чтобы смапать пгшный тип на произвольный растовский тип? По идее это мапа должна быть в ваших файлах сигнатур, но я не заметил по списку того что там может быть возможность задать мапу. Либо сигнатуры должны описывать генератор специфичные секции либо это должен быть еще один слой поверх сигнатур. Короче не похоже что без больших изменений это возможно - а надо.
Дальше вопрос по композитам, продолжение вопроса переименований, но в общем виде. Допустим мой кверик возвращает строку целиком + еще один скалярный флаг/енум. У меня есть такие кверики и я бы хотел что бы часть результирующего композита, которая строка, получила тип в расте тот же что я задал для обычного селекта * для этой таблицы. Короче мне надо не просто задавать производьные имена типам в расте, но и еще чтобы генератор понимал, что он должен использовать эти имена консистентно и в композитах тоже, что бы раст обьекты совпадали.
Миграции - у нас сложные миграции не чистый скул, скул + код. Есть миграции на тайпскрипте и недавно начали писать на расте. Как подружить ваш пигини с такими миграциями? Думаю никак, тут у вас ошибка проектирования генератор не должен требовать каких либо миграций вообще. Это не его дело, ему нужна тупо конекция к аптудэйт базе, а как она мигрируется это не его забота. Малотого анализ работает не совсем одинаково на пустой и не пустой базе, но это слабый аргумент т.к. к прод базе конечно никто ради анализа не полезет, но все равно очень удобно иметь не пустую базу (у нас даунсайз прода в отдельном контейнере и мигрируется также как и прод) и дебажить кверики на ней. Да и генерация быстрее не надо мигрировать каждый раз чтобы сигнатуры посчитать.
Я это все спрашиваю не просто так, у нас есть похожий генератор, только проще. Ограничен пг -> раст. Мы используем sqlx, тоже есть много чего, анализ индексов телеметрия (у вас кстати нет), композиты, нулаблы, форс нот нулл в результатах, компайл тайм генерация. Короче разница с вашим проектом в простоте https://github.com/thepartly/automodel
Ваш пигини конечно более правильный архитектурно (за исключением миграций), но нам ехать, а не шашечки.

mojojojo Автор
15.04.2026 10:22Спасибо за развернутый фидбек! Очень ценно.
Классный термин вы ввели: “Reverse ORM”. Ёмко! Не встречал.
А можно ли подружить ваш пигини с sqlx?
Не проблема! Хотел бы только узнать, почему предпочитаете его tokio-postgres?
Спрашиваю потому, что изначально колебался для какой библиотеки писать первый генератор. В итоге выбрал tokio-postgres, потому что он более легковесный и сфокусирован на PostgreSQL. Но с другой стороны для него явно не хватает решения с одной кассой. С пулом, prepared statements, транзакциями и нормальной эргономикой. deadpool-postgres не дотягивает. Я подумывал под это дело расширить генератор, чтобы опционально с батарейками код выдавал или библиотеку расширительную выпустить, но тут надо принимать решение с пользователями. Отсюда и мои вопросы :)
Я уже понял что придется писать свой генератор, вопрос на сколько это сложно?
Несложно. Процесс устаканился. Уже есть 3 генератора как пример. Берёте входные данные как из того же демо-проекта. Руками пишете код, который хотели бы, чтобы генератор на эту модель выдавал. Передаёте это всё в LLM, а дальше напильником доводите до ума. Практика показывает, что нескольких итераций хватает для чего-то, чем можно начинать пользоваться.
Соответственно, если задача - расширить существующий генератор, то это ещё проще. Форкаете, командуете LLM изменить генератор так, чтобы он выдавал то, что вы хотите.
Полагаю, что получилось так просто благодаря тому, что для генераторов использовался Dhall, который строго типизирован и очень ограничен, что даёт LLM внятную структуру и резко сужает пространство для бреда. При этом язык достаточен для написания любого генератора.
Если хотите расширить сам инструмент, приглашаю в Github Discussions. Если понадобится помощь с приватным кодом, обращайтесь по линии консалтинга. Я в РФ.
Допустим у меня один сэлект возвращает всю строку целиком. По моим конвенциям названия таблиц множественное число, а в расте я хочу что бы названия типов были единственного числа, значит мне нужно переименовывать пгшные типы по правилам в общем случае и я бы хотел вручную задать имя типа в расте не по правилам когда надо. Генератор такое умеет?
Названия таблиц pGenie вообще не касаются. Из названий его заботят только три категории: название запроса, его параметров и колонок в его результатах. Это предопределяет структуру генерируемого кода.
Так, в примере генерируемого запроса из демо-проекта, на исходный запрос
/queries/select_album_by_format.sqlсоздаётся модульstatements::select_album_by_format, в котором его параметрам соответствует тип данныхInput, а его результатам -Output, который является алиасом к вектору над типомOutputRow, который объявляется в том же модуле.Такая структура кода не типична для кодогенераторов, однако она воплощает архитектурный принцип
low-coupling/high-cohesion. Запросы не связаны между собой содержащим модулем (они объявляются в отдельных модулях), а содержимое каждого модуля запроса максимально сплочённо (все детали, относящиеся к одному запросу, находятся в одном месте). Помимо предсказуемости и удобства навигации это ещё ускоряет компиляцию, так как компилятор может компилировать модули параллельно, а изменения в одном запросе не влияют на другие. Когда запросов накопятся сотни, это будет иметь значение.Тут вытекает вопрос: а что из этого вы хотели бы переименовывать?
Думаю правило не сложно сделать в самом генераторе, а вот как передать метаинформацию в гегератор чтобы смапать пгшный тип на произвольный растовский тип? По идее это мапа должна быть в ваших файлах сигнатур, но я не заметил по списку того что там может быть возможность задать мапу.
Генераторы конфигурируемы. Генератор сам определяет контракт требуемой им конфигурации. Так что, как минимум, вы можете передавать общие правила или те же мапы. Вот пример конфигурации специфичной для генератора java.
Либо сигнатуры должны описывать генератор специфичные секции либо это должен быть еще один слой поверх сигнатур. Короче не похоже что без больших изменений это возможно - а надо.
Сигнатуры пока не содержат возможности передавать генераторо-специфичные настройки, но потенциально в будущем такая возможность может появиться. Нужен только востребованный кейс.
Дальше вопрос по композитам, продолжение вопроса переименований, но в общем виде. Допустим мой кверик возвращает строку целиком + еще один скалярный флаг/енум. У меня есть такие кверики и я бы хотел что бы часть результирующего композита, которая строка, получила тип в расте тот же что я задал для обычного селекта * для этой таблицы. Короче мне надо не просто задавать производьные имена типам в расте, но и еще чтобы генератор понимал, что он должен использовать эти имена консистентно и в композитах тоже, что бы раст обьекты совпадали.
Это уж очень тонкая настройка пошла. Так ли она важна практически? Реализовывать дорого как в плане дизайна, так и усложнения системы, а практическая польза в чём?
Похоже, это ещё и вступает в противоречие с одним из инсайтов из обсуждаемой статьи:
Попытки переиспользовать “типы строк” между разными запросами только создают скрытые зависимости и закладывают будущие поломки из разряда “меняем один запрос, ломается другой”.
Сейчас правило простое: у каждого запроса свои типы. Чего предлагаемое усложнение нам даст с практической точки зрения?
Миграции - у нас сложные миграции не чистый скул, скул + код. Есть миграции на тайпскрипте и недавно начали писать на расте. Как подружить ваш пигини с такими миграциями? Думаю никак, тут у вас ошибка проектирования генератор не должен требовать каких либо миграций вообще. Это не его дело, ему нужна тупо конекция к аптудэйт базе, а как она мигрируется это не его забота. Малотого анализ работает не совсем одинаково на пустой и не пустой базе, но это слабый аргумент т.к. к прод базе конечно никто ради анализа не полезет, но все равно очень удобно иметь не пустую базу (у нас даунсайз прода в отдельном контейнере и мигрируется также как и прод) и дебажить кверики на ней. Да и генерация быстрее не надо мигрировать каждый раз чтобы сигнатуры посчитать.
Можно написать простой скрипт, который проиграет во временной БД ваши миграции, используя ваше текущее решение. Дальше сделать дамп получившейся схемы в миграцию
/migrations/1.sql. Дальше запускается pGenie.Для разработки это может стать билд-скриптом или инструкцией Makefile. В CI/CD это настроить тоже не проблема. Заоптимизировать тоже пространство есть: обновлять
/migrations/1.sqlтолько при изменении ваших текущих миграций.Я это все спрашиваю не просто так, у нас есть похожий генератор, только проще. Ограничен пг -> раст. Мы используем sqlx, тоже есть много чего, анализ индексов телеметрия (у вас кстати нет), композиты, нулаблы, форс нот нулл в результатах, компайл тайм генерация. Короче разница с вашим проектом в простоте https://github.com/thepartly/automodel
Познакомлюсь с вашим проектом плотнее, спасибо за ссылку.
Соглашусь, телеметрию стоит, как минимум, опционально встроить в генератор. Ну это просто вопрос доработки генератора.
Ваш пигини конечно более правильный архитектурно (за исключением миграций), но нам ехать, а не шашечки.
Понимаю и нацелен двигаться в сторону практических кейсов. Если вам интересно, может сделать с вами пилот. Вам интеграция, мне бизнес-кейс. Rust для меня - приоритетное направление.

boldape
15.04.2026 10:22Это уж очень тонкая настройка пошла. Так ли она важна практически?
Это очень важное практическое свойство. Там выше вы писали что каждый кверик генерирует свой набор типов в расте - это ошибка проектирования. Все кверики врзвращаюшие одинаковые данные должны использовать одинаковые типы иначе как всем этим пользоваться? Писать руками from для каждого кверика в доменный тип? А зачем тогда вообще использовать ваш генератор?
Поймите ценность гегератора не в том что бы с ноля начинать писать проект по феншую (как автор генератора считает правильным) а в том чтобы он облегчал практические задачи (особенно бойлерплэйт) вокруг уже существующего кода. Доменные типы уже есть, никто не хочет тратить время на написание в ручную и поддержку преобразований в/из типов базы. Это кстати ответ почему sqlx он позволяет смапать доменный тип на тип базы и проверить тоже в компил тайме только дороже по времени компиляции чем наш генератор. Еще sqlx умееет в динамические кверики, ну и самый главный критерий - так решила команда.
Реализовывать дорого как в плане дизайна, так и усложнения системы
Неа, это как раз не оч сложно, но вероятно не так хорошо дружит с вашей идеей раздельной компиляции, но все нормальные языки умеют в модули импорт/экспорт - это решение для этой задачи если прям оч хочется раздельной компиляции, но работа с пг / составление сигнатур это большая часть времени, код ген намного быстрее так что имхо это та самая оптимизация которая все только усложняет.
Сейчас правило простое: у каждого запроса свои типы. Чего предлагаемое усложнение нам даст с практической точки зрения?
Ну надеюсь из вышесказанного понятно зачем это надо. У нас много таких квериков уже и это очень важно чтобы интеграция с существующим кодом была максимально проста. Ваше боязнь скрытых зависимостей необоснована, во первых они не скрытые, а задаются самими типами из базы - ну какой смысл генерировать 100500 обсалютно одинаковых (по полям) но совершенно ге вщаимозаменяемых структур? Если один кверик читающий строку целиком поломался то очевидно все остальные которые выдают точно такую же строку обязаны поломаться, а при ваших 100500 разных типов я буду должен руками фиксить это не один раз а 100500.
Я понял у нас с вами несовместимость по философии - вам важно шашечки / как "правильно", а нам важно ехать. На практике это значит, что генератор не имеет право ничего навязывать/требовать изменить в проекте, мне не надо тратить время писать какие то скрипты, что бы после миграций еще и сдампить схему чтобы потом ее импортнуть во временную базу ровно для того что уже и так доступно без всех этих телодвижений. Юникс философия, утилита должна заниматься своим делом и не лезть со своими советами/требованиями как жить всем остальным. Никто не будет танцевать вокруг того, что там вы как автор генератора считаете правильным. Если ваш генератор больно подружить с тем что есть, ну значит не судьба. А ваш генератор больно подружить. Простые/маленькие кастомизации должны требовать простых/маленьких усилий. Поменять драйвер на sqlx это большая кастомизация - писать новый генератор адекватная затрата. Смапать тип из базы на доменный это маленькая задача - писать для этого новый генератор не адекватное усилие. Подключить генератор к существующей базе должно быть оч просто, а не напиши скрипт который сдампит базу. В общем с такой философией нам не попути.

mojojojo Автор
15.04.2026 10:22Дальше вопрос по композитам, продолжение вопроса переименований, но в общем виде. Допустим мой кверик возвращает строку целиком + еще один скалярный флаг/енум. У меня есть такие кверики и я бы хотел что бы часть результирующего композита, которая строка, получила тип в расте тот же что я задал для обычного селекта * для этой таблицы.
Я не обратил внимание, что ваш изначальный вопрос был про композиты. Композиты переиспользуются между запросами без дополнительных настроек и ваш кейс вполне реализуем с помощью композитов уже сейчас.
Вот пример объявления композита в миграции. А вот пример запроса, который засовывает часть колонок к этот композит и дополняет ещё одной скалярной колонкой. Похоже на то, что вы описали?
Вот во что этот композит превращается в расте, вот во что превращается запрос в расте. И да, композит может использоваться в нескольких запросах, и в каждом запросе он будет превращаться в один и тот же тип в расте, вот пример соседнего запроса, использующего тот же композит.
Неа, это как раз не оч сложно, но вероятно не так хорошо дружит с вашей идеей раздельной компиляции, но все нормальные языки умеют в модули импорт/экспорт - это решение для этой задачи если прям оч хочется раздельной компиляции, но работа с пг / составление сигнатур это большая часть времени, код ген намного быстрее так что имхо это та самая оптимизация которая все только усложняет.
Я имел в виду усложнение системы pGenie, а не вашего проекта. Чтобы тут нормальное решение задизайнить надо проблему глубоко проработать, а дальше реализовывать это потребует изменений в логике, контракте кодогенератора, и изменения кодогенератора. Поэтому это дорого и ключевым вопросом становится: насколько оно оправдано. Пока что вы первый человек с таким запросом.
Ваше боязнь скрытых зависимостей необоснована, во первых они не скрытые, а задаются самими типами из базы - ну какой смысл генерировать 100500 обсалютно одинаковых (по полям) но совершенно ге вщаимозаменяемых структур? Если один кверик читающий строку целиком поломался то очевидно все остальные которые выдают точно такую же строку обязаны поломаться, а при ваших 100500 разных типов я буду должен руками фиксить это не один раз а 100500.
Вы тут смотрите на систему в моменте времени. Да, в моменте два запроса могут возвращать одинаковый набор колонок. Давайте теперь взглянём в динамике. Разные запросы развиваются независимо и с течением времени возникнет потребность в один из связанных запросов добавить колонку. И тогда вам придётся менять код запросов, которые к этому изменению никакого отношения не имеют, увеличивая размер задачи. Таким образом, с упрощением системы в моменте, закладывается усложнение задач поддержки в будущем. Иначе говоря, происходит накопление технического долга.
На практике это значит, что генератор не имеет право ничего навязывать/требовать изменить в проекте, мне не надо тратить время писать какие то скрипты, что бы после миграций еще и сдампить схему чтобы потом ее импортнуть во временную базу ровно для того что уже и так доступно без всех этих телодвижений.
Хорошо. Только не забывайте, что в подходе с подключением к живым базам, использующимся для других задач, вы за удобство расплачиваетесь воспроизводимостью сборки.
Никто не будет танцевать вокруг того, что там вы как автор генератора считаете правильным.
Это одна из причин, почему я сделал кодогенераторы децентрализованными. У каждой компании свои приоритеты, у каждого техлида свой путь роста и набивания шишек, у каждого разработчика свои предпочтения в стиле кодирования. Всем не угодить.
Я предлагаю своё видение оптимального сгенерированного кода и подкрепляю публикациями вроде обсуждаемой. Тем не менее, согласятся с этим видением не все, и это нормально. Для несогласных есть возможность форкнуть кодогенератор и реализовать своё видение. В эпоху LLM это просто. Для совсем не согласных есть альтернативные инструменты и подходы, и это тоже вполне нормально.
Моё предложение о пилоте всё ещё в силе.

boldape
15.04.2026 10:22Пример с композитом близок, но не полностью. Представьте есть select * from tracks; и есть with... select (*)::tracks as row, 0 as result_code; Дальше по правилам именований на расте струтура для строки трэка должна быть Track без s на конце + второй кверик должен вернуть что нибудь типа struct ComplexSelectItem { row: Track, result_code: i32} Обратите внимание что я могу задать имя как всему компощиту так и сам генератор должен понять что для row ему не надо генерировать новый тип Tracks а надо использовать имя которое я задал для результата у первом обычном селекте.
Чтобы тут нормальное решение задизайнить надо проблему глубоко проработать, а дальше реализовывать это потребует изменений в логике, контракте кодогенератора, и изменения кодогенератора.
Это не сложно сделать. Я же не зря упомянул обычные языки с их импортами/экспортами - проблема решенная, решение простое, ничего изобретать и усложнять не надо.
Да, в моменте два запроса могут возвращать одинаковый набор колонок.
Большинство реальных запросов очень простые и возвращают строки целиком, такими и останутся на всегда, мне не нужно чтобы результат каждого запроса с тем же самым результатом генерировал уникальные не взаимозаменямые структуры, мне нужно роано наоборот.
Придётся менять код запросов, которые к этому изменению никакого отношения не имеют, увеличивая размер задачи.
Вот когда один из запросов поменяется и получит уникальный набор колонок в результате, вот тогда пусть генератор и сделает уникальную структуру, но не раньше, а все другие запросы что раньше шарили с этим результат продолжат его шарить, но уже без этого кверика - для них ничего не поламается. При добавлении колонки в результат ВСЕ колл сайты такого кверика надо будет руками потрогать, это вам раст, а не абы че, вы не сможете втихую проигнорировать новое поле в структуре. Зависимый от этого кверика код гарантировано поломается - так и должно быть поэтому никакого увеличения задачи нет от слова совсем.
вы за удобство расплачиваетесь воспроизводимостью сборки.
Вы эту проблему придумали или реально наблюдали в живой природе? 5 лет(но старой закрытой версией генератора) так делаем ни разу еще 2 разных артифакта не генерировалось. Смотрите база лежит в контейнере и резетится в первоначальное состояние при каждой сборке простым сбросом контейнера - откуда у вас проблемы не воспроизводимых сборок? Сами данные на типы никак не влияют, в лучшем случае можно надеятся на более качественный анализ по индексам, но это и так опциональная фича что у вас что у нас. Короче проблемы просто не существует.
Смотрите вы сюда пришли за фитбэком, а теперь мягко уклоняетесь от него. Так вот мой фидбэк такой - сигнатуры и сверху отдельные генераторы - отличная идея. Миграции которые ходят строем и кидают зигу при виде фюрера - оч плохая идея. Переписывать генератор с ноля при необходимости мальешей кастомизации - просто плохая идея.
Гегерация разных типов в расте для 100% одинаковых в ПГ вообще за гранью добра и зла только из за этого никто кроме вас этим пользоваться не будет.
Если не знаете куда двигать проект простой совет - интеграция в сложные реальные кодобазы должна быть максимально простая поэтому никаких предположений о том как правильно - как есть так и правильно, а философию оставьте себе. Генератор должен упрощать жизнь, а не усложнять.

mojojojo Автор
15.04.2026 10:22За подробный фидбек я вам премного благодарен!
Представьте есть select * from tracks; и есть with… select (*)::tracks as row, 0 as result_code; Дальше по правилам именований на расте структура для строки трэка должна быть Track без s на конце + второй кверик должен вернуть что нибудь типа struct ComplexSelectItem { row: Track, result_code: i32}
Это вы отличный пример дали!
Вас устроит писать
select (tracks.*)::tracks from tracksвместоselect * from tracksчтобы получать переиспользуемый тип в результате запроса? Потому что в таком случае противоречие, как будто, пропадает, так как результат будет мапиться в один растовский тип, переиспользуемый во множестве запросов.Переименование композитов можно реализовать в конфиге генератора. На первой итерации это может быть достаточно простой доработкой, изолированной только на генератор для Rust. Скорее всего, сам за реализацию этого я возьмусь не сразу, так как есть много приоритетных задач. Однако PR принимается, если кто-то захочет этим заняться.
Благодаря вашему примеру я вскрыл баг в движке анализа, из-за которого имплицитные композитные типы для строк таблиц (
(tracks.*)::tracks) не вытаскивались. Исправлю это в приоритете.Насколько я понимаю, если расширить
pgnрежимом работы с живой БД напрямую (без докера), то и вопросы к способу выведения схемы отпадут. Верно?
boldape
15.04.2026 10:22Вас устроит писать select (tracks.*)::tracks from tracks вместо select * from tracks чтобы получать переиспользуемый тип в результате запроса?
Меня лично устроит, но команду врядли.
Переименование композитов можно реализовать в конфиге генератора
Это не так просто, не все таблицы у нас заканчиваются на s потому что без с иногда лучше, не все таблицы названы в базе удачно поэтому в коде нпзвваются по другому. Не всем заходит автоматически придуманное имя структуры результата запроса. Поэтому простой конфиг для генератора не прокатит, нужны метаданные на уровне сигнатур. Вот товарищ вверху давал вам ссылку на свой модел фест подход, я пошел посмотреть, но там еще более оторванней от реальности (по сути он предлагает выкинуть N лет наработок написать модель с ноля и уже с нее генерить и базу и кверики и миграции и код; ну удачи ему в построении замков из слоновьей кости - это так никогда не работало), хотя аналог сигнатур (эксэмл схема) у него богаче и как раз позволяет эти самые мапы переименований задавать.
Скул сам по себе не позволяет дать имя эд-хок агрегату результат кверика. Если в скуле еще как то можно без этого прожить (это по сути тупл с именованными полями) то в других языках это нужно. Значит надо предоставить способ это сделать - задавать имя структуры результата. Раст не умеет в именованные туплы, по моему только сишарп на такое способен. Но туплы концептуально подошли бы так как они дак тайпятся друг на друга если совпадают по полям, тогда бы этой проблемы не стояло.
Что я хочу сказать - надо решить проблему как смапать тупл с именованными полями на структуру. Как минимум девелопер должен иметь способ задать имя этой структуры (фиг с ним что она будет сгенерированная) + указать набор трэйтов для дерайва, как максимум эта структура уже существует милион лет до начало использования генератора и надо просто смапать на нее. Вот такая задачка. Вы же генератор делаете с прицелом на разные языки, подумайте на счет возвращать туплы вместо структур возможно так будет проще, да и сам скул используют туплы.
Насколько я понимаю, если расширить pgn режимом работы с живой БД напрямую (без докера), то и вопросы к способу выведения схемы отпадут. Верно?
Верно. Только для меня слово расширить звучит странно. Генератору кроме коннекции к базе и папке с квериками вообще ничего из внешнего мира не нужно, а тут у вас терминология с ног на голову поставлена - базовая реализация это система миграций + коннекция, а расширением вы почему то называете редукцию до просто конекции.
Врядли мы будем использовать ваш генератор он для прода не готов, sqlx не подвезли и раст не в приоритете, наш прибит гвоздями к sqlx и использует не чистый скул (это самая сильная притензия) но нам ничего другого кроме sqlx и не надо, тюнится намного проще и фич имеет больше чем ваш. Зачем нам менять шило на мыло?
Kahelman
Если сделаете ещё один шаг то сможете прийти к идее компонентов доступа к БД которые во времена Delphi/ Interbase активно испольщовались и позволяли использовать логику на уровне БД хранимые процедуры.
Как вариант борьбы с рассинхронизацией схемы и запросов в БД давно придумали views.
Которые позволяют адаптировать изменения под «старые» требования
mojojojo Автор
Согласен с вашим ходом мысли. Как хранимые процедуры, так и View предоставляют возможность обеспечивать обратную совместимость запросов при миграции БД. На эту тему был интересный доклад от Макса Грамина на PGConf в этом году (https://pgconf.ru/talk/3033632).
Однако в обоих случаях остаётся открытым вопрос интеграции со стороны языка. Ни View, ни хранимые процедуры не дают статических проверок, интеграционного кода и тестов в языке программирования: это остаётся на плечах программиста.