Current section
Files
Jump to
Current section
Files
lib/xqlite/type_extension.ex
defmodule Xqlite.TypeExtension do
@moduledoc """
Behaviour for converting between Elixir types and SQLite storage values.
SQLite supports five storage types: NULL, INTEGER, REAL, TEXT, and BLOB.
Type extensions bridge the gap between richer Elixir types (DateTime, Date,
custom structs) and these storage types.
## Callbacks
* `encode/1` ā converts an Elixir term to a SQLite-compatible value.
Return `{:ok, sqlite_value}` on success or `:skip` to pass to the next
extension in the chain.
* `decode/1` ā converts a SQLite value back to an Elixir term.
Return `{:ok, elixir_term}` on success or `:skip` to pass to the next
extension.
## Built-in extensions
* `Xqlite.TypeExtension.DateTime` ā `DateTime` ā ISO 8601 text
* `Xqlite.TypeExtension.NaiveDateTime` ā `NaiveDateTime` ā ISO 8601 text
* `Xqlite.TypeExtension.Date` ā `Date` ā `YYYY-MM-DD` text
* `Xqlite.TypeExtension.Time` ā `Time` ā `HH:MM:SS` text
## Extension ordering
Extensions are applied in list order. The first extension that returns
`{:ok, value}` wins ā remaining extensions are not consulted. This matters
most for `decode/1`, where multiple extensions might match the same string
format. Place more specific extensions before general ones.
## Example
defmodule MyApp.DecimalExtension do
@behaviour Xqlite.TypeExtension
@impl true
def encode(%Decimal{} = d), do: {:ok, Decimal.to_string(d)}
def encode(_), do: :skip
@impl true
def decode(_), do: :skip
end
# Usage with Xqlite.stream/4:
Xqlite.stream(conn, "SELECT amount FROM payments", [],
type_extensions: [MyApp.DecimalExtension, Xqlite.TypeExtension.DateTime])
"""
@doc """
Converts an Elixir term to a SQLite-compatible storage value.
Return `{:ok, sqlite_value}` where `sqlite_value` is an integer, float,
binary, or nil. Return `:skip` if this extension does not handle the
given value.
"""
@callback encode(value :: term()) :: {:ok, Xqlite.sqlite_value()} | :skip
@doc """
Converts a SQLite storage value back to an Elixir term.
Return `{:ok, elixir_term}` on successful conversion. Return `:skip`
if this extension does not handle the given value.
"""
@callback decode(value :: Xqlite.sqlite_value()) :: {:ok, term()} | :skip
@doc """
Encodes a list of query parameters through the extension chain.
Handles both positional parameter lists and keyword parameter lists.
Values that no extension handles pass through unchanged.
"""
@spec encode_params(params :: list() | keyword(), extensions :: [module()]) ::
list() | keyword()
def encode_params(params, []), do: params
def encode_params([{key, _} | _] = params, extensions) when is_atom(key) do
Enum.map(params, fn
{k, value} when is_atom(k) -> {k, encode_value(value, extensions)}
other -> other
end)
end
def encode_params(params, extensions) when is_list(params) do
Enum.map(params, fn value -> encode_value(value, extensions) end)
end
@doc """
Decodes result rows through the extension chain.
Each cell in each row is passed through the extension chain.
Values that no extension handles pass through unchanged.
"""
@spec decode_rows(rows :: [[term()]], extensions :: [module()]) :: [[term()]]
def decode_rows(rows, []), do: rows
def decode_rows(rows, extensions) do
Enum.map(rows, fn row ->
Enum.map(row, fn value -> decode_value(value, extensions) end)
end)
end
@doc """
Encodes a single value through the extension chain.
Returns the encoded value from the first extension that handles it,
or the original value if no extension matches.
"""
@spec encode_value(value :: term(), extensions :: [module()]) :: term()
def encode_value(value, []), do: value
def encode_value(value, [ext | rest]) do
case ext.encode(value) do
{:ok, encoded} -> encoded
:skip -> encode_value(value, rest)
end
end
@doc """
Decodes a single value through the extension chain.
Returns the decoded value from the first extension that handles it,
or the original value if no extension matches.
"""
@spec decode_value(value :: term(), extensions :: [module()]) :: term()
def decode_value(value, []), do: value
def decode_value(value, [ext | rest]) do
case ext.decode(value) do
{:ok, decoded} -> decoded
:skip -> decode_value(value, rest)
end
end
end