Current section
Files
Jump to
Current section
Files
src/kura_query.erl
-module(kura_query).
-moduledoc """
Composable, functional query builder.
Build queries by piping through `from/1`, `where/2`, `select/2`, `order_by/2`,
etc. Queries are compiled to parameterized SQL by `kura_query_compiler`.
```erlang
Q = kura_query:from(my_user),
Q1 = kura_query:where(Q, {active, true}),
Q2 = kura_query:order_by(Q1, [{name, asc}]),
Q3 = kura_query:limit(Q2, 10).
```
""".
-include("kura.hrl").
-export([
from/1,
select/2,
select_expr/2,
where/2,
join/4, join/5,
order_by/2,
group_by/2,
having/2,
limit/2,
offset/2,
distinct/1, distinct/2,
lock/2,
prefix/2,
preload/2,
with_cte/3,
union/2,
union_all/2,
intersect/2,
except/2,
scope/2,
with_deleted/1,
only_deleted/1,
count/1, count/2,
sum/2,
avg/2,
min/2,
max/2
]).
-doc "Start a query from the given schema module or table atom.".
-spec from(atom() | module()) -> #kura_query{}.
from(Source) ->
#kura_query{from = Source}.
-doc "Set the SELECT fields. Pass atoms for columns or `{agg, field}` tuples for aggregates.".
-spec select(#kura_query{}, [atom() | term()]) -> #kura_query{}.
select(Q, Fields) ->
Q#kura_query{select = Fields}.
-doc "Set raw SQL expressions in SELECT with aliases. Exprs = `[{Alias, {fragment, SQL, Params}}]`.".
-spec select_expr(#kura_query{}, [term()]) -> #kura_query{}.
select_expr(Q, Exprs) ->
Q#kura_query{select = {exprs, Exprs}}.
-doc "Add a WHERE condition. Conditions: `{field, value}`, `{field, op, value}`, `{'and', [...]}`, etc.".
-spec where(#kura_query{}, term()) -> #kura_query{}.
where(Q = #kura_query{wheres = W}, Condition) ->
Q#kura_query{wheres = W ++ [Condition]}.
-doc """
Add a JOIN clause. `Table` can be a schema module or raw table atom.
`On` is `{LeftCol, RightCol}` where LeftCol is on the previous table
(FROM for the first join, or the last joined table) and RightCol is on
the joined table. For chained joins, the left side automatically advances.
```erlang
Q = kura_query:from(user_schema),
Q1 = kura_query:join(Q, inner, post_schema, {id, user_id}),
%% => users.id = posts.user_id
Q2 = kura_query:join(Q1, inner, comment_schema, {id, post_id}).
%% => posts.id = comments.post_id
```
""".
-spec join(#kura_query{}, inner | left | right | full, atom(), {atom(), atom()}) -> #kura_query{}.
join(Q, Type, Table, On) ->
join(Q, Type, Table, On, undefined).
-spec join(
#kura_query{}, inner | left | right | full, atom(), {atom(), atom()}, atom() | undefined
) -> #kura_query{}.
join(Q = #kura_query{joins = J}, Type, Table, On, As) ->
Q#kura_query{joins = J ++ [{Type, Table, On, As}]}.
-doc "Set ORDER BY clauses as `[{field, asc | desc}]`.".
-spec order_by(#kura_query{}, [{atom(), asc | desc}]) -> #kura_query{}.
order_by(Q, Orders) ->
Q#kura_query{order_bys = Orders}.
-spec group_by(#kura_query{}, [atom()]) -> #kura_query{}.
group_by(Q, Fields) ->
Q#kura_query{group_bys = Fields}.
-spec having(#kura_query{}, term()) -> #kura_query{}.
having(Q = #kura_query{havings = H}, Condition) ->
Q#kura_query{havings = H ++ [Condition]}.
-spec limit(#kura_query{}, non_neg_integer()) -> #kura_query{}.
limit(Q, N) ->
Q#kura_query{limit = N}.
-spec offset(#kura_query{}, non_neg_integer()) -> #kura_query{}.
offset(Q, N) ->
Q#kura_query{offset = N}.
-spec distinct(#kura_query{}) -> #kura_query{}.
distinct(Q) ->
Q#kura_query{distinct = true}.
-spec distinct(#kura_query{}, [atom()]) -> #kura_query{}.
distinct(Q, Fields) ->
Q#kura_query{distinct = Fields}.
-spec lock(#kura_query{}, binary()) -> #kura_query{}.
lock(Q, LockExpr) ->
Q#kura_query{lock = LockExpr}.
-spec prefix(#kura_query{}, binary()) -> #kura_query{}.
prefix(Q, Schema) ->
Q#kura_query{prefix = Schema}.
-doc "Add associations to preload after query execution.".
-spec preload(#kura_query{}, [atom() | {atom(), list()}]) -> #kura_query{}.
preload(Q = #kura_query{preloads = P}, Assocs) ->
Q#kura_query{preloads = P ++ Assocs}.
-doc "Add a Common Table Expression (WITH clause).".
-spec with_cte(#kura_query{}, binary(), #kura_query{}) -> #kura_query{}.
with_cte(Q = #kura_query{ctes = CTEs}, Name, CteQuery) ->
Q#kura_query{ctes = CTEs ++ [{Name, CteQuery}]}.
-doc "Combine queries with UNION (removes duplicates).".
-spec union(#kura_query{}, #kura_query{}) -> #kura_query{}.
union(Q = #kura_query{combinations = C}, Q2) ->
Q#kura_query{combinations = C ++ [{union, Q2}]}.
-doc "Combine queries with UNION ALL (keeps duplicates).".
-spec union_all(#kura_query{}, #kura_query{}) -> #kura_query{}.
union_all(Q = #kura_query{combinations = C}, Q2) ->
Q#kura_query{combinations = C ++ [{union_all, Q2}]}.
-doc "Combine queries with INTERSECT.".
-spec intersect(#kura_query{}, #kura_query{}) -> #kura_query{}.
intersect(Q = #kura_query{combinations = C}, Q2) ->
Q#kura_query{combinations = C ++ [{intersect, Q2}]}.
-doc "Combine queries with EXCEPT.".
-spec except(#kura_query{}, #kura_query{}) -> #kura_query{}.
except(Q = #kura_query{combinations = C}, Q2) ->
Q#kura_query{combinations = C ++ [{except, Q2}]}.
-doc "Apply a composable query transform function.".
-spec scope(#kura_query{}, fun((#kura_query{}) -> #kura_query{})) -> #kura_query{}.
scope(Query, Fun) when is_function(Fun, 1) ->
Fun(Query).
-doc "Include soft-deleted records in query results.".
-spec with_deleted(#kura_query{}) -> #kura_query{}.
with_deleted(Q) ->
Q#kura_query{include_deleted = true}.
-doc "Return only soft-deleted records.".
-spec only_deleted(#kura_query{}) -> #kura_query{}.
only_deleted(Q) ->
Q1 = Q#kura_query{include_deleted = true},
where(Q1, {deleted_at, is_not_nil}).
-doc "Set query to SELECT COUNT(*).".
-spec count(#kura_query{}) -> #kura_query{}.
count(Q) ->
Q#kura_query{select = [{count, '*'}]}.
-spec count(#kura_query{}, atom()) -> #kura_query{}.
count(Q, Field) ->
Q#kura_query{select = [{count, Field}]}.
-spec sum(#kura_query{}, atom()) -> #kura_query{}.
sum(Q, Field) ->
Q#kura_query{select = [{sum, Field}]}.
-spec avg(#kura_query{}, atom()) -> #kura_query{}.
avg(Q, Field) ->
Q#kura_query{select = [{avg, Field}]}.
-spec min(#kura_query{}, atom()) -> #kura_query{}.
min(Q, Field) ->
Q#kura_query{select = [{min, Field}]}.
-spec max(#kura_query{}, atom()) -> #kura_query{}.
max(Q, Field) ->
Q#kura_query{select = [{max, Field}]}.