O erro de row-level security do PostgreSQL esconde seis causas diferentes
Um levantamento testou onze tipos de escrita contra cinco configurações de política de RLS no PostgreSQL 17.11 e mostrou que a mesma mensagem de erro cobre causas bem distintas, da falta de tenant até o RETURNING que lê antes de gravar.

Um levantamento testou onze tipos de escrita contra cinco configurações de política de RLS no PostgreSQL↳PostgreSQL11 conteúdosPostgreSQL via SSL com GolangData · abr 20195 itens legais sobre data types do PostgreSQLData · mar 20195 serviços gratuitos na cloud para bancos de dados PostgresData · fev 2025Ver tudo em Data → 17.11 e mostrou que a mesma mensagem de erro cobre causas bem distintas, da falta de tenant até o RETURNING que lê antes de gravar.
Uma mensagem, seis causas
"new row violates row-level security policy for table" é a frase que o PostgreSQL devolve para praticamente toda escrita recusada por row-level security (RLS). O problema é que ela não diz qual política falhou, qual expressão recusou a linha nem por quê. Um levantamento publicado no Planet PostgreSQL, medido em 9 de outubro de 2026 contra o PostgreSQL 17.11, rodou onze tipos de escrita em cinco configurações de política (e mais algumas com política restritiva) para separar o que essa frase única esconde.
O resultado: a mesma sentença cobre seis causas distintas, e a mensagem completa tem quatro variações, nem sempre fáceis de diferenciar numa aplicação que só olha o SQLSTATE 42501 (o mesmo código de permission denied for table).
As quatro formas da mensagem
A tabela abaixo resume o que cada variação indica, segundo o levantamento:
| Mensagem | O que ela indica |
|---|---|
new row violates row-level security policy for table "notes" | a linha não passou em nenhuma política permissiva para um dos comandos que a instrução conta (insert, update ou select) |
new row violates row-level security policy "p_short" for table "notes" | a linha passou na política permissiva, mas uma política restritiva nomeada a recusou |
new row violates row-level security policy (USING expression) for table "slugs" | um ON CONFLICT DO UPDATE atingiu uma linha existente que nenhuma política de update deixa atualizar |
new row violates row-level security policy "p_lock" (USING expression) for table "notes" | o mesmo caso acima, com uma política restritiva de update nomeada recusando a linha existente |
A forma com nome de política só aparece quando há uma política restritiva envolvida, seja ela de insert ou de select. No teste, inserir sem RETURNING passava, mas o mesmo insert com RETURNING id nomeava a política restritiva de select, porque RETURNING lê a linha recém-gravada.
A árvore de decisão
O levantamento propõe um fluxo simples para ir da mensagem até a causa, com o tenant guardado numa variável de sessão (app.tenant):
- A mensagem traz um nome de política? Uma política restritiva recusou a linha. Leia o
WITH CHECKdela, ou oUSINGse não houverWITH CHECK. Pode ser uma política de select:RETURNINGeON CONFLICTcom alvo de conflito também a aplicam à linha nova. - Vem como
(USING expression), sem nome? Um upsert atingiu uma linha que o papel não pode atualizar: no teste, uma linha de outro tenant, ou uma linha própria sem política de update. - Não tem nem nome nem
(USING expression)? Verifique se a mesma linha entra com umINSERTsimples, semRETURNINGe semON CONFLICT. Se entra, nenhuma política de select deixa a linha nova passar peloRETURNINGou peloON CONFLICT. - Também falha no insert simples? Confira, na mesma transação, o valor de
current_setting('app.tenant', true)logo antes da escrita. Vazio ou errado: o tenant não está configurado onde a escrita roda. Correto: falta política de insert para o papel, ou a linha é mesmo de outro tenant.
Um ponto que a árvore destaca: a checagem precisa rodar na mesma transação e antes de qualquer erro anterior, porque depois de um erro a transação fica abortada e qualquer SELECT de diagnóstico também falha.
Por que um upsert falha mesmo quando a linha já existe
A causa mais contraintuitiva do levantamento: um INSERT ... ON CONFLICT DO UPDATE pode falhar com a mensagem de RLS mesmo quando a ação final seria só um update, porque a política de insert é avaliada antes de o PostgreSQL saber se há conflito. Numa configuração com política de select e de update, mas sem política de insert, tentar gravar uma linha que colide com uma existente falhou mesmo quando o resultado real seria zero mudanças.
A documentação do PostgreSQL confirma o comportamento:
Um INSERT com ON CONFLICT DO NOTHING/UPDATE verifica as expressões WITH CHECK das políticas de INSERT para todas as linhas propostas para inserção, independentemente de elas serem ou não efetivamente inseridas.
an INSERT with ON CONFLICT DO NOTHING/UPDATE will check the INSERT policies' WITH CHECK expressions for all rows proposed for insertion, regardless of whether or not they end up being insertedDocumentação do PostgreSQL 17, CREATE POLICY
MERGE se comporta de outro jeito: como não existe política própria para MERGE, o PostgreSQL aplica a política de cada ação conforme ela é executada. No teste, um MERGE que só atualiza uma linha existente funcionou numa configuração sem política de insert, porque nenhuma linha nova passou pela ação de insert. Bastou incluir uma linha nova no mesmo MERGE para o comando inteiro falhar com a mesma mensagem genérica.
RETURNING e ON CONFLICT leem antes de escrever
Outra causa prática, e provavelmente a mais comum em produção: RETURNING e ON CONFLICT com alvo de conflito fazem a instrução ler a linha que está gravando, e leitura passa pelas políticas de select, não pelas de insert ou update. Numa configuração com política de insert mas sem política de select, um INSERT simples funcionou, e o mesmo INSERT ... RETURNING id falhou.
Isso afeta diretamente quem usa ORM: muitos adicionam RETURNING por padrão aos inserts para recuperar a chave gerada. O resultado é um insert que funciona perfeitamente num psql manual e falha só quando disparado pela aplicação, porque a aplicação pede de volta o id e o teste manual, não.
O caso de uma coluna UNIQUE fora da chave primária composta por tenant ilustra o risco: ao tentar gravar um slug que já pertence a outro tenant, o comportamento muda conforme a instrução:
| Instrução | Resultado |
|---|---|
INSERT simples | erro de chave duplicada |
INSERT ... ON CONFLICT (slug) DO NOTHING | INSERT 0 0, sem erro algum |
INSERT ... ON CONFLICT (slug) DO UPDATE | erro de RLS na forma (USING expression) |
MERGE ... ON s.slug = v.slug | erro de chave duplicada |
O DO NOTHING é o caso mais perigoso: reporta sucesso com zero linhas afetadas, e esse mesmo zero também aparece quando o tenant já tem aquele slug. A aplicação não tem como distinguir "já existe, era seu" de "já existe, é de outro tenant" olhando só o retorno da instrução.
O que não dispara o erro
Seis comportamentos do levantamento mostram que a ausência do erro pode ser tão reveladora quanto a presença dele:
COPY FROMcomo papel de aplicação falha antes de olhar qualquer linha, com a mensagemCOPY FROM not supported with row-level security. Importação em massa sob RLS precisa virarINSERT, ou usar um papel que contorne a política.- Um
GRANTausente devolvepermission denied for table, com o mesmoSQLSTATE 42501do erro de RLS: código isolado não diferencia os dois casos. - O dono da tabela, sem
FORCE ROW LEVEL SECURITY, grava fora do próprio tenant sem erro nenhum. Se a aplicação conecta como dono da tabela, a ausência do erro é o problema real. - Um
UPDATEouDELETEfiltrado pela política simplesmente reportaUPDATE 0ouDELETE 0, sem lançar exceção alguma. - Um
MERGEque enxerga a linha mas não pode atualizá-la lança uma mensagem irmã,target row violates row-level security policy, diferente da mensagem de insert. - Um gatilho
BEFORE INSERTque reescreve otenant_idmuda o resultado da checagem, porque oWITH CHECKroda depois do gatilhoBEFORE ROW.
Como diagnosticar na própria base
O roteiro de diagnóstico proposto se resume a duas consultas, rodadas na mesma transação da escrita que falhou:
select current_user, current_setting('app.tenant', true);
select policyname, cmd, permissive, roles, qual, with_check
from pg_policies
where tablename = 'notes';Com isso em mãos, a ordem de checagem é: o tenant está configurado corretamente agora, nesta conexão? Existe política para o comando e para este papel (coluna cmd igual a INSERT ou ALL, com o papel, um papel do qual ele é membro, ou {public} em roles)? E se a instrução usa RETURNING ou ON CONFLICT com alvo, existe também uma política de SELECT ou ALL? Só depois disso faz sentido comparar o tenant_id da linha com o valor da sessão.
O que fica de fora
O levantamento cobre apenas o PostgreSQL 17.11; não testou outras versões, MERGE com ação de DELETE, views com barreira de segurança, tabelas particionadas ou tabelas estrangeiras. Plataformas hospedadas que colocam papéis próprios na frente da tabela introduzem causas adicionais fora do escopo testado. Para quem modela RLS multi-tenant, o ponto prático é este: a mensagem genérica não é um bug de legibilidade, é uma decisão de design que trata recusa de insert, update e select da mesma forma. Tratar 42501 como um erro único no código da aplicação, sem checar pg_policies antes, é dívida técnica disfarçada de tratamento de erro.
Fonte: Planet PostgreSQL
Este artigo foi escrito por Roberto Diniz, colunista de banco de dados. Conteúdo produzido por agente de IA da redação iMasters, sob revisão editorial humana. Saiba como produzimos no expediente.
PMM 3.7 traz Real-Time Analytics para rastrear operações ao vivo no MongoDB
O recurso Real-Time Analytics, disponível desde a versão 3.7.0 do Percona Monitoring and Management, mostra operações do MongoDB em andamento a cada dois segundos, sem precisar ficar rodando `db.currentOp()` na mão durante um incidente.












