CHECK Constraint: a regra que toda linha precisa obedecer
Um CHECK constraint é uma expressão booleana que você anexa a uma tabela. O SQLite avalia essa expressão em todo INSERT e UPDATE, e se o resultado for falso, a operação é abortada. É uma forma de gravar uma regra de negócio — "preço não pode ser negativo", "status precisa ser um destes três valores" — direto no schema, validando os dados no SQLite sem depender da aplicação.
As duas primeiras linhas entram normalmente. A terceira dispara CHECK constraint failed e é rejeitada — a tabela nem chega a ver esse valor. A constraint vale para qualquer origem de escrita: sua aplicação, um script de migration ou alguém fuçando direto no CLI.
CHECK no nível da coluna vs no nível da tabela
Dá para escrever um CHECK em dois lugares: logo depois da definição de uma coluna (nível de coluna) ou depois de todas as colunas (nível de tabela). O comportamento é o mesmo — a escolha é só uma questão de qual fica mais fácil de ler.
A primeira reserva é inserida normalmente. A segunda falha — a data de término vem antes da data de início. Regras de uma coluna só ficam mais legíveis no nível da coluna; qualquer coisa que compare duas ou mais colunas fica melhor no nível da tabela.
Restringindo valores a uma lista
Um uso muito comum é obrigar uma coluna a aceitar apenas um conjunto fixo de valores. Como o SQLite não tem um tipo enum nativo, o jeito idiomático é usar CHECK ... IN (...):
A terceira linha falha — 'pending' não está na lista de valores permitidos. Se em algum momento você precisar adicionar um novo status, vai ter que recriar a tabela (falo mais sobre isso adiante), então pense bem antes de fechar a lista. Mas para vocabulários realmente fixos, como nomes de papéis ou estados de pedido, essa é exatamente a constraint que você quer.
Dando nome às suas constraints
Por padrão, uma constraint é anônima. A mensagem de erro mostra apenas "CHECK constraint failed" junto com a expressão, o que até serve quando existe uma única CHECK na tabela — mas vira uma confusão quando são cinco. Para nomear, use CONSTRAINT:
Agora a mensagem de erro traz o nome da constraint, então você sabe na hora qual regra foi violada. Dar nome custa alguns caracteres a mais e se paga na primeira vez que algo quebra em produção.
CHECK e NULL: a pegadinha
O CHECK passa quando a expressão é verdadeira ou NULL. Ele só falha quando o resultado é explicitamente falso. Parece estranho à primeira vista, mas faz sentido quando você lembra que praticamente qualquer comparação envolvendo NULL retorna NULL — e não verdadeiro ou falso.
A linha com NULL entra sem problema — NULL >= 0 resulta em NULL, e não em falso, então o CHECK não barra. Se a sua intenção é barrar tanto números negativos quanto valores ausentes, combine NOT NULL com o CHECK:
Agora o insert falha no NOT NULL antes mesmo do CHECK rodar. As duas constraints se complementam: NOT NULL cuida da ausência, e o CHECK cuida do formato.
Funções nativas úteis dentro do CHECK
A expressão aceita praticamente todas as funções nativas do SQLite. Algumas que aparecem com frequência:
Três falhas: um e-mail mal formatado, um nome de usuário curto demais e um código de país em minúsculas. O LIKE dá conta de padrões simples; length(), upper(), lower() e operações aritméticas estão todos liberados. Só mantenha a expressão determinística — usar algo como random() ou current_timestamp dentro de um CHECK cria regras que podem mudar de uma linha para outra, e raramente é isso que você quer.
CHECK constraint vs trigger no SQLite
Tanto o CHECK quanto os triggers conseguem barrar dados inválidos, e quem está começando sempre fica em dúvida sobre qual usar. A regra prática é esta:
- CHECK quando a regra depende apenas da linha que está sendo gravada. "Esta coluna comparada com aquela coluna", "este valor dentro de uma faixa", "esta string casa com um padrão".
- Trigger (mais especificamente um trigger
BEFORE INSERT/UPDATEque chamaRAISE) quando a regra depende de outras linhas, outras tabelas ou precisa fazer algo mais elaborado do que uma única expressão booleana.
O CHECK é mais rápido, mais simples e fica visível no schema — qualquer pessoa lendo o CREATE TABLE enxerga a regra. Só recorra a um trigger quando o CHECK não der conta do recado.
Não dá para remover um CHECK com ALTER
Esse é o ponto chato. O SQLite não tem ALTER TABLE ... DROP CONSTRAINT. Para remover ou alterar um CHECK, você precisa reconstruir a tabela:
BEGIN;
CREATE TABLE products_new (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
price REAL NOT NULL CHECK (price >= 0 AND price <= 1000000)
);
INSERT INTO products_new SELECT * FROM products;
DROP TABLE products;
ALTER TABLE products_new RENAME TO products;
COMMIT;
Envolva tudo em uma transação, assim, se algo falhar no meio do caminho, o banco fica intacto. Se outras tabelas tiverem chaves estrangeiras apontando para a tabela que você está reconstruindo, a dança fica mais longa — desabilite foreign_keys, reconstrua, reabilite e revalide. Vamos abordar isso no documento sobre migrations, mais à frente no currículo.
A seguir: constraints UNIQUE
O CHECK valida o formato dos valores dentro de uma linha. A próxima constraint, UNIQUE, valida relações entre linhas — garantindo que duas linhas nunca compartilhem o mesmo valor em uma coluna ou conjunto de colunas. É o que vem por aí.
Perguntas frequentes
O que é uma CHECK constraint no SQLite?
É uma expressão booleana atrelada à tabela que toda linha precisa satisfazer. O SQLite avalia essa expressão em cada INSERT ou UPDATE e rejeita a operação se o resultado for falso. É a forma mais simples de garantir uma regra do tipo 'preço tem que ser positivo' sem precisar escrever código na aplicação.
Uma CHECK constraint no SQLite pode envolver mais de uma coluna?
Pode sim — basta declarar como constraint no nível da tabela, e não presa a uma coluna só. Por exemplo: CHECK (start_date <= end_date) declarado depois da lista de colunas consegue referenciar as duas. Tecnicamente uma checagem no nível da coluna também consegue olhar outras colunas, mas no nível da tabela fica bem mais legível quando há mais de uma envolvida.
Por que minha CHECK constraint não dispara quando o valor é NULL?
O CHECK passa quando a expressão é verdadeira ou NULL — ele só falha quando o resultado é explicitamente falso. Então CHECK (age >= 0) aceita um age NULL, porque NULL >= 0 é NULL, não falso. Se você quer barrar NULL também, adicione um NOT NULL junto da CHECK.
Dá para remover ou alterar uma CHECK constraint no SQLite?
Diretamente, não. O SQLite não suporta ALTER TABLE ... DROP CONSTRAINT. Para mudar uma CHECK, você tem duas opções: editar a sqlite_schema usando PRAGMA writable_schema (avançado e arriscado) ou refazer a tabela — cria uma nova com as constraints desejadas, copia os dados, dropa a antiga e renomeia. Dar nome às suas constraints deixa esse script de reconstrução bem mais fácil de ler.