@fayz-ai/db 0.14.0 → 0.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/migrations/011_leadcontrol_joins_the_family.sql +5 -1
- package/migrations/029_o_administrador_enxerga_o_que_existe.sql +167 -0
- package/migrations/031_ticketcontrol_joins_the_family.sql +109 -0
- package/migrations/032_a_marca_e_um_conjunto_de_tokens.sql +291 -0
- package/migrations/041_a_entrega_tem_onde_guardar_a_politica.sql +53 -0
- package/migrations/042_o_token_de_marca_fecha_a_porta_anon.sql +17 -0
- package/migrations/043_fiscalcontrol_joins_the_family.sql +75 -0
- package/migrations/044_o_teste_gratis_dura_trinta_dias.sql +15 -0
- package/migrations/045_o_espelho_manda_enquanto_o_v2_nao_escreveu.sql +519 -0
- package/migrations/046_the_tick_advances_the_flows.sql +77 -0
- package/migrations/047_the_tick_sweeps_the_offers.sql +74 -0
- package/migrations/052_fullcontrol_joins_the_family.sql +213 -0
- package/migrations/053_o_erp_para_de_exigir_o_que_nao_desenha.sql +37 -0
- package/migrations/054_o_arquivo_migrado_ganha_bytes.sql +439 -0
- package/migrations/061_a_linha_sem_data_pode_ser_estrutura.sql +253 -0
- package/migrations/063_o_anexo_ja_registrado_recebe_os_bytes.sql +303 -0
- package/migrations/064_um_arquivo_anexado_a_dezenove_contas.sql +178 -0
- package/migrations/065_apagar_o_sobrevivente_nao_apaga_o_tenant.sql +59 -0
- package/migrations/066_the_port_machinery_closes_its_doors.sql +51 -0
- package/migrations/067_the_erp_keeps_the_front_door.sql +56 -0
- package/migrations/068_o_ledger_encontra_a_linha_pelo_caminho_que_o_writer_usa.sql +55 -0
- package/migrations/069_a_sondagem_de_irma_usa_indice.sql +293 -0
- package/migrations/071_o_sdr_trabalha_o_funil_e_mais_nada.sql +110 -0
- package/migrations/072_o_porte_nao_avisa_a_plataforma_de_uma_venda_antiga.sql +91 -0
- package/migrations/073_a_exclusao_e_contrato_do_cliente_nao_do_cluster.sql +300 -0
- package/migrations/074_a_classificacao_quebrada_nao_leva_o_dinheiro_junto.sql +332 -0
- package/migrations/075_o_apelido_da_referencia_nao_disputa_com_a_variavel.sql +276 -0
- package/migrations/076_o_desligado_tambem_precisa_atravessar.sql +110 -0
- package/migrations/077_a_unidade_escolhida_chega_ao_banco.sql +128 -0
- package/migrations/078_onde_o_profissional_atende_atravessa.sql +71 -0
- package/migrations/079_quem_aparece_na_agenda_e_quem_atende.sql +210 -0
- package/migrations/080_o_contexto_da_requisicao_se_calcula_uma_vez.sql +240 -0
- package/migrations/081_a_permissao_por_unidade_se_resolve_uma_vez_por_unidade.sql +88 -0
- package/migrations/082_a_cerca_tambem_se_lembra.sql +110 -0
- package/migrations/083_quem_e_profissional_continua_agendavel.sql +32 -0
- package/migrations/084_quem_atende_em_varias_nao_cabe_numa_coluna.sql +60 -0
- package/migrations/085_a_unidade_do_profissional_vem_de_onde_ele_atende.sql +47 -0
- package/migrations/086_a_ponte_de_clientes_volta_a_existir.sql +89 -0
- package/migrations/087_a_liberacao_pergunta_ao_indice_antes_da_funcao.sql +79 -0
- package/migrations/088_quem_pode_executar_o_que_o_porte_criou.sql +50 -0
- package/migrations/089_a_memoria_de_transacao_nao_serve_para_isto.sql +86 -0
- package/migrations/090_a_marca_tambem_conta_como_mudanca_de_configuracao.sql +75 -0
- package/migrations/091_o_shell_tambem_e_configuracao_do_tenant.sql +71 -0
- package/migrations/092_a_marca_tem_uma_grafia_so_e_uma_porta_so.sql +159 -0
- package/migrations/093_a_marca_carrega_o_tema_inteiro.sql +179 -0
- package/migrations/094_a_porta_do_dominio_aprende_a_grafia_nova.sql +55 -0
- package/migrations/095_o_aplicativo_tem_um_preset_que_a_conta_estende.sql +98 -0
- package/migrations/096_o_sublinhado_tambem_e_nome_de_modulo.sql +100 -0
- package/migrations/097_o_oficio_diz_o_que_oferece_e_a_conta_o_que_usa.sql +137 -0
- package/migrations/098_a_arvore_do_oficio_nomeia_quem_ainda_desenha.sql +22 -0
- package/migrations/099_o_estoque_mora_dentro_de_produtos.sql +48 -0
- package/migrations/100_o_chefcontrol_desenha_abas_de_modulo.sql +24 -0
- package/migrations/101_o_oficio_agrupa_o_plugin_nomeia.sql +93 -0
- package/migrations/102_tres_marcas_ganham_fundo_e_contraste.sql +73 -0
- package/migrations/103_great_djs_e_iam_club_entram_na_frota.sql +75 -0
- package/migrations/104_o_curso_e_a_bilheteria_ganham_preset.sql +29 -0
- package/migrations/105_o_que_o_oficio_exige_ele_tambem_oferece.sql +78 -0
- package/migrations/106_a_comunidade_se_gere_como_escola.sql +84 -0
- package/migrations/107_o_logo_da_coluna_e_projecao_do_documento.sql +53 -0
- package/migrations/108_o_modulo_do_oficio_vizinho_tem_onde_ser_visto.sql +78 -0
- package/migrations/109_a_conta_diz_o_que_nao_usa_e_a_marca_pinta_a_casa.sql +67 -0
- package/migrations/110_a_tabela_de_ramos_aprende_o_vocabulario_fechado.sql +60 -0
- package/migrations/111_o_erp_generico_nao_vende_por_pedido.sql +50 -0
- package/migrations/112_o_cache_do_contexto_sabe_de_quem_ele_e.sql +327 -0
- package/migrations/113_o_ramo_volta_a_saber_que_papeis_a_conta_nasce_tendo.sql +124 -0
- package/migrations/114_a_resposta_inicial_volta_a_dizer_o_que_a_pessoa_pode.sql +93 -0
- package/migrations/115_o_rail_e_de_quem_tem_um.sql +108 -0
- package/package.json +1 -1
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
-- ---------------------------------------------------------------------------
|
|
2
|
+
-- 064_um_arquivo_anexado_a_dezenove_contas.sql — a identidade do ledger de bytes
|
|
3
|
+
-- era o objeto de origem, e ela não basta.
|
|
4
|
+
--
|
|
5
|
+
-- Medido em 19/09/2026 no Pertinho do Céu, logo depois da 063:
|
|
6
|
+
--
|
|
7
|
+
-- 1.192 registros de anexo apontam para 681 arquivos distintos
|
|
8
|
+
-- 41 arquivos estão anexados a mais de uma conta
|
|
9
|
+
-- `payroll/8/1781889562635_RECIBO_DE_PAGAMENTO_05.2026.pdf` está em 19
|
|
10
|
+
--
|
|
11
|
+
-- E faz sentido: um recibo de folha cobre dezenove funcionários, então o V1
|
|
12
|
+
-- anexou o MESMO arquivo às dezenove contas. Não é sujeira — é como a casa
|
|
13
|
+
-- trabalha.
|
|
14
|
+
--
|
|
15
|
+
-- ── O que quebrava ────────────────────────────────────────────────────────
|
|
16
|
+
--
|
|
17
|
+
-- A 054 chaveia o ledger de bytes por `(projeto, 'storage:<bucket>', md5 do
|
|
18
|
+
-- caminho de origem)`. Para o avatar e a foto de ponto isso é exato: um objeto,
|
|
19
|
+
-- um registro. Para o anexo não, e o efeito é silencioso — a primeira das
|
|
20
|
+
-- dezenove sobe e fica `migrated`; as outras dezoito batem no mesmo ledger,
|
|
21
|
+
-- voltam `skipped` por "bytes iguais", e ficam `v1-pending` **para sempre**,
|
|
22
|
+
-- apontando para um V1 que vai sair do ar.
|
|
23
|
+
--
|
|
24
|
+
-- São **511 dos 1.192** registros nessa situação. E o pior: a rodada termina
|
|
25
|
+
-- verde. `skipped` é a resposta certa para "já atravessou", e nada distingue
|
|
26
|
+
-- "já atravessou" de "atravessou para OUTRO registro".
|
|
27
|
+
--
|
|
28
|
+
-- ── A identidade certa ────────────────────────────────────────────────────
|
|
29
|
+
--
|
|
30
|
+
-- Um byte que atravessa tem DUAS pontas, e o ledger precisa das duas: de onde
|
|
31
|
+
-- veio, e para qual registro foi. Quando o backlog nomeia um `document_id`, ele
|
|
32
|
+
-- entra na chave. Quando não nomeia — avatar, foto de ponto —, a chave é a de
|
|
33
|
+
-- antes, porque ali o registro é derivado do objeto e a segunda ponta não
|
|
34
|
+
-- acrescenta nada.
|
|
35
|
+
--
|
|
36
|
+
-- Cada um dos dezenove ganha a sua linha de ledger, o seu objeto no bucket e o
|
|
37
|
+
-- seu checksum. Dezenove cópias do mesmo PDF é o preço, e é o preço certo:
|
|
38
|
+
-- registros independentes, um apagado não cega os outros dezoito, e a conta de
|
|
39
|
+
-- "quantos arquivos eu tenho desta pessoa" continua fechando.
|
|
40
|
+
--
|
|
41
|
+
-- ── As cinco linhas já gravadas ───────────────────────────────────────────
|
|
42
|
+
--
|
|
43
|
+
-- O ensaio da 063 gravou cinco linhas com a chave antiga. Elas não são
|
|
44
|
+
-- apagadas: a chave nova não as encontra, o backlog reoferece os registros, e o
|
|
45
|
+
-- upload vai com `x-upsert`. O que sobra é cinco linhas de ledger órfãs
|
|
46
|
+
-- apontando para objetos que existem — inofensivas, e preferíveis a um DELETE
|
|
47
|
+
-- num ledger de produção para arrumar aparência.
|
|
48
|
+
--
|
|
49
|
+
-- Idempotente e replay-safe.
|
|
50
|
+
-- ---------------------------------------------------------------------------
|
|
51
|
+
|
|
52
|
+
CREATE OR REPLACE FUNCTION migration._attach_asset_impl(
|
|
53
|
+
p_batch uuid, p_kind text, p_source_path text, p_row jsonb)
|
|
54
|
+
RETURNS jsonb
|
|
55
|
+
LANGUAGE plpgsql SECURITY DEFINER SET search_path = ''
|
|
56
|
+
AS $$
|
|
57
|
+
DECLARE
|
|
58
|
+
b migration.batches%ROWTYPE;
|
|
59
|
+
k migration.asset_kinds%ROWTYPE;
|
|
60
|
+
l migration.ledger%ROWTYPE;
|
|
61
|
+
v_source_table text;
|
|
62
|
+
v_source_uuid uuid;
|
|
63
|
+
v_checksum text := p_row ->> 'checksum';
|
|
64
|
+
v_doc uuid;
|
|
65
|
+
v_person uuid := nullif(p_row ->> 'person_id', '')::uuid;
|
|
66
|
+
v_adotado uuid := nullif(p_row ->> 'document_id', '')::uuid;
|
|
67
|
+
BEGIN
|
|
68
|
+
SELECT * INTO b FROM migration.batches WHERE id = p_batch;
|
|
69
|
+
IF NOT FOUND THEN RAISE EXCEPTION 'attach_asset: lote % desconhecido', p_batch USING ERRCODE = 'P0002'; END IF;
|
|
70
|
+
IF b.status <> 'running' THEN
|
|
71
|
+
RAISE EXCEPTION 'attach_asset: o lote % está % (só um lote em execução aceita arquivo)', p_batch, b.status USING ERRCODE = '55000';
|
|
72
|
+
END IF;
|
|
73
|
+
PERFORM migration._assert_fence(b.tenant_id);
|
|
74
|
+
|
|
75
|
+
SELECT * INTO k FROM migration.asset_kinds WHERE kind = p_kind;
|
|
76
|
+
IF NOT FOUND THEN
|
|
77
|
+
RAISE EXCEPTION 'attach_asset: espécie % não está em migration.asset_kinds', p_kind USING ERRCODE = '22023';
|
|
78
|
+
END IF;
|
|
79
|
+
IF v_checksum IS NULL OR p_row ->> 'target_path' IS NULL THEN
|
|
80
|
+
RAISE EXCEPTION 'attach_asset: preciso de checksum e target_path' USING ERRCODE = '22023';
|
|
81
|
+
END IF;
|
|
82
|
+
|
|
83
|
+
v_source_table := 'storage:' || k.source_bucket;
|
|
84
|
+
-- As duas pontas do byte. O `document_id` só entra quando existe: sem ele a
|
|
85
|
+
-- chave é a da 054, e um avatar já migrado continua sendo reconhecido.
|
|
86
|
+
v_source_uuid := md5(v_source_table || ':' || p_source_path
|
|
87
|
+
|| coalesce('::' || v_adotado::text, ''))::uuid;
|
|
88
|
+
|
|
89
|
+
SELECT * INTO l FROM migration.ledger
|
|
90
|
+
WHERE source_project = b.source_project AND source_table = v_source_table
|
|
91
|
+
AND source_uuid = v_source_uuid AND coalesce(target_table, '') = 'public.documents'
|
|
92
|
+
FOR UPDATE;
|
|
93
|
+
IF NOT FOUND THEN
|
|
94
|
+
INSERT INTO migration.ledger (source_project, source_table, source_uuid, tenant_id,
|
|
95
|
+
target_table, checksum, cursor, batch_id, status)
|
|
96
|
+
VALUES (b.source_project, v_source_table, v_source_uuid, b.tenant_id,
|
|
97
|
+
'public.documents', v_checksum, p_source_path, p_batch, 'pending')
|
|
98
|
+
RETURNING * INTO l;
|
|
99
|
+
ELSIF l.tenant_id <> b.tenant_id THEN
|
|
100
|
+
RETURN migration._quarantine(l.id, p_batch, 'tenant',
|
|
101
|
+
format('o objeto %s já é do tenant %s; este lote migra o %s', p_source_path, l.tenant_id, b.tenant_id), p_row);
|
|
102
|
+
ELSIF l.status = 'migrated' AND l.checksum = v_checksum THEN
|
|
103
|
+
PERFORM migration._count(p_batch, 'skipped');
|
|
104
|
+
RETURN jsonb_build_object('status', 'skipped', 'reason', 'bytes iguais',
|
|
105
|
+
'target_table', 'public.documents', 'target_id', l.target_id, 'ledger_id', l.id);
|
|
106
|
+
END IF;
|
|
107
|
+
|
|
108
|
+
-- O registro ADOTADO vem primeiro: quando o backlog nomeia um documento,
|
|
109
|
+
-- aquela linha é o anexo, e cunhar outra deixaria o mesmo arquivo duas vezes
|
|
110
|
+
-- na conta (063).
|
|
111
|
+
v_doc := coalesce(v_adotado, l.target_id, md5('document:' || p_kind || ':' || (p_row ->> 'target_path'))::uuid);
|
|
112
|
+
|
|
113
|
+
IF v_adotado IS NOT NULL AND NOT EXISTS (
|
|
114
|
+
SELECT 1 FROM public.documents d WHERE d.id = v_adotado AND d.tenant_id = b.tenant_id) THEN
|
|
115
|
+
RETURN migration._quarantine(l.id, p_batch, 'fk',
|
|
116
|
+
format('o registro %s não existe neste tenant — o backlog está apontando para um documento que sumiu', v_adotado), p_row);
|
|
117
|
+
END IF;
|
|
118
|
+
|
|
119
|
+
INSERT INTO public.documents (
|
|
120
|
+
id, tenant_id, kind, person_id, subject_type, subject_id, title, status,
|
|
121
|
+
file_url, file_name, file_size, mime_type,
|
|
122
|
+
storage_provider, storage_bucket, storage_path, checksum, metadata)
|
|
123
|
+
VALUES (
|
|
124
|
+
v_doc, b.tenant_id, p_kind, v_person,
|
|
125
|
+
p_row ->> 'subject_type', nullif(p_row ->> 'subject_id', '')::uuid,
|
|
126
|
+
p_row ->> 'title', 'active',
|
|
127
|
+
p_row ->> 'public_url', p_row ->> 'file_name',
|
|
128
|
+
nullif(p_row ->> 'file_size', '')::integer, p_row ->> 'mime_type',
|
|
129
|
+
'supabase', k.target_bucket, p_row ->> 'target_path', v_checksum,
|
|
130
|
+
jsonb_build_object('migration', jsonb_build_object(
|
|
131
|
+
'source_project', b.source_project, 'source_bucket', k.source_bucket,
|
|
132
|
+
'source_path', p_source_path, 'batch_id', p_batch)))
|
|
133
|
+
ON CONFLICT (id) DO UPDATE SET
|
|
134
|
+
person_id = coalesce(EXCLUDED.person_id, public.documents.person_id),
|
|
135
|
+
-- Num registro adotado, quem sabe o sujeito e o nome é a linha que já está
|
|
136
|
+
-- lá: ela veio do de-para. O que o transporte tem a dizer é onde os bytes
|
|
137
|
+
-- foram parar.
|
|
138
|
+
subject_type = coalesce(public.documents.subject_type, EXCLUDED.subject_type),
|
|
139
|
+
subject_id = coalesce(public.documents.subject_id, EXCLUDED.subject_id),
|
|
140
|
+
title = coalesce(public.documents.title, EXCLUDED.title),
|
|
141
|
+
file_url = EXCLUDED.file_url,
|
|
142
|
+
file_name = coalesce(public.documents.file_name, EXCLUDED.file_name),
|
|
143
|
+
file_size = EXCLUDED.file_size,
|
|
144
|
+
mime_type = coalesce(EXCLUDED.mime_type, public.documents.mime_type),
|
|
145
|
+
storage_provider = EXCLUDED.storage_provider,
|
|
146
|
+
storage_bucket = EXCLUDED.storage_bucket,
|
|
147
|
+
storage_path = EXCLUDED.storage_path,
|
|
148
|
+
checksum = EXCLUDED.checksum,
|
|
149
|
+
is_active = true,
|
|
150
|
+
metadata = migration._merge_meta(public.documents.metadata, EXCLUDED.metadata),
|
|
151
|
+
updated_at = now();
|
|
152
|
+
|
|
153
|
+
IF k.pointer_fn IS NOT NULL THEN
|
|
154
|
+
EXECUTE format('SELECT %s($1, $2, $3)', k.pointer_fn)
|
|
155
|
+
USING b.tenant_id, v_doc, p_row || jsonb_build_object('bucket', k.target_bucket);
|
|
156
|
+
END IF;
|
|
157
|
+
|
|
158
|
+
UPDATE migration.ledger
|
|
159
|
+
SET status = 'migrated', target_id = v_doc, checksum = v_checksum,
|
|
160
|
+
cursor = p_source_path, batch_id = p_batch, reason = NULL
|
|
161
|
+
WHERE id = l.id;
|
|
162
|
+
PERFORM migration._count(p_batch, 'migrated');
|
|
163
|
+
|
|
164
|
+
RETURN jsonb_build_object('status', 'migrated', 'target_table', 'public.documents',
|
|
165
|
+
'target_id', v_doc, 'ledger_id', l.id);
|
|
166
|
+
END $$;
|
|
167
|
+
|
|
168
|
+
COMMENT ON FUNCTION migration._attach_asset_impl(uuid, text, text, jsonb) IS
|
|
169
|
+
'Um arquivo que atravessou. A identidade no ledger é o OBJETO DE ORIGEM mais o REGISTRO de destino quando há um (064): sem a segunda ponta, um arquivo anexado a dezenove contas dava bytes a uma e deixava dezoito apontando para o V1, com a rodada terminando verde.';
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
-- ── quem pode executar ────────────────────────────────────────────────────
|
|
173
|
+
--
|
|
174
|
+
-- Função nova nasce executável por PUBLIC, e `anon`/`authenticated` herdam
|
|
175
|
+
-- dali. Estas são de PORTE: SECURITY DEFINER, escrevem dados de qualquer
|
|
176
|
+
-- inquilino. Só o service_role, que é quem roda a onda.
|
|
177
|
+
REVOKE ALL ON FUNCTION migration._attach_asset_impl(uuid, text, text, jsonb) FROM PUBLIC, anon, authenticated;
|
|
178
|
+
GRANT EXECUTE ON FUNCTION migration._attach_asset_impl(uuid, text, text, jsonb) TO service_role;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
-- 065_apagar_o_sobrevivente_nao_apaga_o_tenant.sql — quem foi fundido volta a
|
|
2
|
+
-- poder ser apagado.
|
|
3
|
+
--
|
|
4
|
+
-- `person_erase()` é o RPC de apagamento da LGPD. Ele faz `DELETE FROM
|
|
5
|
+
-- public.people` e confia nas chaves estrangeiras, como diz o próprio
|
|
6
|
+
-- comentário dele: "What references it with ON DELETE SET NULL keeps its shape
|
|
7
|
+
-- (a sale still happened) and loses the person".
|
|
8
|
+
--
|
|
9
|
+
-- Uma dessas chaves é a de `people` para ela mesma:
|
|
10
|
+
--
|
|
11
|
+
-- people_merged_into_fk
|
|
12
|
+
-- FOREIGN KEY (tenant_id, merged_into_id)
|
|
13
|
+
-- REFERENCES people(tenant_id, id) ON DELETE SET NULL
|
|
14
|
+
--
|
|
15
|
+
-- SET NULL composto SEM lista de colunas anula TODAS as colunas da chave. E
|
|
16
|
+
-- `tenant_id` é NOT NULL. Então apagar alguém que é SOBREVIVENTE de um merge
|
|
17
|
+
-- tenta anular o `tenant_id` da lápide e morre:
|
|
18
|
+
--
|
|
19
|
+
-- 23502: null value in column "tenant_id" of relation "people"
|
|
20
|
+
-- violates not-null constraint
|
|
21
|
+
--
|
|
22
|
+
-- O efeito é preciso e ruim: **um pedido de exclusão de titular falha
|
|
23
|
+
-- exatamente para quem já apareceu duas vezes no cadastro** — que é, por
|
|
24
|
+
-- construção, a pessoa mais provável de ter duas fichas e de pedir para sumir.
|
|
25
|
+
--
|
|
26
|
+
-- Não aparecia porque outra coisa barrava o DELETE antes: quatro chaves do CRM
|
|
27
|
+
-- estavam em NO ACTION e faziam `person_erase` falhar mais cedo, com outro
|
|
28
|
+
-- erro. Corrigidas aquelas (plugin-crm/017), este defeito ficou na frente.
|
|
29
|
+
--
|
|
30
|
+
-- É o MESMO erro que plugin-crm/013 já tinha documentado ao criar
|
|
31
|
+
-- `plg_crm_deals.company_id`, e lá a lista de colunas foi escrita desde o
|
|
32
|
+
-- começo justamente por causa disto. Aqui faltou.
|
|
33
|
+
--
|
|
34
|
+
-- A correção é a lista de colunas — `SET NULL (merged_into_id)`, suportada
|
|
35
|
+
-- desde o Postgres 15. A lápide perde o ponteiro para quem foi apagado e
|
|
36
|
+
-- continua sendo uma linha do seu tenant, que é o que ela sempre deveria ter
|
|
37
|
+
-- sido: registro de que aquelas duas fichas eram a mesma pessoa.
|
|
38
|
+
|
|
39
|
+
DO $$
|
|
40
|
+
BEGIN
|
|
41
|
+
IF EXISTS (
|
|
42
|
+
SELECT 1 FROM pg_constraint
|
|
43
|
+
WHERE conname = 'people_merged_into_fk'
|
|
44
|
+
AND conrelid = 'public.people'::regclass
|
|
45
|
+
-- Só recria se ainda estiver na forma antiga. `confdelsetcols` vazio é
|
|
46
|
+
-- "anula a chave inteira"; preenchido é a lista que queremos.
|
|
47
|
+
AND coalesce(array_length(confdelsetcols, 1), 0) = 0
|
|
48
|
+
) THEN
|
|
49
|
+
ALTER TABLE public.people DROP CONSTRAINT people_merged_into_fk;
|
|
50
|
+
ALTER TABLE public.people
|
|
51
|
+
ADD CONSTRAINT people_merged_into_fk
|
|
52
|
+
FOREIGN KEY (tenant_id, merged_into_id)
|
|
53
|
+
REFERENCES public.people (tenant_id, id)
|
|
54
|
+
ON DELETE SET NULL (merged_into_id);
|
|
55
|
+
END IF;
|
|
56
|
+
END $$;
|
|
57
|
+
|
|
58
|
+
COMMENT ON CONSTRAINT people_merged_into_fk ON public.people IS
|
|
59
|
+
'A lápide aponta para o sobrevivente. SET NULL com LISTA DE COLUNAS: sem ela o Postgres anula tambem o tenant_id, que é NOT NULL, e apagar um sobrevivente de merge passa a ser impossível (23502). Ver o cabeçalho de 065.';
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
-- ---------------------------------------------------------------------------
|
|
2
|
+
-- 066_the_port_machinery_closes_its_doors.sql — the V1 port's new pieces get
|
|
3
|
+
-- the posture the rest of schema `migration` already has.
|
|
4
|
+
--
|
|
5
|
+
-- 045 (the mirror) and 054 (migrated files get their bytes) added two tables
|
|
6
|
+
-- and a set of functions to schema `migration` and gave them none of the
|
|
7
|
+
-- contract 090_migration_ledger/001 grades:
|
|
8
|
+
--
|
|
9
|
+
-- · `migration.asset_kinds` and `migration.retire_policy` had row security
|
|
10
|
+
-- off and no grant to service_role — the one role that is supposed to
|
|
11
|
+
-- write them, and the only one that should.
|
|
12
|
+
-- · every function the two files created kept PostgreSQL's default
|
|
13
|
+
-- `EXECUTE TO PUBLIC`, which on Supabase means `anon`. Among them
|
|
14
|
+
-- `retire_row`, `attach_asset` and `register_asset_kind`: writers of the
|
|
15
|
+
-- port's own ledger, callable by anyone holding the anon key.
|
|
16
|
+
--
|
|
17
|
+
-- 045 and 054 are applied on the cluster and their checksums are in the
|
|
18
|
+
-- ledger, so the fix is this file and not an edit to them.
|
|
19
|
+
--
|
|
20
|
+
-- The function sweep is by schema, not by name: whatever `migration` holds
|
|
21
|
+
-- when this runs loses PUBLIC/anon/authenticated and keeps service_role. The
|
|
22
|
+
-- plugins that add functions to this schema after the spine (plugin-financial)
|
|
23
|
+
-- run the same sweep at the end of their own chain.
|
|
24
|
+
-- ---------------------------------------------------------------------------
|
|
25
|
+
|
|
26
|
+
DO $$
|
|
27
|
+
DECLARE t text;
|
|
28
|
+
BEGIN
|
|
29
|
+
FOREACH t IN ARRAY ARRAY['asset_kinds', 'retire_policy'] LOOP
|
|
30
|
+
IF to_regclass(format('migration.%I', t)) IS NOT NULL THEN
|
|
31
|
+
EXECUTE format('ALTER TABLE migration.%I ENABLE ROW LEVEL SECURITY', t);
|
|
32
|
+
EXECUTE format('GRANT SELECT, INSERT, UPDATE, DELETE ON migration.%I TO service_role', t);
|
|
33
|
+
END IF;
|
|
34
|
+
END LOOP;
|
|
35
|
+
END $$;
|
|
36
|
+
|
|
37
|
+
DO $$
|
|
38
|
+
DECLARE f regprocedure;
|
|
39
|
+
BEGIN
|
|
40
|
+
FOR f IN
|
|
41
|
+
SELECT p.oid::regprocedure
|
|
42
|
+
FROM pg_proc p
|
|
43
|
+
JOIN pg_namespace n ON n.oid = p.pronamespace
|
|
44
|
+
WHERE n.nspname = 'migration'
|
|
45
|
+
AND (has_function_privilege('anon', p.oid, 'EXECUTE')
|
|
46
|
+
OR has_function_privilege('authenticated', p.oid, 'EXECUTE'))
|
|
47
|
+
LOOP
|
|
48
|
+
EXECUTE format('REVOKE ALL ON FUNCTION %s FROM PUBLIC, anon, authenticated', f);
|
|
49
|
+
EXECUTE format('GRANT EXECUTE ON FUNCTION %s TO service_role', f);
|
|
50
|
+
END LOOP;
|
|
51
|
+
END $$;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
-- ---------------------------------------------------------------------------
|
|
2
|
+
-- 067_the_erp_keeps_the_front_door.sql — FullControl requires `marketing`
|
|
3
|
+
-- again.
|
|
4
|
+
--
|
|
5
|
+
-- 053 took `marketing` out of the app's `requires` on the reading that "the
|
|
6
|
+
-- acquisition machine is LeadControl's product". But 052 §3 retired the
|
|
7
|
+
-- LeadControl row (`active = false`, `url = NULL`): there is no LeadControl
|
|
8
|
+
-- left to own it. Read together, the two files left campaigns, the funnel and
|
|
9
|
+
-- the landing page with its capture form — the only public door a lead comes
|
|
10
|
+
-- through — without an app.
|
|
11
|
+
--
|
|
12
|
+
-- FullControl is where that work lives now. The ERP is the wider house; the
|
|
13
|
+
-- sales engine built as LeadControl (company model, identity, economic group,
|
|
14
|
+
-- more than one funnel, cadences, the cascading offer) is its commercial
|
|
15
|
+
-- module, and a commercial module whose leads cannot arrive is a list someone
|
|
16
|
+
-- has to type into by hand. So the app mounts plugin-marketing again, and
|
|
17
|
+
-- `requires` has to say so: it is the list a tenant is seeded with when it
|
|
18
|
+
-- ACTIVATES the app, and a plugin the bundle mounts but the tenant never got
|
|
19
|
+
-- renders as a dead rail entry.
|
|
20
|
+
--
|
|
21
|
+
-- `inventory` stays out, as 053 decided.
|
|
22
|
+
--
|
|
23
|
+
-- ── Why a new file ──────────────────────────────────────────────────────────
|
|
24
|
+
--
|
|
25
|
+
-- 052 and 053 are applied and the ledger holds their sha256. And 052's
|
|
26
|
+
-- ON CONFLICT only ever GROWS `requires` on a reapply, so it cannot be the
|
|
27
|
+
-- vehicle either. This is the explicit step, the same way 053 was.
|
|
28
|
+
--
|
|
29
|
+
-- ── Who gets the plugin now ─────────────────────────────────────────────────
|
|
30
|
+
--
|
|
31
|
+
-- Every tenant that carried LeadControl already has `marketing` lit: 011 put
|
|
32
|
+
-- it in `requires` and 011 §3 seeded it, and `app.tenant_plugins` has no
|
|
33
|
+
-- app_id, so 052's rename did not touch it. §2 below only reaches a tenant
|
|
34
|
+
-- that activated `full` between 053 and this file — and, as in 052 §5, the
|
|
35
|
+
-- plan ceiling wins over the default: a plan that does not permit `marketing`
|
|
36
|
+
-- does not get it lit, and the seeding does not fail because of it.
|
|
37
|
+
-- ---------------------------------------------------------------------------
|
|
38
|
+
|
|
39
|
+
-- ── §1 the seeding list ──────────────────────────────────────────────────
|
|
40
|
+
--
|
|
41
|
+
-- Additive on purpose: a later milestone may have appended a plugin after
|
|
42
|
+
-- this file was written, and overwriting the array here would undo it
|
|
43
|
+
-- silently.
|
|
44
|
+
UPDATE app.apps
|
|
45
|
+
SET requires = (SELECT array_agg(DISTINCT p ORDER BY p)
|
|
46
|
+
FROM unnest(requires || ARRAY['marketing']) AS p)
|
|
47
|
+
WHERE id = 'full';
|
|
48
|
+
|
|
49
|
+
-- ── §2 the tenants that activated FullControl without it ───────────────────
|
|
50
|
+
INSERT INTO app.tenant_plugins (tenant_id, plugin_id, facet, status, source)
|
|
51
|
+
SELECT DISTINCT ta.tenant_id, 'marketing', '', 'active', 'default'
|
|
52
|
+
FROM app.tenant_apps ta
|
|
53
|
+
WHERE ta.app_id = 'full'
|
|
54
|
+
AND ta.status = 'active'
|
|
55
|
+
AND public.plan_permits('marketing', '', ta.tenant_id)
|
|
56
|
+
ON CONFLICT (tenant_id, plugin_id, facet) DO NOTHING;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
-- O ledger encontra a linha pelo caminho que o writer usa.
|
|
2
|
+
--
|
|
3
|
+
-- Todo writer que resolve uma referência pergunta ao ledger a mesma coisa:
|
|
4
|
+
--
|
|
5
|
+
-- SELECT target_id FROM migration.ledger
|
|
6
|
+
-- WHERE tenant_id = … AND source_table = … AND source_numeric_id = …
|
|
7
|
+
-- AND target_table = … AND status = 'migrated'
|
|
8
|
+
--
|
|
9
|
+
-- Nenhum índice cobre essa forma. O que existe —
|
|
10
|
+
-- `ledger_source_numeric_key (source_project, source_table, source_numeric_id, …)`
|
|
11
|
+
-- — começa por `source_project`, que a consulta não informa. Sem a coluna da
|
|
12
|
+
-- frente, o Postgres faz varredura.
|
|
13
|
+
--
|
|
14
|
+
-- Medido em 19/09/2026 no tenant Espaço Facial, UMA busca:
|
|
15
|
+
--
|
|
16
|
+
-- Index Scan using ledger_source_numeric_key (actual time=5416.611..5416.634 rows=0)
|
|
17
|
+
-- Buffers: shared hit=11 read=6891
|
|
18
|
+
-- Execution Time: 5423.387 ms
|
|
19
|
+
--
|
|
20
|
+
-- **5,4 segundos e 55 MB lidos, por linha.** E o custo cresce com a ledger: o
|
|
21
|
+
-- porte do Espaço Facial a levou de ~60 mil para 298 mil linhas numa noite, e
|
|
22
|
+
-- `account_central` sozinha paga essa conta 125.945 vezes. É O(n) por registro
|
|
23
|
+
-- num porte de 1,4 milhão — a razão de a carga não terminar.
|
|
24
|
+
--
|
|
25
|
+
-- Ninguém escreveu essa consulta errada; ela é a pergunta certa. O que faltava
|
|
26
|
+
-- era o índice que a responde.
|
|
27
|
+
--
|
|
28
|
+
-- ── Por que estas colunas, nesta ordem ──────────────────────────────────────
|
|
29
|
+
--
|
|
30
|
+
-- `tenant_id` primeiro porque é o recorte que sempre existe e o que mais
|
|
31
|
+
-- elimina. Depois `source_table` e `source_numeric_id`, que juntos identificam
|
|
32
|
+
-- a linha da origem. `target_table` por último, porque a mesma linha do V1 tem
|
|
33
|
+
-- mais de um destino — uma `invoice` vira pedido E título — e é ela que separa
|
|
34
|
+
-- os dois.
|
|
35
|
+
--
|
|
36
|
+
-- `status` fica FORA: ele muda (`pending` → `migrated` → `retired`), e uma
|
|
37
|
+
-- coluna que muda dentro do índice faz cada transição reescrever a entrada. O
|
|
38
|
+
-- filtro por status sobre as duas ou três linhas que sobram é grátis.
|
|
39
|
+
--
|
|
40
|
+
-- O índice é PARCIAL em `source_numeric_id IS NOT NULL` porque a origem que se
|
|
41
|
+
-- identifica por uuid não usa este caminho — e um índice parcial que cobre o
|
|
42
|
+
-- caso real é menor e mais rápido que um completo que carrega o resto.
|
|
43
|
+
--
|
|
44
|
+
-- Idempotente.
|
|
45
|
+
|
|
46
|
+
CREATE INDEX IF NOT EXISTS ledger_tenant_table_numeric_idx
|
|
47
|
+
ON migration.ledger (tenant_id, source_table, source_numeric_id, target_table)
|
|
48
|
+
WHERE source_numeric_id IS NOT NULL;
|
|
49
|
+
|
|
50
|
+
COMMENT ON INDEX migration.ledger_tenant_table_numeric_idx IS
|
|
51
|
+
'A forma exata em que os writers consultam o ledger para resolver uma '
|
|
52
|
+
'referência. Sem ele a busca é varredura: 5,4 s e 55 MB por linha, medido '
|
|
53
|
+
'com 298 mil linhas de ledger — o que torna impossível um porte de milhão.';
|
|
54
|
+
|
|
55
|
+
ANALYZE migration.ledger;
|