# Python Psycopg Yoyo Persistence Writing

> Используй при реализации или ревью PostgreSQL persistence-адаптеров на Python через psycopg и миграций через yoyo. Триггеры — репозитории, mapping строк БД в domain или DTO порта, одиночное и пакетное чтение/сохранение, optimistic locking, PostgreSQL Unit of Work и его фабрика, connection manager и pool, безопасный SQL, создание и изменение таблиц, ограничений, индексов и других объектов схемы миграциями yoyo. Не применять для определения application-портов, доменных правил, публичных API-контрактов и хранилищ на других технологиях.

- Skill: `nemagu/python-psycopg-yoyo-persistence-writing` (Agent Skill, multi-file: 11 files)
- Install (CLI): `npx skillmds@latest add nemagu/python-psycopg-yoyo-persistence-writing`
- Raw SKILL.md: https://api.skillmd.com/api/skills/nemagu/python-psycopg-yoyo-persistence-writing/raw
- Safety review: pending (external: skill-scanner PASS, skillspector PASS)
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Data & Analytics
- Author: Nemagu (https://skillmd.com/u/nemagu)
- Updated: 2026-09-21
- Page: https://skillmd.com/skills/nemagu/python-psycopg-yoyo-persistence-writing

---


# Адаптеры хранения psycopg и миграции yoyo

Общие правила оформления Python-кода брать из `$python-code-style-writing` и не
дублировать здесь; этот скил определяет persistence-контракты и SQL-безопасность.

## Порядок работы

1. Извлечь точный контракт порта: методы, типы, отсутствие, конфликты,
   атомарность, версию и согласованность чтения.
2. Сверить таблицы, столбцы, типы, nullable и ограничения с миграциями.
3. Реализовать connection manager, фабрику UoW, UoW и репозитории с одним
   владельцем транзакции.
4. Преобразовывать DB-данные только в тип результата порта.
5. Оборачивать ожидаемые ошибки зависимости в `AppPortError`.
6. Для изменения схемы сначала показать пользователю таблицы и план миграции,
   получить разрешения и только затем менять файлы.
7. Проверить mapping unit-тестами, SQL — на PostgreSQL, миграции —
   `apply → rollback → apply` через yoyo.

## Граница ответственности

- Реализовывать существующий application-порт без изменения публичного контракта
  под удобство PostgreSQL.
- Не возвращать `DictRow`, cursor, connection, SQL-модель или тип psycopg наружу.
- Не определять доменные инварианты и публичную семантику исходов.
- При недостаточном или противоречивом контракте остановиться и назвать
  конкретное противоречие.

## Транзакционная модель

- Connection manager владеет pool и арендой соединений.
- `UnitOfWorkFactory` — долгоживущий stateless-адаптер; каждый вызов возвращает
  новый UoW.
- UoW получает соединение при входе, открывает явный
  `connection.transaction()` и предоставляет репозиториям одно соединение.
- UoW единолично определяет commit/rollback. Connection manager и репозитории не
  придают выходу прикладную семантику.
- Не использовать `pool.connection()` внутри UoW как второй автоматический
  transaction manager: арендовать через `getconn`/`putconn` или эквивалентный
  lease без скрытого commit.
- Один UoW нельзя повторно или конкурентно использовать. После выхода очищать
  connection, transaction context и repository-группы.
- Retry всей операции создаёт новый UoW; отдельный SQL внутри сломанной
  транзакции не повторять.
- Isolation level, read-only, savepoints и retry добавлять только по требованиям.

Подробности: [единица работы](references/unit_of_work.md) и
[менеджер подключений](references/connection_manager.md).

## Репозитории

### Устройство

- Публичные методы точно реализуют порт и делегируют DB-операции приватным.
- Базовый репозиторий хранит только connection, общий enum таблиц и действительно
  общие технические helpers. Не создавать универсальный CRUD.
- Статический SQL держать рядом с репозиторием; не строить собственный ORM.
- Использовать явный список колонок вместо `SELECT *`.
- Каждый приватный DB-метод закрывает cursor контекстным менеджером и
  преобразует результат до выхода.

### Сохранение и чтение

- Одиночный `save` делегирует в `_create`/`_update` с обычным `execute`; не
  оборачивать объект в `batch_save`.
- `batch_save` заранее преобразует всю пачку и использует `executemany`, не
  выполняя SQL в цикле.
- Batch-чтение реализовывать set-based запросом; пустой набор завершать без SQL.
- Connection-bound репозитории UoW не получают новое соединение самостоятельно.
- Самостоятельный read-адаптер арендует соединение на один публичный вызов и не
  хранит его между вызовами.
- Не объединять оба режима через поле `connection | manager`.

Подробности: [сохранение](references/save_patterns.md) и
[чтение](references/read_patterns.md).

### Преобразование

- Настраивать psycopg с `row_factory=dict_row`, типизировать строки как
  `DictRow` и читать колонки по именам.
- Агрегат или доменную проекцию восстанавливать в `_model_to_domain` только через
  доменную фабрику и проектный `@handle_domain_errors`.
- DTO порта создавать в `_model_to_dto`.
- Parameter mapper-ы и DB-to-result mapper-ы делать чистыми и переиспользовать
  между одиночной и пакетной операцией.
- Не менять приватные поля агрегата и не исправлять повреждённые данные молча.
- Преобразовывать вложенные коллекции результата в неизменяемые структуры.

Подробности: [преобразование моделей](references/mapping_patterns.md).

### Идентификаторы, время и версия

- Получать доменный ID и значимое время уже назначенными вызывающим слоем.
- Не генерировать UUID/время в репозитории, не вызывать соответствующие порты и
  не подменять значения `NOW()`.
- DB-generated ключ или время допустимы только как технические детали, не
  пересекающие границу адаптера.
- Выбирать create/update и точки сохранения версии только по контракту.
- При optimistic locking включать ожидаемую сохранённую версию в `WHERE` и
  проверять число изменённых строк.
- Не выполнять скрытый retry, перечитывание или last-write-wins.

### Полиморфный outbox

- Реализовывать единый outbox только по заданному порту и схеме.
- Сохранять стабильный идентификатор события из outbox DTO; не генерировать и не
  заменять его в persistence-адаптере. Технический ключ строки outbox хранить
  отдельно, если он предусмотрен схемой.
- Сопоставлять конкретные DTO с заранее определёнными SQL-объектами и записями
  реестра таблиц; не принимать имя таблицы из данных и не использовать f-строки.
- Если схема хранит идентификатор агрегата и версию, создавать outbox-запись без
  предварительного чтения физического идентификатора версии.
- Выбирать ожидающую outbox-запись в заданном постоянном порядке, затем отдельным
  запросом получать конкретную версию. Два запроса допустимы; не объединять все
  таблицы громоздким SQL без измеренной необходимости.
- Реализовывать каждый терминальный переход статуса с условием на текущее
  состояние. Тот же исход делать идемпотентным, а смену терминального исхода —
  конфликтом, если это задано портом.
- Для batch-save группировать DTO по runtime-типу и выполнять set-based запрос на
  непустую группу, а не запрос на каждый элемент.

## Ошибки

- Использовать параметризованный `@handle_postgres_errors` на приватных
  DB-методах; сохранять sync/async-сигнатуру через `functools.wraps`.
- Перехватывать только ожидаемые ошибки psycopg на минимальной операции.
- Создавать `AppPortError` с безопасным контекстом, исходной причиной в
  `wrap_error` и цепочкой `raise ... from error`.
- Если порт различает конфликт или недоступность, передавать стабильную
  application-категорию в `data`; use case не анализирует тип psycopg.
- Известные constraints сопоставлять с категориями в приватной карте, не по
  тексту ошибки. Неизвестный constraint — общая ошибка порта.
- Не раскрывать SQLSTATE, constraint name, SQL, параметры, DSN и secrets.
- Не перехватывать `BaseException`, отмену задачи и ошибки программирования.
- `_model_to_domain` оставлять под отдельным `@handle_domain_errors`.
- Не логировать пробрасываемую или обработанную локальную ошибку. Возвращать
  результат либо пробрасывать типизированную ошибку с безопасным контекстом.

Подробности: [ошибки](references/errors_and_transactions.md).

## SQL и конкурентность

- Передавать данные только через placeholders.
- Собирать схемы, таблицы и колонки через `psycopg.sql.Identifier`.
- Типизировать статический литерал как `SQL`, произвольный безопасный SQL-фрагмент
  как `Composable`, а результат `SQL.format()`, `SQL.join()` и композиции — как
  `Composed`. Не объявлять результат форматирования типом `SQL`.
- Сопоставлять поля и направления сортировки с закрытым набором SQL-объектов.
- Хранить фактические имена таблиц в общем `StrEnum` persistence-модуля и
  сверять их с миграциями.
- Для пагинации использовать детерминированный `ORDER BY` с уникальным
  tie-breaker.
- Обязательный tenant/context scope включать в тот же `SELECT`/`UPDATE`/`DELETE`,
  что выполняет операцию.
- Не маскировать различимый конфликт через `ON CONFLICT DO NOTHING/DO UPDATE`.
- Не повторять отдельный statement после deadlock/serialization failure; при
  заданном retry повторять всю операцию с новым UoW.

Подробности: [безопасный SQL](references/sql_safety_patterns.md).

## Обязательное согласование миграции

До создания или изменения файла:

1. Перечислить создаваемые, изменяемые и удаляемые таблицы и их назначение.
2. Для каждой таблицы показать:

   | Столбец | Тип PostgreSQL | NULL | По умолчанию | Ограничения | Назначение |
   |---|---|---|---|---|---|

3. Для существующих столбцов показать:

   | Столбец | Текущее состояние | Новое состояние | Перенос данных | Риск |
   |---|---|---|---|---|

4. Перечислить ключи, связи, `ON DELETE`/`ON UPDATE`, `CHECK`, `UNIQUE` и другие
   ограничения.
5. Показать способ применения, отката, совместимость версий и риск блокировок.
6. Получить явное подтверждение схемы до редактирования файлов.

Для view, enum, sequence, function, trigger, extension и schema показывать
определение, назначение, зависимости, привилегии, влияние и откат.

### Индексы и уникальность

- Любой индекс создавать только с явного разрешения.
- Показать таблицу, колонки, метод, условие, ускоряемые запросы, стоимость записи,
  место и блокировки.
- Уникальный индекс и `UNIQUE` согласовывать отдельно с бизнес-правилом и
  стратегией обработки дубликатов.
- Для принятого `UNIQUE` задавать явное стабильное имя constraint-а через
  `CONSTRAINT <name> UNIQUE (...)`; не полагаться на имя, автоматически
  сформированное PostgreSQL. Создаваемый им constraint-backed индекс отдельно не
  дублировать явным уникальным индексом.
- Прямо заказанный индекс разрешён только в указанном составе.

### Существующие миграции

- Перед перезаписью перечислить точные файлы и спросить, какие разрешено менять.
- Уточнить, применялись ли они в общих или production-окружениях.
- Не менять файл без разрешения; при запрете создать корректирующую миграцию.
- Даже при разрешении предупредить о расхождении уже мигрировавших БД.

### Реестр физических таблиц

Если схема требует общий реестр таблиц:

- создавать реестр раньше зависимых объектов и, при полном составе, добавлять его
  собственную запись сразу после создания;
- в миграции создания каждой таблицы добавлять её точное физическое имя в реестр;
- в миграции переименования синхронно обновлять запись реестра;
- в rollback сначала устранять полиморфные ссылки и удалять запись реестра, затем
  удалять таблицу;
- при полном откате удалять реестр последним;
- проверять integration-тестом прямой и обратный lifecycle записи вместе с
  соответствующей таблицей.

### Разрушительные изменения

Отдельно подтверждать удаление объектов/данных, сужение типов, потенциально
необратимый rollback, изменение связей и несовместимое переименование. Показать
затрагиваемые данные, перенос/резервирование, совместимость и ограничения отката.

Подробности: [миграции yoyo](references/yoyo_migrations.md).

## Тестирование

- Unit-тестами проверять чистые mapper-ы, параметры, UoW lifecycle и
  классификацию ошибок.
- Для UoW использовать небольшие fake transaction/connection manager вместо
  хрупких цепочек `AsyncMock`.
- SQL и атомарность нескольких репозиториев проверять integration-тестами на
  PostgreSQL, не SQLite.
- Проверять отмену, cleanup и отсутствие утечки состояния, если этот код написан.
- Создавать domain-объекты только через фабрики.
- Объединять схожие случаи `parametrize` с `ids`.
- Применять миграции штатным yoyo, не исполнять прочитанный из файла SQL.
- Поднимать временную инфраструктуру автоматически и удалять после проверки.

## Антипаттерны

- Изменение application-порта под удобство SQL.
- Один UoW, сохранённый в повторно используемом use case.
- DB-типы за границей адаптера.
- Доменный ID или значимое время, созданные репозиторием.
- `commit`/`rollback` в репозитории либо двойной transaction manager.
- Однострочный `save → batch_save`.
- N+1 при наличии batch-контракта.
- Анализ psycopg-исключения в use case.
- SQL через f-строки, `SELECT *`, ручное экранирование.
- Аннотация `SQL` для результата `format()`, `join()` или другой композиции,
  фактически возвращающей `Composed`.
- Миграция, импортирующая runtime domain/application-код.
- Перезапись миграции, индекс или разрушительное изменение без разрешения.
- Создание миграции до демонстрации таблицы.
- `IF EXISTS`/`IF NOT EXISTS`, скрывающие неожиданную схему.

## Критерии готовности

- Порт реализован без раскрытия DB-деталей.
- Фабрика создаёт новый UoW на каждую попытку.
- UoW единолично управляет транзакцией и общим соединением репозиториев.
- Одиночные и пакетные операции не создают лишних запросов.
- Mapping создаёт domain через фабрику либо DTO порта.
- Ошибки преобразованы в `AppPortError`.
- SQL параметризован, идентификаторы безопасно скомпонованы.
- Scope, пагинация, кардинальность и optimistic locking соответствуют контракту.
- Схема и migration chain согласованы с кодом.
- Все необходимые разрешения пользователя получены.
- Миграция проверена через yoyo `apply → rollback → apply`.
- Unit- и integration-тесты прошли.

## Материалы

- [Сохранение](references/save_patterns.md)
- [Чтение и пагинация](references/read_patterns.md)
- [Преобразование моделей](references/mapping_patterns.md)
- [Ошибки](references/errors_and_transactions.md)
- [Единица работы](references/unit_of_work.md)
- [Менеджер подключений](references/connection_manager.md)
- [Безопасный SQL](references/sql_safety_patterns.md)
- [Миграции yoyo](references/yoyo_migrations.md)
- [Чеклист](references/persistence_checklist.md)

