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