fix(tradein/db): не затирать manager_id при повторном прогоне сида (#2557)
All checks were successful
CI / changes (pull_request) Successful in 12s
CI Trade-In / changes (pull_request) Successful in 12s
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / backend-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
CI Trade-In / backend-tests (pull_request) Successful in 2m0s

Deep-review #2564: manager_id синкался из EXCLUDED безусловно — повторный прогон сида
тихо обнулял связь сотрудник->менеджер, назначенную через team-API (#2563), сотрудник
выпадал из _LIST_EMPLOYEES_BY_MANAGER_SQL. Защищён COALESCE, как остальные UI-managed
поля.

is_active убран из ON CONFLICT DO UPDATE SET вовсе (не COALESCE — колонка NOT NULL
DEFAULT true делала бы COALESCE-ветку недостижимой, мёртвый код вводил в заблуждение
симметрией с реально работающими COALESCE-полями). Open/close доступа — решение
владельца продукта через UI (#2556), не повторный прогон seed-файла.
This commit is contained in:
bot-backend 2026-07-30 22:08:14 +03:00
parent e7a658c7fa
commit f915e1ee4d

View file

@ -22,8 +22,9 @@
-- --
-- WHAT: -- WHAT:
-- 1. ASCII-CHECK: tradein_users_username_ascii_ck CHECK (username ~ '^[A-Za-z0-9._-]{3,64}$'). -- 1. ASCII-CHECK: tradein_users_username_ascii_ck CHECK (username ~ '^[A-Za-z0-9._-]{3,64}$').
-- Добавлен ДО seed-инсертов (порядок важен: constraint должен успеть провалидировать -- Добавлен ДО seed-инсертов ниже для читаемости файла (CHECK — immediate constraint,
-- вставляемые строки, а не просто существовать после них). -- Postgres валидирует им и ROW-строки транзакции независимо от того, в каком месте
-- файла он объявлен относительно INSERT, так что порядок сам по себе не критичен).
-- 2. Seed — org-карта, утверждённая владельцем продукта (2026-07-30): -- 2. Seed — org-карта, утверждённая владельцем продукта (2026-07-30):
-- admin role=admin, manager_id=NULL, is_active=true (владелец) -- admin role=admin, manager_id=NULL, is_active=true (владелец)
-- kopylov role=manager, manager_id=NULL, is_active=true (отдельный клиент) -- kopylov role=manager, manager_id=NULL, is_active=true (отдельный клиент)
@ -46,16 +47,27 @@
-- `ADD CONSTRAINT IF NOT EXISTS` для CHECK) — паттерн из -- `ADD CONSTRAINT IF NOT EXISTS` для CHECK) — паттерн из
-- 189_account_estimate_usage_nonnegative.sql. -- 189_account_estimate_usage_nonnegative.sql.
-- - INSERT ... ON CONFLICT (username) DO UPDATE, но НЕ безусловно: password_hash, -- - INSERT ... ON CONFLICT (username) DO UPDATE, но НЕ безусловно: password_hash,
-- is_active, display_name, org_name, email защищены COALESCE(текущее, EXCLUDED) — -- manager_id, display_name, org_name, email защищены COALESCE(текущее, EXCLUDED) —
-- если админ уже проставил пароль / поменял is_active / display_name вручную через -- если админ уже проставил пароль / назначил manager_id (team-API #2563 пишет
-- team-UI (#2556) между двумя прогонами этого файла (например ручной re-apply при -- manager_id при создании сотрудника менеджером) / поменял display_name вручную
-- recovery — обычный auto-apply тречит filename в _schema_migrations и не запускает -- через team-UI (#2556) между двумя прогонами этого файла (например ручной re-apply
-- файл дважды на одном окружении, но scratch/staging БД такого трекинга не имеют), -- при recovery — обычный auto-apply тречит filename в _schema_migrations и не
-- повторный прогон НЕ должен затереть это состояние NULL-ом / seed-дефолтом. -- запускает файл дважды на одном окружении, но scratch/staging БД такого
-- role/manager_id намеренно синкаются с EXCLUDED (не защищены) — это и есть источник -- трекинга не имеют), повторный прогон НЕ должен затереть это состояние NULL-ом /
-- истины org-карты из этой миграции; если владелец продукта поправит эту таблицу -- seed-дефолтом. Deep-review #2564 нашёл это живым багом: manager_id, назначенный
-- новой миграцией поверх, DO UPDATE-ветка должна донести исправление, а не заморозить -- через #2563, тихо обнулялся повторным прогоном сида — employee выпадал из
-- первый прогон навсегда. -- `_LIST_EMPLOYEES_BY_MANAGER_SQL`, менеджер переставал видеть его в дашборде.
-- role намеренно синкается с EXCLUDED (не защищён) — это и есть источник истины
-- org-карты из этой миграции; если владелец продукта поправит эту таблицу новой
-- миграцией поверх, DO UPDATE-ветка должна донести исправление роли, а не
-- заморозить первый прогон навсегда.
-- - is_active НАМЕРЕННО отсутствует в SET (не COALESCE — колонка NOT NULL DEFAULT
-- true, COALESCE(NOT NULL, x) никогда не берёт x, это была бы мёртвая, вводящая в
-- заблуждение симметрия с password_hash/manager_id, deep-review #2564 medium).
-- Открытие/закрытие доступа (is_active) — решение владельца продукта, принимается
-- через UI (#2556), НЕ повторным прогоном этого seed-файла: после первой вставки
-- колонка сознательно «замораживается» на текущем значении в БД, seed её больше
-- не трогает.
-- --
-- Dependencies: 192_tradein_users_auth.sql (создаёт tradein_users, tradein_sessions). -- Dependencies: 192_tradein_users_auth.sql (создаёт tradein_users, tradein_sessions).
-- Deploy order: после 192 (Foundation). Backend auth-код (login/password-verify) и -- Deploy order: после 192 (Foundation). Backend auth-код (login/password-verify) и
@ -64,7 +76,7 @@
BEGIN; BEGIN;
-- Часть 1: ASCII-CHECK — должен применяться ДО вставки seed-строк ниже. -- Часть 1: ASCII-CHECK (immediate constraint — валидирует и вставляемые ниже строки).
DO $$ DO $$
BEGIN BEGIN
IF NOT EXISTS ( IF NOT EXISTS (
@ -100,13 +112,16 @@ VALUES
('user9', NULL, 'employee', NULL, NULL, NULL, NULL, true), ('user9', NULL, 'employee', NULL, NULL, NULL, NULL, true),
('user10', NULL, 'employee', NULL, NULL, NULL, NULL, true) ('user10', NULL, 'employee', NULL, NULL, NULL, NULL, true)
ON CONFLICT (username) DO UPDATE SET ON CONFLICT (username) DO UPDATE SET
role = EXCLUDED.role, role = EXCLUDED.role,
manager_id = EXCLUDED.manager_id, -- manager_id защищён COALESCE: team-API (#2563) пишет manager_id при назначении
-- сотрудника менеджером, повторный прогон seed'а не должен тихо обнулять эту связь.
manager_id = COALESCE(tradein_users.manager_id, EXCLUDED.manager_id),
password_hash = COALESCE(tradein_users.password_hash, EXCLUDED.password_hash), password_hash = COALESCE(tradein_users.password_hash, EXCLUDED.password_hash),
is_active = COALESCE(tradein_users.is_active, EXCLUDED.is_active), display_name = COALESCE(tradein_users.display_name, EXCLUDED.display_name),
display_name = COALESCE(tradein_users.display_name, EXCLUDED.display_name), org_name = COALESCE(tradein_users.org_name, EXCLUDED.org_name),
org_name = COALESCE(tradein_users.org_name, EXCLUDED.org_name), email = COALESCE(tradein_users.email, EXCLUDED.email),
email = COALESCE(tradein_users.email, EXCLUDED.email), -- is_active НЕ в SET: NOT NULL DEFAULT true колонка, COALESCE был бы мёртвым кодом
updated_at = now(); -- (см. IDEMPOTENCY выше) — open/close доступа решается через UI (#2556), не seed'ом.
updated_at = now();
COMMIT; COMMIT;