Current section

Files

Jump to
walex lib walex casting types.ex
Raw

lib/walex/casting/types.ex

defmodule WalEx.Casting.Types do
@moduledoc """
Cast from Postgres to Elixir types
Implementation inspired by Cainophile, Supabase Realtime and Sequin
"""
@doc """
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.
"""
def cast_record("t", "bool"), do: true
def cast_record("f", "bool"), do: false
# Handle interval type before general integer pattern
def cast_record(record, "interval") when is_binary(record), do: record
# Handle special numeric values before general numeric patterns
def cast_record("NaN", type) when type in ["float4", "float8", "numeric"], do: :nan
def cast_record("Infinity", type) when type in ["float4", "float8", "numeric"], do: :infinity
def cast_record("-Infinity", type) when type in ["float4", "float8", "numeric"],
do: :neg_infinity
def cast_record(record, <<"int", _::binary>>) when is_binary(record) do
case Integer.parse(record) do
{int, _} ->
int
:error ->
record
end
end
def cast_record(record, <<"float", _::binary>>) when is_binary(record) do
case Float.parse(record) do
{float, _} ->
float
:error ->
record
end
end
def cast_record(record, "numeric") when is_binary(record), do: Decimal.new(record)
def cast_record(record, "decimal"), do: cast_record(record, "numeric")
def cast_record(record, "timestamp") when is_binary(record) do
with {:ok, %NaiveDateTime{} = naive_date_time} <- Timex.parse(record, "{RFC3339}"),
%DateTime{} = date_time <- Timex.to_datetime(naive_date_time) do
date_time
else
_ -> record
end
end
def cast_record(record, "timestamptz") when is_binary(record) do
case Timex.parse(record, "{RFC3339}") do
{:ok, %DateTime{} = date_time} ->
date_time
_ ->
record
end
end
def cast_record(record, "jsonb") when is_binary(record) do
case Jason.decode(record) do
{:ok, json} ->
json
_ ->
record
end
end
def cast_record(record, "json"), do: cast_record(record, "jsonb")
def cast_record(record, "uuid") when is_binary(record), do: record
def cast_record(record, "date") when is_binary(record) do
case Date.from_iso8601(record) do
{:ok, date} -> date
_ -> record
end
end
def cast_record(record, "time") when is_binary(record) do
case Time.from_iso8601(record) do
{:ok, time} -> time
_ -> record
end
end
def cast_record(record, "timetz") when is_binary(record) do
# PostgreSQL timetz format includes timezone offset
# For now, just parse as regular time
case Time.from_iso8601(String.slice(record, 0..7)) do
{:ok, time} -> time
_ -> record
end
end
def cast_record(record, "money") when is_binary(record) do
# Remove currency symbol and convert to decimal
record
|> String.replace(~r/[^\d.-]/, "")
|> Decimal.new()
end
def cast_record(record, "bytea") when is_binary(record) do
# PostgreSQL bytea hex format starts with \x
if String.starts_with?(record, "\\x") do
record
|> String.slice(2..-1//1)
|> Base.decode16!(case: :mixed)
else
record
end
end
def cast_record(record, "inet") when is_binary(record), do: record
def cast_record(record, "cidr") when is_binary(record), do: record
def cast_record(record, "macaddr") when is_binary(record), do: record
def cast_record(record, "macaddr8") when is_binary(record), do: record
def cast_record(record, "xml") when is_binary(record), do: record
# Geometric types - return as strings for now
def cast_record(record, "point") when is_binary(record), do: record
def cast_record(record, "line") when is_binary(record), do: record
def cast_record(record, "lseg") when is_binary(record), do: record
def cast_record(record, "box") when is_binary(record), do: record
def cast_record(record, "path") when is_binary(record), do: record
def cast_record(record, "polygon") when is_binary(record), do: record
def cast_record(record, "circle") when is_binary(record), do: record
# Range types - return as strings for now
def cast_record(record, "int4range") when is_binary(record), do: record
def cast_record(record, "int8range") when is_binary(record), do: record
def cast_record(record, "numrange") when is_binary(record), do: record
def cast_record(record, "tsrange") when is_binary(record), do: record
def cast_record(record, "tstzrange") when is_binary(record), do: record
def cast_record(record, "daterange") when is_binary(record), do: record
# Text search types
def cast_record(record, "tsvector") when is_binary(record), do: record
def cast_record(record, "tsquery") when is_binary(record), do: record
# Other specialized types
def cast_record(record, "bit") when is_binary(record), do: record
def cast_record(record, "varbit") when is_binary(record), do: record
def cast_record(record, "oid") when is_binary(record), do: record
def cast_record(record, "regclass") when is_binary(record), do: record
def cast_record(record, "regproc") when is_binary(record), do: record
def cast_record(record, "regtype") when is_binary(record), do: record
def cast_record(record, "regrole") when is_binary(record), do: record
def cast_record(record, "regnamespace") when is_binary(record), do: record
# PostgreSQL internal types
def cast_record(record, "name") when is_binary(record), do: record
def cast_record(record, "pg_lsn") when is_binary(record), do: record
def cast_record(record, "pg_snapshot") when is_binary(record), do: record
def cast_record(record, "txid_snapshot") when is_binary(record), do: record
# Array type casting - integer arrays with support for multidimensional arrays
def cast_record(array_string, <<"_int", _::binary>>) when is_binary(array_string) do
case WalEx.Casting.ArrayParser.parse(array_string) do
{:ok, elements} ->
cast_array_elements(elements, &String.to_integer/1)
{:error, _} ->
array_string
end
end
# Array type casting - float arrays
def cast_record(array_string, <<"_float", _::binary>>) when is_binary(array_string) do
case WalEx.Casting.ArrayParser.parse(array_string) do
{:ok, elements} ->
cast_array_elements(elements, &String.to_float/1)
{:error, _} ->
array_string
end
end
# Array type casting - text/varchar arrays
def cast_record(array_string, column_type)
when is_binary(array_string) and column_type in ["_text", "_varchar"] do
case WalEx.Casting.ArrayParser.parse(array_string) do
{:ok, elements} -> elements
{:error, _} -> array_string
end
end
# Array type casting - boolean arrays
def cast_record(array_string, "_bool") when is_binary(array_string) do
case WalEx.Casting.ArrayParser.parse(array_string) do
{:ok, elements} ->
Enum.map(elements, fn
nil -> nil
"t" -> true
"f" -> false
other -> other
end)
{:error, _} ->
array_string
end
end
# Array type casting - numeric/decimal arrays
def cast_record(array_string, "_numeric") when is_binary(array_string) do
case WalEx.Casting.ArrayParser.parse(array_string) do
{:ok, elements} ->
Enum.map(elements, fn
nil -> nil
elem -> Decimal.new(elem)
end)
{:error, _} ->
array_string
end
end
def cast_record(array_string, "_decimal"), do: cast_record(array_string, "_numeric")
# Array type casting - timestamptz arrays
def cast_record(array_string, "_timestamptz") when is_binary(array_string) do
case WalEx.Casting.ArrayParser.parse(array_string) do
{:ok, elements} ->
Enum.map(elements, fn
nil ->
nil
elem ->
case Timex.parse(elem, "{RFC3339}") do
{:ok, %DateTime{} = dt} -> dt
_ -> elem
end
end)
{:error, _} ->
array_string
end
end
# Array type casting - timestamp arrays
def cast_record(array_string, "_timestamp") when is_binary(array_string) do
case WalEx.Casting.ArrayParser.parse(array_string) do
{:ok, elements} ->
Enum.map(elements, fn
nil ->
nil
elem ->
with {:ok, %NaiveDateTime{} = naive} <- Timex.parse(elem, "{RFC3339}"),
%DateTime{} = dt <- Timex.to_datetime(naive) do
dt
else
_ -> elem
end
end)
{:error, _} ->
array_string
end
end
# Array type casting - UUID arrays
def cast_record(array_string, "_uuid") when is_binary(array_string) do
case WalEx.Casting.ArrayParser.parse(array_string) do
{:ok, elements} -> elements
{:error, _} -> array_string
end
end
# Array type casting - JSONB arrays
def cast_record(array_string, "_jsonb") when is_binary(array_string) do
case WalEx.Casting.ArrayParser.parse(array_string) do
{:ok, elements} ->
Enum.map(elements, fn
nil ->
nil
elem ->
case Jason.decode(elem) do
{:ok, json} -> json
_ -> elem
end
end)
{:error, _} ->
array_string
end
end
def cast_record(array_string, "_json"), do: cast_record(array_string, "_jsonb")
# Array type casting - date arrays
def cast_record(array_string, "_date") when is_binary(array_string) do
case WalEx.Casting.ArrayParser.parse(array_string) do
{:ok, elements} ->
Enum.map(elements, fn
nil ->
nil
elem ->
case Date.from_iso8601(elem) do
{:ok, date} -> date
_ -> elem
end
end)
{:error, _} ->
array_string
end
end
# Array type casting - time arrays
def cast_record(array_string, "_time") when is_binary(array_string) do
case WalEx.Casting.ArrayParser.parse(array_string) do
{:ok, elements} ->
Enum.map(elements, fn
nil ->
nil
elem ->
case Time.from_iso8601(elem) do
{:ok, time} -> time
_ -> elem
end
end)
{:error, _} ->
array_string
end
end
# Array type casting - network address arrays (inet, cidr, macaddr)
def cast_record(array_string, "_inet") when is_binary(array_string) do
case WalEx.Casting.ArrayParser.parse(array_string) do
{:ok, elements} -> elements
{:error, _} -> array_string
end
end
def cast_record(array_string, "_cidr") when is_binary(array_string) do
case WalEx.Casting.ArrayParser.parse(array_string) do
{:ok, elements} -> elements
{:error, _} -> array_string
end
end
def cast_record(array_string, "_macaddr") when is_binary(array_string) do
case WalEx.Casting.ArrayParser.parse(array_string) do
{:ok, elements} -> elements
{:error, _} -> array_string
end
end
# Array type casting - money arrays
def cast_record(array_string, "_money") when is_binary(array_string) do
case WalEx.Casting.ArrayParser.parse(array_string) do
{:ok, elements} ->
Enum.map(elements, fn
nil ->
nil
elem ->
elem
|> String.replace(~r/[^\d.-]/, "")
|> Decimal.new()
end)
{:error, _} ->
array_string
end
end
# Array type casting - bytea arrays
def cast_record(array_string, "_bytea") when is_binary(array_string) do
case WalEx.Casting.ArrayParser.parse(array_string) do
{:ok, elements} ->
Enum.map(elements, fn
nil ->
nil
elem ->
if String.starts_with?(elem, "\\x") do
elem
|> String.slice(2..-1//1)
|> Base.decode16!(case: :mixed)
else
elem
end
end)
{:error, _} ->
array_string
end
end
# Fallback - return record unchanged if no specific casting is defined
def cast_record(record, _column_type) do
record
end
@doc false
# Helper function to recursively cast array elements, supporting nested arrays
defp cast_array_elements(elements, cast_fn) do
Enum.map(elements, fn
nil ->
nil
elem when is_list(elem) ->
# Handle nested arrays recursively
cast_array_elements(elem, cast_fn)
elem ->
cast_fn.(elem)
end)
end
end