Replicant.Casting.Types (Replicant v1.3.0)

Copy Markdown View Source

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:

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

cast_record(record, type)

@spec cast_record(binary(), term()) :: term()

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")
:nan

Returns the original value if casting fails.