pg_try_cast

CI PostgreSQL 16+Licença: PostgreSQL

TRY_CAST e TRY_CONVERT para PostgreSQL. Converte um valor para qualquer tipo e devolve NULL (ou um valor padrão que você escolher) quando ele não é válido. A query continua rodando, em vez de abortar com erro.

🇺🇸 Read in English

SELECT try_cast('42',  NULL::int);   -- 42
SELECT try_cast('abc', NULL::int);   -- NULL   (sem erro!)
SELECT try_cast('abc', 0);           -- 0      (valor padrão)

Por quê?

No PostgreSQL, basta uma linha ruim para derrubar a query inteira:

SELECT id, preco::numeric FROM importacao;
-- ERROR:  invalid input syntax for type numeric: "R$ 10,00"

O SQL Server resolve isso com TRY_CAST e TRY_CONVERT, e o Oracle com CAST ... DEFAULT ... ON CONVERSION ERROR. O PostgreSQL ainda não tem nenhum dos dois. As gambiarras de sempre são uma regex antes de cada cast (frágil, e diferente para cada tipo) ou uma função PL/pgSQL com bloco EXCEPTION, que abre uma subtransação a cada linha e é lenta.

O pg_try_cast é uma extensão pequena em C que faz isso do jeito certo. Funciona com qualquer tipo que o PostgreSQL saiba ler a partir de texto: inteiros, numéricos, datas, timestamps, uuid, json/jsonb, arrays, ranges, enums, domínios e tipos compostos. E é cerca de 16× mais rápida que a solução em PL/pgSQL.

Como usar

try_cast(valor text, padrao anyelement) → anyelement

O tipo de padrao define para qual tipo valor será convertido. O valor de padrao é o que volta quando a conversão falha.

SELECT try_cast('2026-10-17', NULL::date);      -- 2026-10-17
SELECT try_cast('2026-02-30', NULL::date);      -- NULL
SELECT try_cast('99999999999', NULL::int);      -- NULL (fora do intervalo)
SELECT try_cast('n/d', 0);                      -- 0
SELECT try_cast('{"a":', '{}'::jsonb);          -- {}
SELECT try_cast(NULL, 0);                       -- NULL (entra NULL, sai NULL)

try_convert(padrao anyelement, valor text) → anyelement

É a mesma função, com a ordem de argumentos do SQL Server, para facilitar migrações:

SELECT try_convert(NULL::int, '42');            -- 42
SELECT try_convert(NULL::int, 'abc');           -- NULL

Exemplo real: limpando uma importação

SELECT id,
       try_cast(valor_bruto, NULL::numeric(12,2)) AS valor,
       try_cast(data_bruta,  NULL::date)          AS pago_em,
       try_cast(email_bruto, NULL::varchar(120))  AS email
FROM   importacao;

-- encontrar as linhas com problema
SELECT * FROM importacao
WHERE  valor_bruto IS NOT NULL
AND    try_cast(valor_bruto, NULL::numeric) IS NULL;

Colinha SQL Server → PostgreSQL

SQL Server pg_try_cast
TRY_CAST(x AS int) try_cast(x, NULL::int)
TRY_CAST(x AS decimal(10,2)) try_cast(x, NULL::numeric(10,2))
TRY_CONVERT(date, x) try_convert(NULL::date, x)
ISNULL(TRY_CAST(x AS int), 0) try_cast(x, 0)
TRY_CAST(x AS uniqueidentifier) try_cast(x, NULL::uuid)

Instalação

Requer PostgreSQL 16 ou mais novo, porque usa as funções de entrada com “erro suave” (soft error) que surgiram no PostgreSQL 16.

Linux (Debian / Ubuntu)

sudo apt install build-essential postgresql-server-dev-17   # use a versão do seu PostgreSQL
git clone https://github.com/SEU-USUARIO/pg_try_cast.git
cd pg_try_cast
make
sudo make install

Linux (RHEL / Rocky / Fedora)

sudo dnf install gcc make postgresql17-devel   # o nome do pacote pode variar conforme o repositório
make PG_CONFIG=/usr/pgsql-17/bin/pg_config
sudo make install PG_CONFIG=/usr/pgsql-17/bin/pg_config

