Current section
Files
Jump to
Current section
Files
src/sqlode@runtime.erl
-module(sqlode@runtime).
-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]).
-define(FILEPATH, "src/sqlode/runtime.gleam").
-export([raw_query_for_test/7, null/0, string/1, int/1, float/1, bool/1, bytes/1, array/1, nullable/2, param_marker/1, slice_marker/1, param_marker_checked/1, slice_marker_checked/1, expand_slice_placeholders/4, prepare/2, expand_slice_placeholders_checked/4]).
-export_type([query_command/0, value/0, query_info/0, placeholder_style/0, raw_query/1, marker_error/0, expand_error/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.
-type query_command() :: query_one |
query_many |
query_exec |
query_exec_result |
query_exec_rows |
query_exec_last_id |
query_batch_one |
query_batch_many |
query_batch_exec |
query_copy_from.
-type value() :: sql_null |
{sql_string, binary()} |
{sql_int, integer()} |
{sql_float, float()} |
{sql_bool, boolean()} |
{sql_bytes, bitstring()} |
{sql_array, list(value())}.
-type query_info() :: {query_info,
binary(),
binary(),
query_command(),
integer()}.
-type placeholder_style() :: dollar_numbered |
question_numbered |
question_positional.
-type raw_query(HCF) :: {raw_query,
binary(),
binary(),
query_command(),
integer(),
placeholder_style(),
fun((HCF) -> list(value())),
fun((HCF) -> list({integer(), integer()}))}.
-type marker_error() :: {marker_index_non_positive, integer()}.
-type expand_error() :: {slice_length_negative, integer(), integer()} |
{slice_index_out_of_range, integer(), integer()} |
{total_params_negative, integer()}.
-file("src/sqlode/runtime.gleam", 84).
?DOC(
" Construct a `RawQuery` directly. **Test-only**: production callers\n"
" should always go through codegen-emitted `RawQuery` values, which\n"
" `sqlode generate` produces from declarative SQL fixtures.\n"
"\n"
" Use this helper when:\n"
"\n"
" - writing a custom adapter (in-memory test database, SQLite WASM\n"
" shim, query-log middleware, ...) that needs to exercise\n"
" `prepare(query, params)` against a hand-rolled `RawQuery`\n"
" without running the codegen pipeline; or\n"
" - writing property / regression tests for the runtime surface\n"
" (`prepare`, `expand_slice_placeholders`) without regenerating\n"
" fixtures every time.\n"
"\n"
" Behaviour is identical to invoking the `RawQuery(...)` constructor\n"
" directly; the named helper exists so the intent (\\\"this is a\n"
" hand-rolled RawQuery, not codegen output\\\") is obvious at the call\n"
" site and discoverable via the docs.\n"
).
-spec raw_query_for_test(
binary(),
binary(),
query_command(),
integer(),
placeholder_style(),
fun((HCG) -> list(value())),
fun((HCG) -> list({integer(), integer()}))
) -> raw_query(HCG).
raw_query_for_test(
Name,
Sql,
Command,
Param_count,
Placeholder_style,
Encode,
Slice_info
) ->
{raw_query,
Name,
Sql,
Command,
Param_count,
Placeholder_style,
Encode,
Slice_info}.
-file("src/sqlode/runtime.gleam", 123).
-spec null() -> value().
null() ->
sql_null.
-file("src/sqlode/runtime.gleam", 127).
-spec string(binary()) -> value().
string(Value) ->
{sql_string, Value}.
-file("src/sqlode/runtime.gleam", 131).
-spec int(integer()) -> value().
int(Value) ->
{sql_int, Value}.
-file("src/sqlode/runtime.gleam", 135).
-spec float(float()) -> value().
float(Value) ->
{sql_float, Value}.
-file("src/sqlode/runtime.gleam", 139).
-spec bool(boolean()) -> value().
bool(Value) ->
{sql_bool, Value}.
-file("src/sqlode/runtime.gleam", 143).
-spec bytes(bitstring()) -> value().
bytes(Value) ->
{sql_bytes, Value}.
-file("src/sqlode/runtime.gleam", 147).
-spec array(list(value())) -> value().
array(Values) ->
{sql_array, Values}.
-file("src/sqlode/runtime.gleam", 151).
-spec nullable(gleam@option:option(HCO), fun((HCO) -> value())) -> value().
nullable(Value, Encode) ->
case Value of
{some, V} ->
Encode(V);
none ->
sql_null
end.
-file("src/sqlode/runtime.gleam", 171).
?DOC(
" Marker prefix emitted by the generator for a regular `sqlode.arg` /\n"
" `sqlode.narg` / `@name` parameter at the given **1-based** index.\n"
" Rendered into the final placeholder at runtime by\n"
" `expand_slice_placeholders`.\n"
"\n"
" Panics with\n"
" `\"sqlode.runtime.param_marker: index must be >= 1 (1-based) (got <n>)\"`\n"
" when `index < 1`. The runtime expand step is keyed by 1-based positions,\n"
" so a `0` or negative index would either round-trip to a syntactically\n"
" broken SQL string or silently match an unrelated marker — both\n"
" constitute SQL-shape bugs. Use `param_marker_checked/1` instead when\n"
" the caller wants to surface the precondition as a `Result` (for\n"
" example, in a custom adapter that accepts user-supplied indices).\n"
).
-spec param_marker(integer()) -> binary().
param_marker(Index) ->
case Index < 1 of
true ->
erlang:error(#{gleam_error => panic,
message => (<<<<"sqlode.runtime.param_marker: index must be >= 1 (1-based) (got "/utf8,
(erlang:integer_to_binary(Index))/binary>>/binary,
")"/utf8>>),
file => <<?FILEPATH/utf8>>,
module => <<"sqlode/runtime"/utf8>>,
function => <<"param_marker"/utf8>>,
line => 174});
false ->
<<<<"__sqlode_param_"/utf8,
(erlang:integer_to_binary(Index))/binary>>/binary,
"__"/utf8>>
end.
-file("src/sqlode/runtime.gleam", 192).
?DOC(
" Marker prefix emitted by the generator for a `sqlode.slice` parameter at\n"
" the given **1-based** index. Rendered into the expanded placeholder list\n"
" at runtime by `expand_slice_placeholders`.\n"
"\n"
" Panics with\n"
" `\"sqlode.runtime.slice_marker: index must be >= 1 (1-based) (got <n>)\"`\n"
" when `index < 1`. Same reasoning as `param_marker/1`. Use\n"
" `slice_marker_checked/1` instead when the caller wants to surface the\n"
" precondition as a `Result`.\n"
).
-spec slice_marker(integer()) -> binary().
slice_marker(Index) ->
case Index < 1 of
true ->
erlang:error(#{gleam_error => panic,
message => (<<<<"sqlode.runtime.slice_marker: index must be >= 1 (1-based) (got "/utf8,
(erlang:integer_to_binary(Index))/binary>>/binary,
")"/utf8>>),
file => <<?FILEPATH/utf8>>,
module => <<"sqlode/runtime"/utf8>>,
function => <<"slice_marker"/utf8>>,
line => 195});
false ->
<<<<"__sqlode_slice_"/utf8,
(erlang:integer_to_binary(Index))/binary>>/binary,
"__"/utf8>>
end.
-file("src/sqlode/runtime.gleam", 223).
?DOC(
" Like `param_marker/1`, but returns the precondition failure as a\n"
" `Result` instead of panicking. Use this when `index` comes from a\n"
" custom adapter or hand-rolled `RawQuery` and the caller wants to\n"
" surface bookkeeping mistakes without crashing the process. Same\n"
" reasoning as `expand_slice_placeholders_checked` (#546).\n"
"\n"
" On success the returned string is identical to `param_marker(index)`.\n"
).
-spec param_marker_checked(integer()) -> {ok, binary()} |
{error, marker_error()}.
param_marker_checked(Index) ->
case Index < 1 of
true ->
{error, {marker_index_non_positive, Index}};
false ->
{ok,
<<<<"__sqlode_param_"/utf8,
(erlang:integer_to_binary(Index))/binary>>/binary,
"__"/utf8>>}
end.
-file("src/sqlode/runtime.gleam", 233).
?DOC(
" Like `slice_marker/1`, but returns the precondition failure as a\n"
" `Result` instead of panicking. Same shape and reasoning as\n"
" `param_marker_checked/1`.\n"
).
-spec slice_marker_checked(integer()) -> {ok, binary()} |
{error, marker_error()}.
slice_marker_checked(Index) ->
case Index < 1 of
true ->
{error, {marker_index_non_positive, Index}};
false ->
{ok,
<<<<"__sqlode_slice_"/utf8,
(erlang:integer_to_binary(Index))/binary>>/binary,
"__"/utf8>>}
end.
-file("src/sqlode/runtime.gleam", 334).
-spec render_placeholder(placeholder_style(), integer()) -> binary().
render_placeholder(Style, Index) ->
case Style of
dollar_numbered ->
<<"$"/utf8, (erlang:integer_to_binary(Index))/binary>>;
question_numbered ->
<<"?"/utf8, (erlang:integer_to_binary(Index))/binary>>;
question_positional ->
<<"?"/utf8>>
end.
-file("src/sqlode/runtime.gleam", 396).
-spec validate_slice_entries(list({integer(), integer()}), integer()) -> {ok,
nil} |
{error, expand_error()}.
validate_slice_entries(Slices, Total_params) ->
case Slices of
[] ->
{ok, nil};
[{Idx, Len} | Rest] ->
case Len < 0 of
true ->
{error, {slice_length_negative, Idx, Len}};
false ->
case (Idx < 1) orelse (Idx > Total_params) of
true ->
{error,
{slice_index_out_of_range, Idx, Total_params}};
false ->
validate_slice_entries(Rest, Total_params)
end
end
end.
-file("src/sqlode/runtime.gleam", 386).
-spec validate_slices(list({integer(), integer()}), integer()) -> {ok, nil} |
{error, expand_error()}.
validate_slices(Slices, Total_params) ->
case Total_params < 0 of
true ->
{error, {total_params_negative, Total_params}};
false ->
validate_slice_entries(Slices, Total_params)
end.
-file("src/sqlode/runtime.gleam", 254).
?DOC(
" Render a parameter marker into the final engine-specific placeholder.\n"
"\n"
" The generator emits `__sqlode_param_N__` / `__sqlode_slice_N__` in the SQL\n"
" template regardless of the target engine. At runtime this function\n"
" replaces each marker with the correct placeholder string (for example\n"
" `$3` for PostgreSQL, `?3` for SQLite, `?` for MySQL) and expands slice\n"
" markers to a comma-separated list sized by the caller-provided\n"
" `slices`. Non-slice markers are renumbered sequentially across the\n"
" whole SQL text so that slices that precede them shift their index.\n"
"\n"
" Using markers instead of rewriting `prefix<>index` directly means\n"
" placeholder-like text inside string literals or comments is never\n"
" touched, and MySQL (which uses bare `?` rather than `?N`) works\n"
" without special-casing the placeholder format.\n"
).
-spec expand_slice_placeholders(
binary(),
list({integer(), integer()}),
integer(),
placeholder_style()
) -> binary().
expand_slice_placeholders(Sql, Slices, Total_params, Style) ->
case validate_slices(Slices, Total_params) of
{ok, _} ->
nil;
{error, {slice_length_negative, Index, Length}} ->
erlang:error(#{gleam_error => panic,
message => (<<<<<<<<"sqlode.expand_slice_placeholders: slice length must be >= 0 (got index="/utf8,
(erlang:integer_to_binary(Index))/binary>>/binary,
", length="/utf8>>/binary,
(erlang:integer_to_binary(Length))/binary>>/binary,
")"/utf8>>),
file => <<?FILEPATH/utf8>>,
module => <<"sqlode/runtime"/utf8>>,
function => <<"expand_slice_placeholders"/utf8>>,
line => 263});
{error, {slice_index_out_of_range, Index@1, Total}} ->
erlang:error(#{gleam_error => panic,
message => (<<<<<<<<<<<<"sqlode.expand_slice_placeholders: slice index must be in 1.."/utf8,
(erlang:integer_to_binary(Total))/binary>>/binary,
" (got index="/utf8>>/binary,
(erlang:integer_to_binary(Index@1))/binary>>/binary,
", total_params="/utf8>>/binary,
(erlang:integer_to_binary(Total))/binary>>/binary,
")"/utf8>>),
file => <<?FILEPATH/utf8>>,
module => <<"sqlode/runtime"/utf8>>,
function => <<"expand_slice_placeholders"/utf8>>,
line => 271});
{error, {total_params_negative, Total@1}} ->
erlang:error(#{gleam_error => panic,
message => (<<<<"sqlode.expand_slice_placeholders: total_params must be >= 0 (got "/utf8,
(erlang:integer_to_binary(Total@1))/binary>>/binary,
"). Use expand_slice_placeholders_checked for a Result-returning variant. (#565)"/utf8>>),
file => <<?FILEPATH/utf8>>,
module => <<"sqlode/runtime"/utf8>>,
function => <<"expand_slice_placeholders"/utf8>>,
line => 281})
end,
{_, Mapping} = gleam@int:range(
1,
Total_params + 1,
{1, []},
fun(Acc, Orig_idx) ->
{Next_new_idx, Map} = Acc,
case gleam@list:find(
Slices,
fun(S) -> erlang:element(1, S) =:= Orig_idx end
) of
{ok, {_, Len}} ->
Marker = slice_marker(Orig_idx),
case Len of
0 ->
{Next_new_idx, [{Marker, <<"NULL"/utf8>>} | Map]};
_ ->
Expanded = begin
_pipe = gleam@int:range(
Next_new_idx,
Next_new_idx + Len,
[],
fun(Items, I) ->
[render_placeholder(Style, I) | Items]
end
),
_pipe@1 = lists:reverse(_pipe),
gleam@string:join(_pipe@1, <<", "/utf8>>)
end,
{Next_new_idx + Len, [{Marker, Expanded} | Map]}
end;
{error, _} ->
Marker@1 = param_marker(Orig_idx),
{Next_new_idx + 1,
[{Marker@1, render_placeholder(Style, Next_new_idx)} |
Map]}
end
end
),
_pipe@2 = Mapping,
_pipe@3 = lists:reverse(_pipe@2),
gleam@list:fold(
_pipe@3,
Sql,
fun(S@1, Entry) ->
{Marker@2, Replacement} = Entry,
gleam@string:replace(S@1, Marker@2, Replacement)
end
).
-file("src/sqlode/runtime.gleam", 110).
?DOC(
" Prepare a raw query for execution by encoding parameters and expanding\n"
" the engine-agnostic placeholder markers that the generator emits. The\n"
" target placeholder dialect is read from `query.placeholder_style`, so\n"
" callers no longer need to pass a separate style argument.\n"
" Returns the final SQL string and the flattened parameter values, ready\n"
" to be passed to a database driver.\n"
).
-spec prepare(raw_query(HCK), HCK) -> {binary(), list(value())}.
prepare(Query, Params) ->
Values = (erlang:element(7, Query))(Params),
Slices = (erlang:element(8, Query))(Params),
Sql = expand_slice_placeholders(
erlang:element(3, Query),
Slices,
erlang:element(5, Query),
erlang:element(6, Query)
),
{Sql, Values}.
-file("src/sqlode/runtime.gleam", 370).
?DOC(
" Like `expand_slice_placeholders`, but returns the validation\n"
" failure as a `Result` instead of panicking. Use this when `slices`\n"
" or `total_params` come from a custom adapter / hand-rolled\n"
" `RawQuery` and the caller wants to surface bookkeeping mistakes\n"
" without crashing the process.\n"
"\n"
" On success the returned string is identical to\n"
" `expand_slice_placeholders(sql, slices, total_params, style)`.\n"
).
-spec expand_slice_placeholders_checked(
binary(),
list({integer(), integer()}),
integer(),
placeholder_style()
) -> {ok, binary()} | {error, expand_error()}.
expand_slice_placeholders_checked(Sql, Slices, Total_params, Style) ->
case Total_params < 0 of
true ->
{error, {total_params_negative, Total_params}};
false ->
case validate_slices(Slices, Total_params) of
{error, E} ->
{error, E};
{ok, _} ->
{ok,
expand_slice_placeholders(
Sql,
Slices,
Total_params,
Style
)}
end
end.