Current section
Files
Jump to
Current section
Files
src/ducky.erl
-module(ducky).
-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]).
-define(FILEPATH, "src/ducky.gleam").
-export([path/1, 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, get/2, connect/1, close/1, with_connection/2, transaction/2, exec/2, prepare/2, finalize/1, with_statement/3, append_rows/3, 'query'/2, query_params/3, execute/2]).
-export_type([error/0, value/0, row/0, data_frame/0, 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"
" ```gleam\n"
" import ducky\n"
" import gleam/int\n"
" import gleam/io\n"
" import gleam/result\n"
"\n"
" pub fn main() {\n"
" use conn <- ducky.with_connection(\":memory:\")\n"
"\n"
" // Create our duck pond\n"
" use _ <- result.try(ducky.exec(conn, \"\n"
" CREATE TABLE ducks (name TEXT, quack_volume INT, is_rubber BOOLEAN)\n"
" \"))\n"
" use _ <- result.try(ducky.exec(conn, \"\n"
" INSERT INTO ducks VALUES\n"
" ('Sir Quacksalot', 95, false),\n"
" ('Duck Norris', 100, false),\n"
" ('Mallard Fillmore', 72, false),\n"
" ('Squeaky', 0, true)\n"
" \"))\n"
"\n"
" // Find the loudest quacker\n"
" use result <- result.map(ducky.query(conn, \"\n"
" SELECT name, quack_volume FROM ducks\n"
" WHERE is_rubber = false\n"
" ORDER BY quack_volume DESC LIMIT 1\n"
" \"))\n"
"\n"
" case result.rows {\n"
" [ducky.Row([ducky.Text(name), ducky.Integer(volume)])] ->\n"
" io.println(name <> \" wins at \" <> int.to_string(volume) <> \" decibels!\")\n"
" _ -> io.println(\"The pond is empty...\")\n"
" }\n"
" }\n"
" // => Duck Norris wins at 100 decibels!\n"
" ```\n"
).
-type error() :: {connection_failed, binary()} |
{query_syntax_error, binary()} |
{unsupported_parameter_type, binary()} |
statement_finalized |
{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 data_frame() :: {data_frame, list(binary()), list(row())}.
-opaque connection() :: {connection, ducky@internal@connection:connection()}.
-opaque statement() :: {statement, ducky@internal@ffi:native_statement()}.
-file("src/ducky.gleam", 151).
?DOC(" Returns the database path for a connection.\n").
-spec path(connection()) -> binary().
path(Conn) ->
ducky@internal@connection:path(erlang:element(2, Conn)).
-file("src/ducky.gleam", 443).
?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", 451).
?DOC(" Creates an integer parameter value.\n").
-spec int(integer()) -> value().
int(Value) ->
{integer, Value}.
-file("src/ducky.gleam", 456).
?DOC(" Creates a float parameter value.\n").
-spec float(float()) -> value().
float(Value) ->
{double, Value}.
-file("src/ducky.gleam", 461).
?DOC(" Creates a text parameter value.\n").
-spec text(binary()) -> value().
text(Value) ->
{text, Value}.
-file("src/ducky.gleam", 466).
?DOC(" Creates a blob parameter value.\n").
-spec blob(bitstring()) -> value().
blob(Value) ->
{blob, Value}.
-file("src/ducky.gleam", 471).
?DOC(" Creates a boolean parameter value.\n").
-spec bool(boolean()) -> value().
bool(Value) ->
{boolean, Value}.
-file("src/ducky.gleam", 476).
?DOC(" Creates a null parameter value.\n").
-spec null() -> value().
null() ->
null.
-file("src/ducky.gleam", 491).
?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((DTW) -> value()), gleam@option:option(DTW)) -> value().
nullable(Inner, Value) ->
case Value of
{some, V} ->
Inner(V);
none ->
null
end.
-file("src/ducky.gleam", 499).
?DOC(" Creates a timestamp parameter value (microseconds since Unix epoch).\n").
-spec timestamp(integer()) -> value().
timestamp(Micros) ->
{timestamp, Micros}.
-file("src/ducky.gleam", 504).
?DOC(" Creates a date parameter value (days since Unix epoch).\n").
-spec date(integer()) -> value().
date(Days) ->
{date, Days}.
-file("src/ducky.gleam", 509).
?DOC(" Creates a time parameter value (microseconds since midnight).\n").
-spec time(integer()) -> value().
time(Micros) ->
{time, Micros}.
-file("src/ducky.gleam", 514).
?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", 519).
?DOC(" Creates a decimal parameter value from a string representation.\n").
-spec decimal(binary()) -> value().
decimal(Value) ->
{decimal, Value}.
-file("src/ducky.gleam", 523).
-spec list_at(list(DTY), integer()) -> gleam@option:option(DTY).
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", 423).
?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", 533).
?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", 538).
?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", 548).
?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", 675).
?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", 743).
?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", 772).
?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", 786).
-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", 797).
-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", 820).
?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", 810).
?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", 127).
?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", 145).
?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", 165).
?DOC(
" Executes operations with automatic connection cleanup.\n"
"\n"
" Connection closes automatically on success or error.\n"
"\n"
" ## Examples\n"
"\n"
" ```gleam\n"
" use conn <- with_connection(\":memory:\")\n"
" query(conn, \"SELECT 42\")\n"
" ```\n"
).
-spec with_connection(
binary(),
fun((connection()) -> {ok, DSN} | {error, error()})
) -> {ok, DSN} | {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", 188).
?DOC(
" Executes operations within a transaction.\n"
"\n"
" Commits on success, rolls back on error.\n"
"\n"
" ## Examples\n"
"\n"
" ```gleam\n"
" transaction(conn, fn(conn) {\n"
" use _ <- result.try(query(conn, \"UPDATE accounts ...\"))\n"
" query(conn, \"SELECT * FROM accounts\")\n"
" })\n"
" ```\n"
).
-spec transaction(
connection(),
fun((connection()) -> {ok, DSS} | {error, error()})
) -> {ok, DSS} | {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", 227).
?DOC(
" Executes a SQL statement that returns no rows.\n"
"\n"
" Use for DDL and DML statements (CREATE, INSERT, UPDATE, DELETE, etc.).\n"
" For statements that return rows, use `query` or `query_params`.\n"
"\n"
" ## Examples\n"
"\n"
" ```gleam\n"
" exec(conn, \"CREATE TABLE users (id INT, name VARCHAR)\")\n"
" // => Ok(Nil)\n"
"\n"
" exec(conn, \"INSERT INTO users VALUES (1, 'Alice')\")\n"
" // => Ok(Nil)\n"
" ```\n"
).
-spec exec(connection(), binary()) -> {ok, nil} | {error, error()}.
exec(Conn, Sql) ->
_pipe = ducky_nif:execute_query(
ducky@internal@connection:native(erlang:element(2, Conn)),
Sql,
[]
),
_pipe@1 = gleam@result:map_error(_pipe, fun decode_nif_error/1),
gleam@result:replace(_pipe@1, nil).
-file("src/ducky.gleam", 307).
?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", 345).
?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", 365).
?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, DTL} | {error, error()})
) -> {ok, DTL} | {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", 393).
?DOC(
" Bulk-appends rows via DuckDB's appender API. Bypasses SQL parsing.\n"
"\n"
" Atomic: all rows succeed or none are committed on error.\n"
" The table name is resolved by catalog lookup, not SQL interpolation.\n"
" Empty rows return `Ok(0)` without a NIF call.\n"
"\n"
" ## Examples\n"
"\n"
" ```gleam\n"
" let assert Ok(count) = append_rows(conn, \"users\", [\n"
" [int(1), text(\"Alice\")],\n"
" [int(2), text(\"Bob\")],\n"
" [int(3), text(\"Charlie\")],\n"
" ])\n"
" // count == 3\n"
" ```\n"
).
-spec append_rows(connection(), binary(), list(list(value()))) -> {ok,
integer()} |
{error, error()}.
append_rows(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", 614).
?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", 589).
?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", 575).
?DOC(" Decodes raw NIF result into a DataFrame.\n").
-spec decode_dataframe({list(binary()), list(list(gleam@dynamic:dynamic_()))}) -> {ok,
data_frame()} |
{error, error()}.
decode_dataframe(Raw) ->
{Columns, Rows} = Raw,
gleam@result:'try'(
gleam@list:try_map(
Rows,
fun(Row) ->
gleam@result:map(
gleam@list:try_map(Row, fun decode_value/1),
fun(Values) -> {row, Values} end
)
end
),
fun(Decoded_rows) -> {ok, {data_frame, Columns, Decoded_rows}} end
).
-file("src/ducky.gleam", 248).
?DOC(
" Executes a SQL query and returns structured results.\n"
"\n"
" The query runs on a dirty scheduler to avoid blocking the BEAM.\n"
" Results are loaded into memory. For large datasets, use LIMIT/OFFSET\n"
" or filter in SQL to reduce memory usage.\n"
"\n"
" ## Examples\n"
"\n"
" ```gleam\n"
" query(conn, \"SELECT id, name FROM users WHERE active = true\")\n"
" // => Ok(DataFrame(columns: [\"id\", \"name\"], rows: [...]))\n"
"\n"
" // For large datasets, paginate:\n"
" query(conn, \"SELECT * FROM users LIMIT 1000 OFFSET 0\")\n"
" ```\n"
).
-spec 'query'(connection(), binary()) -> {ok, data_frame()} | {error, error()}.
'query'(Conn, Sql) ->
_pipe = ducky_nif:execute_query(
ducky@internal@connection:native(erlang:element(2, Conn)),
Sql,
[]
),
_pipe@1 = gleam@result:map_error(_pipe, fun decode_nif_error/1),
gleam@result:'try'(_pipe@1, fun decode_dataframe/1).
-file("src/ducky.gleam", 276).
?DOC(
" Executes a parameterized SQL query with bound parameters to prevent SQL injection.\n"
"\n"
" ## Examples\n"
"\n"
" ```gleam\n"
" query_params(conn, \"SELECT * FROM users WHERE id = ? AND age > ?\", [\n"
" int(42),\n"
" int(18),\n"
" ])\n"
" // => Ok(DataFrame(...))\n"
" ```\n"
"\n"
" ## Security\n"
"\n"
" Always use this function when including user input in queries:\n"
" ```gleam\n"
" // UNSAFE - SQL injection risk\n"
" query(conn, \"SELECT * FROM users WHERE name = '\" <> user_input <> \"'\")\n"
"\n"
" // SAFE - parameters are properly escaped\n"
" query_params(conn, \"SELECT * FROM users WHERE name = ?\", [text(user_input)])\n"
" ```\n"
).
-spec query_params(connection(), binary(), list(value())) -> {ok, data_frame()} |
{error, error()}.
query_params(Conn, Sql, Params) ->
gleam@result:'try'(
gleam@list:try_map(Params, fun value_to_dynamic/1),
fun(Dynamic_params) ->
_pipe = ducky_nif:execute_query(
ducky@internal@connection:native(erlang:element(2, Conn)),
Sql,
Dynamic_params
),
_pipe@1 = gleam@result:map_error(_pipe, fun decode_nif_error/1),
gleam@result:'try'(_pipe@1, fun decode_dataframe/1)
end
).
-file("src/ducky.gleam", 325).
?DOC(
" Executes a prepared statement with parameters.\n"
"\n"
" Returns a DataFrame with the query results, or an empty DataFrame\n"
" for DDL/DML statements.\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, data_frame()} |
{error, error()}.
execute(Stmt, Params) ->
gleam@result:'try'(
gleam@list:try_map(Params, fun value_to_dynamic/1),
fun(Dynamic_params) ->
_pipe = ducky_nif:execute_prepared(
erlang:element(2, Stmt),
Dynamic_params
),
_pipe@1 = gleam@result:map_error(_pipe, fun decode_nif_error/1),
gleam@result:'try'(_pipe@1, fun decode_dataframe/1)
end
).
-file("src/ducky.gleam", 627).
?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", 687).
?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", 702).
?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", 727).
?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", 647).
?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.