# Como o pg_synonym funciona por dentro Este guia é para quem quer entender, manter ou evoluir a extensão sem ser especialista em C. Quase tudo aqui é SQL e PL/pgSQL. Só uma peça pequena é escrita em C, e ela também está explicada. ## 1. A ideia O PostgreSQL não deixa extensões criarem comandos novos, então não dá para ter `CREATE SYNONYM`. Mas dá para chegar muito perto juntando quatro peças que o PostgreSQL já tem: | Peça | Para que serve | |---|---| | **Uma tabela de catálogo** (`synonym.catalog`) | Guarda os sinônimos. É a "fonte da verdade". | | **Uma view por sinônimo** | É o que as queries usam: `SELECT * FROM vendas.cliente`. | | **Event triggers** | Avisam a extensão quando alguém roda DDL (CREATE, ALTER, DROP). | | **Uma função em C** | Enxerga o comando *antes* de ele rodar, para liberar o caminho. | A view de cada sinônimo é sempre esta: ```sql CREATE VIEW vendas.cliente WITH (security_invoker = true) AS SELECT * FROM erp.clientes; ``` Por que isso funciona tão bem: - **Views simples são atualizáveis.** Uma view que é só `SELECT * FROM uma_tabela` aceita INSERT, UPDATE, DELETE e `ON CONFLICT`, e usa os defaults da tabela. Isso existe no PostgreSQL desde a versão 9.3. - **O planner elimina a view.** Ele reescreve `SELECT ... FROM vendas.cliente` como `SELECT ... FROM erp.clientes`, então não há custo nenhum nas queries. - **`security_invoker = true`** (PostgreSQL 15+) faz o PostgreSQL checar as permissões de quem está *usando* a view, e não do dono dela. Por isso o sinônimo nunca dá acesso extra a ninguém. É também o motivo de a extensão exigir PostgreSQL 15. O problema de uma view comum é o `ALTER TABLE`. Ela "congela" as colunas na hora em que foi criada, e o PostgreSQL recusa `DROP COLUMN`, `ALTER COLUMN TYPE` e `DROP TABLE` enquanto a view existir. O resto da extensão existe para resolver isso. ## 2. Os arquivos ``` pg_synonym/ ├── pg_synonym.control ← identidade: versão, schema fixo "synonym", exige superusuário ├── pg_synonym--1.0.sql ← quase tudo: catálogo, funções, event triggers ├── src/pg_synonym.c ← só a função "antes do comando" ├── Makefile ← compilação com PGXS └── test/sql, test/expected ← testes de regressão ``` ### O `.control` ```ini relocatable = false schema = synonym superuser = true ``` - `schema = synonym`: tudo da extensão fica no schema `synonym`, criado automaticamente. Por isso a chamada é `synonym.create(...)`. - `superuser = true`: só superusuário instala, porque criar event triggers exige superusuário. Depois de instalada, qualquer usuário pode criar sinônimos. ## 3. O catálogo e a segurança por linha (RLS) ```sql CREATE TABLE synonym.catalog ( syn_schema, syn_name, -- onde fica o sinônimo tgt_schema, tgt_name, -- para onde ele aponta owner, created_at ); ``` Qualquer pessoa pode ler essa tabela. Para escrever nela existem três políticas de RLS (row-level security): - **INSERT** só é permitido se `owner = current_user` e se a pessoa tem `CREATE` no schema do sinônimo. Ou seja, ninguém registra sinônimo em nome de outro, nem em schema onde não poderia criar uma view. - **DELETE** só é permitido para o dono, ou para quem é membro do papel dono. - **UPDATE** não tem política, então ninguém pode alterar linhas. Só as funções internas da extensão, que rodam como superusuário, fazem isso. `pg_extension_config_dump` faz o `pg_dump` incluir as linhas do catálogo no backup. ## 4. Os estados de um sinônimo: `synonym._states()` Esta função é o "cérebro" da extensão. Em **uma única consulta**, ela olha todos os sinônimos e diz em que estado cada um está: | Estado | Situação | O que a sincronização faz | |---|---|---| | `ok` | View existe e bate com o destino | nada | | `pending` | Destino não existe | nada (espera) | | `missing_view` | Destino existe, view não | cria a view | | `columns_changed` | Colunas do destino mudaram | ajusta a view | | `target_renamed` | Destino foi renomeado ou mudou de schema | atualiza o catálogo | | `conflict` | Outro objeto ocupa o nome do sinônimo | avisa (WARNING) | | `schema_missing` | O schema do sinônimo foi apagado | remove o sinônimo do catálogo | Dois detalhes importantes: - **Como saber para qual tabela a view aponta, mesmo depois de um RENAME?** Pelas dependências: o PostgreSQL registra em `pg_depend` que a regra (`pg_rewrite`) da view depende das colunas da tabela. A consulta segue esse caminho, que acompanha a tabela mesmo que ela troque de nome. - **Como saber se as colunas mudaram?** Comparando dois arrays: nomes e tipos das colunas da view, contra nomes e tipos das colunas do destino, na ordem. Ser uma consulta só, em vez de um laço linha a linha, fez o custo por DDL cair de ~100 ms para ~11 ms com 500 sinônimos. ## 5. A sincronização: `synonym._sync_one()` e `synonym._sync_all()` `_sync_one` pega um sinônimo, pergunta o estado e age. Ela repete isso (até 5 vezes) até chegar a `ok` ou `pending`, porque uma ação pode levar a outra: por exemplo, primeiro atualizar o nome do destino, depois ajustar as colunas. Para ajustar colunas há três caminhos, do mais suave ao mais drástico: 1. **Só colunas novas no fim** → `CREATE OR REPLACE VIEW`. Views que outras pessoas criaram em cima do sinônimo continuam intactas. 2. **Só nomes trocados** → `ALTER VIEW ... RENAME COLUMN`. Para o caso de duas colunas trocarem de nome entre si, isso é feito em duas passadas, com nomes temporários no meio, para não colidir. 3. **Qualquer outra coisa** → apaga e recria a view. `_sync_all` chama `_sync_one` para cada sinônimo fora de `ok`/`pending`. Se um sinônimo der erro, ela transforma o erro em **WARNING**, porque um sinônimo quebrado nunca deve fazer falhar o `ALTER TABLE` de outra pessoa em outra tabela. ### Por que `SECURITY DEFINER`? Imagine: a Ana é dona de `erp.clientes`, e o Beto criou o sinônimo `app.cliente`. A view `app.cliente` é do Beto. Quando a Ana roda `ALTER TABLE erp.clientes ADD COLUMN ...`, o sinônimo do Beto precisa ser atualizado, mas a Ana não tem permissão na view do Beto. Por isso as funções de sincronização são `SECURITY DEFINER`: rodam com os privilégios de quem instalou a extensão (um superusuário). Para isso ser seguro: - elas fixam `SET search_path = pg_catalog, pg_temp`, para ninguém conseguir "sequestrar" um nome de função ou operador; - todos os nomes são montados com `format('%I.%I', ...)`, que coloca aspas corretamente e impede SQL injection; - antes de criar uma view, checam se o dono ainda existe e ainda tem `CREATE` no schema; - a view criada é entregue ao dono certo com `ALTER VIEW ... OWNER TO`. ## 6. Os três event triggers Event triggers são gatilhos que disparam em comandos DDL, e não em linhas de tabela. ### Depois do comando: `synonym._on_ddl_end()` (PL/pgSQL) Dispara no fim de `CREATE`, `ALTER` e `DROP` de tabelas, views e schemas, e chama `_sync_all()`. Tem dois atalhos: - se a variável de sessão `pg_synonym.syncing` estiver `on`, sai na hora. As funções da extensão ligam essa variável enquanto rodam (com `SET pg_synonym.syncing = on` na definição), para que os `CREATE VIEW` e `DROP VIEW` delas não disparem o gatilho de novo em loop; - se o catálogo estiver vazio, sai na hora. Bancos sem sinônimos quase não pagam nada. ### Antes do comando: `synonym._on_ddl_start()` (C) O PostgreSQL recusa estes comandos enquanto existir uma view dependendo da tabela: ```sql DROP TABLE erp.clientes; ALTER TABLE erp.clientes DROP COLUMN x; ALTER TABLE erp.clientes ALTER COLUMN x TYPE bigint; ``` A solução é apagar as views dos sinônimos envolvidos **antes** do comando rodar. Depois, o gatilho do fim recria o que fizer sentido. O problema é que um event trigger em PL/pgSQL, no início do comando, não sabe sobre qual tabela o comando é. Um event trigger em C sabe, porque recebe a **árvore do comando** (parse tree) já interpretada. O que o C faz, passo a passo (`src/pg_synonym.c`): 1. **`collect_targets()`** olha a árvore: - se é `DropStmt` de tabela, view, materialized view ou foreign table, pega cada objeto; - se é `AlterTableStmt` com algum `DROP COLUMN` ou `ALTER COLUMN TYPE`, pega a tabela. Os nomes viram OIDs com `RangeVarGetRelid(..., missing_ok = true)`. Também anota se o usuário pediu `CASCADE`. 2. **`call_detach()`** chama a função PL/pgSQL `synonym._detach(oids, cascade)`, que apaga as views dos sinônimos que apontam para essas tabelas. Por segurança, `_detach` tem `EXECUTE` revogado de PUBLIC: ninguém consegue chamá-la por SQL. O C a chama diretamente pelo *function manager* do PostgreSQL (`OidFunctionCall2`), que não checa essa permissão. Antes, o C confere que a função realmente pertence à extensão (`getExtensionOfObject`). E se o usuário não tiver permissão para apagar a tabela? O `DROP TABLE` falha, a transação inteira é desfeita, e junto com ela as views que o C apagou. Nada se perde. ### Quando a view é apagada direto: `synonym._on_sql_drop()` (PL/pgSQL) Se alguém roda `DROP VIEW vendas.cliente`, ou `DROP OWNED BY` o dono do sinônimo, a intenção é apagar o sinônimo. Sem este gatilho, o gatilho do fim recriaria a view logo em seguida, o que seria uma surpresa. O evento `sql_drop` entrega a lista do que foi apagado (`pg_event_trigger_dropped_objects()`), com uma coluna `original`: `true` quando o objeto foi citado no comando, `false` quando caiu em cascata. Só as views de sinônimo com `original = true` saem do catálogo. Assim, quando a view cai porque o schema do **destino** foi apagado com `CASCADE`, o sinônimo continua e fica `pending`, esperando o destino voltar. Esta função roda como quem apagou a view. Quem apaga uma view é o dono dela (ou um superusuário), então a política de DELETE do catálogo deixa remover a linha. ## 7. As funções públicas - **`synonym.create(nome, destino)`** roda com as permissões de quem chama (não é `SECURITY DEFINER`). Ela interpreta os nomes com `parse_ident()`, que entende aspas e pontos, valida tudo com mensagens claras, insere no catálogo (o RLS confere de novo) e chama `_sync_one` para criar a view. - **`synonym.drop(nome)`** também roda como quem chama: confere se a pessoa é dona, apaga a view (como dona) e depois a linha do catálogo. - **`synonym.refresh()`** força uma sincronização geral. Normalmente não é necessária. - **`synonym.list`** é uma view que junta o catálogo com `_states()`. ## 8. Testes São cinco arquivos em `test/sql/`, cada um com a saída esperada em `test/expected/`: | Arquivo | O que cobre | |---|---| | `basic` | criar, listar, apagar, nomes com aspas, `search_path`, mensagens de erro, `DROP VIEW` e `DROP OWNED` | | `dml` | INSERT/UPDATE/DELETE, ON CONFLICT, constraints, joins, EXPLAIN, views e particionadas como destino | | `target_changes` | ADD/RENAME/ALTER TYPE/DROP COLUMN, troca de nomes, RENAME e SET SCHEMA do destino, views dependentes, ROLLBACK | | `late_binding` | sinônimo antes do destino, DROP e recriação do destino, schema que some e volta, conflito de nome | | `security` | quatro usuários diferentes: quem cria, quem lê, quem não pode nada, RLS no destino, tentativas de burlar o catálogo | Rode com `make installcheck`. Para adicionar um teste: escreva o SQL, rode, **confira a saída em `test/results` linha por linha** e só então copie para `test/expected`. ## 9. Ideias para a versão 1.1 - **Sinônimos de funções e procedures.** Criar uma função "casca" com a mesma assinatura que chama a original. O desafio são sobrecargas, parâmetros com default, `VARIADIC` e `OUT`. - **Sinônimos de sequences**, para `nextval('sinonimo')`. - **Acompanhar `ALTER SCHEMA ... RENAME`** do schema do sinônimo. Hoje o sinônimo vira uma view comum. - **Desempenho com milhares de sinônimos:** usar `pg_event_trigger_ddl_commands()` para olhar só os sinônimos afetados pelo comando, em vez de todos. Para lançar a 1.1, crie `pg_synonym--1.0--1.1.sql` (atualiza quem já tem a 1.0) e `pg_synonym--1.1.sql` (instalação do zero), mude `default_version` no `.control` e acrescente os dois em `DATA` no Makefile.