Current section
Files
Jump to
Current section
Files
src/q.erl
-module(q).
-include("query.hrl").
-include("ast_helpers.hrl").
-export([
pipe/2,
get/2
]).
-export([
from/1,
recursive/2,
join/2, join/3, join/4,
where/1, where/2,
select/1, select/2,
set/1, set/2,
data/1, data/2,
group_by/1, group_by/2,
order_by/1,
limit/1, limit/2,
offset/1, offset/2
]).
-type model() :: schema() | module().
-type query() :: #query{}.
-type schema() :: #{}.
-type data() :: [#{}].
-type select() :: #{atom() => qast:ast_node()} | qast:ast_node().
-type set() :: #{atom() => qast:ast_node()}.
-type order() :: [{qast:ast_node(), asc | desc}].
-type join_type() :: inner | left | right | full | {left, outer} | {right, outer} | {full, outer}.
-type qfun() :: fun((query()) -> query()).
-export_type([query/0]).
-export_type([
model/0,
schema/0,
data/0,
select/0,
set/0,
order/0,
join_type/0,
qfun/0
]).
%% = Flow ======================================================================
-spec pipe(Q, [qfun()]) -> Q when Q :: query().
pipe(Query, Funs) ->
lists:foldl(fun(F, Q) -> F(Q) end, Query, Funs).
-spec get(schema, query()) -> schema();
(data, query()) -> data().
get(schema, #query{schema=Schema}) -> Schema;
get(data, #query{data=Data}) -> Data.
%% = Query builders ============================================================
-spec from(model()) -> query().
from(Info) when is_map(Info); is_atom(Info) ->
Schema = get_schema(Info),
SchemaFields = maps:get(fields, Schema, #{}),
Table = maps:get(table, Schema),
TRef = make_ref(),
Fields = maps:map(
fun(N, Opts) -> qast:field(TRef, N, Opts) end,
SchemaFields),
#query{
schema = Schema,
data=[Fields],
select=Fields,
tables=[{real, equery_utils:wrap(Table), TRef}]
}.
%% = Recursive =================================================================
recursive(#query{select=RFields}=BaseQuery, UnionFun) when is_map(RFields) ->
Schema = ?MODULE:get(schema, BaseQuery),
TRef = make_ref(),
Fields = maps:map(
fun(_N, Ast) -> qast:opts(Ast) end,
RFields),
FieldsExp = maps:map(
fun(N, Opts) -> qast:field(TRef, N, Opts)
end, Fields),
InternalQ = #query{
schema = (maps:with([model], Schema))#{
fields => Fields
},
data = [FieldsExp],
select = FieldsExp,
tables = [{alias, TRef}]
},
WithExpression = qast:exp([
qast:raw("with recursive "),
qast:table(TRef),
qast:raw(" as ("),
qsql:select(BaseQuery),
qast:raw(" union all "),
qsql:select(call(UnionFun, [InternalQ])),
qast:raw(") ")
]),
InternalQ#query{with=WithExpression}.
-spec join(model() | query(), fun((data()) -> qast:ast_node())) -> qfun().
join(Info, Fun) ->
join(inner, Info, Fun).
-spec join(join_type(), model() | query(), fun((data()) -> qast:ast_node())) -> qfun().
join(JoinType, Info, Fun) ->
fun(Q) -> join(JoinType, Info, Fun, Q) end.
-spec join(join_type(), model() | query(), fun((data()) -> qast:ast_node()), Q) -> Q when Q :: query().
join(JoinType, #query{select=Fields}=JoinQ, Fun, #query{data=Data, joins=Joins}=Q) ->
TRef = make_ref(),
NewData = Data ++ [Fields],
JoinAst = qast:exp([
qast:raw("("),
qsql:select(JoinQ),
qast:raw(") as "),
qast:table(TRef)
]),
Q#query{
data=NewData,
joins=[{JoinType, JoinAst, call(Fun, [NewData])}|Joins]
};
join(JoinType, Info, Fun, #query{data=Data, joins=Joins}=Q) ->
JoinSchema = get_schema(Info),
SchemaFields = maps:get(fields, JoinSchema, #{}),
Table = maps:get(table, JoinSchema),
TRef = make_ref(),
Fields = maps:map(
fun(N, O) -> qast:field(TRef, N, O) end,
SchemaFields),
NewData = Data ++ [Fields],
JoinAst = qast:exp([
qast:raw([equery_utils:wrap(Table), " as "]),
qast:table(TRef)
]),
Q#query{
data=NewData,
joins=[{JoinType, JoinAst, call(Fun, [NewData])}|Joins]
}.
-spec where(fun((data()) -> qast:ast_node())) -> qfun().
where(Fun) -> fun(Q) -> where(Fun, Q) end.
-spec where(fun((data()) -> qast:ast_node()), Q) -> Q when Q :: query().
where(Fun, #query{data=Data, where=OldWhere}=Q) ->
Where = call(Fun, [Data]),
NewWhere =
case OldWhere of
undefined -> Where;
_ -> pg_sql:'andalso'(OldWhere, Where)
end,
Q#query{where = NewWhere}.
-spec select(Fun) -> qfun() when
Fun :: fun((data()) -> select()) |
fun((select(), data()) -> select()).
select(Fun) -> fun(Q) -> select(Fun, Q) end.
-spec select(Fun, Q) -> Q when
Fun :: fun((data()) -> select()) |
fun((select(), data()) -> select()),
Q :: query().
select(Fun, #query{data=Data}=Q) when is_function(Fun, 1) ->
Q#query{select=call(Fun, [Data])};
select(Fun, #query{select=PrevSelect, data=Data}=Q) when is_function(Fun, 2) ->
Q#query{select=call(Fun, [PrevSelect, Data])}.
-spec set(Fun) -> qfun() when
Fun :: fun((data()) -> set()) |
fun((set(), data()) -> set()).
set(Fun) -> fun(Q) -> set(Fun, Q) end.
-spec set(Fun, Q) -> Q when
Fun :: fun((data()) -> set()) |
fun((set(), data()) -> set()),
Q :: query().
set(Fun, #query{data=Data}=Q) when is_function(Fun, 1) ->
Set = call(Fun, [Data]),
is_map(Set) orelse error(bad_map),
Q#query{set=Set};
set(Fun, #query{set=PrevSet, data=Data}=Q) when is_function(Fun, 2) ->
Set = call(Fun, [PrevSet, Data]),
is_map(Set) orelse error(bad_map),
Q#query{set=Set}.
-spec group_by(fun((data()) -> qast:ast_node())) -> qfun().
group_by(Fun) -> fun(Q) -> group_by(Fun, Q) end.
-spec group_by(fun((data()) -> qast:ast_node()), Q) -> Q when Q :: query().
group_by(Fun, #query{data=Data}=Q) ->
Q#query{group_by=call(Fun, [Data])}.
-spec order_by(fun((data()) -> order())) -> qfun().
order_by(Fun) -> fun(Q) -> order_by(Fun, Q) end.
-spec order_by(fun((data()) -> order()), Q) -> Q when Q :: query().
order_by(Fun, #query{data=Data}=Q) ->
Q#query{order_by=call(Fun, [Data])}.
-spec limit(non_neg_integer()) -> qfun().
limit(Value) -> fun(Q) -> limit(Value, Q) end.
-spec limit(non_neg_integer(), Q) -> Q when Q :: query().
limit(Value, Q) ->
Q#query{limit=Value}.
-spec offset(non_neg_integer()) -> qfun().
offset(Value) -> fun(Q) -> offset(Value, Q) end.
-spec offset(non_neg_integer(), Q) -> Q when Q :: query().
offset(Value, Q) ->
Q#query{offset=Value}.
-spec data(fun((data()) -> data())) -> qfun().
data(Fun) -> fun(Q) -> data(Fun, Q) end.
-spec data(fun((data()) -> data()), Q) -> Q when Q :: query().
data(Fun, #query{data=Data}=Q) ->
Data2 = call(Fun, [Data]),
is_list(Data2) orelse error(bad_list),
Q#query{data=Data2}.
%% =============================================================================
%% Internal functions
%% =============================================================================
call(Fun, Args) -> apply(equery_pt:transform_fun(Fun), Args).
get_schema(Schema) when is_map(Schema) -> Schema;
get_schema(Module) when is_atom(Module) -> (Module:schema())#{model => Module}.