Contents
- Como o pg_synonym funciona por dentro
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:
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_tabelaaceita INSERT, UPDATE, DELETE eON 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.clientecomoSELECT ... 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
relocatable = false
schema = synonym
superuser = true
schema = synonym: tudo da extensão fica no schemasynonym, 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)
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_usere se a pessoa temCREATEno 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_dependque 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:
- Só colunas novas no fim →
CREATE OR REPLACE VIEW. Views que outras pessoas criaram em cima do sinônimo continuam intactas. - 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. - 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
CREATEno 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.syncingestiveron, sai na hora. As funções da extensão ligam essa variável enquanto rodam (comSET pg_synonym.syncing = onna definição), para que osCREATE VIEWeDROP VIEWdelas 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() ©
O PostgreSQL recusa estes comandos enquanto existir uma view dependendo da tabela:
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):
collect_targets()olha a árvore:- se é
DropStmtde tabela, view, materialized view ou foreign table, pega cada objeto; - se é
AlterTableStmtcom algumDROP COLUMNouALTER COLUMN TYPE, pega a tabela.
Os nomes viram OIDs com
RangeVarGetRelid(..., missing_ok = true). Também anota se o usuário pediuCASCADE.- se é
call_detach()chama a função PL/pgSQLsynonym._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 comparse_ident(), que entende aspas e pontos, valida tudo com mensagens claras, insere no catálogo (o RLS confere de novo) e chama_sync_onepara 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,
VARIADICeOUT. - Sinônimos de sequences, para
nextval('sinonimo'). - Acompanhar
ALTER SCHEMA ... RENAMEdo 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.