DEV Community

CPFHub
CPFHub

Posted on Originally published at cpfhub.hashnode.dev

Validando CPF dentro do PostgreSQL: função, CHECK constraint e domínio

Transparência: sou fundador da CPFHub, citada no final. O conteúdo é puramente SQL e não depende de nenhum serviço.

Validar CPF só na aplicação deixa uma porta aberta: scripts de importação, outro microsserviço, um UPDATE manual feito às pressas. Se o banco é a fonte da verdade, faz sentido que ele recuse CPFs malformados. Vamos implementar isso no PostgreSQL.

1. Função de validação

CREATE OR REPLACE FUNCTION cpf_valido(p text)
RETURNS boolean
LANGUAGE plpgsql IMMUTABLE STRICT PARALLEL SAFE AS $$
DECLARE
  d text := regexp_replace(p, '\D', '', 'g');
  soma int;
  dv int;
BEGIN
  IF length(d) <> 11 OR d ~ '^(\d)\1{10}$' THEN
    RETURN false;
  END IF;
  FOR t IN 9..10 LOOP
    soma := 0;
    FOR i IN 1..t LOOP
      soma := soma + substr(d, i, 1)::int * (t + 2 - i);
    END LOOP;
    dv := (soma * 10) % 11 % 10;
    IF dv <> substr(d, t + 1, 1)::int THEN
      RETURN false;
    END IF;
  END LOOP;
  RETURN true;
END $$;
Enter fullscreen mode Exit fullscreen mode

Atenção ao índice: em SQL, substr começa em 1. Para o primeiro DV (t = 9), o peso do primeiro dígito é 10 (9 + 2 - 1); para o segundo (t = 10), é 11.

Marcar como IMMUTABLE permite usá-la em índices e constraints. STRICT faz cpf_valido(NULL) devolver NULL, o que em um CHECK significa "passa", então a obrigatoriedade fica com o NOT NULL.

SELECT cpf_valido('529.982.247-25'),  -- t
       cpf_valido('111.111.111-11'),  -- f
       cpf_valido('529.982.247-24');  -- f
Enter fullscreen mode Exit fullscreen mode

2. Domínio

Um domínio deixa a regra reutilizável em várias tabelas:

CREATE DOMAIN cpf AS char(11)
  CHECK (VALUE ~ '^\d{11}$' AND cpf_valido(VALUE));

CREATE TABLE clientes (
  id    bigserial PRIMARY KEY,
  nome  text NOT NULL,
  cpf   cpf  NOT NULL UNIQUE,
  criado_em timestamptz NOT NULL DEFAULT now()
);
Enter fullscreen mode Exit fullscreen mode

O ^\d{11}$ dentro do domínio impede gravar com máscara. A aplicação normaliza; o banco garante.

3. Adicionando a uma tabela existente

Em uma tabela antiga, cheia de lixo, não dá para simplesmente criar a constraint. Use NOT VALID:

ALTER TABLE clientes_legado
  ADD CONSTRAINT cpf_ok CHECK (cpf_valido(cpf)) NOT VALID;
Enter fullscreen mode Exit fullscreen mode

Novas linhas e updates passam a ser checados imediatamente. As antigas ficam como estão até você limpá-las:

SELECT id, cpf FROM clientes_legado WHERE NOT cpf_valido(cpf);
-- corrija ou mova para quarentena, depois:
ALTER TABLE clientes_legado VALIDATE CONSTRAINT cpf_ok;
Enter fullscreen mode Exit fullscreen mode

VALIDATE CONSTRAINT usa um lock mais leve que a criação direta, o que importa em tabelas grandes.

4. Normalizando o legado

UPDATE clientes_legado
SET cpf = lpad(regexp_replace(cpf, '\D', '', 'g'), 11, '0')
WHERE cpf ~ '\D' OR length(cpf) BETWEEN 9 AND 10;
Enter fullscreen mode Exit fullscreen mode

O lpad recupera zeros à esquerda perdidos em exportações para Excel. Antes de rodar, conte quantas linhas serão afetadas e faça em transação.

5. Duplicados disfarçados

SELECT regexp_replace(cpf, '\D', '', 'g') AS d, count(*)
FROM clientes_legado
GROUP BY 1 HAVING count(*) > 1;
Enter fullscreen mode Exit fullscreen mode

6. Performance

A função é chamada a cada INSERT/UPDATE da coluna. Em PL/pgSQL ela custa microssegundos; não vai ser o seu gargalo. Se você importa dezenas de milhões de linhas, desabilite a constraint na carga e valide no final.

7. SQLite e MySQL

No MySQL 8, CHECK constraints funcionam, mas não aceitam funções armazenadas. A alternativa é um trigger BEFORE INSERT. No SQLite, registre a função a partir da aplicação (por exemplo, create_function no Python) ou valide na camada de app.

8. E a existência do CPF?

O banco garante que o número é bem formado. Saber se ele existe e a quem pertence depende de uma fonte externa, e isso deve acontecer na aplicação, não em um trigger: uma chamada HTTP dentro de transação segura locks e transforma latência de rede em latência de banco. Um padrão melhor é gravar com status_verificacao = 'pendente' e ter um worker que consulta a API e atualiza a linha.

Sobre a CPFHub

Para esse worker, a API de consulta de CPF oferece consulta individual (GET /cpf/{cpf}) e em lote (POST /cpf/bulk, até 10.000 CPFs por envio, assíncrono), com 50 créditos grátis por mês.

Top comments (0)