Files
agenciapsilmno/database-novo/migrations/20260612000001_f1_tenants_slug.sql
T
Leonardo 05c6746e33 schema-per-tenant: F0 categorizacao + F1 template/helpers + F2 provisionamento
- docs/F0_categorizacao.md: varredura completa (137 tabelas -> 84 tenant + 53
  global, 66 funcoes, FKs, policies, edge functions) + decisoes Q1-Q4
- F1 (migrations 01-05): tenants.slug, helpers de schema, _tenant_template
  (84 tabelas sem tenant_id, singletons, views __SCHEMA__/__TENANT_ID__),
  clone_tenant_template/drop_tenant_schema, channel_routing, tenant_schemas
- F2 (migration 06): provision_account_tenant/create_clinic_tenant/
  ensure_personal_tenant_for_user clonam schema na mesma transacao
- db.cjs: psqlFile agora usa ON_ERROR_STOP=1 (falha de migration nao passa
  mais como sucesso silencioso)
- blueprint original em novo-rumo.txt; wiki Obsidian atualizada

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 11:58:46 -03:00

88 lines
3.0 KiB
PL/PgSQL

-- =============================================================================
-- F1.1 — Schema-per-tenant: coluna tenants.slug
-- Plano: novo-rumo.txt + docs/F0_categorizacao.md (decisão Q1)
-- Slug é a base do nome do schema físico: tenant_<slug>. Unico e imutável.
-- =============================================================================
BEGIN;
ALTER TABLE public.tenants ADD COLUMN IF NOT EXISTS slug text;
-- Geração de slug a partir do nome (sanitizado pra identificador Postgres)
CREATE OR REPLACE FUNCTION public.generate_tenant_slug(p_name text)
RETURNS text
LANGUAGE plpgsql
STABLE
SECURITY DEFINER
SET search_path TO 'public', 'pg_temp'
AS $$
DECLARE
base text;
cand text;
n int := 1;
BEGIN
base := lower(coalesce(nullif(trim(p_name), ''), 'tenant'));
base := translate(base,
'áàâãäåéèêëíìîïóòôõöúùûüçñýÿ',
'aaaaaaeeeeiiiiooooouuuucnyy');
base := regexp_replace(base, '[^a-z0-9]+', '_', 'g');
base := regexp_replace(base, '^_+|_+$', '', 'g');
base := left(base, 48);
IF base = '' OR base !~ '^[a-z]' THEN
base := 't_' || base;
base := left(base, 48);
END IF;
cand := base;
WHILE EXISTS (SELECT 1 FROM public.tenants WHERE slug = cand) LOOP
n := n + 1;
cand := left(base, 44) || '_' || n;
END LOOP;
RETURN cand;
END;
$$;
-- Backfill dos tenants existentes
DO $$
DECLARE r record;
BEGIN
FOR r IN SELECT id, name FROM public.tenants WHERE slug IS NULL ORDER BY created_at, id LOOP
UPDATE public.tenants SET slug = public.generate_tenant_slug(r.name) WHERE id = r.id;
RAISE NOTICE 'tenant % -> slug %', r.id, (SELECT slug FROM public.tenants WHERE id = r.id);
END LOOP;
END $$;
ALTER TABLE public.tenants ALTER COLUMN slug SET NOT NULL;
ALTER TABLE public.tenants ADD CONSTRAINT tenants_slug_key UNIQUE (slug);
ALTER TABLE public.tenants ADD CONSTRAINT tenants_slug_format CHECK (slug ~ '^[a-z][a-z0-9_]{1,47}$');
-- Auto-gera no INSERT (provisionamento atual não conhece slug); imutável no UPDATE
CREATE OR REPLACE FUNCTION public.trg_tenants_slug()
RETURNS trigger
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path TO 'public', 'pg_temp'
AS $$
BEGIN
IF TG_OP = 'INSERT' THEN
IF NEW.slug IS NULL OR trim(NEW.slug) = '' THEN
NEW.slug := public.generate_tenant_slug(NEW.name);
END IF;
RETURN NEW;
END IF;
IF NEW.slug IS DISTINCT FROM OLD.slug THEN
RAISE EXCEPTION 'tenants.slug é imutável (tenant %, % -> %)', OLD.id, OLD.slug, NEW.slug;
END IF;
RETURN NEW;
END;
$$;
DROP TRIGGER IF EXISTS trg_tenants_slug_ins ON public.tenants;
CREATE TRIGGER trg_tenants_slug_ins BEFORE INSERT ON public.tenants
FOR EACH ROW EXECUTE FUNCTION public.trg_tenants_slug();
DROP TRIGGER IF EXISTS trg_tenants_slug_upd ON public.tenants;
CREATE TRIGGER trg_tenants_slug_upd BEFORE UPDATE OF slug ON public.tenants
FOR EACH ROW EXECUTE FUNCTION public.trg_tenants_slug();
COMMIT;