Current section

Files

Jump to
sqlode src sqlode@runtime.erl
Raw

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/7, raw_query_simple/6, 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() :: one |
many |
exec |
exec_result |
exec_rows |
exec_last_id |
batch_one |
batch_many |
batch_exec |
copy_from.
-type value() :: null |
{string, binary()} |
{int, integer()} |
{float, float()} |
{bool, boolean()} |
{bytes, bitstring()} |
{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()} |
{empty_slice, integer()} |
{slice_start_non_positive, integer()} |
{slice_start_after_params, integer(), integer()} |
{slice_length_exceeds_params, integer(), integer(), integer()}.
-file("src/sqlode/runtime.gleam", 101).
?DOC(
" Construct a `RawQuery` directly. Behaviour is identical to\n"
" invoking the `RawQuery(...)` constructor; the named helper exists\n"
" so the intent (\"this is a hand-rolled `RawQuery`, not codegen\n"
" output\") is obvious at the call site and discoverable via the\n"
" docs.\n"
"\n"
" Use this helper when:\n"
"\n"
" - using `sqlode/runtime` as a library, bypassing `sqlode generate`\n"
" codegen for hand-rolled queries that live next to your handler\n"
" code;\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"
" Production callers who run codegen should keep using the\n"
" `RawQuery` values `sqlode generate` produces from declarative SQL\n"
" fixtures — they pin the SQL string and parameter shape next to the\n"
" schema, which this helper does not.\n"
).
-spec raw_query(
binary(),
binary(),
query_command(),
integer(),
placeholder_style(),
fun((HCG) -> list(value())),
fun((HCG) -> list({integer(), integer()}))
) -> raw_query(HCG).
raw_query(
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", 140).
?DOC(
" Thin wrapper around `raw_query/7` for hand-rolled queries that do\n"
" not use slice-expanded `IN ($1)` placeholders. Forwards every\n"
" labelled argument to `raw_query` and supplies\n"
" `slice_info: fn(_) { [] }` so ad-hoc callers do not have to repeat\n"
" the always-empty lambda at every call site.\n"
"\n"
" Reach for `raw_query_simple` when:\n"
"\n"
" - writing a hand-rolled `INSERT` / `UPDATE` / `DELETE` or a fixed-\n"
" shape `SELECT` whose SQL has zero slice markers; or\n"
" - prototyping a query against `sqlode/runtime` as a library, before\n"
" moving the SQL into a declarative fixture and regenerating\n"
" through `sqlode generate`.\n"
"\n"
" Keep using `raw_query/7` when the SQL contains slice-expanded\n"
" `IN ($N)` placeholders — those queries need the real `slice_info`\n"
" callback to expand at `prepare/2` time. `sqlode generate` codegen\n"
" continues to emit `raw_query/7` (or the bare `RawQuery(...)`\n"
" constructor) so this wrapper only affects ad-hoc callers.\n"
).
-spec raw_query_simple(
binary(),
binary(),
query_command(),
integer(),
placeholder_style(),
fun((HCK) -> list(value()))
) -> raw_query(HCK).
raw_query_simple(Name, Sql, Command, Param_count, Placeholder_style, Encode) ->
raw_query(
Name,
Sql,
Command,
Param_count,
Placeholder_style,
Encode,
fun(_) -> [] end
).
-file("src/sqlode/runtime.gleam", 165).
?DOC(
" Deprecated alias for `raw_query/7`. The `_for_test` suffix\n"
" discouraged library-mode callers from using the only sanctioned\n"
" non-constructor path even though library use is exactly what the\n"
" helper was built for. `raw_query/7` carries the same behaviour\n"
" without the suffix.\n"
).
-spec raw_query_for_test(
binary(),
binary(),
query_command(),
integer(),
placeholder_style(),
fun((HCN) -> list(value())),
fun((HCN) -> list({integer(), integer()}))
) -> raw_query(HCN).
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", 204).
-spec null() -> value().
null() ->
null.
-file("src/sqlode/runtime.gleam", 208).
-spec string(binary()) -> value().
string(Value) ->
{string, Value}.
-file("src/sqlode/runtime.gleam", 212).
-spec int(integer()) -> value().
int(Value) ->
{int, Value}.
-file("src/sqlode/runtime.gleam", 216).
-spec float(float()) -> value().
float(Value) ->
{float, Value}.
-file("src/sqlode/runtime.gleam", 220).
-spec bool(boolean()) -> value().
bool(Value) ->
{bool, Value}.
-file("src/sqlode/runtime.gleam", 224).
-spec bytes(bitstring()) -> value().
bytes(Value) ->
{bytes, Value}.
-file("src/sqlode/runtime.gleam", 228).
-spec array(list(value())) -> value().
array(Values) ->
{array, Values}.
-file("src/sqlode/runtime.gleam", 232).
-spec nullable(gleam@option:option(HCV), fun((HCV) -> value())) -> value().
nullable(Value, Encode) ->
case Value of
{some, V} ->
Encode(V);
none ->
null
end.
-file("src/sqlode/runtime.gleam", 252).
?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 => 255});
false ->
<<<<"__sqlode_param_"/utf8,
(erlang:integer_to_binary(Index))/binary>>/binary,
"__"/utf8>>
end.
-file("src/sqlode/runtime.gleam", 273).
?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 => 276});
false ->
<<<<"__sqlode_slice_"/utf8,
(erlang:integer_to_binary(Index))/binary>>/binary,
"__"/utf8>>
end.
-file("src/sqlode/runtime.gleam", 304).
?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", 314).
?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", 429).
-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", 534).
-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 Len =:= 0 of
true ->
{error, {empty_slice, Idx}};
false ->
case Idx < 1 of
true ->
{error, {slice_start_non_positive, Idx}};
false ->
case Idx > Total_params of
true ->
{error,
{slice_start_after_params,
Idx,
Total_params}};
false ->
case ((Idx + Len) - 1) > Total_params of
true ->
{error,
{slice_length_exceeds_params,
Idx,
Len,
Total_params}};
false ->
validate_slice_entries(
Rest,
Total_params
)
end
end
end
end
end
end.
-file("src/sqlode/runtime.gleam", 524).
?DOC(
" Strict validator used by `expand_slice_placeholders_checked`.\n"
"\n"
" Produces one of the four post-#585 refined variants (`EmptySlice`,\n"
" `SliceStartNonPositive`, `SliceStartAfterParams`,\n"
" `SliceLengthExceedsParams`) in addition to the pre-existing\n"
" `SliceLengthNegative` and `TotalParamsNegative`. The legacy\n"
" `SliceIndexOutOfRange` variant is never produced here — it stays\n"
" in the type purely for source compatibility.\n"
).
-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", 589).
-spec validate_slice_entries_for_panic(list({integer(), integer()}), integer()) -> {ok,
nil} |
{error, expand_error()}.
validate_slice_entries_for_panic(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_for_panic(Rest, Total_params)
end
end
end.
-file("src/sqlode/runtime.gleam", 579).
?DOC(
" Lenient validator used by the panicking\n"
" `expand_slice_placeholders`. Preserves the pre-#585 panic shape:\n"
" `length = 0` is accepted (the expand loop collapses the slice to\n"
" `IN (NULL)`), and any other invalid start/length combination is\n"
" folded into the umbrella `SliceIndexOutOfRange` variant the panic\n"
" message expects.\n"
).
-spec validate_slices_for_panic(list({integer(), integer()}), integer()) -> {ok,
nil} |
{error, expand_error()}.
validate_slices_for_panic(Slices, Total_params) ->
case Total_params < 0 of
true ->
{error, {total_params_negative, Total_params}};
false ->
validate_slice_entries_for_panic(Slices, Total_params)
end.
-file("src/sqlode/runtime.gleam", 335).
?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_for_panic(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 => 344});
{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 => 352});
{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 => 362});
{error, {empty_slice, _}} ->
nil;
{error, {slice_start_non_positive, _}} ->
nil;
{error, {slice_start_after_params, _, _}} ->
nil;
{error, {slice_length_exceeds_params, _, _, _}} ->
nil
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", 191).
?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(HCR), HCR) -> {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", 500).
?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"
" Validation rules follow a 1-based indexing convention: each\n"
" `slices` entry `#(start, length)` is valid iff `start >= 1` and\n"
" `start + length - 1 <= total_params`. A `length` of `0` is rejected\n"
" as `EmptySlice` (post-#585); see the `ExpandError` doc-comment for\n"
" the full failure taxonomy.\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.