Extensions
- pg_try_cast 1.0.0
- TRY_CAST / TRY_CONVERT for PostgreSQL
Documentation
- CHANGELOG
- Changelog
- COMO_FUNCIONA
- Como o pg_try_cast funciona por dentro
- README.pt-BR
- pg_try_cast
README
Contents
pg_try_cast
TRY_CAST and TRY_CONVERT for PostgreSQL. Convert a value to any type and get
NULL (or a fallback of your choice) when it doesn’t fit, instead of an error that aborts the whole query.
SELECT try_cast('42', NULL::int); -- 42
SELECT try_cast('abc', NULL::int); -- NULL (no error!)
SELECT try_cast('abc', 0); -- 0 (fallback value)
Why?
One bad row is enough to break a query in PostgreSQL:
SELECT id, price::numeric FROM staging_import;
-- ERROR: invalid input syntax for type numeric: "R$ 10,00"
SQL Server has TRY_CAST and TRY_CONVERT for this, and Oracle has CAST ... DEFAULT ... ON CONVERSION ERROR.
PostgreSQL has neither yet. The usual workarounds are a regex in front of every cast, which is fragile and different
for each type, or a PL/pgSQL function with an EXCEPTION block, which opens a subtransaction on every row and is slow.
pg_try_cast is a small C extension that does it properly. It works with any type that PostgreSQL can read
from text, including integers, numerics, dates, timestamps, uuid, json/jsonb, arrays, ranges, enums, domains and
composite types. It is also ~16× faster than the PL/pgSQL approach.
Usage
try_cast(value text, fallback anyelement) → anyelement
The type of fallback decides what value is converted to. Its value is what you get back when the
conversion fails.
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 (out of range)
SELECT try_cast('n/a', 0); -- 0
SELECT try_cast('{"a":', '{}'::jsonb); -- {}
SELECT try_cast(NULL, 0); -- NULL (NULL in, NULL out)
try_convert(fallback anyelement, value text) → anyelement
The same function with SQL Server’s argument order, to make migrations easier:
SELECT try_convert(NULL::int, '42'); -- 42
SELECT try_convert(NULL::int, 'abc'); -- NULL
Real-world example: cleaning an import
SELECT id,
try_cast(raw_amount, NULL::numeric(12,2)) AS amount,
try_cast(raw_date, NULL::date) AS paid_on,
try_cast(raw_email, NULL::varchar(120)) AS email
FROM staging_import;
-- find the rows that need attention
SELECT * FROM staging_import
WHERE raw_amount IS NOT NULL
AND try_cast(raw_amount, NULL::numeric) IS NULL;
SQL Server → PostgreSQL cheat sheet
| 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) |
Installation
Requires PostgreSQL 16 or newer, because it relies on the “soft error” input functions added in PostgreSQL 16.
Linux (Debian / Ubuntu)
sudo apt install build-essential postgresql-server-dev-17 # use your PostgreSQL major version
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 # package name may vary with your repo
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 (any OS, including Windows)
docker build -t pg_try_cast . # PostgreSQL 17 by default
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
Build error mentioning
clangorllvm? Some PostgreSQL packages were built with JIT support and try to compile extra LLVM bitcode. You don’t need it. Build withmake with_llvm=no && sudo make with_llvm=no install.
Enable it in your database
CREATE EXTENSION pg_try_cast;
The extension is marked trusted, so any user with CREATE privilege on the database can enable it.
Superuser is not required.
How it behaves
- NULL in, NULL out.
try_cast(NULL, 0)returnsNULL, likeCAST(NULL AS int). - The input is text. Other types can be passed with
::text, for exampletry_cast(my_col::text, NULL::int). - Type modifiers are respected, following the same rules as
INSERTorCOPYinto a column of that type:try_cast('abcd', NULL::varchar(3))returnsNULLbecause the value does not fit. A plainCASTwould truncate it to'abc'.try_cast('123.456', NULL::numeric(5,2))returns123.46, rounded asnumeric(5,2)does.try_cast('12345.6', NULL::numeric(5,2))returnsNULL(overflow).
- Domain
CHECKconstraints are checked. IfCREATE DOMAIN positive AS int CHECK (VALUE > 0), thentry_cast('-5', NULL::positive)returnsNULL. - The fallback type must be known.
try_cast('1', NULL)is an error. WriteNULL::int.
Limitations
- A few input functions do not support soft errors, so they still raise an error:
regtypewith a syntactically invalid type name (for example'###'). Unknown but valid names returnNULL.- Data types from other extensions whose authors have not adopted the soft-error API.
- Only conversion from text is supported, because that is what dirty data usually is. Casts such as
numeric → intwith rounding are a different operation and are not covered here.
Performance
1,000,000 rows, 10% invalid, PostgreSQL 16, no parallel workers, median of 5 runs:
| Method | Time |
|---|---|
try_cast(v, NULL::int) |
~110 ms |
CASE WHEN pg_input_is_valid(v, 'int4') THEN v::int END |
~117 ms |
PL/pgSQL function with BEGIN ... EXCEPTION ... END |
~1,790 ms |
try_cast runs as fast as the built-in pg_input_is_valid() check, because both use the same mechanism. The
advantage is that one call does both the check and the conversion, works with a fallback value and knows the type
modifier of the target. Run it yourself with psql -f bench/benchmark.sql.
Doesn’t PostgreSQL already have this?
Not yet. PostgreSQL 16 added pg_input_is_valid() and pg_input_error_info(), which tell you whether a value
is valid but do not convert it. The SQL-standard syntax CAST(x AS int DEFAULT 0 ON CONVERSION ERROR) has been
discussed on pgsql-hackers, but it is not part of PostgreSQL 19 (as of RC 1). pg_try_cast works today, from
PostgreSQL 16 onward.
Running the tests
make
sudo make install
make installcheck # needs a running server you can connect to
Under the hood
A walkthrough of the C code, written for people who have never built a PostgreSQL extension, is available in Portuguese: docs/COMO_FUNCIONA.md.
Contributing
Issues and pull requests are welcome. Please add a regression test in test/sql/ and its expected output in
test/expected/ for any change in behaviour.
License
Released under the PostgreSQL License.