Current section
Files
Jump to
Current section
Files
lib/quack_db/type.ex
defmodule QuackDB.Type do
@moduledoc """
DuckDB SQL type rendering shared by DDL and source helpers.
The protocol codec has its own logical type metadata for decoding wire values.
This module is the SQL-facing counterpart: it renders user-friendly type specs
into DuckDB type names for generated SQL.
"""
@scalar_types %{
boolean: "BOOLEAN",
bool: "BOOLEAN",
tinyint: "TINYINT",
smallint: "SMALLINT",
integer: "INTEGER",
int: "INTEGER",
bigint: "BIGINT",
utinyint: "UTINYINT",
usmallint: "USMALLINT",
uinteger: "UINTEGER",
uint: "UINTEGER",
ubigint: "UBIGINT",
hugeint: "HUGEINT",
uhugeint: "UHUGEINT",
float: "FLOAT",
real: "FLOAT",
double: "DOUBLE",
decimal: "DECIMAL",
varchar: "VARCHAR",
string: "VARCHAR",
text: "VARCHAR",
char: "CHAR",
blob: "BLOB",
json: "JSON",
date: "DATE",
time: "TIME",
time_tz: "TIMETZ",
time_ns: "TIME_NS",
timestamp: "TIMESTAMP",
timestamp_s: "TIMESTAMP_S",
timestamp_ms: "TIMESTAMP_MS",
timestamp_ns: "TIMESTAMP_NS",
timestamp_tz: "TIMESTAMPTZ",
timestamptz: "TIMESTAMPTZ",
interval: "INTERVAL",
uuid: "UUID",
bit: "BIT",
bignum: "BIGNUM",
geometry: "GEOMETRY"
}
@type spec ::
atom()
| String.t()
| {:varchar, pos_integer()}
| {:char, pos_integer()}
| {:decimal, pos_integer(), non_neg_integer()}
| {:list, spec()}
| {:array, spec(), pos_integer()}
| {:map, spec(), spec()}
| {:struct, keyword(spec()) | map()}
@doc "Renders a DuckDB SQL type spec as iodata."
@spec to_sql(spec()) :: iodata()
def to_sql(type) when is_atom(type) do
case Map.fetch(@scalar_types, type) do
{:ok, sql} -> sql
:error -> raise ArgumentError, "unsupported DuckDB column type: #{inspect(type)}"
end
end
def to_sql({:varchar, size}), do: ["VARCHAR(", integer!(size), ")"]
def to_sql({:char, size}), do: ["CHAR(", integer!(size), ")"]
def to_sql({:decimal, width, scale}),
do: ["DECIMAL(", integer!(width), ", ", integer!(scale), ")"]
def to_sql({:list, child_type}), do: [to_sql(child_type), "[]"]
def to_sql({:array, child_type, size}), do: [to_sql(child_type), "[", integer!(size), "]"]
def to_sql({:map, key_type, value_type}),
do: ["MAP(", to_sql(key_type), ", ", to_sql(value_type), ")"]
def to_sql({:struct, fields}) when is_list(fields) or is_map(fields) do
fields =
fields |> Enum.map(fn {name, type} -> [quote_identifier(name), " ", to_sql(type)] end)
["STRUCT(", Enum.intersperse(fields, ", "), ")"]
end
def to_sql(type) when is_binary(type), do: type
def to_sql(type) do
raise ArgumentError, "unsupported DuckDB column type: #{inspect(type)}"
end
@doc "Renders an identifier with DuckDB SQL quoting."
@spec quote_identifier(atom() | String.t()) :: iodata()
def quote_identifier(value) when is_atom(value),
do: value |> Atom.to_string() |> quote_identifier()
def quote_identifier(value) when is_binary(value) do
[~s("), String.replace(value, ~s("), ~s("")), ~s(")]
end
def quote_identifier(value) do
raise ArgumentError, "expected identifier as atom or string, got: #{inspect(value)}"
end
defp integer!(value) when is_integer(value) and value >= 0, do: Integer.to_string(value)
defp integer!(value) do
raise ArgumentError, "expected non-negative integer, got: #{inspect(value)}"
end
end