Current section

Files

Jump to
equery src pg_sql.erl
Raw

src/pg_sql.erl

-module(pg_sql).
-moduledoc """
SQL expression builders.
Operators, scalar functions, aggregates, CASE, type casts. These produce
`qast:ast_node()` values that go inside `q:where`/`q:select`/etc.
closures.
Inside DSL closures, native Erlang operators (`=:=`, `>`, `andalso`,
`+`, …) are rewritten by `equery_pt` to corresponding `pg_sql:`
functions, so you rarely call these directly for primitive operations.
Use the explicit `pg_sql:` form for:
- Functions (`pg_sql:count/1`, `pg_sql:lower/1`, `pg_sql:date_trunc/2`, …);
- Operators that aren't Erlang operators (`pg_sql:like/2`, `pg_sql:'~'/2`);
- Aggregates and `pg_sql:filter/2`;
- `pg_sql:case_when/1,2`;
- Type casts (`pg_sql:as/2`).
JSON/JSONB live in [`qjson`](`m:qjson`).
""".
-include("query.hrl").
-include("cth.hrl").
-export([
'andalso'/2,
'orelse'/2,
'=:='/2,
'=/='/2,
'>'/2,
'>='/2,
'<'/2,
'=<'/2,
'not'/1,
'is'/2,
is_null/1,
is_not_null/1,
is_distinct_from/2,
is_not_distinct_from/2,
'+'/2,
'-'/2,
'*'/2,
'/'/2,
'div'/2,
'rem'/2,
'abs'/1
]).
-export([
mod/2,
round/1, round/2,
ceil/1,
floor/1,
power/2,
sqrt/1,
ln/1,
log/1, log/2,
exp/1,
sign/1,
random/0
]).
-export([
'~'/2,
'~*'/2,
like/2,
ilike/2
]).
-export([
call/3
]).
-export([
sum/1,
count/1,
min/1,
max/1,
distinct/1,
array_agg/1, array_agg/2,
avg/1,
bool_and/1,
bool_or/1,
every/1,
string_agg/2, string_agg/3,
json_agg/1, json_agg/2,
jsonb_agg/1, jsonb_agg/2,
json_object_agg/2,
jsonb_object_agg/2,
percentile_cont/2,
percentile_disc/2,
mode/1,
filter/2,
trunc/2
]).
-export([
min/2,
max/2,
greatest/1,
least/1,
row/1,
row/2
]).
-export([
coalesce/1,
case_when/1,
case_when/2,
in/2,
exists/1
]).
%% Date/time functions
-export([
now/0,
current_timestamp/0,
current_date/0,
current_time/0,
date_trunc/2,
extract/2,
date_part/2,
age/1, age/2,
to_char/2,
to_date/2,
to_timestamp/1, to_timestamp/2
]).
%% String functions
-export([
concat/1, concat/2,
length/1,
char_length/1,
lower/1,
upper/1,
trim/1, trim/2,
ltrim/1, ltrim/2,
rtrim/1, rtrim/2,
replace/3,
split_part/3,
substring/2, substring/3,
strpos/2,
starts_with/2,
regexp_replace/3, regexp_replace/4,
regexp_match/2, regexp_match/3
]).
%% Array functions
-export([
'@>'/2,
'<@'/2,
'&&'/2,
array/1,
array_length/1, array_length/2,
array_position/2,
array_append/2,
array_prepend/2,
array_remove/2,
array_replace/3,
array_cat/2,
unnest/1
]).
%% String / array concat
-export([
'||'/2
]).
%% Type function
-export([
as/2,
set_type/2
]).
-ifdef(TEST).
-export([type_str/1]).
-endif.
%% =============================================================================
%% Sql operations
%% =============================================================================
%% DSL expression: either an AST node, or any term that gets auto-wrapped
%% as a `$value` placeholder at SQL generation time.
-type expr() :: qast:ast_node() | term().
-export_type([expr/0]).
%% = Primitive =================================================================
-doc(#{group => <<"Logical">>}).
-doc "Logical `AND`. Short-circuits on boolean literals. See [Logical Operators](https://www.postgresql.org/docs/current/functions-logical.html).".
-spec 'andalso'(V, V) -> V when V :: boolean() | qast:ast_node().
'andalso'(true, B) -> B;
'andalso'(A, true) -> A;
'andalso'(false, _) -> false;
'andalso'(_, false) -> false;
'andalso'(A, B) ->
qast:exp([qast:raw("("), A, qast:raw(" and "), B, qast:raw(")")], #{type => boolean}).
-doc(#{group => <<"Logical">>}).
-doc "Logical `OR`. Short-circuits on boolean literals. See [Logical Operators](https://www.postgresql.org/docs/current/functions-logical.html).".
-spec 'orelse'(V, V) -> V when V :: boolean() | qast:ast_node().
'orelse'(true, _) -> true;
'orelse'(_, true) -> true;
'orelse'(false, B) -> B;
'orelse'(A, false) -> A;
'orelse'(A, B) ->
qast:exp([qast:raw("("), A, qast:raw(" or "), B, qast:raw(")")], #{type => boolean}).
-doc(#{group => <<"Logical">>}).
-doc "Logical `NOT`. See [Logical Operators](https://www.postgresql.org/docs/current/functions-logical.html).".
-spec 'not'(V) -> V when V :: boolean() | qast:ast_node().
'not'(A) when is_boolean(A) -> not A;
'not'(A) ->
qast:exp([qast:raw("not "), A], #{type => boolean}).
-doc(#{group => <<"Comparison">>}).
-doc "`A = B`. See [Comparison Operators](https://www.postgresql.org/docs/current/functions-comparison.html).".
-spec '=:='(expr(), expr()) -> qast:ast_node().
'=:='(A, B) ->
qast:exp([qast:raw("("), A, qast:raw(" = "), B, qast:raw(")")], #{type => boolean}).
-doc(#{group => <<"Comparison">>}).
-doc "`A <> B`. See [Comparison Operators](https://www.postgresql.org/docs/current/functions-comparison.html).".
-spec '=/='(expr(), expr()) -> qast:ast_node().
'=/='(A, B) -> 'not'('=:='(A,B)).
-doc(#{group => <<"Comparison">>}).
-doc "`A > B`. See [Comparison Operators](https://www.postgresql.org/docs/current/functions-comparison.html).".
-spec '>'(expr(), expr()) -> qast:ast_node().
'>'(A, B) ->
qast:exp([qast:raw("("), A, qast:raw(" > "), B, qast:raw(")")], #{type => boolean}).
-doc(#{group => <<"Comparison">>}).
-doc "`A >= B`.".
-spec '>='(expr(), expr()) -> qast:ast_node().
'>='(A, B) ->
qast:exp([qast:raw("("), A, qast:raw(" >= "), B, qast:raw(")")], #{type => boolean}).
-doc(#{group => <<"Comparison">>}).
-doc "`A < B`.".
-spec '<'(expr(), expr()) -> qast:ast_node().
'<'(A, B) ->
qast:exp([qast:raw("("), A, qast:raw(" < "), B, qast:raw(")")], #{type => boolean}).
-doc(#{group => <<"Comparison">>}).
-doc "`A <= B`.".
-spec '=<'(expr(), expr()) -> qast:ast_node().
'=<'(A, B) ->
qast:exp([qast:raw("("), A, qast:raw(" <= "), B, qast:raw(")")], #{type => boolean}).
-doc(#{group => <<"Comparison">>}).
-doc "`A IS B`. Low-level building block for `IS NULL`, `IS TRUE`, etc. See [Comparison Predicates](https://www.postgresql.org/docs/current/functions-comparison.html#FUNCTIONS-COMPARISON-PRED-TABLE).".
-spec 'is'(expr(), expr()) -> qast:ast_node().
is(A, B) ->
qast:exp([A, qast:raw(" is "), B], #{type => boolean}).
-doc(#{group => <<"Comparison">>}).
-doc "`A IS NULL`. See [Comparison Predicates](https://www.postgresql.org/docs/current/functions-comparison.html#FUNCTIONS-COMPARISON-PRED-TABLE).".
-spec 'is_null'(expr()) -> qast:ast_node().
is_null(A) ->
is(A, qast:raw("null")).
-doc(#{group => <<"Comparison">>}).
-doc "`A IS NOT NULL`.".
-spec 'is_not_null'(expr()) -> qast:ast_node().
is_not_null(A) ->
qast:exp([A, qast:raw(" is not null")], #{type => boolean}).
-doc(#{group => <<"Comparison">>}).
-doc """
`A IS DISTINCT FROM B` — NULL-safe inequality (treats `NULL = NULL` as
equal). See [Comparison Predicates](https://www.postgresql.org/docs/current/functions-comparison.html#FUNCTIONS-COMPARISON-PRED-TABLE).
""".
-spec is_distinct_from(expr(), expr()) -> qast:ast_node().
is_distinct_from(A, B) ->
qast:exp([A, qast:raw(" is distinct from "), B], #{type => boolean}).
-doc(#{group => <<"Comparison">>}).
-doc "`A IS NOT DISTINCT FROM B` — NULL-safe equality.".
-spec is_not_distinct_from(expr(), expr()) -> qast:ast_node().
is_not_distinct_from(A, B) ->
qast:exp([A, qast:raw(" is not distinct from "), B], #{type => boolean}).
%% @TODO type opts
-doc(#{group => <<"Arithmetic">>}).
-doc "`A + B`. See [Mathematical Operators](https://www.postgresql.org/docs/current/functions-math.html#FUNCTIONS-MATH-OP-TABLE).".
-spec '+'(expr(), expr()) -> qast:ast_node().
'+'(A, B) ->
qast:exp([qast:raw("("), A, qast:raw(" + "), B, qast:raw(")")]).
-doc(#{group => <<"Arithmetic">>}).
-doc "`A - B`.".
-spec '-'(expr(), expr()) -> qast:ast_node().
'-'(A, B) ->
qast:exp([qast:raw("("), A, qast:raw(" - "), B, qast:raw(")")]).
-doc(#{group => <<"Arithmetic">>}).
-doc "`A * B`.".
-spec '*'(expr(), expr()) -> qast:ast_node().
'*'(A, B) ->
qast:exp([qast:raw("("), A, qast:raw(" * "), B, qast:raw(")")]).
-doc(#{group => <<"Arithmetic">>}).
-doc "`A / B` — division. For integers, truncates toward zero.".
-spec '/'(expr(), expr()) -> qast:ast_node().
'/'(A, B) ->
qast:exp([qast:raw("("), A, qast:raw(" / "), B, qast:raw(")")]).
-doc(#{group => <<"Arithmetic">>}).
-doc "`abs(A)` — absolute value. See [Math Functions](https://www.postgresql.org/docs/current/functions-math.html#FUNCTIONS-MATH-FUNC-TABLE).".
-spec 'abs'(expr()) -> qast:ast_node().
abs(A) ->
qast:exp([qast:raw("abs("), A, qast:raw(")")], qast:opts(A)).
-doc(#{group => <<"Arithmetic">>}).
-doc "`div(A, B)` — integer quotient, truncated toward zero.".
-spec 'div'(expr(), expr()) -> qast:ast_node().
'div'(A, B) ->
call("div", [A, B], qast:opts(A)).
-doc(#{group => <<"Arithmetic">>}).
-doc "Alias for [`mod/2`](`mod/2`). Erlang's `rem` operator inside DSL closures rewrites to this.".
-spec 'rem'(expr(), expr()) -> qast:ast_node().
'rem'(A, B) -> mod(A, B).
%% = Numeric functions =========================================================
-doc(#{group => <<"Numeric">>}).
-doc "`mod(A, B)` — remainder of `A / B`. See [Math Functions](https://www.postgresql.org/docs/current/functions-math.html#FUNCTIONS-MATH-FUNC-TABLE).".
-spec mod(expr(), expr()) -> qast:ast_node().
mod(A, B) ->
call("mod", [A, B], qast:opts(A)).
-doc(#{group => <<"Numeric">>}).
-doc "`round(A)` — round to nearest integer (banker's rounding for numeric).".
-spec round(expr()) -> qast:ast_node().
round(A) ->
call("round", [A], qast:opts(A)).
-doc(#{group => <<"Numeric">>}).
-doc "`round(A, N)` — round to `N` decimal places.".
-spec round(expr(), expr()) -> qast:ast_node().
round(A, N) ->
call("round", [A, N], qast:opts(A)).
-doc(#{group => <<"Numeric">>}).
-doc "`ceil(A)` — ceiling (smallest integer not less than `A`).".
-spec ceil(expr()) -> qast:ast_node().
ceil(A) ->
call("ceil", [A], qast:opts(A)).
-doc(#{group => <<"Numeric">>}).
-doc "`floor(A)` — floor (largest integer not greater than `A`).".
-spec floor(expr()) -> qast:ast_node().
floor(A) ->
call("floor", [A], qast:opts(A)).
-doc(#{group => <<"Numeric">>}).
-doc "`power(A, B)` — `A` raised to the power of `B`.".
-spec power(expr(), expr()) -> qast:ast_node().
power(A, B) ->
call("power", [A, B], qast:opts(A)).
-doc(#{group => <<"Numeric">>}).
-doc "`sqrt(A)` — square root.".
-spec sqrt(expr()) -> qast:ast_node().
sqrt(A) ->
call("sqrt", [A], qast:opts(A)).
-doc(#{group => <<"Numeric">>}).
-doc "`ln(A)` — natural logarithm.".
-spec ln(expr()) -> qast:ast_node().
ln(A) ->
call("ln", [A], qast:opts(A)).
-doc(#{group => <<"Numeric">>}).
-doc "`log(A)` — base-10 logarithm.".
-spec log(expr()) -> qast:ast_node().
log(A) ->
call("log", [A], qast:opts(A)).
-doc(#{group => <<"Numeric">>}).
-doc "`log(B, A)` — logarithm of `A` to base `B`.".
-spec log(expr(), expr()) -> qast:ast_node().
log(B, A) ->
call("log", [B, A], qast:opts(A)).
-doc(#{group => <<"Numeric">>}).
-doc "`exp(A)` — exponential (`e^A`).".
-spec exp(expr()) -> qast:ast_node().
exp(A) ->
call("exp", [A], qast:opts(A)).
-doc(#{group => <<"Numeric">>}).
-doc "`sign(A)` — `-1`, `0`, or `1` according to sign.".
-spec sign(expr()) -> qast:ast_node().
sign(A) ->
call("sign", [A], qast:opts(A)).
-doc(#{group => <<"Numeric">>}).
-doc "`random()` — pseudo-random `double precision` in `[0.0, 1.0)`.".
-spec random() -> qast:ast_node().
random() ->
call("random", [], #{}).
%% = LIKE ======================================================================
-doc(#{group => <<"Pattern matching">>}).
-doc "POSIX regex match `A ~ B`. See [POSIX Regular Expressions](https://www.postgresql.org/docs/current/functions-matching.html#FUNCTIONS-POSIX-REGEXP).".
-spec '~'(expr(), expr()) -> qast:ast_node().
'~'(A, B) ->
qast:exp([qast:raw("("), A, qast:raw(" ~ "), B, qast:raw(")")], #{type => boolean}).
-doc(#{group => <<"Pattern matching">>}).
-doc "POSIX regex match (case-insensitive) `A ~* B`.".
-spec '~*'(expr(), expr()) -> qast:ast_node().
'~*'(A, B) ->
qast:exp([qast:raw("("), A, qast:raw(" ~* "), B, qast:raw(")")], #{type => boolean}).
-doc(#{group => <<"Pattern matching">>}).
-doc "`A LIKE B`. See [LIKE](https://www.postgresql.org/docs/current/functions-matching.html#FUNCTIONS-LIKE).".
like(A, B) ->
qast:exp([A, qast:raw(" like "), B], #{type => boolean}).
-doc(#{group => <<"Pattern matching">>}).
-doc "`A ILIKE B` — case-insensitive `LIKE` (PostgreSQL extension).".
ilike(A, B) ->
qast:exp([A, qast:raw(" ilike "), B], #{type => boolean}).
%% = Aggregators ===============================================================
-doc(#{group => <<"Aggregates">>}).
-doc "`sum(A)`. See [Aggregate Functions](https://www.postgresql.org/docs/current/functions-aggregate.html).".
-spec sum(qast:ast_node()) -> qast:ast_node().
sum(Ast) ->
call("sum", [Ast], qast:opts(Ast)).
-doc(#{group => <<"Aggregates">>}).
-doc "`count(A)` — non-NULL row count. Returns `bigint`.".
-spec count(qast:ast_node()) -> qast:ast_node().
count(Ast) ->
call("count", [Ast], #{type => integer}).
-doc(#{group => <<"Aggregates">>}).
-doc "`min(A)` — aggregate minimum.".
-spec 'min'(expr()) -> qast:ast_node().
min(Ast) ->
call("min", [Ast], qast:opts(Ast)).
-doc(#{group => <<"Aggregates">>}).
-doc "`max(A)` — aggregate maximum.".
-spec 'max'(expr()) -> qast:ast_node().
max(Ast) ->
call("max", [Ast], qast:opts(Ast)).
-doc(#{group => <<"Aggregates">>}).
-doc "`DISTINCT A` qualifier — typically used inside another aggregate, e.g. `count(distinct(x))`.".
-spec 'distinct'(expr()) -> qast:ast_node().
distinct(Ast) ->
call("distinct ", [Ast], qast:opts(Ast)).
-doc(#{group => <<"Aggregates">>}).
-doc "`array_agg(A)` — collect values into an array. NULLs are included.".
-spec 'array_agg'(expr()) -> qast:ast_node().
array_agg(Ast) ->
Opts = qast:opts(Ast),
Type = maps:get(type, Opts, undefined),
NewOpts = Opts#{type => {array, Type}},
call("array_agg", [Ast], NewOpts).
-type agg_order_spec() :: expr()
| {expr(), asc | desc}
| {expr(), asc | desc, nulls_first | nulls_last}.
-type agg_order_specs() :: [agg_order_spec(), ...].
-doc(#{group => <<"Aggregates">>}).
-doc "`array_agg(A ORDER BY ...)` — collect into array with explicit ordering.".
-spec 'array_agg'(expr(), agg_order_specs()) -> qast:ast_node().
array_agg(Ast, OrderSpecs) ->
Opts = qast:opts(Ast),
Type = maps:get(type, Opts, undefined),
NewOpts = Opts#{type => {array, Type}},
qast:exp([
qast:raw("array_agg("), Ast,
agg_order_by(OrderSpecs),
qast:raw(")")
], NewOpts).
-doc(#{group => <<"Aggregates">>}).
-doc "`avg(A)` — arithmetic mean. Returns `numeric` or `double precision`.".
-spec avg(expr()) -> qast:ast_node().
avg(A) ->
call("avg", [A], qast:opts(A)).
-doc(#{group => <<"Aggregates">>}).
-doc "`bool_and(A)` — true iff every non-NULL input is true.".
-spec bool_and(expr()) -> qast:ast_node().
bool_and(A) ->
call("bool_and", [A], #{type => boolean}).
-doc(#{group => <<"Aggregates">>}).
-doc "`bool_or(A)` — true iff any non-NULL input is true.".
-spec bool_or(expr()) -> qast:ast_node().
bool_or(A) ->
call("bool_or", [A], #{type => boolean}).
-doc(#{group => <<"Aggregates">>}).
-doc "`every(A)` — SQL standard alias for [`bool_and/1`](`bool_and/1`).".
-spec every(expr()) -> qast:ast_node().
every(A) ->
call("every", [A], #{type => boolean}).
-doc(#{group => <<"Aggregates">>}).
-doc "`string_agg(expr, sep)` — concatenate string values with a separator. NULL inputs are ignored.".
-spec string_agg(expr(), expr()) -> qast:ast_node().
string_agg(Expr, Sep) ->
call("string_agg", [Expr, Sep], #{type => text}).
-doc(#{group => <<"Aggregates">>}).
-doc """
`string_agg(expr, sep ORDER BY ...)` — concatenate with explicit ordering.
Without `ORDER BY`, `string_agg` is non-deterministic. Use the 3-arg form
when result order matters.
""".
-spec string_agg(expr(), expr(), agg_order_specs()) -> qast:ast_node().
string_agg(Expr, Sep, OrderSpecs) ->
qast:exp([
qast:raw("string_agg("), Expr, qast:raw(","), Sep,
agg_order_by(OrderSpecs),
qast:raw(")")
], #{type => text}).
-doc(#{group => <<"Aggregates">>}).
-doc "`json_agg(A)` — aggregate into a JSON array. See [JSON Functions](https://www.postgresql.org/docs/current/functions-json.html).".
-spec json_agg(expr()) -> qast:ast_node().
json_agg(A) ->
call("json_agg", [A], #{type => json}).
-doc(#{group => <<"Aggregates">>}).
-doc "`json_agg(A ORDER BY ...)`.".
-spec json_agg(expr(), agg_order_specs()) -> qast:ast_node().
json_agg(A, OrderSpecs) ->
qast:exp([
qast:raw("json_agg("), A,
agg_order_by(OrderSpecs),
qast:raw(")")
], #{type => json}).
-doc(#{group => <<"Aggregates">>}).
-doc "`jsonb_agg(A)` — aggregate into a JSONB array.".
-spec jsonb_agg(expr()) -> qast:ast_node().
jsonb_agg(A) ->
call("jsonb_agg", [A], #{type => jsonb}).
-doc(#{group => <<"Aggregates">>}).
-doc "`jsonb_agg(A ORDER BY ...)`.".
-spec jsonb_agg(expr(), agg_order_specs()) -> qast:ast_node().
jsonb_agg(A, OrderSpecs) ->
qast:exp([
qast:raw("jsonb_agg("), A,
agg_order_by(OrderSpecs),
qast:raw(")")
], #{type => jsonb}).
-doc(#{group => <<"Aggregates">>}).
-doc "`json_object_agg(K, V)` — aggregate into a JSON object.".
-spec json_object_agg(expr(), expr()) -> qast:ast_node().
json_object_agg(K, V) ->
call("json_object_agg", [K, V], #{type => json}).
-doc(#{group => <<"Aggregates">>}).
-doc "`jsonb_object_agg(K, V)` — aggregate into a JSONB object.".
-spec jsonb_object_agg(expr(), expr()) -> qast:ast_node().
jsonb_object_agg(K, V) ->
call("jsonb_object_agg", [K, V], #{type => jsonb}).
-doc(#{group => <<"Aggregates">>}).
-doc """
`percentile_cont(Frac) WITHIN GROUP (ORDER BY x)` — interpolated continuous
percentile. `Frac` ∈ [0, 1].
See [Ordered-Set Aggregate Functions](https://www.postgresql.org/docs/current/functions-aggregate.html#FUNCTIONS-ORDEREDSET-TABLE).
""".
-spec percentile_cont(expr(), expr()) -> qast:ast_node().
percentile_cont(Frac, OrderExpr) ->
qast:exp([
qast:raw("percentile_cont("), Frac,
qast:raw(") within group (order by "), OrderExpr,
qast:raw(")")
], qast:opts(OrderExpr)).
-doc(#{group => <<"Aggregates">>}).
-doc "`percentile_disc(Frac) WITHIN GROUP (ORDER BY x)` — discrete percentile (picks one of the input values).".
-spec percentile_disc(expr(), expr()) -> qast:ast_node().
percentile_disc(Frac, OrderExpr) ->
qast:exp([
qast:raw("percentile_disc("), Frac,
qast:raw(") within group (order by "), OrderExpr,
qast:raw(")")
], qast:opts(OrderExpr)).
-doc(#{group => <<"Aggregates">>}).
-doc "`mode() WITHIN GROUP (ORDER BY x)` — most frequent value (statistical mode).".
-spec mode(expr()) -> qast:ast_node().
mode(OrderExpr) ->
qast:exp([
qast:raw("mode() within group (order by "), OrderExpr,
qast:raw(")")
], qast:opts(OrderExpr)).
-doc(#{group => <<"Aggregates">>}).
-doc """
Wrap any aggregate with `FILTER (WHERE Cond)` — limits which rows the
aggregate sees, without affecting the rest of the query. Essential for
dashboard-style queries with multiple conditional metrics.
```erlang
#{
total => pg_sql:count(Id),
paid => pg_sql:filter(pg_sql:count(Id), Status =:= <<"paid">>)
}.
```
See [Aggregate Expressions](https://www.postgresql.org/docs/current/sql-expressions.html#SYNTAX-AGGREGATES).
""".
-spec filter(qast:ast_node(), expr()) -> qast:ast_node().
filter(AggAst, Cond) ->
qast:exp([
AggAst, qast:raw(" filter (where "), Cond, qast:raw(")")
], qast:opts(AggAst)).
agg_order_by(Specs) ->
Exps = lists:map(fun agg_order_spec_exp/1, Specs),
qast:exp([qast:raw(" order by "), qast:join(Exps, qast:raw(","))]).
agg_order_spec_exp({_F, D} = T) when D =:= asc; D =:= desc ->
equery_utils:order_item_exp(T);
agg_order_spec_exp({_F, D, N} = T) when
(D =:= asc orelse D =:= desc),
(N =:= nulls_first orelse N =:= nulls_last) ->
equery_utils:order_item_exp(T);
agg_order_spec_exp(F) -> F.
-doc(#{group => <<"Numeric">>}).
-doc "`trunc(V, N)` — truncate to `N` decimal places.".
-spec 'trunc'(expr(), qast:ast_node() | non_neg_integer()) -> qast:ast_node().
'trunc'(V, N) ->
call("trunc", [V, N], qast:opts(V)).
%% = Math ======================================================================
-doc(#{group => <<"Conditional">>}).
-doc "`LEAST(A, B)` — minimum of two values, ignoring NULLs. See [Conditional Expressions](https://www.postgresql.org/docs/current/functions-conditional.html#FUNCTIONS-GREATEST-LEAST).".
-spec 'min'(expr(), expr()) -> qast:ast_node().
min(A, B) ->
qast:exp([qast:raw("LEAST("), A, qast:raw(","), B, qast:raw(")")], qast:opts(A)).
-doc(#{group => <<"Conditional">>}).
-doc "`GREATEST(A, B)` — maximum of two values, ignoring NULLs.".
-spec 'max'(expr(), expr()) -> qast:ast_node().
max(A, B) ->
qast:exp([qast:raw("GREATEST("), A, qast:raw(","), B, qast:raw(")")], qast:opts(A)).
-doc(#{group => <<"Conditional">>}).
-doc "`GREATEST(A, B, C, ...)` — N-ary form. Returns max of all non-NULL inputs.".
-spec greatest([expr(), ...]) -> qast:ast_node().
greatest([H | _] = List) ->
call("GREATEST", List, qast:opts(H)).
-doc(#{group => <<"Conditional">>}).
-doc "`LEAST(A, B, C, ...)` — N-ary form. Returns min of all non-NULL inputs.".
-spec least([expr(), ...]) -> qast:ast_node().
least([H | _] = List) ->
call("LEAST", List, qast:opts(H)).
%% = Additional operations =====================================================
-doc(#{group => <<"Misc">>}).
-doc "`ROW(...)` constructor with anonymous record type. See [Row Constructors](https://www.postgresql.org/docs/current/sql-expressions.html#SQL-SYNTAX-ROW-CONSTRUCTORS).".
-spec row(#{atom() => qast:ast_node()}) -> qast:ast_node().
row(Fields) when is_map(Fields) ->
row(undefined, Fields).
-doc(#{group => <<"Misc">>}).
-doc "`ROW(...)` constructor tagged with a model module (for type inference in row-typed projections).".
-spec row(Model :: module(), #{atom() => qast:ast_node()}) -> qast:ast_node().
row(Model, Fields) when is_map(Fields) ->
FieldsList = ?MAPS_TO_LIST(Fields),
Type = {record, {model, Model, [{F, qast:opts(Node)} || {F, Node} <- FieldsList]}},
qast:exp([
qast:raw("row("),
qast:join([Node || {_F, Node} <- FieldsList], qast:raw(",")),
qast:raw(")")
], #{type => Type}).
-doc(#{group => <<"Conditional">>}).
-doc """
`COALESCE(a, b, c, ...)` — returns the first non-NULL argument.
See [Conditional Expressions](https://www.postgresql.org/docs/current/functions-conditional.html#FUNCTIONS-COALESCE-NVL-IFNULL).
""".
coalesce([H|_]=List) ->
qast:exp([
qast:raw("coalesce("),
qast:join([Node || Node <- List], qast:raw(",")),
qast:raw(")")
], maps:with([type], qast:opts(H))).
-doc(#{group => <<"Conditional">>}).
-doc "Searched `CASE WHEN cond THEN val ... END` without `ELSE` — `NULL` if no branch matches.".
-spec case_when([{expr(), expr()}, ...]) -> qast:ast_node().
case_when(Whens) ->
case_when(Whens, undefined).
-doc(#{group => <<"Conditional">>}).
-doc """
`CASE WHEN cond THEN val ... ELSE default END`.
```erlang
pg_sql:case_when([
{Id > 100, <<"big">>},
{Id > 10, <<"medium">>}
], <<"small">>).
```
See [Conditional Expressions](https://www.postgresql.org/docs/current/functions-conditional.html#FUNCTIONS-CASE).
""".
-spec case_when([{expr(), expr()}, ...], expr() | undefined) -> qast:ast_node().
case_when([{_, FirstThen}|_]=Whens, ElseSpec) ->
Opts = maps:with([type], qast:opts(FirstThen)),
WhenExps = lists:map(fun({W, T}) ->
qast:exp([qast:raw(" when "), W, qast:raw(" then "), T])
end, Whens),
ElseExp = case ElseSpec of
undefined -> qast:raw("");
E -> qast:exp([qast:raw(" else "), E])
end,
qast:exp([
qast:raw("case"),
qast:exp(WhenExps),
ElseExp,
qast:raw(" end")
], Opts).
-doc(#{group => <<"Misc">>}).
-doc """
`A IN (...)` — membership test.
- With a `query()` argument, emits `A IN (subquery)`. See [Subquery Expressions](https://www.postgresql.org/docs/current/functions-subquery.html#FUNCTIONS-SUBQUERY-IN).
- With a list, emits `A = ANY($1)` (parameterized).
- With a single-element list, optimized to `A = item`.
""".
in(A, #query{}=Q) ->
qast:exp([A, qast:raw(" in ("), qsql:select(Q), qast:raw(")")], #{type => boolean});
in(A, [Item]) ->
'=:='(A, Item);
in(A, B) ->
qast:exp([A, qast:raw(" = ANY("), B, qast:raw(")")], #{type => boolean}).
-doc(#{group => <<"Misc">>}).
-doc "`EXISTS (subquery)`. See [EXISTS](https://www.postgresql.org/docs/current/functions-subquery.html#FUNCTIONS-SUBQUERY-EXISTS).".
exists(#query{}=Q) ->
qast:exp([qast:raw("exists ("), qsql:select(Q), qast:raw(")")], #{type => boolean}).
-doc(#{group => <<"Misc">>}).
-doc """
Build a function call AST: `FunName(Args...)`.
Used internally by all `pg_sql:` function builders. Public for custom
PG functions not yet wrapped by the library:
```erlang
pg_sql:call("my_extension_fn", [Arg1, Arg2], #{type => text}).
```
> #### Warning {: .warning}
> `FunName` is **inlined raw**. Never pass user-controlled input here;
> use a literal string.
""".
-spec call(iodata(), [expr()], qast:opts()) -> qast:ast_node().
call(FunName, Args, Opts) ->
qast:exp([
qast:raw([FunName, "("]),
qast:join(Args, qast:raw(",")),
qast:raw(")")
], Opts).
%% = Array oprterations ========================================================
-doc(#{group => <<"Arrays">>}).
-doc "`A @> B` — contains. See [Array Functions and Operators](https://www.postgresql.org/docs/current/functions-array.html).".
'@>'(A, B) ->
qast:exp([A, qast:raw(" @> "), B], #{type => boolean}).
-doc(#{group => <<"Arrays">>}).
-doc "`A <@ B` — is contained by.".
-spec '<@'(expr(), expr()) -> qast:ast_node().
'<@'(A, B) ->
qast:exp([A, qast:raw(" <@ "), B], #{type => boolean}).
-doc(#{group => <<"Arrays">>}).
-doc "`A && B` — arrays overlap (have any common element).".
-spec '&&'(expr(), expr()) -> qast:ast_node().
'&&'(A, B) ->
qast:exp([A, qast:raw(" && "), B], #{type => boolean}).
-doc(#{group => <<"Arrays">>}).
-doc "`array_length(arr, 1)` — length of the first dimension.".
-spec array_length(expr()) -> qast:ast_node().
array_length(Arr) ->
array_length(Arr, qast:value(1, #{type => integer})).
-doc(#{group => <<"Arrays">>}).
-doc "`array_length(arr, dim)` — length of dimension `dim`.".
-spec array_length(expr(), expr()) -> qast:ast_node().
array_length(Arr, Dim) ->
call("array_length", [Arr, Dim], #{type => integer}).
-doc(#{group => <<"Arrays">>}).
-doc "`array_position(arr, elem)` — 1-based index of `elem` in `arr`, or NULL.".
-spec array_position(expr(), expr()) -> qast:ast_node().
array_position(Arr, Elem) ->
call("array_position", [Arr, Elem], #{type => integer}).
-doc(#{group => <<"Arrays">>}).
-doc "`array_append(arr, elem)` — append element.".
-spec array_append(expr(), expr()) -> qast:ast_node().
array_append(Arr, Elem) ->
call("array_append", [Arr, Elem], qast:opts(Arr)).
-doc(#{group => <<"Arrays">>}).
-doc "`array_prepend(elem, arr)` — prepend element.".
-spec array_prepend(expr(), expr()) -> qast:ast_node().
array_prepend(Elem, Arr) ->
call("array_prepend", [Elem, Arr], qast:opts(Arr)).
-doc(#{group => <<"Arrays">>}).
-doc "`array_remove(arr, elem)` — remove all occurrences of `elem`.".
-spec array_remove(expr(), expr()) -> qast:ast_node().
array_remove(Arr, Elem) ->
call("array_remove", [Arr, Elem], qast:opts(Arr)).
-doc(#{group => <<"Arrays">>}).
-doc "`array_replace(arr, from, to)` — replace all `from` elements with `to`.".
-spec array_replace(expr(), expr(), expr()) -> qast:ast_node().
array_replace(Arr, From, To) ->
call("array_replace", [Arr, From, To], qast:opts(Arr)).
-doc(#{group => <<"Arrays">>}).
-doc "`array_cat(a, b)` — concatenate two arrays. Equivalent to `a || b`.".
-spec array_cat(expr(), expr()) -> qast:ast_node().
array_cat(A, B) ->
call("array_cat", [A, B], qast:opts(A)).
-doc(#{group => <<"Arrays">>}).
-doc """
`unnest(arr)` — set-returning function that produces one row per array
element.
Integrates with `q:from/1` and `q:lateral_join/2,3,4`: the resulting
table has a single column named `unnest` (PostgreSQL default).
```erlang
q:pipe(q:from(?USER), [
q:lateral_join(inner, fun([#{tags := T}]) ->
q:from(pg_sql:unnest(T))
end),
q:select(fun([#{name := N}, #{unnest := Tag}]) ->
#{name => N, tag => Tag}
end)
]).
```
""".
-spec unnest(expr()) -> qast:ast_node().
unnest(Arr) ->
ElemType =
case maps:find(type, qast:opts(Arr)) of
{ok, {array, T}} -> T;
_ -> undefined
end,
qast:exp(
[qast:raw("unnest("), Arr, qast:raw(")")],
#{type => {model, undefined, [{unnest, #{type => ElemType}}]}}
).
-doc(#{group => <<"Arrays">>}).
-doc """
`ARRAY[v1, v2, ...]` constructor. Element type is inferred from the
first element's opts.
Empty array (`ARRAY[]`) needs an explicit cast in PG; use
[`as/2`](`as/2`): `pg_sql:as(pg_sql:array([]), {array, int})`.
""".
-spec array([expr()]) -> qast:ast_node().
array(Items) when is_list(Items) ->
Opts = case Items of
[] -> #{};
[H | _] ->
ElemType = maps:get(type, qast:opts(H), undefined),
#{type => {array, ElemType}}
end,
qast:exp([
qast:raw("ARRAY["),
qast:join(Items, qast:raw(",")),
qast:raw("]")
], Opts).
%% = String / array concat =====================================================
-doc(#{group => <<"Strings">>}).
-doc """
`A || B` — concatenation operator.
NULL-propagating: `'foo' || NULL` → `NULL`. For NULL-skipping behavior
use [`concat/2`](`concat/2`). Same operator works for arrays.
See [String Functions](https://www.postgresql.org/docs/current/functions-string.html).
""".
-spec '||'(expr(), expr()) -> qast:ast_node().
'||'(A, B) ->
qast:exp([qast:raw("("), A, qast:raw(" || "), B, qast:raw(")")], qast:opts(A)).
%% = Date/time functions =======================================================
-doc(#{group => <<"Date/time">>}).
-doc "`now()` — current transaction timestamp (with timezone). PostgreSQL extension; equivalent to [`current_timestamp/0`](`current_timestamp/0`).".
-spec now() -> qast:ast_node().
now() ->
call("now", [], #{type => timestamptz}).
-doc(#{group => <<"Date/time">>}).
-doc "`current_timestamp` — start-of-transaction timestamp with timezone. Standard SQL.".
-spec current_timestamp() -> qast:ast_node().
current_timestamp() ->
qast:raw("current_timestamp", #{type => timestamptz}).
-doc(#{group => <<"Date/time">>}).
-doc "`current_date` — current date (date only).".
-spec current_date() -> qast:ast_node().
current_date() ->
qast:raw("current_date", #{type => date}).
-doc(#{group => <<"Date/time">>}).
-doc "`current_time` — current time of day with timezone.".
-spec current_time() -> qast:ast_node().
current_time() ->
qast:raw("current_time", #{type => timetz}).
-type datetime_field() ::
century | day | decade | dow | doy | epoch | hour |
isodow | isoyear | julian | microseconds | millennium |
milliseconds | minute | month | quarter | second |
timezone | timezone_hour | timezone_minute | week | year.
-spec datetime_field_str(datetime_field()) -> binary().
datetime_field_str(century) -> <<"century">>;
datetime_field_str(day) -> <<"day">>;
datetime_field_str(decade) -> <<"decade">>;
datetime_field_str(dow) -> <<"dow">>;
datetime_field_str(doy) -> <<"doy">>;
datetime_field_str(epoch) -> <<"epoch">>;
datetime_field_str(hour) -> <<"hour">>;
datetime_field_str(isodow) -> <<"isodow">>;
datetime_field_str(isoyear) -> <<"isoyear">>;
datetime_field_str(julian) -> <<"julian">>;
datetime_field_str(microseconds) -> <<"microseconds">>;
datetime_field_str(millennium) -> <<"millennium">>;
datetime_field_str(milliseconds) -> <<"milliseconds">>;
datetime_field_str(minute) -> <<"minute">>;
datetime_field_str(month) -> <<"month">>;
datetime_field_str(quarter) -> <<"quarter">>;
datetime_field_str(second) -> <<"second">>;
datetime_field_str(timezone) -> <<"timezone">>;
datetime_field_str(timezone_hour) -> <<"timezone_hour">>;
datetime_field_str(timezone_minute) -> <<"timezone_minute">>;
datetime_field_str(week) -> <<"week">>;
datetime_field_str(year) -> <<"year">>.
-doc(#{group => <<"Date/time">>}).
-doc """
`date_trunc('field', source)` — truncate a timestamp to the specified
precision. Field is a closed enum (validated at build time).
See [Date/Time Functions](https://www.postgresql.org/docs/current/functions-datetime.html#FUNCTIONS-DATETIME-TRUNC).
""".
-spec date_trunc(datetime_field(), expr()) -> qast:ast_node().
date_trunc(Field, Source) ->
FieldBin = datetime_field_str(Field),
call("date_trunc", [qast:value(FieldBin, #{type => text}), Source], qast:opts(Source)).
-doc(#{group => <<"Date/time">>}).
-doc """
`EXTRACT(field FROM source)` — get a sub-field as `numeric`.
See [EXTRACT](https://www.postgresql.org/docs/current/functions-datetime.html#FUNCTIONS-DATETIME-EXTRACT).
""".
-spec extract(datetime_field(), expr()) -> qast:ast_node().
extract(Field, Source) ->
FieldBin = datetime_field_str(Field),
qast:exp([
qast:raw("extract("),
qast:raw(FieldBin),
qast:raw(" from "),
Source,
qast:raw(")")
], #{type => numeric}).
-doc(#{group => <<"Date/time">>}).
-doc "`date_part('field', source)` — same as `extract`, returns `double precision`.".
-spec date_part(datetime_field(), expr()) -> qast:ast_node().
date_part(Field, Source) ->
FieldBin = datetime_field_str(Field),
call("date_part", [qast:value(FieldBin, #{type => text}), Source], #{type => float8}).
-doc(#{group => <<"Date/time">>}).
-doc "`age(t)` — interval since `current_date` to `t` (or vice-versa).".
-spec age(expr()) -> qast:ast_node().
age(A) ->
call("age", [A], #{type => interval}).
-doc(#{group => <<"Date/time">>}).
-doc "`age(t1, t2)` — symbolic interval `t1 - t2`.".
-spec age(expr(), expr()) -> qast:ast_node().
age(A, B) ->
call("age", [A, B], #{type => interval}).
-doc(#{group => <<"Date/time">>}).
-doc "`to_char(val, fmt)` — format a date/timestamp/number as text. See [Data Type Formatting Functions](https://www.postgresql.org/docs/current/functions-formatting.html).".
-spec to_char(expr(), expr()) -> qast:ast_node().
to_char(A, Fmt) ->
call("to_char", [A, Fmt], #{type => text}).
-doc(#{group => <<"Date/time">>}).
-doc "`to_date(text, fmt)` — parse a string into `date`.".
-spec to_date(expr(), expr()) -> qast:ast_node().
to_date(A, Fmt) ->
call("to_date", [A, Fmt], #{type => date}).
-doc(#{group => <<"Date/time">>}).
-doc "`to_timestamp(epoch)` — Unix-epoch seconds to `timestamptz`.".
-spec to_timestamp(expr()) -> qast:ast_node().
to_timestamp(A) ->
call("to_timestamp", [A], #{type => timestamptz}).
-doc(#{group => <<"Date/time">>}).
-doc "`to_timestamp(text, fmt)` — parse a formatted string into `timestamptz`.".
-spec to_timestamp(expr(), expr()) -> qast:ast_node().
to_timestamp(A, Fmt) ->
call("to_timestamp", [A, Fmt], #{type => timestamptz}).
%% = String functions ==========================================================
-doc(#{group => <<"Strings">>}).
-doc "`concat(a, b, c, ...)` — concatenate. **NULLs are skipped** (unlike `||`). See [String Functions](https://www.postgresql.org/docs/current/functions-string.html).".
-spec concat([expr(), ...]) -> qast:ast_node().
concat(List) when is_list(List) ->
call("concat", List, #{type => text}).
-doc(#{group => <<"Strings">>}).
-doc "`concat(a, b)` — 2-arg form for convenience.".
-spec concat(expr(), expr()) -> qast:ast_node().
concat(A, B) ->
call("concat", [A, B], #{type => text}).
-doc(#{group => <<"Strings">>}).
-doc "`length(s)` — string length in characters.".
-spec length(expr()) -> qast:ast_node().
length(A) ->
call("length", [A], #{type => integer}).
-doc(#{group => <<"Strings">>}).
-doc "`char_length(s)` — SQL-standard alias for `length`.".
-spec char_length(expr()) -> qast:ast_node().
char_length(A) ->
call("char_length", [A], #{type => integer}).
-doc(#{group => <<"Strings">>}).
-doc "`lower(s)` — lowercase.".
-spec lower(expr()) -> qast:ast_node().
lower(A) ->
call("lower", [A], qast:opts(A)).
-doc(#{group => <<"Strings">>}).
-doc "`upper(s)` — uppercase.".
-spec upper(expr()) -> qast:ast_node().
upper(A) ->
call("upper", [A], qast:opts(A)).
-doc(#{group => <<"Strings">>}).
-doc "`trim(s)` — strip whitespace from both ends.".
-spec trim(expr()) -> qast:ast_node().
trim(A) ->
call("trim", [A], qast:opts(A)).
-doc(#{group => <<"Strings">>}).
-doc "`trim(s, chars)` — strip any character listed in `chars` from both ends.".
-spec trim(expr(), expr()) -> qast:ast_node().
trim(A, Chars) ->
call("trim", [A, Chars], qast:opts(A)).
-doc(#{group => <<"Strings">>}).
-doc "`ltrim(s)` — strip whitespace from the left.".
-spec ltrim(expr()) -> qast:ast_node().
ltrim(A) ->
call("ltrim", [A], qast:opts(A)).
-doc(#{group => <<"Strings">>}).
-doc "`ltrim(s, chars)` — strip listed characters from the left.".
-spec ltrim(expr(), expr()) -> qast:ast_node().
ltrim(A, Chars) ->
call("ltrim", [A, Chars], qast:opts(A)).
-doc(#{group => <<"Strings">>}).
-doc "`rtrim(s)` — strip whitespace from the right.".
-spec rtrim(expr()) -> qast:ast_node().
rtrim(A) ->
call("rtrim", [A], qast:opts(A)).
-doc(#{group => <<"Strings">>}).
-doc "`rtrim(s, chars)` — strip listed characters from the right.".
-spec rtrim(expr(), expr()) -> qast:ast_node().
rtrim(A, Chars) ->
call("rtrim", [A, Chars], qast:opts(A)).
-doc(#{group => <<"Strings">>}).
-doc "`replace(s, from, to)` — replace all occurrences of `from` with `to`.".
-spec replace(expr(), expr(), expr()) -> qast:ast_node().
replace(A, From, To) ->
call("replace", [A, From, To], qast:opts(A)).
-doc(#{group => <<"Strings">>}).
-doc "`split_part(s, sep, n)` — `n`-th field after splitting `s` by `sep` (1-indexed).".
-spec split_part(expr(), expr(), expr()) -> qast:ast_node().
split_part(A, Delim, N) ->
call("split_part", [A, Delim, N], #{type => text}).
-doc(#{group => <<"Strings">>}).
-doc "`substring(s, from)` — from the given 1-based start position to end.".
-spec substring(expr(), expr()) -> qast:ast_node().
substring(A, From) ->
call("substring", [A, From], qast:opts(A)).
-doc(#{group => <<"Strings">>}).
-doc "`substring(s, from, len)` — `len` characters starting at `from`.".
-spec substring(expr(), expr(), expr()) -> qast:ast_node().
substring(A, From, Len) ->
call("substring", [A, From, Len], qast:opts(A)).
-doc(#{group => <<"Strings">>}).
-doc "`strpos(haystack, needle)` — 1-based position of `needle` in `haystack`, or 0.".
-spec strpos(expr(), expr()) -> qast:ast_node().
strpos(Haystack, Needle) ->
call("strpos", [Haystack, Needle], #{type => integer}).
-doc(#{group => <<"Strings">>}).
-doc "`starts_with(s, prefix)` — boolean prefix test. Faster than `LIKE 'prefix%'` for indexed lookup.".
-spec starts_with(expr(), expr()) -> qast:ast_node().
starts_with(A, Prefix) ->
call("starts_with", [A, Prefix], #{type => boolean}).
-doc(#{group => <<"Strings">>}).
-doc "`regexp_replace(s, pat, repl)` — replace first match. See [POSIX Regex Match](https://www.postgresql.org/docs/current/functions-matching.html#FUNCTIONS-POSIX-REGEXP).".
-spec regexp_replace(expr(), expr(), expr()) -> qast:ast_node().
regexp_replace(A, Pattern, Repl) ->
call("regexp_replace", [A, Pattern, Repl], qast:opts(A)).
-doc(#{group => <<"Strings">>}).
-doc "`regexp_replace(s, pat, repl, flags)` — `'g'` for global replace, `'i'` case-insensitive, etc.".
-spec regexp_replace(expr(), expr(), expr(), expr()) -> qast:ast_node().
regexp_replace(A, Pattern, Repl, Flags) ->
call("regexp_replace", [A, Pattern, Repl, Flags], qast:opts(A)).
-doc(#{group => <<"Strings">>}).
-doc "`regexp_match(s, pat)` — returns a `text[]` of capture groups, or NULL if no match.".
-spec regexp_match(expr(), expr()) -> qast:ast_node().
regexp_match(A, Pattern) ->
call("regexp_match", [A, Pattern], #{type => {array, text}}).
-doc(#{group => <<"Strings">>}).
-doc "`regexp_match(s, pat, flags)` — with regex flags.".
-spec regexp_match(expr(), expr(), expr()) -> qast:ast_node().
regexp_match(A, Pattern, Flags) ->
call("regexp_match", [A, Pattern, Flags], #{type => {array, text}}).
%% = Type functions ============================================================
-doc(#{group => <<"Type casts">>}).
-doc """
`(Ast)::Type` — SQL type cast.
```erlang
pg_sql:as(Id, text) %% (id)::text
pg_sql:as(V, {varchar, 60}) %% (v)::varchar(60)
pg_sql:as(V, {array, integer}) %% (v)::integer[]
```
See [Type Casts](https://www.postgresql.org/docs/current/sql-expressions.html#SQL-SYNTAX-TYPE-CASTS).
> #### Warning {: .warning}
> `Type` is rendered raw. Use only with literal atoms / tuples; never
> with user-controlled values.
""".
as(Ast, Type) ->
Opts = qast:opts(Ast),
qast:exp([
qast:raw("("), Ast, qast:raw(")::"),
qast:raw(type_str(Type))
], Opts#{type => Type}).
-doc(#{group => <<"Type casts">>}).
-doc """
Annotate an AST node with a type, **without** emitting an SQL cast.
Useful when you know the inferred type is wrong (e.g. when wrapping a
[`call/3`](`call/3`) to a function whose return type isn't auto-inferred)
and want correct downstream type propagation without paying for a
runtime cast.
""".
set_type(Ast, Type) ->
Opts = qast:opts(Ast),
qast:set_opts(Ast, Opts#{type => Type}).
type_str(Atom) when is_atom(Atom) ->
atom_to_binary(Atom, utf8);
type_str({array, Atom}) when is_atom(Atom) ->
iolist_to_binary([type_str(Atom), "[]"]);
type_str({Type, Args}) when Type =/= array ->
iolist_to_binary([
to_iodata(Type),
"(", join([to_iodata(A) || A <- Args], ","), ")"
]).
to_iodata(Atom) when is_atom(Atom) ->
atom_to_list(Atom);
to_iodata(D) when is_list(D); is_binary(D) ->
D;
to_iodata(Int) when is_integer(Int) ->
integer_to_list(Int);
to_iodata(Float) when is_float(Float) ->
io_lib:format("~p", [Float]).
join([], _) -> [];
join([H|T],Sep) -> [H|[[Sep,E]||E<-T]].