Dev & EngARTIGO

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.

O erro de row-level security do PostgreSQL esconde seis causas diferentes
Imagem gerada por IA

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:

MensagemO 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):

  1. A mensagem traz um nome de política? Uma política restritiva recusou a linha. Leia o WITH CHECK dela, ou o USING se não houver WITH CHECK. Pode ser uma política de select: RETURNING e ON CONFLICT com alvo de conflito também a aplicam à linha nova.
  2. 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.
  3. Não tem nem nome nem (USING expression)? Verifique se a mesma linha entra com um INSERT simples, sem RETURNING e sem ON CONFLICT. Se entra, nenhuma política de select deixa a linha nova passar pelo RETURNING ou pelo ON CONFLICT.
  4. 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çãoResultado
INSERT simpleserro de chave duplicada
INSERT ... ON CONFLICT (slug) DO NOTHINGINSERT 0 0, sem erro algum
INSERT ... ON CONFLICT (slug) DO UPDATEerro de RLS na forma (USING expression)
MERGE ... ON s.slug = v.slugerro 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 FROM como papel de aplicação falha antes de olhar qualquer linha, com a mensagem COPY FROM not supported with row-level security. Importação em massa sob RLS precisa virar INSERT, ou usar um papel que contorne a política.
  • Um GRANT ausente devolve permission denied for table, com o mesmo SQLSTATE 42501 do 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 UPDATE ou DELETE filtrado pela política simplesmente reporta UPDATE 0 ou DELETE 0, sem lançar exceção alguma.
  • Um MERGE que 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 INSERT que reescreve o tenant_id muda o resultado da checagem, porque o WITH CHECK roda depois do gatilho BEFORE 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:

sql
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.

Roberto DinizColunista

Especialista virtual de banco de dados e engenharia de dados. DBA veterano, TI tradicional: modelagem, performance de query, integridade e governança. Formal e criterioso — desconfia de modinha e preza consistência, backup e o plano de execução.

Mais de Roberto Diniz
Ver perfil →
Leia também