Адаптеры хранения psycopg и миграции yoyo
Общие правила оформления Python-кода брать из $python-code-style-writing и не
дублировать здесь; этот скил определяет persistence-контракты и SQL-безопасность.
Порядок работы
- Извлечь точный контракт порта: методы, типы, отсутствие, конфликты,
атомарность, версию и согласованность чтения.
- Сверить таблицы, столбцы, типы, nullable и ограничения с миграциями.
- Реализовать connection manager, фабрику UoW, UoW и репозитории с одним
владельцем транзакции.
- Преобразовывать DB-данные только в тип результата порта.
- Оборачивать ожидаемые ошибки зависимости в
AppPortError.
- Для изменения схемы сначала показать пользователю таблицы и план миграции,
получить разрешения и только затем менять файлы.
- Проверить 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 добавлять только по требованиям.
Подробности: единица работы и
менеджер подключений.
Репозитории
Устройство
- Публичные методы точно реализуют порт и делегируют 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.
Подробности: сохранение и
чтение.
Преобразование
- Настраивать psycopg с
row_factory=dict_row, типизировать строки как
DictRow и читать колонки по именам.
- Агрегат или доменную проекцию восстанавливать в
_model_to_domain только через
доменную фабрику и проектный @handle_domain_errors.
- DTO порта создавать в
_model_to_dto.
- Parameter mapper-ы и DB-to-result mapper-ы делать чистыми и переиспользовать
между одиночной и пакетной операцией.
- Не менять приватные поля агрегата и не исправлять повреждённые данные молча.
- Преобразовывать вложенные коллекции результата в неизменяемые структуры.
Подробности: преобразование моделей.
Идентификаторы, время и версия
- Получать доменный 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.
- Не логировать пробрасываемую или обработанную локальную ошибку. Возвращать
результат либо пробрасывать типизированную ошибку с безопасным контекстом.
Подробности: ошибки.
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.
Обязательное согласование миграции
До создания или изменения файла:
Перечислить создаваемые, изменяемые и удаляемые таблицы и их назначение.
Для каждой таблицы показать:
| Столбец |
Тип PostgreSQL |
NULL |
По умолчанию |
Ограничения |
Назначение |
Для существующих столбцов показать:
| Столбец |
Текущее состояние |
Новое состояние |
Перенос данных |
Риск |
Перечислить ключи, связи, ON DELETE/ON UPDATE, CHECK, UNIQUE и другие
ограничения.
Показать способ применения, отката, совместимость версий и риск блокировок.
Получить явное подтверждение схемы до редактирования файлов.
Для view, enum, sequence, function, trigger, extension и schema показывать
определение, назначение, зависимости, привилегии, влияние и откат.
Индексы и уникальность
- Любой индекс создавать только с явного разрешения.
- Показать таблицу, колонки, метод, условие, ускоряемые запросы, стоимость записи,
место и блокировки.
- Уникальный индекс и
UNIQUE согласовывать отдельно с бизнес-правилом и
стратегией обработки дубликатов.
- Для принятого
UNIQUE задавать явное стабильное имя constraint-а через
CONSTRAINT <name> UNIQUE (...); не полагаться на имя, автоматически
сформированное PostgreSQL. Создаваемый им constraint-backed индекс отдельно не
дублировать явным уникальным индексом.
- Прямо заказанный индекс разрешён только в указанном составе.
Существующие миграции
- Перед перезаписью перечислить точные файлы и спросить, какие разрешено менять.
- Уточнить, применялись ли они в общих или production-окружениях.
- Не менять файл без разрешения; при запрете создать корректирующую миграцию.
- Даже при разрешении предупредить о расхождении уже мигрировавших БД.
Реестр физических таблиц
Если схема требует общий реестр таблиц:
- создавать реестр раньше зависимых объектов и, при полном составе, добавлять его
собственную запись сразу после создания;
- в миграции создания каждой таблицы добавлять её точное физическое имя в реестр;
- в миграции переименования синхронно обновлять запись реестра;
- в rollback сначала устранять полиморфные ссылки и удалять запись реестра, затем
удалять таблицу;
- при полном откате удалять реестр последним;
- проверять integration-тестом прямой и обратный lifecycle записи вместе с
соответствующей таблицей.
Разрушительные изменения
Отдельно подтверждать удаление объектов/данных, сужение типов, потенциально
необратимый rollback, изменение связей и несовместимое переименование. Показать
затрагиваемые данные, перенос/резервирование, совместимость и ограничения отката.
Подробности: миграции yoyo.
Тестирование
- 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-тесты прошли.
Материалы
- Сохранение
- Чтение и пагинация
- Преобразование моделей
- Ошибки
- Единица работы
- Менеджер подключений
- Безопасный SQL
- Миграции yoyo
- Чеклист
1---2name: python-psycopg-yoyo-persistence-writing3description: Используй при реализации или ревью PostgreSQL persistence-адаптеров на Python через psycopg и миграций через yoyo. Триггеры — репозитории, mapping строк БД в domain или DTO порта, одиночное и пакетное чтение/сохранение, optimistic locking, PostgreSQL Unit of Work и его фабрика, connection manager и pool, безопасный SQL, создание и изменение таблиц, ограничений, индексов и других объектов схемы миграциями yoyo. Не применять для определения application-портов, доменных правил, публичных API-контрактов и хранилищ на других технологиях.4---56# Адаптеры хранения psycopg и миграции yoyo78Общие правила оформления Python-кода брать из `$python-code-style-writing` и не9дублировать здесь; этот скил определяет persistence-контракты и SQL-безопасность.1011## Порядок работы12131. Извлечь точный контракт порта: методы, типы, отсутствие, конфликты,14 атомарность, версию и согласованность чтения.152. Сверить таблицы, столбцы, типы, nullable и ограничения с миграциями.163. Реализовать connection manager, фабрику UoW, UoW и репозитории с одним17 владельцем транзакции.184. Преобразовывать DB-данные только в тип результата порта.195. Оборачивать ожидаемые ошибки зависимости в `AppPortError`.206. Для изменения схемы сначала показать пользователю таблицы и план миграции,21 получить разрешения и только затем менять файлы.227. Проверить mapping unit-тестами, SQL — на PostgreSQL, миграции —23 `apply → rollback → apply` через yoyo.2425## Граница ответственности2627- Реализовывать существующий application-порт без изменения публичного контракта28 под удобство PostgreSQL.29- Не возвращать `DictRow`, cursor, connection, SQL-модель или тип psycopg наружу.30- Не определять доменные инварианты и публичную семантику исходов.31- При недостаточном или противоречивом контракте остановиться и назвать32 конкретное противоречие.3334## Транзакционная модель3536- Connection manager владеет pool и арендой соединений.37- `UnitOfWorkFactory` — долгоживущий stateless-адаптер; каждый вызов возвращает38 новый UoW.39- UoW получает соединение при входе, открывает явный40 `connection.transaction()` и предоставляет репозиториям одно соединение.41- UoW единолично определяет commit/rollback. Connection manager и репозитории не42 придают выходу прикладную семантику.43- Не использовать `pool.connection()` внутри UoW как второй автоматический44 transaction manager: арендовать через `getconn`/`putconn` или эквивалентный45 lease без скрытого commit.46- Один UoW нельзя повторно или конкурентно использовать. После выхода очищать47 connection, transaction context и repository-группы.48- Retry всей операции создаёт новый UoW; отдельный SQL внутри сломанной49 транзакции не повторять.50- Isolation level, read-only, savepoints и retry добавлять только по требованиям.5152Подробности: [единица работы](references/unit_of_work.md) и53[менеджер подключений](references/connection_manager.md).5455## Репозитории5657### Устройство5859- Публичные методы точно реализуют порт и делегируют DB-операции приватным.60- Базовый репозиторий хранит только connection, общий enum таблиц и действительно61 общие технические helpers. Не создавать универсальный CRUD.62- Статический SQL держать рядом с репозиторием; не строить собственный ORM.63- Использовать явный список колонок вместо `SELECT *`.64- Каждый приватный DB-метод закрывает cursor контекстным менеджером и65 преобразует результат до выхода.6667### Сохранение и чтение6869- Одиночный `save` делегирует в `_create`/`_update` с обычным `execute`; не70 оборачивать объект в `batch_save`.71- `batch_save` заранее преобразует всю пачку и использует `executemany`, не72 выполняя SQL в цикле.73- Batch-чтение реализовывать set-based запросом; пустой набор завершать без SQL.74- Connection-bound репозитории UoW не получают новое соединение самостоятельно.75- Самостоятельный read-адаптер арендует соединение на один публичный вызов и не76 хранит его между вызовами.77- Не объединять оба режима через поле `connection | manager`.7879Подробности: [сохранение](references/save_patterns.md) и80[чтение](references/read_patterns.md).8182### Преобразование8384- Настраивать psycopg с `row_factory=dict_row`, типизировать строки как85 `DictRow` и читать колонки по именам.86- Агрегат или доменную проекцию восстанавливать в `_model_to_domain` только через87 доменную фабрику и проектный `@handle_domain_errors`.88- DTO порта создавать в `_model_to_dto`.89- Parameter mapper-ы и DB-to-result mapper-ы делать чистыми и переиспользовать90 между одиночной и пакетной операцией.91- Не менять приватные поля агрегата и не исправлять повреждённые данные молча.92- Преобразовывать вложенные коллекции результата в неизменяемые структуры.9394Подробности: [преобразование моделей](references/mapping_patterns.md).9596### Идентификаторы, время и версия9798- Получать доменный ID и значимое время уже назначенными вызывающим слоем.99- Не генерировать UUID/время в репозитории, не вызывать соответствующие порты и100 не подменять значения `NOW()`.101- DB-generated ключ или время допустимы только как технические детали, не102 пересекающие границу адаптера.103- Выбирать create/update и точки сохранения версии только по контракту.104- При optimistic locking включать ожидаемую сохранённую версию в `WHERE` и105 проверять число изменённых строк.106- Не выполнять скрытый retry, перечитывание или last-write-wins.107108### Полиморфный outbox109110- Реализовывать единый outbox только по заданному порту и схеме.111- Сохранять стабильный идентификатор события из outbox DTO; не генерировать и не112 заменять его в persistence-адаптере. Технический ключ строки outbox хранить113 отдельно, если он предусмотрен схемой.114- Сопоставлять конкретные DTO с заранее определёнными SQL-объектами и записями115 реестра таблиц; не принимать имя таблицы из данных и не использовать f-строки.116- Если схема хранит идентификатор агрегата и версию, создавать outbox-запись без117 предварительного чтения физического идентификатора версии.118- Выбирать ожидающую outbox-запись в заданном постоянном порядке, затем отдельным119 запросом получать конкретную версию. Два запроса допустимы; не объединять все120 таблицы громоздким SQL без измеренной необходимости.121- Реализовывать каждый терминальный переход статуса с условием на текущее122 состояние. Тот же исход делать идемпотентным, а смену терминального исхода —123 конфликтом, если это задано портом.124- Для batch-save группировать DTO по runtime-типу и выполнять set-based запрос на125 непустую группу, а не запрос на каждый элемент.126127## Ошибки128129- Использовать параметризованный `@handle_postgres_errors` на приватных130 DB-методах; сохранять sync/async-сигнатуру через `functools.wraps`.131- Перехватывать только ожидаемые ошибки psycopg на минимальной операции.132- Создавать `AppPortError` с безопасным контекстом, исходной причиной в133 `wrap_error` и цепочкой `raise ... from error`.134- Если порт различает конфликт или недоступность, передавать стабильную135 application-категорию в `data`; use case не анализирует тип psycopg.136- Известные constraints сопоставлять с категориями в приватной карте, не по137 тексту ошибки. Неизвестный constraint — общая ошибка порта.138- Не раскрывать SQLSTATE, constraint name, SQL, параметры, DSN и secrets.139- Не перехватывать `BaseException`, отмену задачи и ошибки программирования.140- `_model_to_domain` оставлять под отдельным `@handle_domain_errors`.141- Не логировать пробрасываемую или обработанную локальную ошибку. Возвращать142 результат либо пробрасывать типизированную ошибку с безопасным контекстом.143144Подробности: [ошибки](references/errors_and_transactions.md).145146## SQL и конкурентность147148- Передавать данные только через placeholders.149- Собирать схемы, таблицы и колонки через `psycopg.sql.Identifier`.150- Типизировать статический литерал как `SQL`, произвольный безопасный SQL-фрагмент151 как `Composable`, а результат `SQL.format()`, `SQL.join()` и композиции — как152 `Composed`. Не объявлять результат форматирования типом `SQL`.153- Сопоставлять поля и направления сортировки с закрытым набором SQL-объектов.154- Хранить фактические имена таблиц в общем `StrEnum` persistence-модуля и155 сверять их с миграциями.156- Для пагинации использовать детерминированный `ORDER BY` с уникальным157 tie-breaker.158- Обязательный tenant/context scope включать в тот же `SELECT`/`UPDATE`/`DELETE`,159 что выполняет операцию.160- Не маскировать различимый конфликт через `ON CONFLICT DO NOTHING/DO UPDATE`.161- Не повторять отдельный statement после deadlock/serialization failure; при162 заданном retry повторять всю операцию с новым UoW.163164Подробности: [безопасный SQL](references/sql_safety_patterns.md).165166## Обязательное согласование миграции167168До создания или изменения файла:1691701. Перечислить создаваемые, изменяемые и удаляемые таблицы и их назначение.1712. Для каждой таблицы показать:172173 | Столбец | Тип PostgreSQL | NULL | По умолчанию | Ограничения | Назначение |174 |---|---|---|---|---|---|1751763. Для существующих столбцов показать:177178 | Столбец | Текущее состояние | Новое состояние | Перенос данных | Риск |179 |---|---|---|---|---|1801814. Перечислить ключи, связи, `ON DELETE`/`ON UPDATE`, `CHECK`, `UNIQUE` и другие182 ограничения.1835. Показать способ применения, отката, совместимость версий и риск блокировок.1846. Получить явное подтверждение схемы до редактирования файлов.185186Для view, enum, sequence, function, trigger, extension и schema показывать187определение, назначение, зависимости, привилегии, влияние и откат.188189### Индексы и уникальность190191- Любой индекс создавать только с явного разрешения.192- Показать таблицу, колонки, метод, условие, ускоряемые запросы, стоимость записи,193 место и блокировки.194- Уникальный индекс и `UNIQUE` согласовывать отдельно с бизнес-правилом и195 стратегией обработки дубликатов.196- Для принятого `UNIQUE` задавать явное стабильное имя constraint-а через197 `CONSTRAINT <name> UNIQUE (...)`; не полагаться на имя, автоматически198 сформированное PostgreSQL. Создаваемый им constraint-backed индекс отдельно не199 дублировать явным уникальным индексом.200- Прямо заказанный индекс разрешён только в указанном составе.201202### Существующие миграции203204- Перед перезаписью перечислить точные файлы и спросить, какие разрешено менять.205- Уточнить, применялись ли они в общих или production-окружениях.206- Не менять файл без разрешения; при запрете создать корректирующую миграцию.207- Даже при разрешении предупредить о расхождении уже мигрировавших БД.208209### Реестр физических таблиц210211Если схема требует общий реестр таблиц:212213- создавать реестр раньше зависимых объектов и, при полном составе, добавлять его214 собственную запись сразу после создания;215- в миграции создания каждой таблицы добавлять её точное физическое имя в реестр;216- в миграции переименования синхронно обновлять запись реестра;217- в rollback сначала устранять полиморфные ссылки и удалять запись реестра, затем218 удалять таблицу;219- при полном откате удалять реестр последним;220- проверять integration-тестом прямой и обратный lifecycle записи вместе с221 соответствующей таблицей.222223### Разрушительные изменения224225Отдельно подтверждать удаление объектов/данных, сужение типов, потенциально226необратимый rollback, изменение связей и несовместимое переименование. Показать227затрагиваемые данные, перенос/резервирование, совместимость и ограничения отката.228229Подробности: [миграции yoyo](references/yoyo_migrations.md).230231## Тестирование232233- Unit-тестами проверять чистые mapper-ы, параметры, UoW lifecycle и234 классификацию ошибок.235- Для UoW использовать небольшие fake transaction/connection manager вместо236 хрупких цепочек `AsyncMock`.237- SQL и атомарность нескольких репозиториев проверять integration-тестами на238 PostgreSQL, не SQLite.239- Проверять отмену, cleanup и отсутствие утечки состояния, если этот код написан.240- Создавать domain-объекты только через фабрики.241- Объединять схожие случаи `parametrize` с `ids`.242- Применять миграции штатным yoyo, не исполнять прочитанный из файла SQL.243- Поднимать временную инфраструктуру автоматически и удалять после проверки.244245## Антипаттерны246247- Изменение application-порта под удобство SQL.248- Один UoW, сохранённый в повторно используемом use case.249- DB-типы за границей адаптера.250- Доменный ID или значимое время, созданные репозиторием.251- `commit`/`rollback` в репозитории либо двойной transaction manager.252- Однострочный `save → batch_save`.253- N+1 при наличии batch-контракта.254- Анализ psycopg-исключения в use case.255- SQL через f-строки, `SELECT *`, ручное экранирование.256- Аннотация `SQL` для результата `format()`, `join()` или другой композиции,257 фактически возвращающей `Composed`.258- Миграция, импортирующая runtime domain/application-код.259- Перезапись миграции, индекс или разрушительное изменение без разрешения.260- Создание миграции до демонстрации таблицы.261- `IF EXISTS`/`IF NOT EXISTS`, скрывающие неожиданную схему.262263## Критерии готовности264265- Порт реализован без раскрытия DB-деталей.266- Фабрика создаёт новый UoW на каждую попытку.267- UoW единолично управляет транзакцией и общим соединением репозиториев.268- Одиночные и пакетные операции не создают лишних запросов.269- Mapping создаёт domain через фабрику либо DTO порта.270- Ошибки преобразованы в `AppPortError`.271- SQL параметризован, идентификаторы безопасно скомпонованы.272- Scope, пагинация, кардинальность и optimistic locking соответствуют контракту.273- Схема и migration chain согласованы с кодом.274- Все необходимые разрешения пользователя получены.275- Миграция проверена через yoyo `apply → rollback → apply`.276- Unit- и integration-тесты прошли.277278## Материалы279280- [Сохранение](references/save_patterns.md)281- [Чтение и пагинация](references/read_patterns.md)282- [Преобразование моделей](references/mapping_patterns.md)283- [Ошибки](references/errors_and_transactions.md)284- [Единица работы](references/unit_of_work.md)285- [Менеджер подключений](references/connection_manager.md)286- [Безопасный SQL](references/sql_safety_patterns.md)287- [Миграции yoyo](references/yoyo_migrations.md)288- [Чеклист](references/persistence_checklist.md)