Current section

Files

Jump to
ducky src ducky.erl
Raw

src/ducky.erl

-module(ducky).
-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]).
-define(FILEPATH, "src/ducky.gleam").
-export([connect/1, close/1, path/1, with_connection/2, transaction/2, prepare/2, execute/2, finalize/1, with_statement/3, append/3, sql/1, parameter/2, parameters/2, returning/2, run/2, as_columns/2, column/2, timestamp_decoder/0, date_decoder/0, time_decoder/0, interval_decoder/0, decimal_decoder/0, get/2, field/2, int/1, float/1, text/1, blob/1, bool/1, null/0, nullable/2, timestamp/1, date/1, time/1, interval/3, decimal/1]).
-export_type([error/0, value/0, row/0, returned/1, columnar/0, 'query'/1, connection/0, statement/0]).
-if(?OTP_RELEASE >= 27).
-define(MODULEDOC(Str), -moduledoc(Str)).
-define(DOC(Str), -doc(Str)).
-else.
-define(MODULEDOC(Str), -compile([])).
-define(DOC(Str), -compile([])).
-endif.
?MODULEDOC(
" Native DuckDB driver for Gleam.\n"
"\n"
" ## Quick Start\n"
"\n"
" Build a query with `sql`, run it with `run`:\n"
"\n"
" ```gleam\n"
" import ducky\n"
" import gleam/io\n"
" import gleam/result\n"
"\n"
" pub fn main() {\n"
" use conn <- ducky.with_connection(\":memory:\")\n"
"\n"
" use _ <- result.try(\n"
" ducky.sql(\"CREATE TABLE ducks (name TEXT, quack_volume INT)\")\n"
" |> ducky.run(conn),\n"
" )\n"
" use _ <- result.try(\n"
" ducky.sql(\"INSERT INTO ducks VALUES ('Duck Norris', 100)\")\n"
" |> ducky.run(conn),\n"
" )\n"
"\n"
" use loud <- result.map(\n"
" ducky.sql(\"SELECT name FROM ducks ORDER BY quack_volume DESC LIMIT 1\")\n"
" |> ducky.run(conn),\n"
" )\n"
"\n"
" case loud.rows {\n"
" [ducky.Row([ducky.Text(name), ..])] -> io.println(name <> \" wins!\")\n"
" _ -> io.println(\"The pond is empty...\")\n"
" }\n"
" }\n"
" ```\n"
"\n"
" For typed rows, attach a decoder with `returning`:\n"
"\n"
" ```gleam\n"
" import ducky\n"
" import gleam/dynamic/decode\n"
"\n"
" pub type Duck {\n"
" Duck(name: String, quack_volume: Int)\n"
" }\n"
"\n"
" pub fn loudest(conn) {\n"
" let duck_decoder = {\n"
" use name <- decode.field(0, decode.string)\n"
" use quack_volume <- decode.field(1, decode.int)\n"
" decode.success(Duck(name:, quack_volume:))\n"
" }\n"
"\n"
" ducky.sql(\"SELECT name, quack_volume FROM ducks ORDER BY quack_volume DESC\")\n"
" |> ducky.returning(duck_decoder)\n"
" |> ducky.run(conn)\n"
" }\n"
" ```\n"
).
-type error() :: {connection_failed, binary()} |
{query_syntax_error, binary()} |
{unsupported_parameter_type, binary()} |
statement_finalized |
{decode_failed, list(gleam@dynamic@decode:decode_error())} |
{database_error, binary()}.
-type value() :: null |
{boolean, boolean()} |
{tiny_int, integer()} |
{small_int, integer()} |
{integer, integer()} |
{big_int, integer()} |
{float, float()} |
{double, float()} |
{decimal, binary()} |
{text, binary()} |
{blob, bitstring()} |
{timestamp, integer()} |
{date, integer()} |
{time, integer()} |
{interval, integer(), integer(), integer()} |
{list, list(value())} |
{array, list(value())} |
{map, gleam@dict:dict(binary(), value())} |
{struct, gleam@dict:dict(binary(), value())} |
{union, binary(), value()}.
-type row() :: {row, list(value())}.
-type returned(DMU) :: {returned, integer(), list(DMU)}.
-type columnar() :: {columnar, list(binary()), list(list(value()))}.
-opaque 'query'(DMV) :: {'query',
binary(),
list(value()),
fun((list(gleam@dynamic:dynamic_())) -> {ok, DMV} | {error, error()})}.
-opaque connection() :: {connection, ducky@internal@connection:connection()}.
-opaque statement() :: {statement, ducky@internal@ffi:native_statement()}.
-file("src/ducky.gleam", 925).
-spec error_from_tag(binary(), binary()) -> error().
error_from_tag(Tag, Message) ->
case Tag of
<<"connection_failed"/utf8>> ->
{connection_failed, Message};
<<"query_syntax_error"/utf8>> ->
{query_syntax_error, Message};
<<"unsupported_parameter_type"/utf8>> ->
{unsupported_parameter_type, Message};
<<"statement_finalized"/utf8>> ->
statement_finalized;
<<"database_error"/utf8>> ->
{database_error, Message};
_ ->
{database_error,
<<<<<<"["/utf8, Tag/binary>>/binary, "] "/utf8>>/binary,
Message/binary>>}
end.
-file("src/ducky.gleam", 936).
-spec error_tuple_decoder() -> gleam@dynamic@decode:decoder({binary(), binary()}).
error_tuple_decoder() ->
gleam@dynamic@decode:subfield(
[0],
{decoder, fun gleam@dynamic@decode:decode_dynamic/1},
fun(Error_type_dyn) ->
gleam@dynamic@decode:subfield(
[1],
{decoder, fun gleam@dynamic@decode:decode_string/1},
fun(Message) ->
Error_type = case gleam_stdlib:classify_dynamic(
Error_type_dyn
) of
<<"Atom"/utf8>> ->
erlang:atom_to_binary(Error_type_dyn);
_ ->
<<"unknown"/utf8>>
end,
gleam@dynamic@decode:success({Error_type, Message})
end
)
end
).
-file("src/ducky.gleam", 959).
?DOC(" Fallback decoder for unexpected error formats.\n").
-spec fallback_decode(gleam@dynamic:dynamic_()) -> error().
fallback_decode(Err) ->
case gleam@dynamic@decode:run(Err, error_tuple_decoder()) of
{ok, {Tag, Msg}} ->
error_from_tag(Tag, Msg);
{error, _} ->
{database_error,
<<"Unknown error: "/utf8, (gleam@string:inspect(Err))/binary>>}
end.
-file("src/ducky.gleam", 949).
?DOC(" Decodes an error from the NIF layer.\n").
-spec decode_nif_error(gleam@dynamic:dynamic_()) -> error().
decode_nif_error(Err) ->
Decoder = gleam@dynamic@decode:at([1], error_tuple_decoder()),
case gleam@dynamic@decode:run(Err, Decoder) of
{ok, {Tag, Msg}} ->
error_from_tag(Tag, Msg);
{error, _} ->
fallback_decode(Err)
end.
-file("src/ducky.gleam", 163).
?DOC(
" Opens a connection to a DuckDB database.\n"
"\n"
" Must call `close()` when done. Use `with_connection()` instead\n"
" for automatic cleanup.\n"
"\n"
" ## Examples\n"
"\n"
" ```gleam\n"
" connect(\":memory:\")\n"
" // => Ok(Connection(...))\n"
"\n"
" connect(\"data.duckdb\")\n"
" // => Ok(Connection(...))\n"
" ```\n"
).
-spec connect(binary()) -> {ok, connection()} | {error, error()}.
connect(Path) ->
case Path of
<<""/utf8>> ->
{error, {connection_failed, <<"path cannot be empty"/utf8>>}};
_ ->
_pipe = ducky@internal@connection:do_connect(Path),
_pipe@1 = gleam@result:map(
_pipe,
fun(Internal) -> {connection, Internal} end
),
gleam@result:map_error(_pipe@1, fun decode_nif_error/1)
end.
-file("src/ducky.gleam", 181).
?DOC(
" Closes a database connection.\n"
"\n"
" ## Examples\n"
"\n"
" ```gleam\n"
" let assert Ok(conn) = connect(\":memory:\")\n"
" let assert Ok(_) = close(conn)\n"
" ```\n"
).
-spec close(connection()) -> {ok, nil} | {error, error()}.
close(Conn) ->
_pipe = ducky@internal@connection:do_close(erlang:element(2, Conn)),
gleam@result:map_error(_pipe, fun decode_nif_error/1).
-file("src/ducky.gleam", 187).
?DOC(" Useful for logging which database a connection points to.\n").
-spec path(connection()) -> binary().
path(Conn) ->
ducky@internal@connection:path(erlang:element(2, Conn)).
-file("src/ducky.gleam", 199).
?DOC(
" Opens a connection, runs the callback, then closes. Preferred over connect/close.\n"
"\n"
" ## Examples\n"
"\n"
" ```gleam\n"
" use conn <- with_connection(\":memory:\")\n"
" ducky.sql(\"SELECT 42\") |> ducky.run(conn)\n"
" ```\n"
).
-spec with_connection(
binary(),
fun((connection()) -> {ok, DNA} | {error, error()})
) -> {ok, DNA} | {error, error()}.
with_connection(Db_path, Callback) ->
gleam@result:'try'(
connect(Db_path),
fun(Conn) ->
Res = Callback(Conn),
_ = close(Conn),
Res
end
).
-file("src/ducky.gleam", 220).
?DOC(
" Wraps a callback in BEGIN/COMMIT. Rolls back on error.\n"
"\n"
" ## Examples\n"
"\n"
" ```gleam\n"
" transaction(conn, fn(conn) {\n"
" use _ <- result.try(ducky.sql(\"UPDATE accounts ...\") |> ducky.run(conn))\n"
" ducky.sql(\"SELECT * FROM accounts\") |> ducky.run(conn)\n"
" })\n"
" ```\n"
).
-spec transaction(
connection(),
fun((connection()) -> {ok, DNF} | {error, error()})
) -> {ok, DNF} | {error, error()}.
transaction(Conn, Callback) ->
gleam@result:'try'(
begin
_pipe = ducky@internal@connection:execute_raw(
erlang:element(2, Conn),
<<"BEGIN TRANSACTION"/utf8>>
),
gleam@result:map_error(_pipe, fun decode_nif_error/1)
end,
fun(_) -> case Callback(Conn) of
{ok, Value} ->
gleam@result:'try'(
begin
_pipe@1 = ducky@internal@connection:execute_raw(
erlang:element(2, Conn),
<<"COMMIT"/utf8>>
),
gleam@result:map_error(
_pipe@1,
fun decode_nif_error/1
)
end,
fun(_) -> {ok, Value} end
);
{error, Err} ->
_ = ducky@internal@connection:execute_raw(
erlang:element(2, Conn),
<<"ROLLBACK"/utf8>>
),
{error, Err}
end end
).
-file("src/ducky.gleam", 264).
?DOC(
" Prepares a SQL statement for repeated execution.\n"
"\n"
" Validates the SQL syntax immediately and returns a statement handle.\n"
" Use `execute` to run the statement with parameters.\n"
"\n"
" ## Examples\n"
"\n"
" ```gleam\n"
" let assert Ok(stmt) = prepare(conn, \"INSERT INTO users (name, age) VALUES (?, ?)\")\n"
" let assert Ok(_) = execute(stmt, [text(\"Alice\"), int(30)])\n"
" let assert Ok(_) = execute(stmt, [text(\"Bob\"), int(25)])\n"
" let assert Ok(_) = finalize(stmt)\n"
" ```\n"
"\n"
" ## Performance\n"
"\n"
" DuckDB caches parsed query plans internally, so repeated executions\n"
" with different parameters benefit from the cached plan. This can\n"
" provide speedups for bulk operations.\n"
).
-spec prepare(connection(), binary()) -> {ok, statement()} | {error, error()}.
prepare(Conn, Sql) ->
_pipe = ducky_nif:prepare(
ducky@internal@connection:native(erlang:element(2, Conn)),
Sql
),
_pipe@1 = gleam@result:map(_pipe, fun(Native) -> {statement, Native} end),
gleam@result:map_error(_pipe@1, fun decode_nif_error/1).
-file("src/ducky.gleam", 911).
?DOC(" Decodes interval 4-tuple {interval, months, days, nanos}.\n").
-spec decode_interval_tuple(gleam@dynamic:dynamic_()) -> {ok, value()} |
{error, error()}.
decode_interval_tuple(Dyn) ->
Decoder = begin
gleam@dynamic@decode:subfield(
[1],
{decoder, fun gleam@dynamic@decode:decode_int/1},
fun(Months) ->
gleam@dynamic@decode:subfield(
[2],
{decoder, fun gleam@dynamic@decode:decode_int/1},
fun(Days) ->
gleam@dynamic@decode:subfield(
[3],
{decoder, fun gleam@dynamic@decode:decode_int/1},
fun(Nanos) ->
gleam@dynamic@decode:success(
{Months, Days, Nanos}
)
end
)
end
)
end
)
end,
case gleam@dynamic@decode:run(Dyn, Decoder) of
{ok, {Months@1, Days@1, Nanos@1}} ->
{ok, {interval, Months@1, Days@1, Nanos@1}};
{error, _} ->
{error,
{database_error, <<"Failed to decode interval value"/utf8>>}}
end.
-file("src/ducky.gleam", 882).
?DOC(" Decodes temporal tagged tuples (timestamp, date, time).\n").
-spec decode_temporal_tuple(gleam@dynamic:dynamic_()) -> {ok, value()} |
{error, error()}.
decode_temporal_tuple(Dyn) ->
Decoder = begin
gleam@dynamic@decode:subfield(
[0],
{decoder, fun gleam@dynamic@decode:decode_dynamic/1},
fun(Tag_dynamic) ->
gleam@dynamic@decode:subfield(
[1],
{decoder, fun gleam@dynamic@decode:decode_int/1},
fun(Value) ->
Tag = case gleam_stdlib:classify_dynamic(Tag_dynamic) of
<<"Atom"/utf8>> ->
erlang:atom_to_binary(Tag_dynamic);
<<"String"/utf8>> ->
_pipe = gleam@dynamic@decode:run(
Tag_dynamic,
{decoder,
fun gleam@dynamic@decode:decode_string/1}
),
gleam@result:unwrap(_pipe, <<""/utf8>>);
_ ->
<<""/utf8>>
end,
gleam@dynamic@decode:success({Tag, Value})
end
)
end
)
end,
case gleam@dynamic@decode:run(Dyn, Decoder) of
{ok, {Tag@1, Value@1}} ->
case Tag@1 of
<<"timestamp"/utf8>> ->
{ok, {timestamp, Value@1}};
<<"date"/utf8>> ->
{ok, {date, Value@1}};
<<"time"/utf8>> ->
{ok, {time, Value@1}};
_ ->
{error,
{database_error,
<<"Unknown temporal type: "/utf8, Tag@1/binary>>}}
end;
{error, _} ->
{error,
{database_error, <<"Failed to decode temporal value"/utf8>>}}
end.
-file("src/ducky.gleam", 814).
?DOC(" Decodes a decimal tagged tuple {decimal, \"string\"}.\n").
-spec decode_decimal_value(gleam@dynamic:dynamic_()) -> {ok, value()} |
{error, error()}.
decode_decimal_value(Dyn) ->
Decoder = begin
gleam@dynamic@decode:subfield(
[1],
{decoder, fun gleam@dynamic@decode:decode_string/1},
fun(Value) -> gleam@dynamic@decode:success(Value) end
)
end,
case gleam@dynamic@decode:run(Dyn, Decoder) of
{ok, Value@1} ->
{ok, {decimal, Value@1}};
{error, _} ->
{error, {database_error, <<"Failed to decode decimal value"/utf8>>}}
end.
-file("src/ducky.gleam", 866).
?DOC(" Decodes a union tagged tuple {union, tag_string, value}.\n").
-spec decode_union_value(gleam@dynamic:dynamic_()) -> {ok, value()} |
{error, error()}.
decode_union_value(Dyn) ->
Decoder = begin
gleam@dynamic@decode:subfield(
[1],
{decoder, fun gleam@dynamic@decode:decode_string/1},
fun(Tag) ->
gleam@dynamic@decode:subfield(
[2],
{decoder, fun gleam@dynamic@decode:decode_dynamic/1},
fun(Value) -> gleam@dynamic@decode:success({Tag, Value}) end
)
end
)
end,
case gleam@dynamic@decode:run(Dyn, Decoder) of
{ok, {Tag@1, Value@1}} ->
gleam@result:map(
decode_value(Value@1),
fun(Decoded_value) -> {union, Tag@1, Decoded_value} end
);
{error, _} ->
{error, {database_error, <<"Failed to decode union value"/utf8>>}}
end.
-file("src/ducky.gleam", 841).
?DOC(" Decodes a map tagged tuple {map, %{key => value}}.\n").
-spec decode_map_value(gleam@dynamic:dynamic_()) -> {ok, value()} |
{error, error()}.
decode_map_value(Dyn) ->
Decoder = begin
gleam@dynamic@decode:subfield(
[1],
gleam@dynamic@decode:dict(
{decoder, fun gleam@dynamic@decode:decode_string/1},
{decoder, fun gleam@dynamic@decode:decode_dynamic/1}
),
fun(Entries) -> gleam@dynamic@decode:success(Entries) end
)
end,
case gleam@dynamic@decode:run(Dyn, Decoder) of
{ok, Entries@1} ->
Pairs = maps:to_list(Entries@1),
gleam@result:map(
gleam@list:try_map(
Pairs,
fun(Pair) ->
{Key, Val} = Pair,
gleam@result:map(
decode_value(Val),
fun(Decoded_val) -> {Key, Decoded_val} end
)
end
),
fun(Decoded_pairs) -> {map, maps:from_list(Decoded_pairs)} end
);
{error, _} ->
{error, {database_error, <<"Failed to decode map value"/utf8>>}}
end.
-file("src/ducky.gleam", 826).
?DOC(" Decodes an array tagged tuple {array, [elements]}.\n").
-spec decode_array_value(gleam@dynamic:dynamic_()) -> {ok, value()} |
{error, error()}.
decode_array_value(Dyn) ->
Decoder = begin
gleam@dynamic@decode:subfield(
[1],
gleam@dynamic@decode:list(
{decoder, fun gleam@dynamic@decode:decode_dynamic/1}
),
fun(Elements) -> gleam@dynamic@decode:success(Elements) end
)
end,
case gleam@dynamic@decode:run(Dyn, Decoder) of
{ok, Elements@1} ->
gleam@result:map(
gleam@list:try_map(Elements@1, fun decode_value/1),
fun(Decoded_elements) -> {array, Decoded_elements} end
);
{error, _} ->
{error, {database_error, <<"Failed to decode array value"/utf8>>}}
end.
-file("src/ducky.gleam", 786).
?DOC(" Decodes tagged tuples sent as Erlang arrays for various types.\n").
-spec decode_tagged_tuple(gleam@dynamic:dynamic_()) -> {ok, value()} |
{error, error()}.
decode_tagged_tuple(Dyn) ->
Tag_decoder = begin
gleam@dynamic@decode:subfield(
[0],
{decoder, fun gleam@dynamic@decode:decode_dynamic/1},
fun(Tag_dynamic) -> gleam@dynamic@decode:success(Tag_dynamic) end
)
end,
case gleam@dynamic@decode:run(Dyn, Tag_decoder) of
{ok, Tag_dynamic@1} ->
Tag = case gleam_stdlib:classify_dynamic(Tag_dynamic@1) of
<<"Atom"/utf8>> ->
erlang:atom_to_binary(Tag_dynamic@1);
_ ->
<<""/utf8>>
end,
case Tag of
<<"decimal"/utf8>> ->
decode_decimal_value(Dyn);
<<"array"/utf8>> ->
decode_array_value(Dyn);
<<"map"/utf8>> ->
decode_map_value(Dyn);
<<"union"/utf8>> ->
decode_union_value(Dyn);
<<"timestamp"/utf8>> ->
decode_temporal_tuple(Dyn);
<<"date"/utf8>> ->
decode_temporal_tuple(Dyn);
<<"time"/utf8>> ->
decode_temporal_tuple(Dyn);
<<"interval"/utf8>> ->
decode_interval_tuple(Dyn);
_ ->
{error,
{database_error,
<<"Unknown tagged tuple type: "/utf8, Tag/binary>>}}
end;
{error, _} ->
{error, {database_error, <<"Failed to decode tagged tuple"/utf8>>}}
end.
-file("src/ducky.gleam", 753).
?DOC(" Decodes a list with recursive element decoding.\n").
-spec decode_list(gleam@dynamic:dynamic_()) -> {ok, value()} | {error, error()}.
decode_list(Dyn) ->
Decoder = gleam@dynamic@decode:list(
{decoder, fun gleam@dynamic@decode:decode_dynamic/1}
),
case gleam@dynamic@decode:run(Dyn, Decoder) of
{ok, Elements} ->
gleam@result:map(
gleam@list:try_map(Elements, fun decode_value/1),
fun(Decoded_elements) -> {list, Decoded_elements} end
);
{error, _} ->
{error, {database_error, <<"Failed to decode list value"/utf8>>}}
end.
-file("src/ducky.gleam", 766).
?DOC(" Decodes an Erlang map into a Struct with recursive value decoding.\n").
-spec decode_struct(gleam@dynamic:dynamic_()) -> {ok, value()} |
{error, error()}.
decode_struct(Dyn) ->
Decoder = gleam@dynamic@decode:dict(
{decoder, fun gleam@dynamic@decode:decode_string/1},
{decoder, fun gleam@dynamic@decode:decode_dynamic/1}
),
case gleam@dynamic@decode:run(Dyn, Decoder) of
{ok, Fields} ->
Pairs = maps:to_list(Fields),
gleam@result:map(
gleam@list:try_map(
Pairs,
fun(Pair) ->
{Key, Val} = Pair,
gleam@result:map(
decode_value(Val),
fun(Decoded_val) -> {Key, Decoded_val} end
)
end
),
fun(Decoded_pairs) ->
{struct, maps:from_list(Decoded_pairs)}
end
);
{error, _} ->
{error, {database_error, <<"Failed to decode struct value"/utf8>>}}
end.
-file("src/ducky.gleam", 728).
?DOC(" Decodes a dynamic value from the NIF into a typed Value.\n").
-spec decode_value(gleam@dynamic:dynamic_()) -> {ok, value()} | {error, error()}.
decode_value(Dyn) ->
Classification = gleam_stdlib:classify_dynamic(Dyn),
case Classification of
<<"Atom"/utf8>> ->
{ok, null};
<<"Nil"/utf8>> ->
{ok, null};
<<"Dict"/utf8>> ->
decode_struct(Dyn);
<<"List"/utf8>> ->
decode_list(Dyn);
<<"Array"/utf8>> ->
decode_tagged_tuple(Dyn);
_ ->
Value_decoder = gleam@dynamic@decode:one_of(
begin
_pipe = {decoder, fun gleam@dynamic@decode:decode_bool/1},
gleam@dynamic@decode:map(
_pipe,
fun(Field@0) -> {boolean, Field@0} end
)
end,
[begin
_pipe@1 = {decoder,
fun gleam@dynamic@decode:decode_int/1},
gleam@dynamic@decode:map(
_pipe@1,
fun(Field@0) -> {integer, Field@0} end
)
end,
begin
_pipe@2 = {decoder,
fun gleam@dynamic@decode:decode_float/1},
gleam@dynamic@decode:map(
_pipe@2,
fun(Field@0) -> {double, Field@0} end
)
end,
begin
_pipe@3 = {decoder,
fun gleam@dynamic@decode:decode_string/1},
gleam@dynamic@decode:map(
_pipe@3,
fun(Field@0) -> {text, Field@0} end
)
end,
begin
_pipe@4 = {decoder,
fun gleam@dynamic@decode:decode_bit_array/1},
gleam@dynamic@decode:map(
_pipe@4,
fun(Field@0) -> {blob, Field@0} end
)
end]
),
_pipe@5 = gleam@dynamic@decode:run(Dyn, Value_decoder),
gleam@result:map_error(
_pipe@5,
fun(_) ->
{database_error,
<<"Failed to decode value of type: "/utf8,
Classification/binary>>}
end
)
end.
-file("src/ducky.gleam", 526).
-spec decode_raw_row(list(gleam@dynamic:dynamic_())) -> {ok, row()} |
{error, error()}.
decode_raw_row(Raw_row) ->
gleam@result:map(
gleam@list:try_map(Raw_row, fun decode_value/1),
fun(Values) -> {row, Values} end
).
-file("src/ducky.gleam", 686).
?DOC(" Creates a tagged tuple {tag, value} for NIF parameter encoding.\n").
-spec make_tagged(binary(), gleam@dynamic:dynamic_()) -> gleam@dynamic:dynamic_().
make_tagged(Tag, Value) ->
erlang:list_to_tuple([erlang:binary_to_atom(Tag), Value]).
-file("src/ducky.gleam", 691).
?DOC(" Creates an interval 4-tuple {interval, months, days, nanos}.\n").
-spec make_interval_tuple(integer(), integer(), integer()) -> gleam@dynamic:dynamic_().
make_interval_tuple(Months, Days, Nanos) ->
erlang:list_to_tuple(
[erlang:binary_to_atom(<<"interval"/utf8>>),
gleam_stdlib:identity(Months),
gleam_stdlib:identity(Days),
gleam_stdlib:identity(Nanos)]
).
-file("src/ducky.gleam", 701).
?DOC(" Converts a Value to a Dynamic for passing to the NIF.\n").
-spec value_to_dynamic(value()) -> {ok, gleam@dynamic:dynamic_()} |
{error, error()}.
value_to_dynamic(Value) ->
case Value of
null ->
{ok, gleam@dynamic:nil()};
{boolean, B} ->
{ok, gleam_stdlib:identity(B)};
{tiny_int, I} ->
{ok, gleam_stdlib:identity(I)};
{small_int, I@1} ->
{ok, gleam_stdlib:identity(I@1)};
{integer, I@2} ->
{ok, gleam_stdlib:identity(I@2)};
{big_int, I@3} ->
{ok, gleam_stdlib:identity(I@3)};
{float, F} ->
{ok, gleam_stdlib:identity(F)};
{double, F@1} ->
{ok, gleam_stdlib:identity(F@1)};
{text, S} ->
{ok, gleam_stdlib:identity(S)};
{blob, Bits} ->
{ok, gleam_stdlib:identity(Bits)};
{timestamp, Micros} ->
{ok,
make_tagged(<<"timestamp"/utf8>>, gleam_stdlib:identity(Micros))};
{date, Days} ->
{ok, make_tagged(<<"date"/utf8>>, gleam_stdlib:identity(Days))};
{time, Micros@1} ->
{ok, make_tagged(<<"time"/utf8>>, gleam_stdlib:identity(Micros@1))};
{interval, Months, Days@1, Nanos} ->
{ok, make_interval_tuple(Months, Days@1, Nanos)};
{decimal, S@1} ->
{ok, make_tagged(<<"decimal"/utf8>>, gleam_stdlib:identity(S@1))};
{list, _} ->
{error, {unsupported_parameter_type, <<"List"/utf8>>}};
{array, _} ->
{error, {unsupported_parameter_type, <<"Array"/utf8>>}};
{map, _} ->
{error, {unsupported_parameter_type, <<"Map"/utf8>>}};
{struct, _} ->
{error, {unsupported_parameter_type, <<"Struct"/utf8>>}};
{union, _, _} ->
{error, {unsupported_parameter_type, <<"Union"/utf8>>}}
end.
-file("src/ducky.gleam", 279).
?DOC(
" Runs a prepared statement. For hot loops with repeated bindings.\n"
"\n"
" ## Examples\n"
"\n"
" ```gleam\n"
" let assert Ok(stmt) = prepare(conn, \"SELECT * FROM users WHERE age > ?\")\n"
" let assert Ok(result) = execute(stmt, [int(18)])\n"
" // result.rows contains matching users\n"
" ```\n"
).
-spec execute(statement(), list(value())) -> {ok, returned(row())} |
{error, error()}.
execute(Stmt, Params) ->
gleam@result:'try'(
gleam@list:try_map(Params, fun value_to_dynamic/1),
fun(Dynamic_params) ->
gleam@result:'try'(
begin
_pipe = ducky_nif:execute_prepared(
erlang:element(2, Stmt),
Dynamic_params
),
gleam@result:map_error(_pipe, fun decode_nif_error/1)
end,
fun(Raw) ->
{_, Raw_rows} = Raw,
gleam@result:'try'(
gleam@list:try_map(Raw_rows, fun decode_raw_row/1),
fun(Decoded_rows) ->
{ok,
{returned,
erlang:length(Decoded_rows),
Decoded_rows}}
end
)
end
)
end
).
-file("src/ducky.gleam", 308).
?DOC(
" Finalizes a prepared statement, releasing its resources.\n"
"\n"
" After finalization, the statement cannot be used again.\n"
" For automatic cleanup, prefer `with_statement`.\n"
"\n"
" ## Examples\n"
"\n"
" ```gleam\n"
" let assert Ok(stmt) = prepare(conn, \"SELECT 1\")\n"
" // ... use the statement ...\n"
" let assert Ok(_) = finalize(stmt)\n"
" ```\n"
).
-spec finalize(statement()) -> {ok, nil} | {error, error()}.
finalize(Stmt) ->
_pipe = ducky_nif:finalize(erlang:element(2, Stmt)),
_pipe@1 = gleam@result:map(_pipe, fun(_) -> nil end),
gleam@result:map_error(_pipe@1, fun decode_nif_error/1).
-file("src/ducky.gleam", 328).
?DOC(
" Executes operations with a prepared statement, ensuring cleanup.\n"
"\n"
" The statement is automatically finalized when the callback returns,\n"
" regardless of success or failure.\n"
"\n"
" ## Examples\n"
"\n"
" ```gleam\n"
" use stmt <- with_statement(conn, \"INSERT INTO users (name) VALUES (?)\")\n"
" list.try_each(names, fn(name) {\n"
" execute(stmt, [text(name)])\n"
" |> result.map(fn(_) { Nil })\n"
" })\n"
" ```\n"
).
-spec with_statement(
connection(),
binary(),
fun((statement()) -> {ok, DNS} | {error, error()})
) -> {ok, DNS} | {error, error()}.
with_statement(Conn, Sql, Callback) ->
gleam@result:'try'(
prepare(Conn, Sql),
fun(Stmt) ->
Res = Callback(Stmt),
_ = finalize(Stmt),
Res
end
).
-file("src/ducky.gleam", 351).
?DOC(
" Bypasses SQL parsing. Atomic: all rows succeed or none commit.\n"
"\n"
" ## Examples\n"
"\n"
" ```gleam\n"
" let assert Ok(count) = append(conn, \"users\", [\n"
" [int(1), text(\"Alice\")],\n"
" [int(2), text(\"Bob\")],\n"
" ])\n"
" // count == 2\n"
" ```\n"
).
-spec append(connection(), binary(), list(list(value()))) -> {ok, integer()} |
{error, error()}.
append(Conn, Table, Rows) ->
case Rows of
[] ->
{ok, 0};
_ ->
gleam@result:'try'(
gleam@list:try_map(
Rows,
fun(Row) ->
gleam@list:try_map(Row, fun value_to_dynamic/1)
end
),
fun(Dynamic_rows) ->
_pipe = ducky_nif:append_rows(
ducky@internal@connection:native(
erlang:element(2, Conn)
),
Table,
Dynamic_rows
),
gleam@result:map_error(_pipe, fun decode_nif_error/1)
end
)
end.
-file("src/ducky.gleam", 377).
?DOC(
" Builds a query value. No I/O until `run` or `as_columns`.\n"
"\n"
" ## Examples\n"
"\n"
" ```gleam\n"
" ducky.sql(\"SELECT * FROM users\")\n"
" |> ducky.run(conn)\n"
" ```\n"
).
-spec sql(binary()) -> 'query'(row()).
sql(Statement) ->
{'query', Statement, [], fun decode_raw_row/1}.
-file("src/ducky.gleam", 390).
?DOC(
" Binds one parameter to the next `?` placeholder.\n"
"\n"
" ## Examples\n"
"\n"
" ```gleam\n"
" ducky.sql(\"SELECT * FROM users WHERE id = ?\")\n"
" |> ducky.parameter(ducky.int(42))\n"
" |> ducky.run(conn)\n"
" ```\n"
).
-spec parameter('query'(DOC), value()) -> 'query'(DOC).
parameter(Query, Value) ->
{'query',
erlang:element(2, Query),
[Value | erlang:element(3, Query)],
erlang:element(4, Query)}.
-file("src/ducky.gleam", 404).
?DOC(
" Binds multiple parameters at once, in order.\n"
"\n"
" ## Examples\n"
"\n"
" ```gleam\n"
" ducky.sql(\"SELECT * FROM users WHERE id = ? AND age > ?\")\n"
" |> ducky.parameters([ducky.int(42), ducky.int(18)])\n"
" |> ducky.run(conn)\n"
" ```\n"
).
-spec parameters('query'(DOF), list(value())) -> 'query'(DOF).
parameters(Query, Values) ->
{'query',
erlang:element(2, Query),
lists:append(lists:reverse(Values), erlang:element(3, Query)),
erlang:element(4, Query)}.
-file("src/ducky.gleam", 426).
?DOC(
" Sets a decoder for typed rows. Constrained to `Query(Row)`.\n"
" Can only be called once; the compiler enforces this.\n"
"\n"
" ## Examples\n"
"\n"
" ```gleam\n"
" let decoder = {\n"
" use name <- decode.field(0, decode.string)\n"
" use age <- decode.field(1, decode.int)\n"
" decode.success(#(name, age))\n"
" }\n"
"\n"
" ducky.sql(\"SELECT name, age FROM users\")\n"
" |> ducky.returning(decoder)\n"
" |> ducky.run(conn)\n"
" // => Ok(Returned(count: 1, rows: [#(\"Alice\", 30)]))\n"
" ```\n"
).
-spec returning('query'(row()), gleam@dynamic@decode:decoder(DOK)) -> 'query'(DOK).
returning(Query, Decoder) ->
{'query',
erlang:element(2, Query),
erlang:element(3, Query),
fun(Raw_row) ->
Row_tuple = erlang:list_to_tuple(Raw_row),
_pipe = gleam@dynamic@decode:run(Row_tuple, Decoder),
gleam@result:map_error(
_pipe,
fun(Errors) -> {decode_failed, Errors} end
)
end}.
-file("src/ducky.gleam", 443).
?DOC(
" Executes the query. Use `sql` for one-shot queries; `prepare`/`execute` for hot loops.\n"
"\n"
" ## Examples\n"
"\n"
" ```gleam\n"
" ducky.sql(\"SELECT 1 AS x\")\n"
" |> ducky.run(conn)\n"
" // => Ok(Returned(count: 1, rows: [Row([Integer(1)])]))\n"
" ```\n"
).
-spec run('query'(DON), connection()) -> {ok, returned(DON)} | {error, error()}.
run(Query, Conn) ->
Params = lists:reverse(erlang:element(3, Query)),
gleam@result:'try'(
gleam@list:try_map(Params, fun value_to_dynamic/1),
fun(Dynamic_params) ->
gleam@result:'try'(
begin
_pipe = ducky_nif:execute_query(
ducky@internal@connection:native(
erlang:element(2, Conn)
),
erlang:element(2, Query),
Dynamic_params
),
gleam@result:map_error(_pipe, fun decode_nif_error/1)
end,
fun(Raw) ->
{_, Raw_rows} = Raw,
gleam@result:'try'(
gleam@list:try_map(Raw_rows, erlang:element(4, Query)),
fun(Decoded_rows) ->
{ok,
{returned,
erlang:length(Decoded_rows),
Decoded_rows}}
end
)
end
)
end
).
-file("src/ducky.gleam", 471).
?DOC(
" Returns columns instead of rows. DuckDB's columnar strength exposed.\n"
"\n"
" ## Examples\n"
"\n"
" ```gleam\n"
" ducky.sql(\"SELECT name, age FROM users\")\n"
" |> ducky.as_columns(conn)\n"
" // => Ok(Columnar(names: [\"name\", \"age\"], data: [[Text(\"Alice\")], [Integer(30)]]))\n"
" ```\n"
).
-spec as_columns('query'(row()), connection()) -> {ok, columnar()} |
{error, error()}.
as_columns(Query, Conn) ->
Params = lists:reverse(erlang:element(3, Query)),
gleam@result:'try'(
gleam@list:try_map(Params, fun value_to_dynamic/1),
fun(Dynamic_params) ->
gleam@result:'try'(
begin
_pipe = ducky_nif:execute_query_columns(
ducky@internal@connection:native(
erlang:element(2, Conn)
),
erlang:element(2, Query),
Dynamic_params
),
gleam@result:map_error(_pipe, fun decode_nif_error/1)
end,
fun(Raw) ->
gleam@result:'try'(
gleam@list:try_map(
Raw,
fun(Col) ->
{Name, Values} = Col,
gleam@result:map(
gleam@list:try_map(
Values,
fun decode_value/1
),
fun(Decoded_values) ->
{Name, Decoded_values}
end
)
end
),
fun(Decoded) ->
Names = gleam@list:map(
Decoded,
fun(Col@1) -> erlang:element(1, Col@1) end
),
Data = gleam@list:map(
Decoded,
fun(Col@2) -> erlang:element(2, Col@2) end
),
{ok, {columnar, Names, Data}}
end
)
end
)
end
).
-file("src/ducky.gleam", 512).
-spec column_lookup(list(binary()), list(list(value())), binary()) -> gleam@option:option(list(value())).
column_lookup(Names, Data, Target) ->
case {Names, Data} of
{[], _} ->
none;
{_, []} ->
none;
{[N | _], [Col | _]} when N =:= Target ->
{some, Col};
{[_ | Rest_names], [_ | Rest_data]} ->
column_lookup(Rest_names, Rest_data, Target)
end.
-file("src/ducky.gleam", 508).
?DOC(
" Looks up a column by name. Returns None if not found.\n"
"\n"
" ## Examples\n"
"\n"
" ```gleam\n"
" ducky.column(cols, \"name\")\n"
" // => Some([Text(\"Alice\"), Text(\"Bob\")])\n"
" ```\n"
).
-spec column(columnar(), binary()) -> gleam@option:option(list(value())).
column(Columnar, Name) ->
column_lookup(
erlang:element(2, Columnar),
erlang:element(3, Columnar),
Name
).
-file("src/ducky.gleam", 533).
?DOC(
" Raw microseconds since epoch.\n"
" NIF returns temporals as {type_tag, value} tuples.\n"
).
-spec timestamp_decoder() -> gleam@dynamic@decode:decoder(integer()).
timestamp_decoder() ->
gleam@dynamic@decode:subfield(
[1],
{decoder, fun gleam@dynamic@decode:decode_int/1},
fun(Value) -> gleam@dynamic@decode:success(Value) end
).
-file("src/ducky.gleam", 539).
?DOC(" Raw days since epoch.\n").
-spec date_decoder() -> gleam@dynamic@decode:decoder(integer()).
date_decoder() ->
gleam@dynamic@decode:subfield(
[1],
{decoder, fun gleam@dynamic@decode:decode_int/1},
fun(Value) -> gleam@dynamic@decode:success(Value) end
).
-file("src/ducky.gleam", 545).
?DOC(" Raw microseconds since midnight.\n").
-spec time_decoder() -> gleam@dynamic@decode:decoder(integer()).
time_decoder() ->
gleam@dynamic@decode:subfield(
[1],
{decoder, fun gleam@dynamic@decode:decode_int/1},
fun(Value) -> gleam@dynamic@decode:success(Value) end
).
-file("src/ducky.gleam", 551).
?DOC(" Returns #(months, days, nanos). NIF sends {interval, m, d, n}.\n").
-spec interval_decoder() -> gleam@dynamic@decode:decoder({integer(),
integer(),
integer()}).
interval_decoder() ->
gleam@dynamic@decode:subfield(
[1],
{decoder, fun gleam@dynamic@decode:decode_int/1},
fun(Months) ->
gleam@dynamic@decode:subfield(
[2],
{decoder, fun gleam@dynamic@decode:decode_int/1},
fun(Days) ->
gleam@dynamic@decode:subfield(
[3],
{decoder, fun gleam@dynamic@decode:decode_int/1},
fun(Nanos) ->
gleam@dynamic@decode:success({Months, Days, Nanos})
end
)
end
)
end
).
-file("src/ducky.gleam", 559).
?DOC(" Lossless string. Avoids floating-point representation issues.\n").
-spec decimal_decoder() -> gleam@dynamic@decode:decoder(binary()).
decimal_decoder() ->
gleam@dynamic@decode:subfield(
[1],
{decoder, fun gleam@dynamic@decode:decode_string/1},
fun(Value) -> gleam@dynamic@decode:success(Value) end
).
-file("src/ducky.gleam", 676).
-spec list_at(list(DPO), integer()) -> gleam@option:option(DPO).
list_at(Items, Index) ->
case {Items, Index} of
{[], _} ->
none;
{[First | _], 0} ->
{some, First};
{[_ | Rest], N} when N > 0 ->
list_at(Rest, N - 1);
{_, _} ->
none
end.
-file("src/ducky.gleam", 576).
?DOC(
" Get a value from a row by column index.\n"
"\n"
" ## Examples\n"
"\n"
" ```gleam\n"
" let row = Row([Integer(1), Text(\"Alice\")])\n"
" get(row, 0)\n"
" // => Some(Integer(1))\n"
"\n"
" get(row, 5)\n"
" // => None\n"
" ```\n"
).
-spec get(row(), integer()) -> gleam@option:option(value()).
get(Row, Index) ->
case Row of
{row, Values} ->
list_at(Values, Index)
end.
-file("src/ducky.gleam", 596).
?DOC(
" Get a field value from a struct by field name.\n"
"\n"
" Returns None if the value is not a Struct or the field does not exist.\n"
"\n"
" ## Examples\n"
"\n"
" ```gleam\n"
" let person = Struct(dict.from_list([#(\"name\", Text(\"Alice\")), #(\"age\", Integer(30))]))\n"
" field(person, \"name\")\n"
" // => Some(Text(\"Alice\"))\n"
"\n"
" field(person, \"unknown\")\n"
" // => None\n"
" ```\n"
).
-spec field(value(), binary()) -> gleam@option:option(value()).
field(Value, Name) ->
case Value of
{struct, Fields} ->
_pipe = gleam_stdlib:map_get(Fields, Name),
gleam@option:from_result(_pipe);
_ ->
none
end.
-file("src/ducky.gleam", 604).
?DOC(" Creates an integer parameter value.\n").
-spec int(integer()) -> value().
int(Value) ->
{integer, Value}.
-file("src/ducky.gleam", 609).
?DOC(" Creates a float parameter value.\n").
-spec float(float()) -> value().
float(Value) ->
{double, Value}.
-file("src/ducky.gleam", 614).
?DOC(" Creates a text parameter value.\n").
-spec text(binary()) -> value().
text(Value) ->
{text, Value}.
-file("src/ducky.gleam", 619).
?DOC(" Creates a blob parameter value.\n").
-spec blob(bitstring()) -> value().
blob(Value) ->
{blob, Value}.
-file("src/ducky.gleam", 624).
?DOC(" Creates a boolean parameter value.\n").
-spec bool(boolean()) -> value().
bool(Value) ->
{boolean, Value}.
-file("src/ducky.gleam", 629).
?DOC(" Creates a null parameter value.\n").
-spec null() -> value().
null() ->
null.
-file("src/ducky.gleam", 644).
?DOC(
" Creates a nullable parameter value.\n"
"\n"
" ## Examples\n"
"\n"
" ```gleam\n"
" nullable(int, Some(42))\n"
" // => Integer(42)\n"
"\n"
" nullable(int, None)\n"
" // => Null\n"
" ```\n"
).
-spec nullable(fun((DPM) -> value()), gleam@option:option(DPM)) -> value().
nullable(Inner, Value) ->
case Value of
{some, V} ->
Inner(V);
none ->
null
end.
-file("src/ducky.gleam", 652).
?DOC(" Creates a timestamp parameter value (microseconds since Unix epoch).\n").
-spec timestamp(integer()) -> value().
timestamp(Micros) ->
{timestamp, Micros}.
-file("src/ducky.gleam", 657).
?DOC(" Creates a date parameter value (days since Unix epoch).\n").
-spec date(integer()) -> value().
date(Days) ->
{date, Days}.
-file("src/ducky.gleam", 662).
?DOC(" Creates a time parameter value (microseconds since midnight).\n").
-spec time(integer()) -> value().
time(Micros) ->
{time, Micros}.
-file("src/ducky.gleam", 667).
?DOC(" Creates an interval parameter value.\n").
-spec interval(integer(), integer(), integer()) -> value().
interval(Months, Days, Nanos) ->
{interval, Months, Days, Nanos}.
-file("src/ducky.gleam", 672).
?DOC(" Creates a decimal parameter value from a string representation.\n").
-spec decimal(binary()) -> value().
decimal(Value) ->
{decimal, Value}.