macOS (Homebrew)

brew install postgresql@17
make PG_CONFIG=$(brew --prefix postgresql@17)/bin/pg_config
make install PG_CONFIG=$(brew --prefix postgresql@17)/bin/pg_config

Docker (qualquer sistema, inclusive Windows)

docker build -t pg_try_cast .                    # PostgreSQL 17 por padrão
docker build --build-arg PG_MAJOR=18 -t pg_try_cast:18 .
docker run -d --name pgtry -e POSTGRES_PASSWORD=postgres -p 5432:5432 pg_try_cast

Erro de compilação falando de clang ou llvm? Alguns pacotes do PostgreSQL foram compilados com suporte a JIT e tentam gerar um “bitcode” LLVM extra, que você não precisa. Compile com make with_llvm=no && sudo make with_llvm=no install.

Ativando no banco

CREATE EXTENSION pg_try_cast;

A extensão é marcada como trusted. Qualquer usuário com privilégio CREATE no banco pode ativá-la, sem precisar de superusuário.

Como ela se comporta

  • Entra NULL, sai NULL. try_cast(NULL, 0) devolve NULL, igual a CAST(NULL AS int).
  • A entrada é texto. Para outros tipos, use ::text. Exemplo: try_cast(minha_coluna::text, NULL::int).
  • Modificadores de tipo são respeitados, com as mesmas regras de um INSERT ou COPY numa coluna daquele tipo:
    • try_cast('abcd', NULL::varchar(3)) devolve NULL, porque o valor não cabe. Um CAST comum truncaria para 'abc'.
    • try_cast('123.456', NULL::numeric(5,2)) devolve 123.46, arredondado como o numeric(5,2) faz.
    • try_cast('12345.6', NULL::numeric(5,2)) devolve NULL (estouro).
  • Os CHECK de domínios são verificados. Com CREATE DOMAIN positivo AS int CHECK (VALUE > 0), try_cast('-5', NULL::positivo) devolve NULL.
  • O tipo do padrão precisa ser conhecido. try_cast('1', NULL) dá erro. Escreva NULL::int.

Limitações

  • Algumas funções de entrada não suportam erro suave e continuam lançando erro:
    • regtype com um nome sintaticamente inválido (por exemplo '###'). Nomes válidos que não existem devolvem NULL.
    • Tipos de outras extensões cujos autores ainda não adotaram a API de erro suave.
  • Só converte a partir de texto, que é o formato em que dados sujos costumam chegar. Casts como numeric → int com arredondamento são outra operação e não fazem parte desta extensão.

Desempenho

1.000.000 de linhas, 10% inválidas, PostgreSQL 16, sem workers paralelos, mediana de 5 execuções:

Método Tempo
try_cast(v, NULL::int) ~110 ms
CASE WHEN pg_input_is_valid(v, 'int4') THEN v::int END ~117 ms
Função PL/pgSQL com BEGIN ... EXCEPTION ... END ~1.790 ms

O try_cast é tão rápido quanto a checagem nativa pg_input_is_valid(), porque os dois usam o mesmo mecanismo. A vantagem é que uma única chamada faz a checagem e a conversão, aceita valor padrão e respeita o modificador do tipo de destino. Rode você mesmo com psql -f bench/benchmark.sql.

O PostgreSQL já não tem isso?

Ainda não. O PostgreSQL 16 trouxe pg_input_is_valid() e pg_input_error_info(), que dizem se um valor é válido, mas não convertem. A sintaxe do padrão SQL, CAST(x AS int DEFAULT 0 ON CONVERSION ERROR), está em discussão na lista pgsql-hackers, mas não entrou no PostgreSQL 19 (até o RC 1). O pg_try_cast funciona hoje, a partir do PostgreSQL 16.

Rodando os testes

make
sudo make install
make installcheck          # precisa de um servidor rodando ao qual você consiga conectar

Documentação extra

Contribuindo

Issues e pull requests são bem-vindos. Qualquer mudança de comportamento deve vir com um teste de regressão em test/sql/ e a saída esperada em test/expected/.

Licença

Distribuída sob a Licença PostgreSQL.