diff --git a/tradein-mvp/backend/data/sql/193_tradein_users_seed.sql b/tradein-mvp/backend/data/sql/193_tradein_users_seed.sql index 6181b0bb..7eea9d95 100644 --- a/tradein-mvp/backend/data/sql/193_tradein_users_seed.sql +++ b/tradein-mvp/backend/data/sql/193_tradein_users_seed.sql @@ -22,8 +22,9 @@ -- -- WHAT: -- 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): -- admin role=admin, 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) — паттерн из -- 189_account_estimate_usage_nonnegative.sql. -- - INSERT ... ON CONFLICT (username) DO UPDATE, но НЕ безусловно: password_hash, --- is_active, display_name, org_name, email защищены COALESCE(текущее, EXCLUDED) — --- если админ уже проставил пароль / поменял is_active / display_name вручную через --- team-UI (#2556) между двумя прогонами этого файла (например ручной re-apply при --- recovery — обычный auto-apply тречит filename в _schema_migrations и не запускает --- файл дважды на одном окружении, но scratch/staging БД такого трекинга не имеют), --- повторный прогон НЕ должен затереть это состояние NULL-ом / seed-дефолтом. --- role/manager_id намеренно синкаются с EXCLUDED (не защищены) — это и есть источник --- истины org-карты из этой миграции; если владелец продукта поправит эту таблицу --- новой миграцией поверх, DO UPDATE-ветка должна донести исправление, а не заморозить --- первый прогон навсегда. +-- manager_id, display_name, org_name, email защищены COALESCE(текущее, EXCLUDED) — +-- если админ уже проставил пароль / назначил manager_id (team-API #2563 пишет +-- manager_id при создании сотрудника менеджером) / поменял display_name вручную +-- через team-UI (#2556) между двумя прогонами этого файла (например ручной re-apply +-- при recovery — обычный auto-apply тречит filename в _schema_migrations и не +-- запускает файл дважды на одном окружении, но scratch/staging БД такого +-- трекинга не имеют), повторный прогон НЕ должен затереть это состояние NULL-ом / +-- seed-дефолтом. Deep-review #2564 нашёл это живым багом: manager_id, назначенный +-- через #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). -- Deploy order: после 192 (Foundation). Backend auth-код (login/password-verify) и @@ -64,7 +76,7 @@ BEGIN; --- Часть 1: ASCII-CHECK — должен применяться ДО вставки seed-строк ниже. +-- Часть 1: ASCII-CHECK (immediate constraint — валидирует и вставляемые ниже строки). DO $$ BEGIN IF NOT EXISTS ( @@ -100,13 +112,16 @@ VALUES ('user9', NULL, 'employee', NULL, NULL, NULL, NULL, true), ('user10', NULL, 'employee', NULL, NULL, NULL, NULL, true) ON CONFLICT (username) DO UPDATE SET - role = EXCLUDED.role, - manager_id = EXCLUDED.manager_id, + role = EXCLUDED.role, + -- 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), - is_active = COALESCE(tradein_users.is_active, EXCLUDED.is_active), - display_name = COALESCE(tradein_users.display_name, EXCLUDED.display_name), - org_name = COALESCE(tradein_users.org_name, EXCLUDED.org_name), - email = COALESCE(tradein_users.email, EXCLUDED.email), - updated_at = now(); + display_name = COALESCE(tradein_users.display_name, EXCLUDED.display_name), + org_name = COALESCE(tradein_users.org_name, EXCLUDED.org_name), + email = COALESCE(tradein_users.email, EXCLUDED.email), + -- is_active НЕ в SET: NOT NULL DEFAULT true колонка, COALESCE был бы мёртвым кодом + -- (см. IDEMPOTENCY выше) — open/close доступа решается через UI (#2556), не seed'ом. + updated_at = now(); COMMIT;