Cast from Postgres to Elixir types
Implementation inspired by Cainophile, Supabase Realtime and Sequin.
Vendored from walex 4.8.0 (WalEx.Casting.Types).
Raise-sites (decode-boundary contract)
cast_record/2 is lenient by default: every clause that parses a scalar
wraps the parse in a case/Integer.parse/Float.parse and falls back to
the original string on failure, and the catch-all clause returns the value
unchanged for unknown types. Parse-failures therefore never raise.
Three clauses can still raise on truly-malformed input, because they call a bang/raising function with no local rescue:
Decimal.new/1(the"numeric"and"decimal"scalar clauses and the"_numeric"/"_decimal"array clauses) raisesDecimal.Erroron a malformed numeric string.Base.decode16!/2(the"bytea"scalar clause and the"_bytea"array clause) raisesArgumentErroron non-hex bytea payloads.DateTime.from_naive!/2(the"timestamp"scalar clause and the"_timestamp"array clause) raises on an invalid naive datetime; in practice the precedingNaiveDateTime.from_iso8601/1guards it, but the bang call is retained verbatim from upstream.
Jason.decode/1 (the "jsonb"/"json" scalar and "_jsonb"/"_json"
array clauses) does NOT raise — it returns {:error, _}, which the
surrounding case collapses to the lenient fallback. money (1.3.0) is
lenient too: it parses only the strict C/en-US shapes and delivers the
original string otherwise, because money output is locale-dependent
(lc_monetary) and a forced Decimal is a silently WRONG value under a
non-C locale — see ADR-0008. timetz (1.3.0) delivers the raw string
(no Elixir type carries a time WITH its offset; the old truncation silently
dropped fractional seconds and offset).
Because malformed numeric/bytea inputs raise through cast_record/2, the
Assembler (Task 13) MUST invoke cast_record/2 inside a decode boundary —
either Replicant.Decoder.decode/1 (Task 8) or its own try/rescue — so a
single malformed cell scrubs to a boundary error rather than crashing the
pipeline. Do NOT widen cast_record/2 to swallow these: the lenient
fallback already covers ordinary parse-failures; only genuinely-malformed
input reaches the raising path, and the boundary is the correct place to
scrub it.
Array clauses (multidimensional, 1.3.0)
Every casted array type recurses through cast_array_elements/2, so a 2-D+
literal (numeric[][], timestamptz[][], …) casts element-wise exactly like
its scalar clause — including the raise-sites above (which the decode
boundary scrubs) and the lenient fallbacks. Only _int*/_float* recursed
before 1.3.0; every other array clause fed a nested LIST into its scalar
parser and raised on ordinary Postgres data.
Summary
Functions
Casts a PostgreSQL string value to its appropriate Elixir type.
Functions
Casts a PostgreSQL string value to its appropriate Elixir type.
Examples
iex> cast_record("t", "bool")
true
iex> cast_record("123", "int4")
123
iex> cast_record("123.45", "numeric")
#Decimal<123.45>
iex> cast_record("{1,2,3}", "_int4")
[1, 2, 3]
iex> cast_record("2024-01-15T10:30:00Z", "timestamptz")
#DateTime<2024-01-15 10:30:00Z>Special values like NaN and Infinity are handled:
iex> cast_record("NaN", "float8")
:nanReturns the original value if casting fails.