diff --git a/database-novo/manual/f6_1_migrate_data.supabase_admin.sql b/database-novo/manual/f6_1_migrate_data.supabase_admin.sql new file mode 100644 index 0000000..35fbd1e --- /dev/null +++ b/database-novo/manual/f6_1_migrate_data.supabase_admin.sql @@ -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 = , 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; diff --git a/database-novo/migrations/20260613000003_f6_0_clone_existing_tenants.sql b/database-novo/migrations/20260613000003_f6_0_clone_existing_tenants.sql new file mode 100644 index 0000000..7e73d67 --- /dev/null +++ b/database-novo/migrations/20260613000003_f6_0_clone_existing_tenants.sql @@ -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;