Current section

Files

Jump to
cake src cake@fragment.erl
Raw

src/cake@fragment.erl

-module(cake@fragment).
-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]).
-define(FILEPATH, "src/cake/fragment.gleam").
-export([int/1, prepared/2, literal/1, bool/1, true/0, false/0, float/1, string/1, null/0, date/1]).
-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(
" Fragments are low level building blocks of queries which allow direct\n"
" manipulation of the query string.\n"
"\n"
" If you want to insert parameters, you are required to use prepared\n"
" fragments, which will be validated against the number of parameters given\n"
" and the parameters are automatically escaped by the RDBMS to prevent SQL\n"
" injections.\n"
"\n"
" Low-level building blocks for injecting raw SQL into queries while keeping\n"
" parameter binding safe.\n"
"\n"
" ## Aliases\n"
"\n"
" ```gleam\n"
" import cake/fragment as f\n"
" ```\n"
"\n"
" ---\n"
"\n"
" ## When to use fragments\n"
"\n"
" Use fragments when Cake's typed builder functions do not cover your use case:\n"
"\n"
" - Database-specific functions (`NOW()`, `ARRAY_AGG(...)`, `JSON_BUILD_OBJECT(...)`)\n"
" - Type casts (`$1::uuid`, `? AS UNSIGNED`)\n"
" - Any SQL expression not expressible through the standard API\n"
"\n"
" > ⛔ **Never** pass uncontrolled user input through `f.literal()`.\n"
" > Always use `f.prepared()` with typed params to bind user data safely.\n"
"\n"
" ---\n"
"\n"
" ## Fragment constructors\n"
"\n"
" ### `prepared(string, params) -> Fragment`\n"
"\n"
" Creates a fragment where `?` placeholders (the value of `f.placeholder`) are\n"
" replaced with safely-bound parameters at query execution time.\n"
"\n"
" ```gleam\n"
" f.prepared(\"?::uuid\", [f.string(\"0000-0000-4000-a000-a00000000000\")])\n"
" ```\n"
"\n"
" The placeholder character is exported as the constant `f.placeholder` (the `?`\n"
" grapheme). Use it when constructing fragment strings dynamically.\n"
"\n"
" > ⛔ If the number of placeholders does not match the number of params, an\n"
" > error is printed to stderr and the fragment is created with best-effort\n"
" > fallback behaviour:\n"
" >\n"
" > - Too many placeholders → last param is repeated.\n"
" > - Too many params → excess params are ignored.\n"
"\n"
" ### `literal(string) -> Fragment`\n"
"\n"
" Creates a fragment from a **static, developer-controlled** SQL string.\n"
" No parameter substitution occurs.\n"
"\n"
" ```gleam\n"
" f.literal(\"NOW()\")\n"
" f.literal(\"CURRENT_TIMESTAMP\")\n"
" f.literal(\"COUNT(*)\")\n"
" ```\n"
"\n"
" > ⛔ **YOU ARE FORBIDDEN TO INSERT UNCONTROLLED USER INPUT THIS WAY.**\n"
"\n"
" ---\n"
"\n"
" ## Placeholder\n"
"\n"
" ```gleam\n"
" f.placeholder // the \"?\" grapheme used inside prepared fragment strings\n"
" ```\n"
"\n"
" ---\n"
"\n"
" ## Param constructors\n"
"\n"
" Params are the typed values passed to `f.prepared()`. They are escaped by the\n"
" database driver, preventing SQL injection.\n"
"\n"
" | Function | Gleam type | SQL type |\n"
" | ---------- | ---------- | ------------- |\n"
" | `bool(value)` | `Bool` | Boolean |\n"
" | `true()` | — | `TRUE` |\n"
" | `false()` | — | `FALSE` |\n"
" | `float(value)` | `Float` | Float |\n"
" | `int(value)` | `Int` | Integer |\n"
" | `string(value)` | `String` | String / Text |\n"
" | `null()` | — | `NULL` |\n"
" | `date(value)` | `calendar.Date` | Date |\n"
"\n"
" ---\n"
"\n"
" ## Using fragments in queries\n"
"\n"
" Fragments can appear in several positions depending on context:\n"
"\n"
" ```mermaid\n"
" flowchart TD\n"
" A[Fragment] --> B[SelectValue\\ns.fragment]\n"
" A --> C[InsertValue\\ni.fragment]\n"
" A --> D[UpdateSet\\nu.set_fragment]\n"
" A --> E[WhereValue\\nw.fragment_value]\n"
" A --> F[Where condition\\nw.fragment]\n"
" ```\n"
"\n"
" ### In SELECT projections\n"
"\n"
" ```gleam\n"
" import cake/select as s\n"
" import cake/fragment as f\n"
"\n"
" s.new()\n"
" |> s.from_table(\"orders\")\n"
" |> s.select(s.fragment(f.literal(\"SUM(amount)\")) |> s.alias(\"total\"))\n"
" ```\n"
"\n"
" ### In INSERT values\n"
"\n"
" ```gleam\n"
" import cake/insert as i\n"
" import cake/fragment as f\n"
"\n"
" i.from_values(\"users\", [\"id\", \"name\"], [\n"
" i.row([\n"
" i.fragment(f.prepared(\"?::uuid\", [f.string(\"abc-123\")])),\n"
" i.string(\"Alice\"),\n"
" ]),\n"
" ])\n"
" ```\n"
"\n"
" ### In UPDATE SET\n"
"\n"
" ```gleam\n"
" import cake/update as u\n"
" import cake/fragment as f\n"
"\n"
" u.new()\n"
" |> u.table(\"sessions\")\n"
" |> u.set(u.set_fragment(\"expires_at\", f.literal(\"NOW() + INTERVAL '1 hour'\")))\n"
" ```\n"
"\n"
" ### In WHERE — as a value operand\n"
"\n"
" ```gleam\n"
" import cake/where as w\n"
" import cake/fragment as f\n"
"\n"
" w.eq(\n"
" w.col(\"tags\"),\n"
" w.fragment_value(f.prepared(\"?::jsonb\", [f.string(\"[\\\"gleam\\\"]\")])),\n"
" )\n"
" ```\n"
"\n"
" ### In WHERE — as a full condition\n"
"\n"
" ```gleam\n"
" w.fragment(f.prepared(\"? @> ?::jsonb\", [\n"
" f.string(\"tags\"),\n"
" f.string(\"[\\\"gleam\\\"]\"),\n"
" ]))\n"
" ```\n"
"\n"
" ---\n"
"\n"
" ## Safety reference\n"
"\n"
" | | `f.prepared` | `f.literal` |\n"
" | ------ | ---------- | ----------- |\n"
" | User input safe | ✅ | ❌ |\n"
" | Param substitution | ✅ | ❌ |\n"
" | Static SQL only | ❌ | ✅ |\n"
"\n"
" ---\n"
"\n"
" ## Full Example\n"
"\n"
" ```gleam\n"
" import cake/select as s\n"
" import cake/where as w\n"
" import cake/fragment as f\n"
"\n"
" // Select users whose tags JSON array contains \"gleam\"\n"
" s.new()\n"
" |> s.from_table(\"users\")\n"
" |> s.select_cols([\"id\", \"name\"])\n"
" |> s.select(s.fragment(f.literal(\"tags::text\")) |> s.alias(\"tags_text\"))\n"
" |> s.where(w.fragment(\n"
" f.prepared(\"tags @> ?::jsonb\", [f.string(\"[\\\"gleam\\\"]\")]),\n"
" ))\n"
" |> s.order_by_asc(\"name\")\n"
" |> s.to_query\n"
" ```\n"
"\n"
"\n"
" <!-- html assets for docs gen -->\n"
" <style>\n"
" .page {\n"
" display: block;\n"
" }\n"
" .content {\n"
" width: auto;\n"
" max-width: none;\n"
" }\n"
" </style>\n"
" <!--<script src=\"https://cdn.jsdelivr.net/npm/@mermaid-js/tiny@11/dist/mermaid.tiny.js\"></script>-->\n"
" <script\n"
" src=\"https://cdn.jsdelivr.net/npm/mermaid/dist/mermaid.min.js\"\n"
" integrity=\"sha256-cBN+d7snO7LvlyuG6LBADMqL5TyyW/xFkRoYbcmGZd4=\"\n"
" crossorigin=\"anonymous\"\n"
" ></script>\n"
" <script>\n"
" (callback => document.readyState !== 'loading' ? callback() : document.addEventListener('DOMContentLoaded', callback, { once: true }))(() => {\n"
" mermaid.initialize({ startOnLoad: false })\n"
" mermaid.run({\n"
" querySelector: \".language-mermaid\",\n"
" })\n"
" })\n"
" </script>\n"
"\n"
).
-file("src/cake/fragment.gleam", 360).
?DOC(" Create a new `Param` with an `Int` value.\n").
-spec int(integer()) -> cake@param:param().
int(Value) ->
_pipe = Value,
{int_param, _pipe}.
-file("src/cake/fragment.gleam", 264).
?DOC(
" Create a new fragment from a string and a list of parameters.\n"
"\n"
" ⛔ ⛔ ⛔\n"
"\n"
" If you mismatch the number of placeholders with the number of\n"
" parameters, an error will be printed to stderr and the fragment will be\n"
" created with the given parameters:\n"
"\n"
" - If there are too many placeholders, the fragment will be created with the\n"
" given parameters and the last parameter will be repeated for the remaining\n"
" placeholders.\n"
" - If there are too many parameters, the fragment will be created with the\n"
" given parameters and the excess parameters will be ignored.\n"
"\n"
" ⛔ ⛔ ⛔\n"
).
-spec prepared(binary(), list(cake@param:param())) -> cake@internal@read_query:fragment().
prepared(String, Params) ->
Placeholder_count = begin
_pipe = String,
_pipe@1 = cake@internal@read_query:fragment_prepared_split_string(_pipe),
cake@internal@read_query:fragment_count_placeholders(_pipe@1)
end,
Param_count = begin
_pipe@2 = Params,
erlang:length(_pipe@2)
end,
case {Placeholder_count,
Param_count,
begin
_pipe@3 = Placeholder_count,
gleam@int:compare(_pipe@3, Param_count)
end} of
{0, 0, eq} ->
_pipe@4 = String,
{fragment_literal, _pipe@4};
{_, _, eq} ->
_pipe@5 = String,
{fragment_prepared, _pipe@5, Params};
{0, _, _} ->
gleam_stdlib:println_error(
<<<<<<<<"Fragment had 0 "/utf8, (<<"$"/utf8>>)/binary>>/binary,
"-placeholders, but "/utf8>>/binary,
(begin
_pipe@6 = Param_count,
erlang:integer_to_binary(_pipe@6)
end)/binary>>/binary,
" params given!"/utf8>>
),
_pipe@7 = String,
{fragment_literal, _pipe@7};
{_, 0, _} ->
gleam_stdlib:println_error(
<<<<<<<<"Fragment had "/utf8,
(begin
_pipe@8 = Placeholder_count,
erlang:integer_to_binary(_pipe@8)
end)/binary>>/binary,
" "/utf8>>/binary,
(<<"$"/utf8>>)/binary>>/binary,
"-placeholders, but 0 params given!"/utf8>>
),
_pipe@9 = String,
{fragment_literal, _pipe@9};
{_, _, _} ->
gleam_stdlib:println_error(
<<<<<<<<<<<<"Fragment had "/utf8,
(begin
_pipe@10 = Placeholder_count,
erlang:integer_to_binary(_pipe@10)
end)/binary>>/binary,
" "/utf8>>/binary,
(<<"$"/utf8>>)/binary>>/binary,
"-placeholders, but "/utf8>>/binary,
(begin
_pipe@11 = Param_count,
erlang:integer_to_binary(_pipe@11)
end)/binary>>/binary,
" params given!"/utf8>>
),
_pipe@12 = String,
{fragment_prepared, _pipe@12, Params}
end.
-file("src/cake/fragment.gleam", 326).
?DOC(
" Create a new fragment from a literal string.\n"
"\n"
" ⛔ ⛔ ⛔\n"
"\n"
" WARNING: YOU ARE FORBIDDEN TO INSERT UNCONTROLLED USER INPUT THIS WAY!\n"
"\n"
" ⛔ ⛔ ⛔\n"
).
-spec literal(binary()) -> cake@internal@read_query:fragment().
literal(String) ->
_pipe = String,
{fragment_literal, _pipe}.
-file("src/cake/fragment.gleam", 336).
?DOC(" Create a new `Param` with a `Bool` value.\n").
-spec bool(boolean()) -> cake@param:param().
bool(Value) ->
_pipe = Value,
{bool_param, _pipe}.
-file("src/cake/fragment.gleam", 342).
?DOC(" Create a new `Param` with a `True` value.\n").
-spec true() -> cake@param:param().
true() ->
_pipe = true,
{bool_param, _pipe}.
-file("src/cake/fragment.gleam", 348).
?DOC(" Create a new `Param` with a `False` value.\n").
-spec false() -> cake@param:param().
false() ->
_pipe = false,
{bool_param, _pipe}.
-file("src/cake/fragment.gleam", 354).
?DOC(" Create a new `Param` with a `Float` value.\n").
-spec float(float()) -> cake@param:param().
float(Value) ->
_pipe = Value,
{float_param, _pipe}.
-file("src/cake/fragment.gleam", 366).
?DOC(" Create a new `Param` with a `String` value.\n").
-spec string(binary()) -> cake@param:param().
string(Value) ->
_pipe = Value,
{string_param, _pipe}.
-file("src/cake/fragment.gleam", 372).
?DOC(" Create a new `Param` with an SQL `NULL` value.\n").
-spec null() -> cake@param:param().
null() ->
null_param.
-file("src/cake/fragment.gleam", 378).
?DOC(" Create a new `Param` with a `calendar.Date` value.\n").
-spec date(gleam@time@calendar:date()) -> cake@param:param().
date(Value) ->
_pipe = Value,
{date_param, _pipe}.