Current section

Files

Jump to
ducky src ducky@query.erl
Raw

src/ducky@query.erl

-module(ducky@query).
-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]).
-define(FILEPATH, "src/ducky/query.gleam").
-export(['query'/2, query_params/3]).
-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(" Query execution and result handling.\n").
-file("src/ducky/query.gleam", 125).
?DOC(" Decodes tagged tuples sent as Erlang arrays for temporal types.\n").
-spec decode_temporal_array(gleam@dynamic:dynamic_()) -> ducky@types:value().
decode_temporal_array(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>> ->
{timestamp, Value@1};
<<"date"/utf8>> ->
{date, Value@1};
<<"time"/utf8>> ->
{time, Value@1};
<<"interval"/utf8>> ->
{interval, Value@1};
_ ->
null
end;
{error, _} ->
null
end.
-file("src/ducky/query.gleam", 160).
?DOC(" Converts a Value to a Dynamic for passing to the NIF.\n").
-spec value_to_dynamic(ducky@types:value()) -> gleam@dynamic:dynamic_().
value_to_dynamic(Value) ->
case Value of
null ->
gleam@dynamic:nil();
{boolean, B} ->
gleam_stdlib:identity(B);
{tiny_int, I} ->
gleam_stdlib:identity(I);
{small_int, I@1} ->
gleam_stdlib:identity(I@1);
{integer, I@2} ->
gleam_stdlib:identity(I@2);
{big_int, I@3} ->
gleam_stdlib:identity(I@3);
{float, F} ->
gleam_stdlib:identity(F);
{double, F@1} ->
gleam_stdlib:identity(F@1);
{text, S} ->
gleam_stdlib:identity(S);
{blob, Bits} ->
gleam_stdlib:identity(Bits);
{timestamp, _} ->
gleam@dynamic:nil();
{date, _} ->
gleam@dynamic:nil();
{time, _} ->
gleam@dynamic:nil();
{interval, _} ->
gleam@dynamic:nil();
{list, _} ->
gleam@dynamic:nil();
{struct, _} ->
gleam@dynamic:nil()
end.
-file("src/ducky/query.gleam", 90).
?DOC(" Decodes a list with recursive element decoding.\n").
-spec decode_list(gleam@dynamic:dynamic_()) -> ducky@types:value().
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} ->
Decoded_elements = gleam@list:map(Elements, fun decode_value/1),
{list, Decoded_elements};
{error, _} ->
null
end.
-file("src/ducky/query.gleam", 66).
?DOC(" Decodes a dynamic value from the NIF into a typed Value.\n").
-spec decode_value(gleam@dynamic:dynamic_()) -> ducky@types:value().
decode_value(Dyn) ->
Classification = gleam_stdlib:classify_dynamic(Dyn),
case Classification of
<<"Atom"/utf8>> ->
null;
<<"Dict"/utf8>> ->
decode_struct(Dyn);
<<"List"/utf8>> ->
decode_list(Dyn);
<<"Array"/utf8>> ->
decode_temporal_array(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:unwrap(_pipe@5, null)
end.
-file("src/ducky/query.gleam", 103).
?DOC(" Decodes an Erlang map into a Struct with recursive value decoding.\n").
-spec decode_struct(gleam@dynamic:dynamic_()) -> ducky@types:value().
decode_struct(Dyn) ->
Decoder = begin
_pipe = gleam@dynamic@decode:dict(
{decoder, fun gleam@dynamic@decode:decode_string/1},
{decoder, fun gleam@dynamic@decode:decode_dynamic/1}
),
gleam@dynamic@decode:map(
_pipe,
fun(Fields) ->
Decoded_fields = begin
_pipe@1 = Fields,
_pipe@2 = maps:to_list(_pipe@1),
_pipe@3 = gleam@list:map(
_pipe@2,
fun(Pair) ->
{Key, Val} = Pair,
{Key, decode_value(Val)}
end
),
maps:from_list(_pipe@3)
end,
{struct, Decoded_fields}
end
)
end,
_pipe@4 = gleam@dynamic@decode:run(Dyn, Decoder),
gleam@result:unwrap(_pipe@4, null).
-file("src/ducky/query.gleam", 183).
?DOC(" Decodes raw NIF result into a DataFrame.\n").
-spec decode_dataframe({list(binary()), list(list(gleam@dynamic:dynamic_()))}) -> ducky@types:data_frame().
decode_dataframe(Result) ->
{Columns, Rows} = Result,
Decoded_rows = begin
_pipe = Rows,
gleam@list:map(
_pipe,
fun(Row) ->
Values = gleam@list:map(Row, fun decode_value/1),
{row, Values}
end
)
end,
{data_frame, Columns, Decoded_rows}.
-file("src/ducky/query.gleam", 25).
?DOC(
" Executes a SQL query and returns structured results.\n"
"\n"
" The query runs on a dirty scheduler to avoid blocking the BEAM.\n"
" Large result sets are streamed to prevent memory exhaustion.\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"
).
-spec 'query'(ducky@connection:connection(), binary()) -> {ok,
ducky@types:data_frame()} |
{error, ducky@error:error()}.
'query'(Conn, Sql) ->
_pipe = ducky_nif:execute_query(ducky@connection:native(Conn), Sql, []),
_pipe@1 = gleam@result:map(_pipe, fun decode_dataframe/1),
gleam@result:map_error(
_pipe@1,
fun ducky@internal@error_decoder:decode_nif_error/1
).
-file("src/ducky/query.gleam", 53).
?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"
" types.Integer(42),\n"
" types.Integer(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 = ?\", [types.Text(user_input)])\n"
" ```\n"
).
-spec query_params(
ducky@connection:connection(),
binary(),
list(ducky@types:value())
) -> {ok, ducky@types:data_frame()} | {error, ducky@error:error()}.
query_params(Conn, Sql, Params) ->
Dynamic_params = gleam@list:map(Params, fun value_to_dynamic/1),
_pipe = ducky_nif:execute_query(
ducky@connection:native(Conn),
Sql,
Dynamic_params
),
_pipe@1 = gleam@result:map(_pipe, fun decode_dataframe/1),
gleam@result:map_error(
_pipe@1,
fun ducky@internal@error_decoder:decode_nif_error/1
).