-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.