F6.0+F6.1 schema-per-tenant: clones dos 9 tenants + migracao de dados
- F6.0 (migration): clone_tenant_template pros 9 tenants existentes (schemas vazios; dispara trigger F5 -> expostos no PostgREST) - F6.1 (manual supabase_admin): copia dados public -> schemas com session_replication_role=replica (desabilita FK check, so supabase_admin). Tabelas com tenant_id por filtro; 3 filhas sem tenant_id (commitment_services, insurance_plan_services, recurrence_rule_services) por JOIN no pai; exclui colunas GENERATED (net_amount, margin_brl); reset de 4 sequences; ON CONFLICT DO NOTHING (idempotente). Dados continuam em public (DROP so na F6.3) - Verificado: contagens public vs schemas batem (35 patients, 37 eventos, 355 mensagens, 54 financeiro...); seeds default replicados por schema Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,140 @@
|
||||
-- =============================================================================
|
||||
-- F6.1 — Migração de DADOS public -> schemas tenant (cutover)
|
||||
--
|
||||
-- ⚠️ APLICAR COMO supabase_admin (precisa SET session_replication_role=replica
|
||||
-- pra desabilitar checagem de FK durante o bulk insert — postgres não pode).
|
||||
--
|
||||
-- docker exec -i -e PGPASSWORD=postgres supabase_db_agenciapsi-primesakai \
|
||||
-- psql -U supabase_admin -h 127.0.0.1 -d postgres -v ON_ERROR_STOP=1 \
|
||||
-- < database-novo/manual/f6_1_migrate_data.supabase_admin.sql
|
||||
--
|
||||
-- COPIA (não move) os dados de cada tenant pras suas tabelas no schema. Os
|
||||
-- dados continuam em public até o DROP da F6.3. Idempotente via ON CONFLICT
|
||||
-- DO NOTHING (rodar de novo não duplica).
|
||||
--
|
||||
-- * tabelas com tenant_id: INSERT ... SELECT WHERE tenant_id = <id>, sem a
|
||||
-- coluna tenant_id (não existe no schema)
|
||||
-- * 3 filhas sem tenant_id (commitment_services, insurance_plan_services,
|
||||
-- recurrence_rule_services): particionadas via JOIN no pai
|
||||
-- * financial_categories / therapist_payout_records: 0 linhas, ignoradas
|
||||
-- * as 6 tabelas anon-facing (F1b) NÃO existem no schema → naturalmente fora
|
||||
-- * reset de sequences (4 tabelas bigserial) ao final
|
||||
-- =============================================================================
|
||||
|
||||
SET session_replication_role = replica;
|
||||
|
||||
DO $$
|
||||
DECLARE
|
||||
t_row record;
|
||||
tab record;
|
||||
v_cols text;
|
||||
v_n bigint;
|
||||
-- filhas sem tenant_id: tabela -> (pai, fk_local, pk_pai)
|
||||
child_joins jsonb := jsonb_build_object(
|
||||
'commitment_services', jsonb_build_object('parent','agenda_eventos','fk','commitment_id'),
|
||||
'insurance_plan_services', jsonb_build_object('parent','insurance_plans','fk','insurance_plan_id'),
|
||||
'recurrence_rule_services', jsonb_build_object('parent','recurrence_rules','fk','rule_id')
|
||||
);
|
||||
cj jsonb;
|
||||
BEGIN
|
||||
FOR t_row IN
|
||||
SELECT t.id AS tenant_id, ts.schema_name
|
||||
FROM public.tenants t
|
||||
JOIN public.tenant_schemas ts ON ts.tenant_id = t.id
|
||||
ORDER BY t.created_at, t.id
|
||||
LOOP
|
||||
FOR tab IN
|
||||
SELECT c.relname AS table_name
|
||||
FROM pg_class c
|
||||
WHERE c.relnamespace = t_row.schema_name::regnamespace
|
||||
AND c.relkind = 'r'
|
||||
AND c.relname NOT LIKE '\_%'
|
||||
ORDER BY c.relname
|
||||
LOOP
|
||||
-- pula se a tabela não existe em public (defensivo)
|
||||
IF NOT EXISTS (SELECT 1 FROM information_schema.tables
|
||||
WHERE table_schema='public' AND table_name=tab.table_name) THEN
|
||||
CONTINUE;
|
||||
END IF;
|
||||
|
||||
-- colunas presentes em AMBOS (schema e public): exclui tenant_id
|
||||
-- (some no schema), singleton (só no schema, fica no default) e
|
||||
-- colunas GENERATED (net_amount, margin_brl — não aceitam INSERT)
|
||||
SELECT string_agg(quote_ident(sc.column_name), ', ' ORDER BY sc.ordinal_position)
|
||||
INTO v_cols
|
||||
FROM information_schema.columns sc
|
||||
WHERE sc.table_schema = t_row.schema_name AND sc.table_name = tab.table_name
|
||||
AND sc.is_generated = 'NEVER'
|
||||
AND EXISTS (SELECT 1 FROM information_schema.columns pc
|
||||
WHERE pc.table_schema='public' AND pc.table_name=tab.table_name
|
||||
AND pc.column_name = sc.column_name
|
||||
AND pc.is_generated = 'NEVER');
|
||||
|
||||
IF v_cols IS NULL THEN CONTINUE; END IF;
|
||||
|
||||
cj := child_joins -> tab.table_name;
|
||||
|
||||
IF cj IS NOT NULL THEN
|
||||
-- filha sem tenant_id: particiona via JOIN no pai
|
||||
EXECUTE format(
|
||||
'INSERT INTO %I.%I (%s) SELECT %s FROM public.%I ch '
|
||||
|| 'JOIN public.%I p ON p.id = ch.%I WHERE p.tenant_id = %L '
|
||||
|| 'ON CONFLICT DO NOTHING',
|
||||
t_row.schema_name, tab.table_name, v_cols,
|
||||
(SELECT string_agg('ch.'||quote_ident(x), ', ' ORDER BY ord)
|
||||
FROM (SELECT trim(both ' ' from unnest(string_to_array(v_cols, ','))) AS x,
|
||||
generate_subscripts(string_to_array(v_cols, ','),1) AS ord) y),
|
||||
tab.table_name,
|
||||
(cj->>'parent'), (cj->>'fk'),
|
||||
t_row.tenant_id
|
||||
);
|
||||
ELSIF EXISTS (SELECT 1 FROM information_schema.columns
|
||||
WHERE table_schema='public' AND table_name=tab.table_name AND column_name='tenant_id') THEN
|
||||
-- tabela com tenant_id: filtro direto
|
||||
EXECUTE format(
|
||||
'INSERT INTO %I.%I (%s) SELECT %s FROM public.%I WHERE tenant_id = %L ON CONFLICT DO NOTHING',
|
||||
t_row.schema_name, tab.table_name, v_cols, v_cols, tab.table_name, t_row.tenant_id
|
||||
);
|
||||
ELSE
|
||||
-- sem tenant_id e não é filha mapeada (financial_categories etc.):
|
||||
-- só migra se tiver 0 dependência de tenant — pula (vazias hoje)
|
||||
CONTINUE;
|
||||
END IF;
|
||||
|
||||
GET DIAGNOSTICS v_n = ROW_COUNT;
|
||||
IF v_n > 0 THEN
|
||||
RAISE NOTICE 'F6.1 %.%: % linhas', t_row.schema_name, tab.table_name, v_n;
|
||||
END IF;
|
||||
END LOOP;
|
||||
END LOOP;
|
||||
END $$;
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Reset de sequences (tabelas bigserial) em cada schema
|
||||
-- ---------------------------------------------------------------------------
|
||||
DO $$
|
||||
DECLARE
|
||||
t_row record;
|
||||
r record;
|
||||
v_seq text;
|
||||
BEGIN
|
||||
FOR t_row IN SELECT schema_name FROM public.tenant_schemas LOOP
|
||||
FOR r IN
|
||||
SELECT c.relname AS table_name, a.attname AS column_name
|
||||
FROM pg_attrdef d
|
||||
JOIN pg_class c ON c.oid = d.adrelid
|
||||
JOIN pg_attribute a ON a.attrelid = d.adrelid AND a.attnum = d.adnum
|
||||
WHERE c.relnamespace = t_row.schema_name::regnamespace
|
||||
AND pg_get_expr(d.adbin, d.adrelid) LIKE 'nextval(%'
|
||||
LOOP
|
||||
v_seq := pg_get_serial_sequence(format('%I.%I', t_row.schema_name, r.table_name), r.column_name);
|
||||
IF v_seq IS NOT NULL THEN
|
||||
EXECUTE format('SELECT setval(%L, COALESCE((SELECT MAX(%I) FROM %I.%I), 0) + 1, false)',
|
||||
v_seq, r.column_name, t_row.schema_name, r.table_name);
|
||||
RAISE NOTICE 'F6.1 seq %.% -> %', t_row.schema_name, r.table_name, v_seq;
|
||||
END IF;
|
||||
END LOOP;
|
||||
END LOOP;
|
||||
END $$;
|
||||
|
||||
SET session_replication_role = origin;
|
||||
@@ -0,0 +1,29 @@
|
||||
-- =============================================================================
|
||||
-- F6.0 — Clona os schemas dos tenants JÁ EXISTENTES (cutover)
|
||||
--
|
||||
-- Até aqui só tenants criados PÓS-F2 ganhavam schema. Os 9 tenants que já
|
||||
-- existiam precisam dos seus schemas (ainda vazios — dados migram na F6.1).
|
||||
-- Idempotente: só clona quem não está em tenant_schemas. Cada clone dispara
|
||||
-- o trigger da F5 (expõe no PostgREST).
|
||||
-- =============================================================================
|
||||
|
||||
BEGIN;
|
||||
|
||||
DO $$
|
||||
DECLARE
|
||||
r record;
|
||||
v_schema text;
|
||||
BEGIN
|
||||
FOR r IN
|
||||
SELECT t.id, t.slug
|
||||
FROM public.tenants t
|
||||
LEFT JOIN public.tenant_schemas ts ON ts.tenant_id = t.id
|
||||
WHERE ts.tenant_id IS NULL
|
||||
ORDER BY t.created_at, t.id
|
||||
LOOP
|
||||
v_schema := public.clone_tenant_template(r.id);
|
||||
RAISE NOTICE 'F6.0: tenant % (%) -> %', r.id, r.slug, v_schema;
|
||||
END LOOP;
|
||||
END $$;
|
||||
|
||||
COMMIT;
|
||||
Reference in New Issue
Block a user