├── rebar.lock ├── include ├── equery.hrl ├── cth.hrl ├── query.hrl └── ast_helpers.hrl ├── .gitignore ├── test ├── tree_m.erl ├── test_m.tpl ├── qjson_tests.erl └── q_tests.erl ├── rebar.config ├── src ├── equery.app.src ├── equery_utils.erl ├── qjson.erl ├── equery_pt.erl ├── qast.erl ├── pg_sql.erl ├── qsql.erl └── q.erl ├── .github └── workflows │ └── ci.yml ├── rebar.config.script ├── LICENSE └── README.md /rebar.lock: -------------------------------------------------------------------------------- 1 | []. 2 | -------------------------------------------------------------------------------- /include/equery.hrl: -------------------------------------------------------------------------------- 1 | -compile({parse_transform, equery_pt}). 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .rebar3 2 | _* 3 | .eunit 4 | *.o 5 | *.beam 6 | *.plt 7 | *.swp 8 | *.swo 9 | .erlang.cookie 10 | ebin 11 | log 12 | erl_crash.dump 13 | .rebar 14 | logs 15 | _build 16 | *#* 17 | -------------------------------------------------------------------------------- /include/cth.hrl: -------------------------------------------------------------------------------- 1 | -ifndef(CTH_HRL). 2 | -define(CTH_HRL, true). 3 | 4 | -ifdef(TEST). 5 | -define(MAPS_TO_LIST(M), lists:sort(maps:to_list(M))). 6 | -else. 7 | -define(MAPS_TO_LIST(M), maps:to_list(M)). 8 | -endif. 9 | 10 | -endif. 11 | -------------------------------------------------------------------------------- /test/tree_m.erl: -------------------------------------------------------------------------------- 1 | -module(tree_m). 2 | 3 | -export([schema/0]). 4 | 5 | schema() -> 6 | #{ 7 | fields => #{ 8 | id => #{type => serial}, 9 | parentId => #{type => integer, required => true}, 10 | value => #{type => varchar} 11 | }, 12 | table => <<"tree">> 13 | }. 14 | -------------------------------------------------------------------------------- /rebar.config: -------------------------------------------------------------------------------- 1 | %% -*- erlang -*- 2 | {erl_first_files, ["src/equery.erl", "src/pg.erl", "src/qast.erl"]}. 3 | 4 | {erl_opts, [warn_unused_vars]}. 5 | 6 | {plugins , [coveralls]}. % use hex package 7 | {cover_enabled , true}. 8 | {cover_export_enabled , true}. 9 | {coveralls_coverdata , "_build/test/cover/eunit.coverdata"}. 10 | {coveralls_service_name , "github"}. 11 | {xref_checks, [undefined_function_calls]}. 12 | -------------------------------------------------------------------------------- /src/equery.app.src: -------------------------------------------------------------------------------- 1 | {application, equery, [ 2 | {description, "Sql generator library"}, 3 | {vsn, git}, 4 | {registered, []}, 5 | {applications, [ 6 | kernel, 7 | stdlib, 8 | syntax_tools 9 | ]}, 10 | {env, []}, 11 | {maintainers, ["Yakov Kozlov"]}, 12 | {licenses, ["MIT"]}, 13 | {links, [ 14 | {"Github", "https://github.com/egobrain/equery"}, 15 | {"Blog", "http://www.egobrain.ru/blog/2016/06/02/erlang-orm-part-3"} 16 | ]} 17 | ]}. 18 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | # create this in .github/workflows/ci.yml 2 | on: push 3 | 4 | jobs: 5 | test: 6 | runs-on: ubuntu-latest 7 | name: Erlang/OTP ${{matrix.otp}} / rebar3 ${{matrix.rebar3}} 8 | strategy: 9 | matrix: 10 | otp: ['24.3', '25.3', '26.2', '27.0'] 11 | rebar3: ['3.23.0'] 12 | env: 13 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 14 | steps: 15 | - uses: actions/checkout@v4 16 | - uses: erlef/setup-beam@v1 17 | with: 18 | otp-version: ${{matrix.otp}} 19 | rebar3-version: ${{matrix.rebar3}} 20 | - run: rebar3 do xref,eunit,dialyzer,coveralls send 21 | -------------------------------------------------------------------------------- /test/test_m.tpl: -------------------------------------------------------------------------------- 1 | %% -*- erlang -*- 2 | 3 | -module(test_m). 4 | 5 | -export([ 6 | schema/0, 7 | filter/2, 8 | wrong/1 9 | ]). 10 | 11 | schema() -> 12 | #{fields => #{ 13 | id => #{type => integer} 14 | }, 15 | table => <<"test">> 16 | }. 17 | 18 | filter(Min, Q) -> 19 | q:where( 20 | fun ([#{id := Id}]) -> 21 | Id > Min 22 | bor bnot (bnot Min); %% skiped ops 23 | ([#{id := Id}, _]) -> 24 | Id =:= Min 25 | end, Q). 26 | 27 | wrong(_Q) -> 28 | q:pipe([ 29 | q:from(test_m), 30 | q:where(1) 31 | ]). 32 | -------------------------------------------------------------------------------- /rebar.config.script: -------------------------------------------------------------------------------- 1 | %% -*- erlang -*- 2 | 3 | case {os:getenv("GITHUB_ACTIONS"), os:getenv("GITHUB_TOKEN")} of 4 | {"true", Token} when is_list(Token) -> 5 | CONFIG1 = [{coveralls_repo_token, Token}, 6 | {coveralls_service_job_id, os:getenv("GITHUB_RUN_ID")}, 7 | {coveralls_commit_sha, os:getenv("GITHUB_SHA")}, 8 | {coveralls_service_number, os:getenv("GITHUB_RUN_NUMBER")} | CONFIG], 9 | case os:getenv("GITHUB_EVENT_NAME") =:= "pull_request" 10 | andalso string:tokens(os:getenv("GITHUB_REF"), "/") of 11 | [_, "pull", PRNO, _] -> 12 | [{coveralls_service_pull_request, PRNO} | CONFIG1]; 13 | _ -> 14 | CONFIG1 15 | end; 16 | _ -> 17 | CONFIG 18 | end. 19 | -------------------------------------------------------------------------------- /src/equery_utils.erl: -------------------------------------------------------------------------------- 1 | -module(equery_utils). 2 | 3 | -export([ 4 | wrap/1, 5 | field_name/1, 6 | to_binary/1 7 | ]). 8 | 9 | -spec wrap(iodata()) -> iolist(). 10 | wrap(F) -> 11 | ["\"", F, "\""]. 12 | 13 | -spec field_name(atom()) -> iolist(). 14 | field_name(Atom) when is_atom(Atom) -> 15 | wrap(atom_to_list(Atom)). 16 | 17 | to_binary(Atom) when is_atom(Atom) -> 18 | atom_to_binary(Atom, latin1); 19 | to_binary(Int) when is_integer(Int) -> 20 | integer_to_binary(Int); 21 | to_binary(Bin) when is_binary(Bin) -> 22 | Bin. 23 | 24 | -ifdef(TEST). 25 | -include_lib("eunit/include/eunit.hrl"). 26 | 27 | to_binary_test() -> 28 | ?assertEqual(<<"atom">>, to_binary(atom)), 29 | ?assertEqual(<<"123">>, to_binary(123)), 30 | ?assertEqual(<<"bin">>, to_binary(<<"bin">>)). 31 | 32 | -endif. 33 | -------------------------------------------------------------------------------- /include/query.hrl: -------------------------------------------------------------------------------- 1 | -record(query, { 2 | schema :: q:schema(), 3 | with = undefined, 4 | distinct :: q:distinct() | undefined, 5 | where :: qast:ast_node() | undefined, 6 | data = []:: q:data(), 7 | select = #{} :: q:select(), 8 | set = #{} :: q:set() | #query{}, 9 | tables = [] :: [q:real_table() | q:table()], 10 | joins = [] :: [{q:join_type(), qast:ast_node(), qast:ast_node()}], 11 | group_by = [] :: [qast:ast_node()], 12 | order_by = [] :: q:order(), 13 | on_conflict = #{} :: #{q:conflict_target() => q:conflict_action()}, 14 | limit :: non_neg_integer() | undefined, 15 | offset :: non_neg_integer() | undefined, 16 | lock :: {q:row_lock_level(), [q:real_table()]} | undefined 17 | }). 18 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Yakov 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://github.com/egobrain/equery/actions/workflows/ci.yml/badge.svg)](https://github.com/egobrain/equery/actions/workflows/ci.yml) 2 | [![Coverage Status](https://coveralls.io/repos/github/egobrain/equery/badge.svg?branch=master)](https://coveralls.io/github/egobrain/equery) 3 | [![GitHub tag](https://img.shields.io/github/tag/egobrain/equery.svg)](https://github.com/egobrain/equery) 4 | [![Hex.pm](https://img.shields.io/hexpm/v/equery.svg)](https://hex.pm/packages/equery) 5 | 6 | # equery: erlang postgresql sql generator library 7 | ---------------------------------------------------- 8 | 9 | ## Description ## 10 | 11 | Library for postgresql sql generation. 12 | 13 | ## Simple Example 14 | 15 | ```erlang 16 | 1> Schema = #{ 17 | fields => #{ 18 | id => #{}, 19 | name => #{} 20 | }, 21 | table => <<"users">>}. 22 | 2> Q = q:from(Schema). 23 | 3> Q2 = q:where(fun([#{id := Id}]) -> Id > 3 end, Q). 24 | 4> qast:to_sql(qsql:select(Q2)). 25 | {<<"select \"__table-0\".\"id\",\"__table-0\".\"name\" from \"users\" as \"__table-0\" where (\"__table-0\".\"id\" > $1)">>, 26 | [3]} 27 | ``` 28 | 29 | ## More description will be later... 30 | -------------------------------------------------------------------------------- /src/qjson.erl: -------------------------------------------------------------------------------- 1 | -module(qjson). 2 | 3 | -export([ 4 | '->'/2, 5 | '->>'/2, 6 | '#>'/2, 7 | '#>>'/2, 8 | 9 | '@>'/2, 10 | '<@'/2, 11 | '?'/2, 12 | '?|'/2, 13 | '?&'/2 14 | ]). 15 | 16 | '->'(Field, Name) -> 17 | qast:exp([ 18 | Field, qast:raw(" -> "), Name 19 | ], #{type => json}). 20 | 21 | '->>'(Field, Name) -> 22 | qast:exp([ 23 | Field, qast:raw(" ->> "), Name 24 | ], #{type => text}). 25 | 26 | '#>'(Field, Path) when is_list(Path) -> 27 | qast:exp([ 28 | Field, qast:raw(" #> "), Path 29 | ], #{type => json}). 30 | 31 | '#>>'(Field, Path) when is_list(Path) -> 32 | qast:exp([ 33 | Field, qast:raw(" #>> "), Path 34 | ], #{type => text}). 35 | 36 | '@>'(Field, Obj) -> 37 | qast:exp([ 38 | Field, qast:raw(" @> "), Obj 39 | ], #{type => boolean}). 40 | 41 | '<@'(Field, Obj) -> 42 | qast:exp([ 43 | Field, qast:raw(" <@ "), Obj 44 | ], #{type => boolean}). 45 | 46 | '?'(Field, Key) -> 47 | qast:exp([ 48 | Field, qast:raw(" ? "), Key 49 | ], #{type => boolean}). 50 | 51 | '?|'(Field, Keys) when is_list(Keys) -> 52 | qast:exp([ 53 | Field, qast:raw(" ?| "), Keys 54 | ], #{type => boolean}). 55 | 56 | '?&'(Field, Keys) -> 57 | qast:exp([ 58 | Field, qast:raw(" ?& "), Keys 59 | ], #{type => boolean}). 60 | 61 | -ifdef(TEST). 62 | -include_lib("eunit/include/eunit.hrl"). 63 | 64 | -endif. 65 | -------------------------------------------------------------------------------- /include/ast_helpers.hrl: -------------------------------------------------------------------------------- 1 | -define(atom(Atom),erl_syntax:atom(Atom)). 2 | -define(var(Var),erl_syntax:variable(Var)). 3 | -define(underscore,erl_syntax:underscore()). 4 | 5 | -define(apply(Fun,Args),erl_syntax:application(?atom(Fun),Args)). 6 | -define(apply(Mod,Fun,Args),erl_syntax:application(?atom(Mod),?atom(Fun),Args)). 7 | -define(apply_(Fun,Args),erl_syntax:application(Fun,Args)). 8 | -define(clause(Pattern,Guard,Body),erl_syntax:clause(Pattern,Guard,Body)). 9 | -define(cases(Arg,Clauses),erl_syntax:case_expr(Arg,Clauses)). 10 | -define(ifs(Clauses),erl_syntax:if_expr(Clauses)). 11 | -define(record(Value,Name,Fields),erl_syntax:record_expr(Value,?atom(Name),Fields)). 12 | -define(record(Name,Fields),erl_syntax:record_expr(?atom(Name),Fields)). 13 | -define(field(Name),erl_syntax:record_field(?atom(Name))). 14 | -define(field(Name,Value),erl_syntax:record_field(?atom(Name),Value)). 15 | -define(int(Val),erl_syntax:integer(Val)). 16 | -define(tuple(Elems),erl_syntax:tuple(Elems)). 17 | -define(function(Name,Clauses),erl_syntax:function(?atom(Name),Clauses)). 18 | -define(match(Left,Right),erl_syntax:match_expr(Left,Right)). 19 | -define(abstract(Term),erl_syntax:abstract(Term)). 20 | -define(cons(Head,Tail),erl_syntax:cons(Head,Tail)). 21 | -define(list(Elems),erl_syntax:list(Elems)). 22 | -define(list(Elems,Tail),erl_syntax:list(Elems,Tail)). 23 | -define(string(Str),erl_syntax:string(Str)). 24 | -define(nil,erl_syntax:nil()). 25 | -define(func(Module,Name,Arity),erl_syntax:implicit_fun(?atom(Module),?atom(Name),?int(Arity))). 26 | -define(func(Clauses),erl_syntax:fun_expr(Clauses)). 27 | -define(access(Value,Record,Field),erl_syntax:record_access(Value,?atom(Record),?atom(Field))). 28 | -define(record_index(Record,Field),erl_syntax:record_index_expr(?atom(Record),?atom(Field))). 29 | -define(infix(A,B,C),erl_syntax:infix_expr(A,erl_syntax:operator(B),C)). 30 | -define(eq(A,B),?infix(A,'==',B)). 31 | -define(eeq(A,B),?infix(A,'=:=',B)). 32 | -define(neq(A,B),?infix(A,'=/=',B)). 33 | -define(gt(A,B),?infix(A,'>',B)). 34 | -define(gteq(A,B),?infix(A,'>=',B)). 35 | -define(lt(A,B),?infix(A,'<',B)). 36 | -define(lteq(A,B),?infix(A,'=<',B)). 37 | -define(AND(A,B),?infix(A,'and',B)). 38 | -define(ANDALSO(A,B),?infix(A,'andalso',B)). 39 | -define(OR(A,B),?infix(A,'or',B)). 40 | -define(ORELSE(A,B),?infix(A,'orelse',B)). 41 | 42 | -define(nif_element(N,VALUE),?apply('element',[?int(N),VALUE])). 43 | -define(nif_not(VALUE),?apply('not',[VALUE])). 44 | -define(nif_size(VALUE),?apply('size',[VALUE])). 45 | -define(nif_is_tuple(VALUE),?apply('is_tuple',[VALUE])). 46 | -define(nif_is_function(VALUE),?apply('is_function',[VALUE])). 47 | 48 | -define(list_comp(A, B), erl_syntax:list_comp(A, B)). 49 | -define(generator(A, B), erl_syntax:generator(A, B)). 50 | 51 | -define(arity_qualifier(Name, Arity), erl_syntax:arity_qualifier(?atom(Name), ?int(Arity))). 52 | -define(attribute(Name,Values),erl_syntax:attribute(?atom(Name),Values)). 53 | -define(export(Fun,Arity),?attribute(export,[?list([?arity_qualifier(Fun, Arity)])])). 54 | -define(export_all(List), ?attribute(export,[?list([?arity_qualifier(Fun, Arity) || {Fun, Arity} <- List])])). 55 | -define(def_record(Name, Fields), ?attribute(record,[?atom(Name), ?tuple(Fields)])). 56 | -define(export_fun(Fun), ?export(erl_syntax:atom_value(erl_syntax:function_name(Fun)), erl_syntax:function_arity(Fun))). 57 | -define(export_funs(Funs), ?export_all([{erl_syntax:atom_value(erl_syntax:function_name(F__)), erl_syntax:function_arity(F__)} || F__ <- Funs])). 58 | 59 | -define(ok(A),?tuple([?atom(ok),A])). 60 | -define(error(A),?tuple([?atom(error),A])). 61 | -define(error(A,B),?error(?tuple([A,B]))). 62 | -------------------------------------------------------------------------------- /src/equery_pt.erl: -------------------------------------------------------------------------------- 1 | -module(equery_pt). 2 | 3 | -export([ 4 | parse_transform/2, 5 | transform_fun/1 6 | ]). 7 | 8 | -include("ast_helpers.hrl"). 9 | 10 | -if(?OTP_RELEASE >= 25). 11 | -define(FUN_ENV_MATCH(FBs, FCs), {_FAnno, FBs, _FLf, _FEf, _FUVs, FCs}). 12 | -else. 13 | -define(FUN_ENV_MATCH(FBs, FCs), {FBs, _FLf, _FEf, FCs}). 14 | -endif. 15 | 16 | -record(state, {}). 17 | 18 | parse_transform(Ast, _Opts) -> 19 | {module, _} = code:ensure_loaded(pg_sql), 20 | {Ast2, _} = traverse(fun search_and_compile/2, undefined, Ast), 21 | %% ct:pal("~s", [pretty_print(Ast2)]), 22 | Ast2. 23 | 24 | traverse(Fun, State, List) when is_list(List) -> 25 | lists:mapfoldl(fun(Node, St) -> 26 | traverse(Fun, St, Node) 27 | end, State, List); 28 | traverse(Fun, State, Node) when is_tuple(Node) -> 29 | {Node2, State2} = Fun(Node, State), 30 | List = tuple_to_list(Node2), 31 | {Node3, State3} = traverse(Fun, State2, List), 32 | {list_to_tuple(Node3), State3}; 33 | traverse(_Fun, State, Node) -> 34 | {Node, State}. 35 | 36 | search_and_compile({call, _, {remote, _, {atom, _, q}, {atom, _, F}}, Args}=Node, St) when 37 | F =:= compile; 38 | F =:= data; 39 | F =:= set; 40 | F =:= group_by; 41 | F =:= join; 42 | F =:= with; 43 | F =:= order_by; 44 | F =:= select; 45 | F =:= where; 46 | F =:= recursive; 47 | F =:= on_conflict 48 | -> 49 | {ArgsNode, St2} = lists:mapfoldl( 50 | fun({'fun', L, {clauses, Clauses}}, S) -> 51 | {Node2, S2} = compile(Clauses, S), 52 | RNode = {'fun', L, {clauses, Node2}}, 53 | {RNode, S2}; 54 | (N, S) -> {N, S} 55 | end, St, Args), 56 | RNode = setelement(4, Node, ArgsNode), 57 | {RNode, St2}; 58 | search_and_compile(Node, St) -> 59 | {Node, St}. 60 | 61 | compile(Clauses) -> 62 | {Clauses2, _St} = compile(Clauses, #state{}), 63 | Clauses2. 64 | 65 | compile([{clause, _ColLine, [Cons], [], [Exp]}], St) -> 66 | {[{clause, _ColLine, [Cons], [], [where_exp(Exp)]}], St}; 67 | compile(Ast, St) -> {where_exp(Ast), St}. 68 | 69 | where_exp(Ast) -> 70 | {NewAst, _State} = 71 | traverse_( 72 | fun({op, _CL, Op, A, B} = Node, S) -> 73 | case erlang:function_exported(pg_sql, Op, 2) of 74 | true -> {erl_syntax:revert(?apply(pg_sql, Op, [A,B])), S}; 75 | false -> {Node, S} 76 | end; 77 | ({op, _CL, Op, A} = Node, S) -> 78 | case erlang:function_exported(pg_sql, Op, 1) of 79 | true -> {erl_syntax:revert(?apply(pg_sql, Op, [A])), S}; 80 | false -> {Node, S} 81 | end; 82 | (Node, S) -> 83 | {Node, S} 84 | end, undefined, Ast), 85 | NewAst. 86 | 87 | traverse_(Fun, State, List) when is_list(List) -> 88 | lists:mapfoldl(fun(L, S) -> traverse_(Fun, S, L) end, State, List); 89 | traverse_(Fun, State, Tuple) when is_tuple(Tuple) -> 90 | L = tuple_to_list(Tuple), 91 | {L2, State2} = traverse_(Fun, State, L), 92 | Tuple2 = list_to_tuple(L2), 93 | Fun(Tuple2, State2); 94 | traverse_(_Fun, State, Ast) -> 95 | {Ast, State}. 96 | 97 | transform_fun(Fun) -> 98 | case erlang:fun_info(Fun, module) of 99 | {module, erl_eval} -> 100 | {env, Env} = erlang:fun_info(Fun, env), 101 | case Env of 102 | [?FUN_ENV_MATCH(Bindings, Ast)] -> 103 | Exprs = erl_syntax:revert(?func(compile(Ast))), 104 | {value, Fun2, _} = erl_eval:expr(Exprs, Bindings), 105 | Fun2; 106 | _ -> Fun 107 | end; 108 | _ -> 109 | Fun 110 | end. 111 | 112 | %% ============================================================================= 113 | %% Utils 114 | %% ============================================================================= 115 | 116 | %% pretty_print(Forms0) -> 117 | %% Forms = epp:restore_typed_record_fields(revert(Forms0)), 118 | %% [io_lib:fwrite("~s~n", 119 | %% [lists:flatten([erl_pp:form(Fm) || 120 | %% Fm <- Forms])])]. 121 | 122 | %% revert(Tree) -> 123 | %% [erl_syntax:revert(T) || T <- lists:flatten(Tree)]. 124 | -------------------------------------------------------------------------------- /test/qjson_tests.erl: -------------------------------------------------------------------------------- 1 | -module(qjson_tests). 2 | 3 | -export([schema/0]). 4 | 5 | -include_lib("eunit/include/eunit.hrl"). 6 | -include_lib("equery/include/equery.hrl"). 7 | 8 | schema() -> 9 | #{ 10 | fields => #{ 11 | id => #{ type => serial, index => true }, 12 | payload => #{type => json, required => true} 13 | }, 14 | table => <<"data">> 15 | }. 16 | 17 | '->_test'() -> 18 | {Sql, Args, Type} = to_sql( 19 | qsql:select(q:pipe(q:from(?MODULE), [ 20 | q:select( 21 | fun([#{payload := Payload}|_]) -> 22 | qjson:'->'(Payload, name) 23 | end) 24 | ]))), 25 | ?assertEqual(<<"select " 26 | "\"__alias-0\".\"payload\" -> $1 " 27 | "from \"data\" as \"__alias-0\"">>, Sql), 28 | ?assertEqual([name], Args), 29 | ?assertEqual(json, Type). 30 | 31 | '->>_test'() -> 32 | {Sql, Args, Type} = to_sql( 33 | qsql:select(q:pipe(q:from(?MODULE), [ 34 | q:select( 35 | fun([#{payload := Payload}|_]) -> 36 | qjson:'->>'(Payload, name) 37 | end) 38 | ]))), 39 | ?assertEqual(<<"select " 40 | "\"__alias-0\".\"payload\" ->> $1 " 41 | "from \"data\" as \"__alias-0\"">>, Sql), 42 | ?assertEqual([name], Args), 43 | ?assertEqual(text, Type). 44 | 45 | '#>_test'() -> 46 | {Sql, Args, Type} = to_sql( 47 | qsql:select(q:pipe(q:from(?MODULE), [ 48 | q:select( 49 | fun([#{payload := Payload}|_]) -> 50 | qjson:'#>'(Payload, [emails, 1]) 51 | end) 52 | ]))), 53 | ?assertEqual(<<"select " 54 | "\"__alias-0\".\"payload\" #> $1 " 55 | "from \"data\" as \"__alias-0\"">>, Sql), 56 | ?assertEqual([[emails, 1]], Args), 57 | ?assertEqual(json, Type). 58 | 59 | '#>>_test'() -> 60 | {Sql, Args, Type} = to_sql( 61 | qsql:select(q:pipe(q:from(?MODULE), [ 62 | q:select( 63 | fun([#{payload := Payload}|_]) -> 64 | qjson:'#>>'(Payload, [emails, 1]) 65 | end) 66 | ]))), 67 | ?assertEqual(<<"select " 68 | "\"__alias-0\".\"payload\" #>> $1 " 69 | "from \"data\" as \"__alias-0\"">>, Sql), 70 | ?assertEqual([[emails, 1]], Args), 71 | ?assertEqual(text, Type). 72 | 73 | '@>_test'() -> 74 | {Sql, Args, Type} = to_sql( 75 | qsql:select(q:pipe(q:from(?MODULE), [ 76 | q:select( 77 | fun([#{payload := Payload}|_]) -> 78 | qjson:'@>'(Payload, <<"{\"a\":1}">>) 79 | end) 80 | ]))), 81 | ?assertEqual(<<"select " 82 | "\"__alias-0\".\"payload\" @> $1 " 83 | "from \"data\" as \"__alias-0\"">>, Sql), 84 | ?assertEqual([<<"{\"a\":1}">>], Args), 85 | ?assertEqual(boolean, Type). 86 | 87 | '<@_test'() -> 88 | {Sql, Args, Type} = to_sql( 89 | qsql:select(q:pipe(q:from(?MODULE), [ 90 | q:select( 91 | fun([#{payload := Payload}|_]) -> 92 | qjson:'<@'(Payload, <<"{\"a\":1}">>) 93 | end) 94 | ]))), 95 | ?assertEqual(<<"select " 96 | "\"__alias-0\".\"payload\" <@ $1 " 97 | "from \"data\" as \"__alias-0\"">>, Sql), 98 | ?assertEqual([<<"{\"a\":1}">>], Args), 99 | ?assertEqual(boolean, Type). 100 | 101 | '?_test'() -> 102 | {Sql, Args, Type} = to_sql( 103 | qsql:select(q:pipe(q:from(?MODULE), [ 104 | q:select( 105 | fun([#{payload := Payload}|_]) -> 106 | qjson:'?'(Payload, 1) 107 | end) 108 | ]))), 109 | ?assertEqual(<<"select " 110 | "\"__alias-0\".\"payload\" ? $1 " 111 | "from \"data\" as \"__alias-0\"">>, Sql), 112 | ?assertEqual([1], Args), 113 | ?assertEqual(boolean, Type). 114 | 115 | 116 | '?|_test'() -> 117 | {Sql, Args, Type} = to_sql( 118 | qsql:select(q:pipe(q:from(?MODULE), [ 119 | q:select( 120 | fun([#{payload := Payload}|_]) -> 121 | qjson:'?|'(Payload, [1, 2]) 122 | end) 123 | ]))), 124 | ?assertEqual(<<"select " 125 | "\"__alias-0\".\"payload\" ?| $1 " 126 | "from \"data\" as \"__alias-0\"">>, Sql), 127 | ?assertEqual([[1, 2]], Args), 128 | ?assertEqual(boolean, Type). 129 | 130 | '?&_test'() -> 131 | {Sql, Args, Type} = to_sql( 132 | qsql:select(q:pipe(q:from(?MODULE), [ 133 | q:select( 134 | fun([#{payload := Payload}|_]) -> 135 | qjson:'?&'(Payload, [1, 2]) 136 | end) 137 | ]))), 138 | ?assertEqual(<<"select " 139 | "\"__alias-0\".\"payload\" ?& $1 " 140 | "from \"data\" as \"__alias-0\"">>, Sql), 141 | ?assertEqual([[1, 2]], Args), 142 | ?assertEqual(boolean, Type). 143 | 144 | to_sql(QAst) -> 145 | {Sql, Args} = qast:to_sql(QAst), 146 | Type = maps:get(type, qast:opts(QAst), undefined), 147 | {Sql, Args, Type}. 148 | -------------------------------------------------------------------------------- /src/qast.erl: -------------------------------------------------------------------------------- 1 | -module(qast). 2 | 3 | -export([ 4 | field/3, 5 | value/1, value/2, 6 | raw/1, raw/2, 7 | exp/1, exp/2, 8 | alias/1, alias/2, 9 | 10 | is_ast/1, 11 | opts/1, 12 | set_opts/2, 13 | join/2 14 | ]). 15 | 16 | -export([ 17 | to_sql/1 18 | ]). 19 | 20 | -compile({no_auto_import, [alias/1]}). 21 | 22 | %% ============================================================================= 23 | %% Types 24 | %% ============================================================================= 25 | 26 | -type opts() :: #{type => term(), any() => any()}. 27 | -type raw() :: {'$raw', opts(), iodata()}. 28 | -type value() :: {'$value', opts(), any()}. 29 | -type alias() :: {'$alias', opts(), reference()}. 30 | -type exp() :: {'$exp', opts(), [ast_node() | any()]}. 31 | 32 | -type ast_node() :: raw() | value() | alias() | exp(). 33 | 34 | -export_type([opts/0, raw/0, value/0, alias/0, exp/0, ast_node/0]). 35 | 36 | %% ============================================================================= 37 | %% API 38 | %% ============================================================================= 39 | 40 | -spec field(reference(), atom(), opts()) -> exp(). 41 | field(TableRef, Name, Opts) -> 42 | exp([alias(TableRef), raw([".", equery_utils:field_name(Name)])], Opts). 43 | 44 | -spec value(any()) -> value(). 45 | -spec value(any(), opts()) -> value(). 46 | value(V) -> value(V, #{}). 47 | value(V, Opts) -> {'$value', Opts, V}. 48 | 49 | -spec exp([ast_node() | any()]) -> exp(). 50 | -spec exp([ast_node() | any()], opts()) -> exp(). 51 | exp(V) -> exp(V, #{}). 52 | exp(V, Opts) -> {'$exp', Opts, V}. 53 | 54 | -spec raw(iodata()) -> raw(). 55 | -spec raw(iodata(), opts()) -> raw(). 56 | raw(V) -> raw(V, #{}). 57 | raw(V, Opts) -> {'$raw', Opts, V}. 58 | 59 | -spec alias(reference()) -> alias(). 60 | -spec alias(reference(), opts()) -> alias(). 61 | alias(Ref) -> alias(Ref, #{}). 62 | alias(Ref, Opts) -> {'$alias', Opts, Ref}. 63 | 64 | -spec opts(ast_node()) -> opts(). 65 | opts({'$value', Opts, _}) -> Opts; 66 | opts({'$exp', Opts, _}) -> Opts; 67 | opts({'$raw', Opts, _}) -> Opts; 68 | opts({'$alias', Opts, _}) -> Opts; 69 | opts(_) -> #{}. 70 | 71 | -spec set_opts(ast_node(), opts()) -> ast_node(). 72 | set_opts({'$value', _Opts, Value}, NewOpts) -> value(Value, NewOpts); 73 | set_opts({'$exp', _Opts, Exp}, NewOpts) -> exp(Exp, NewOpts); 74 | set_opts({'$raw', _Opts, Raw}, NewOpts) -> raw(Raw, NewOpts); 75 | set_opts({'$alias', _Opts, TRef}, NewOpts) -> alias(TRef, NewOpts); 76 | set_opts(V, NewOpts) -> value(V, NewOpts). 77 | 78 | -spec is_ast(any()) -> boolean(). 79 | is_ast({'$value', _Opts, _Value}) -> true; 80 | is_ast({'$exp', _Opts, _Exp}) -> true; 81 | is_ast({'$raw', _Opts, _Raw}) -> true; 82 | is_ast({'$alias', _Opts, _Raw}) -> true; 83 | is_ast(_) -> false. 84 | 85 | %% ============================================================================= 86 | %% Utils 87 | %% ============================================================================= 88 | 89 | -spec join([ast_node()], ast_node()) -> exp(). 90 | join([], _Sep) -> qast:exp([]); 91 | join([H|T], Sep) -> 92 | qast:exp([H|lists:foldr(fun(I, Acc) -> [Sep,I|Acc] end, [], T)]). 93 | 94 | -record(state, { 95 | aliases=#{}, aliases_cnt=0, 96 | args=[], args_cnt=0 97 | }). 98 | 99 | -spec to_sql(ast_node()) -> {Sql :: binary(), Args :: [any()]}. 100 | to_sql(Ast) -> 101 | {Sql, #state{args=Args}} = traverse( 102 | fun({'$value', _Opts, V}, #state{args=Vs, args_cnt=Cnt}=St) -> 103 | NewCnt = Cnt+1, 104 | {index(NewCnt), St#state{args=[V|Vs], args_cnt=NewCnt}}; 105 | ({'$alias', _Opts, TRef}, St) -> 106 | get_alias(TRef, St); 107 | ({'$raw', _Opts, V}, St) -> 108 | {V, St} 109 | end, #state{}, Ast), 110 | {iolist_to_binary(Sql), lists:reverse(Args)}. 111 | 112 | %% ============================================================================= 113 | %% Internal 114 | %% ============================================================================= 115 | 116 | traverse(F, Acc, {'$exp', _Opts, List}) -> 117 | lists:mapfoldl(fun(E, A) -> traverse(F, A, E) end, Acc, List); 118 | traverse(F, Acc, {'$raw', _Opts, _}=Item) -> 119 | F(Item, Acc); 120 | traverse(F, Acc, {'$value', _Opts, _V}=Item) -> 121 | F(Item, Acc); 122 | traverse(F, Acc, {'$alias', _Opts, _V}=Item) -> 123 | F(Item, Acc); 124 | %% Other is value 125 | traverse(F, Acc, V) -> 126 | F(qast:value(V), Acc). 127 | 128 | index(N) -> 129 | [ $$, integer_to_binary(N) ]. 130 | 131 | alias_str(Int) -> 132 | equery_utils:wrap(["__alias-",integer_to_list(Int)]). 133 | 134 | get_alias(Ref, #state{aliases=Aliases, aliases_cnt=Cnt}=St) -> 135 | case maps:find(Ref, Aliases) of 136 | {ok, Alias} -> {Alias, St}; 137 | error -> 138 | AliasStr = alias_str(Cnt), 139 | Aliases2 = maps:put(Ref, AliasStr, Aliases), 140 | {AliasStr, St#state{aliases=Aliases2, aliases_cnt=Cnt+1}} 141 | end. 142 | 143 | %% ============================================================================= 144 | %% Tests 145 | %% ============================================================================= 146 | 147 | -ifdef(TEST). 148 | -include_lib("eunit/include/eunit.hrl"). 149 | 150 | ast_utils_test_() -> 151 | NewOpts = #{value => a}, 152 | Tests = [ 153 | {"value", fun value/1, 1, true, NewOpts}, 154 | {"exp", fun exp/1, [], true, NewOpts}, 155 | {"raw", fun raw/1, "test", true, NewOpts}, 156 | {"alias", fun alias/1, make_ref(), true, NewOpts}, 157 | {"some value", fun(A) -> A end, 123, false, NewOpts} 158 | ], 159 | [ 160 | {Name, fun() -> 161 | Ast = CFun(V), 162 | IsAst = is_ast(Ast), 163 | #{} = opts(Ast), 164 | Ast2 = set_opts(Ast, NewOpts), 165 | true = is_ast(Ast2), 166 | RNewOpts = opts(Ast2) 167 | end} || {Name, CFun, V, IsAst, RNewOpts} <- Tests 168 | ]. 169 | 170 | join_test_() -> 171 | Sep = qast:raw(","), 172 | [ 173 | ?_assertEqual( 174 | {<<>>, []}, 175 | qast:to_sql(join([], Sep))), 176 | ?_assertEqual( 177 | {<<"$1,$2">>, [1,2]}, 178 | qast:to_sql(join([1,2], Sep))) 179 | ]. 180 | 181 | -endif. 182 | -------------------------------------------------------------------------------- /src/pg_sql.erl: -------------------------------------------------------------------------------- 1 | -module(pg_sql). 2 | 3 | -include("query.hrl"). 4 | -include("cth.hrl"). 5 | 6 | -export([ 7 | 'andalso'/2, 8 | 'orelse'/2, 9 | 10 | '=:='/2, 11 | '=/='/2, 12 | '>'/2, 13 | '>='/2, 14 | '<'/2, 15 | '=<'/2, 16 | 'not'/1, 17 | 'is'/2, 18 | is_null/1, 19 | 20 | '+'/2, 21 | '-'/2, 22 | '*'/2, 23 | '/'/2, 24 | 'abs'/1 25 | ]). 26 | 27 | -export([ 28 | '~'/2, 29 | '~*'/2, 30 | like/2, 31 | ilike/2 32 | ]). 33 | 34 | -export([ 35 | call/3 36 | ]). 37 | 38 | -export([ 39 | sum/1, 40 | count/1, 41 | min/1, 42 | max/1, 43 | distinct/1, 44 | array_agg/1, 45 | trunc/2 46 | ]). 47 | 48 | -export([ 49 | min/2, 50 | max/2, 51 | row/1, 52 | row/2 53 | ]). 54 | 55 | -export([ 56 | coalesce/1, 57 | in/2, 58 | exists/1 59 | ]). 60 | 61 | %% Array functions 62 | -export([ 63 | '@>'/2 64 | ]). 65 | 66 | %% Type function 67 | -export([ 68 | as/2, 69 | set_type/2 70 | ]). 71 | 72 | %% ============================================================================= 73 | %% Sql operations 74 | %% ============================================================================= 75 | 76 | -type value() :: qast:ast_node() | any(). 77 | 78 | %% = Primitive ================================================================= 79 | 80 | %% @TODO wrap values in $value before validation and add $value match 81 | -spec 'andalso'(V, V) -> V when V :: boolean() | qast:ast_node(). 82 | 'andalso'(true, B) -> B; 83 | 'andalso'(A, true) -> A; 84 | 85 | 'andalso'(false, _) -> false; 86 | 'andalso'(_, false) -> false; 87 | 88 | 'andalso'(A, B) -> 89 | qast:exp([qast:raw("("), A, qast:raw(" and "), B, qast:raw(")")], #{type => boolean}). 90 | 91 | -spec 'orelse'(V, V) -> V when V :: boolean() | qast:ast_node(). 92 | 'orelse'(true, _) -> true; 93 | 'orelse'(_, true) -> true; 94 | 'orelse'(false, B) -> B; 95 | 'orelse'(A, false) -> A; 96 | 'orelse'(A, B) -> 97 | qast:exp([qast:raw("("), A, qast:raw(" or "), B, qast:raw(")")], #{type => boolean}). 98 | 99 | -spec 'not'(V) -> V when V :: boolean() | qast:ast_node(). 100 | 'not'(A) when is_boolean(A) -> not A; 101 | 'not'(A) -> 102 | qast:exp([qast:raw("not "), A], #{type => boolean}). 103 | 104 | -spec '=:='(value(), value()) -> qast:ast_node(). 105 | '=:='(A, B) -> 106 | qast:exp([qast:raw("("), A, qast:raw(" = "), B, qast:raw(")")], #{type => boolean}). 107 | 108 | -spec '=/='(value(), value()) -> qast:ast_node(). 109 | '=/='(A, B) -> 'not'('=:='(A,B)). 110 | 111 | -spec '>'(value(), value()) -> qast:ast_node(). 112 | '>'(A, B) -> 113 | qast:exp([qast:raw("("), A, qast:raw(" > "), B, qast:raw(")")], #{type => boolean}). 114 | -spec '>='(value(), value()) -> qast:ast_node(). 115 | '>='(A, B) -> 116 | qast:exp([qast:raw("("), A, qast:raw(" >= "), B, qast:raw(")")], #{type => boolean}). 117 | -spec '<'(value(), value()) -> qast:ast_node(). 118 | '<'(A, B) -> 119 | qast:exp([qast:raw("("), A, qast:raw(" < "), B, qast:raw(")")], #{type => boolean}). 120 | -spec '=<'(value(), value()) -> qast:ast_node(). 121 | '=<'(A, B) -> 122 | qast:exp([qast:raw("("), A, qast:raw(" <= "), B, qast:raw(")")], #{type => boolean}). 123 | 124 | -spec 'is'(value(), value()) -> qast:ast_node(). 125 | is(A, B) -> 126 | qast:exp([A, qast:raw(" is "), B], #{type => boolean}). 127 | 128 | -spec 'is_null'(value()) -> qast:ast_node(). 129 | is_null(A) -> 130 | is(A, qast:raw("null")). 131 | 132 | %% @TODO type opts 133 | -spec '+'(value(), value()) -> qast:ast_node(). 134 | '+'(A, B) -> 135 | qast:exp([qast:raw("("), A, qast:raw(" + "), B, qast:raw(")")]). 136 | -spec '-'(value(), value()) -> qast:ast_node(). 137 | '-'(A, B) -> 138 | qast:exp([qast:raw("("), A, qast:raw(" - "), B, qast:raw(")")]). 139 | -spec '*'(value(), value()) -> qast:ast_node(). 140 | '*'(A, B) -> 141 | qast:exp([qast:raw("("), A, qast:raw(" * "), B, qast:raw(")")]). 142 | -spec '/'(value(), value()) -> qast:ast_node(). 143 | '/'(A, B) -> 144 | qast:exp([qast:raw("("), A, qast:raw(" / "), B, qast:raw(")")]). 145 | 146 | -spec 'abs'(value()) -> qast:ast_node(). 147 | abs(A) -> 148 | qast:exp([qast:raw("abs("), A, qast:raw(")")], qast:opts(A)). 149 | 150 | %% = LIKE ====================================================================== 151 | 152 | -spec '~'(value(), value()) -> qast:ast_node(). 153 | '~'(A, B) -> 154 | qast:exp([qast:raw("("), A, qast:raw(" ~ "), B, qast:raw(")")], #{type => boolean}). 155 | -spec '~*'(value(), value()) -> qast:ast_node(). 156 | '~*'(A, B) -> 157 | qast:exp([qast:raw("("), A, qast:raw(" ~* "), B, qast:raw(")")], #{type => boolean}). 158 | like(A, B) -> 159 | qast:exp([A, qast:raw(" like "), B], #{type => boolean}). 160 | ilike(A, B) -> 161 | qast:exp([A, qast:raw(" ilike "), B], #{type => boolean}). 162 | 163 | %% = Aggregators =============================================================== 164 | 165 | -spec sum(qast:ast_node()) -> qast:ast_node(). 166 | sum(Ast) -> 167 | call("sum", [Ast], qast:opts(Ast)). 168 | 169 | -spec count(qast:ast_node()) -> qast:ast_node(). 170 | count(Ast) -> 171 | call("count", [Ast], #{type => integer}). 172 | 173 | -spec 'min'(value()) -> qast:ast_node(). 174 | min(Ast) -> 175 | call("min", [Ast], qast:opts(Ast)). 176 | 177 | -spec 'max'(value()) -> qast:ast_node(). 178 | max(Ast) -> 179 | call("max", [Ast], qast:opts(Ast)). 180 | 181 | -spec 'distinct'(value()) -> qast:ast_node(). 182 | distinct(Ast) -> 183 | call("distinct ", [Ast], qast:opts(Ast)). 184 | 185 | -spec 'array_agg'(value()) -> qast:ast_node(). 186 | array_agg(Ast) -> 187 | Opts = qast:opts(Ast), 188 | Type = maps:get(type, Opts, undefined), 189 | NewOpts = Opts#{type => {array, Type}}, 190 | call("array_agg", [Ast], NewOpts). 191 | 192 | -spec 'trunc'(value(), qast:ast_node() | non_neg_integer()) -> qast:ast_node(). 193 | 'trunc'(V, N) -> 194 | call("trunc", [V, N], qast:opts(V)). 195 | 196 | %% = Math ====================================================================== 197 | 198 | -spec 'min'(value(), value()) -> qast:ast_node(). 199 | min(A, B) -> 200 | qast:exp([qast:raw("LEAST("), A, qast:raw(","), B, qast:raw(")")], qast:opts(A)). 201 | 202 | -spec 'max'(value(), value()) -> qast:ast_node(). 203 | max(A, B) -> 204 | qast:exp([qast:raw("GREATEST("), A, qast:raw(","), B, qast:raw(")")], qast:opts(A)). 205 | 206 | %% = Additional operations ===================================================== 207 | 208 | -spec row(#{atom() => qast:ast_node()}) -> qast:ast_node(). 209 | row(Fields) when is_map(Fields) -> 210 | row(undefined, Fields). 211 | 212 | -spec row(Model :: module(), #{atom() => qast:ast_node()}) -> qast:ast_node(). 213 | row(Model, Fields) when is_map(Fields) -> 214 | FieldsList = ?MAPS_TO_LIST(Fields), 215 | Type = {record, {model, Model, [{F, qast:opts(Node)} || {F, Node} <- FieldsList]}}, 216 | qast:exp([ 217 | qast:raw("row("), 218 | qast:join([Node || {_F, Node} <- FieldsList], qast:raw(",")), 219 | qast:raw(")") 220 | ], #{type => Type}). 221 | 222 | coalesce([H|_]=List) -> 223 | qast:exp([ 224 | qast:raw("coalesce("), 225 | qast:join([Node || Node <- List], qast:raw(",")), 226 | qast:raw(")") 227 | ], maps:with([type], qast:opts(H))). 228 | 229 | in(A, #query{}=Q) -> 230 | qast:exp([A, qast:raw(" in ("), qsql:select(Q), qast:raw(")")], #{type => boolean}); 231 | in(A, [Item]) -> 232 | '=:='(A, Item); 233 | in(A, B) -> 234 | qast:exp([A, qast:raw(" = ANY("), B, qast:raw(")")], #{type => boolean}). 235 | 236 | exists(#query{}=Q) -> 237 | qast:exp([qast:raw("exists ("), qsql:select(Q), qast:raw(")")], #{type => boolean}). 238 | 239 | -spec call(iodata(), [value()], qast:opts()) -> qast:ast_node(). 240 | call(FunName, Args, Opts) -> 241 | qast:exp([ 242 | qast:raw([FunName, "("]), 243 | qast:join(Args, qast:raw(",")), 244 | qast:raw(")") 245 | ], Opts). 246 | 247 | %% = Array oprterations ======================================================== 248 | 249 | '@>'(A, B) -> 250 | qast:exp([A, qast:raw(" @> "), B], #{type => boolean}). 251 | 252 | %% = Type functions ============================================================ 253 | 254 | as(Ast, Type) -> 255 | Opts = qast:opts(Ast), 256 | qast:exp([ 257 | qast:raw("("), Ast, qast:raw(")::"), 258 | qast:raw(type_str(Type)) 259 | ], Opts#{type => Type}). 260 | 261 | set_type(Ast, Type) -> 262 | Opts = qast:opts(Ast), 263 | qast:set_opts(Ast, Opts#{type => Type}). 264 | 265 | type_str(Atom) when is_atom(Atom) -> 266 | atom_to_binary(Atom, latin1); 267 | type_str({array, Atom}) when is_atom(Atom) -> 268 | iolist_to_binary([type_str(Atom), "[]"]); 269 | type_str({Type, Args}) when Type =/= array -> 270 | iolist_to_binary([ 271 | to_iodata(Type), 272 | "(", join([to_iodata(A) || A <- Args], ","), ")" 273 | ]). 274 | 275 | to_iodata(Atom) when is_atom(Atom) -> 276 | atom_to_list(Atom); 277 | to_iodata(D) when is_list(D); is_binary(D) -> 278 | D; 279 | to_iodata(Int) when is_integer(Int) -> 280 | integer_to_list(Int); 281 | to_iodata(Float) when is_float(Float) -> 282 | io_lib:format("~p", [Float]). 283 | 284 | join([], _) -> []; 285 | join([H|T],Sep) -> [H|[[Sep,E]||E<-T]]. 286 | 287 | -ifdef(TEST). 288 | -include_lib("eunit/include/eunit.hrl"). 289 | 290 | type_str_test() -> 291 | ?assertEqual(<<"bigint">>, type_str(bigint)), 292 | ?assertEqual(<<"int[]">>, type_str({array, int})), 293 | ?assertEqual(<<"custom()">>, type_str({custom, []})), 294 | ?assertEqual(<<"custom(a,1,2.0)">>, type_str({custom, [<<"a">>, 1, 2.0]})). 295 | 296 | -endif. 297 | -------------------------------------------------------------------------------- /src/qsql.erl: -------------------------------------------------------------------------------- 1 | -module(qsql). 2 | 3 | -include("query.hrl"). 4 | -include("cth.hrl"). 5 | 6 | -export([ 7 | select/1, 8 | insert/1, 9 | update/1, 10 | delete/1 11 | ]). 12 | 13 | -spec select(q:query()) -> qast:ast_node(). 14 | select(#query{ 15 | tables=Tables, 16 | schema=Schema, 17 | with=WithExp, 18 | where=Where, 19 | select=RFields, 20 | distinct=Distinct, 21 | joins=Joins, 22 | group_by=GroupBy, 23 | order_by=OrderBy, 24 | limit=Limit, 25 | offset=Offset, 26 | lock=Lock 27 | }) -> 28 | {Fields, Opts} = fields_and_opts(Schema, RFields), 29 | qast:exp([ 30 | maybe_exp(WithExp), 31 | qast:raw("select "), 32 | distinct_exp(Distinct, RFields), 33 | fields_exp(Fields), 34 | from_exp(Tables), 35 | joins_exp(Joins), 36 | where_exp(Where), 37 | group_by_exp(GroupBy), 38 | order_by_exp(OrderBy), 39 | limit_exp(Limit), 40 | offset_exp(Offset), 41 | lock(Lock) 42 | ], Opts). 43 | 44 | -spec insert(q:query()) -> qast:ast_node(). 45 | insert(#query{ 46 | schema=Schema, 47 | with=WithExp, 48 | tables=[{real, Table, TRef}|Rest], 49 | select=RFields, 50 | set = #query{} = Query, 51 | on_conflict=OnConflict 52 | }) -> 53 | Rest =:= [] orelse error("Unsupported query using operation. See q:using/[1,2]"), 54 | SelectAst = select(Query), 55 | #{type := {model, _, SetFields}} = qast:opts(SelectAst), 56 | {Fields, Opts} = fields_and_opts(Schema, RFields), 57 | qast:exp([ 58 | maybe_exp(WithExp), 59 | qast:raw(["insert into ", equery_utils:wrap(Table), " as "]), 60 | qast:alias(TRef), 61 | qast:raw(" ("), 62 | fields_exp([ 63 | qast:exp([qast:raw(equery_utils:field_name(F))], qast:opts(V)) || {F, V} <- SetFields 64 | ]), 65 | qast:raw([") "]), 66 | SelectAst, 67 | on_conflict_exp(OnConflict), 68 | returning_exp(Fields) 69 | ], Opts); 70 | insert(#query{ 71 | schema=Schema, 72 | tables=[{real, Table, TRef}|Rest], 73 | select=RFields, 74 | set=Set, 75 | on_conflict=OnConflict 76 | }) -> 77 | Rest =:= [] orelse error("Unsupported query using operation. See q:using/[1,2]"), 78 | {Fields, Opts} = fields_and_opts(Schema, RFields), 79 | {SetKeys, SetValues} = lists:unzip([ 80 | {{K, qast:opts(V)}, V} || {K, V} <- ?MAPS_TO_LIST(Set) 81 | ]), 82 | qast:exp([ 83 | qast:raw(["insert into ", equery_utils:wrap(Table), " as "]), 84 | qast:alias(TRef), 85 | qast:raw(" ("), 86 | fields_exp([ 87 | qast:exp([qast:raw(equery_utils:field_name(F))], O) || {F, O} <- SetKeys 88 | ]), 89 | qast:raw([") values ("]), 90 | fields_exp(SetValues), 91 | qast:raw([")"]), 92 | on_conflict_exp(OnConflict), 93 | returning_exp(Fields) 94 | ], Opts). 95 | 96 | -spec update(q:query()) -> qast:ast_node(). 97 | update(#query{schema=Schema, tables=[{real, Table, TRef}|Rest], select=RFields, where=Where, set=Set}) -> 98 | {Fields, Opts} = fields_and_opts(Schema, RFields), 99 | qast:exp([ 100 | qast:raw(["update ", equery_utils:wrap(Table), " as "]), 101 | qast:alias(TRef), 102 | qast:raw(" set "), 103 | qast:join([ 104 | qast:exp([ 105 | qast:raw([equery_utils:field_name(F), " = "]), Node 106 | ]) || {F, Node} <- ?MAPS_TO_LIST(Set) 107 | ], qast:raw(",")), 108 | from_exp(Rest), 109 | where_exp(Where), 110 | returning_exp(Fields) 111 | ], Opts). 112 | 113 | -spec delete(q:query()) -> qast:ast_node(). 114 | delete(#query{schema=Schema, tables=[{real, Table, TRef}|Rest], select=RFields, where=Where}) -> 115 | {Fields, Opts} = fields_and_opts(Schema, RFields), 116 | qast:exp([ 117 | qast:raw(["delete from ", equery_utils:wrap(Table), " as "]), 118 | qast:alias(TRef), 119 | using_exp(Rest), 120 | where_exp(Where), 121 | returning_exp(Fields) 122 | ], Opts). 123 | 124 | %% ============================================================================= 125 | %% Internal functions 126 | %% ============================================================================= 127 | 128 | %% = Exp builders ============================================================== 129 | 130 | fields_and_opts(Schema, RFields) -> 131 | case is_map(RFields) of 132 | true -> 133 | RFieldsList = ?MAPS_TO_LIST(RFields), 134 | Opts = #{type => type(Schema, RFieldsList)}, 135 | Values = lists:map(fun({F, Ast}) -> 136 | as(Ast, F) 137 | end, RFieldsList); 138 | false -> 139 | Opts = qast:opts(RFields), 140 | Values = [RFields] 141 | end, 142 | {Values, Opts}. 143 | 144 | as(Value, Name) -> 145 | qast:exp([ 146 | Value, qast:raw(" as "), qast:raw(equery_utils:field_name(Name)) 147 | ], qast:opts(Value)). 148 | 149 | fields_exp(FieldsExps) -> 150 | qast:join(FieldsExps, qast:raw(",")). 151 | 152 | returning_exp([]) -> qast:raw([]); 153 | returning_exp(Fields) -> 154 | qast:exp([ 155 | qast:raw(" returning "), 156 | fields_exp(Fields) 157 | ]). 158 | 159 | using_exp([]) -> qast:raw([]); 160 | using_exp([_|_]=Tables) -> 161 | qast:exp([qast:raw(" using "), tables_exp_(Tables)]). 162 | 163 | from_exp([]) -> qast:raw([]); 164 | from_exp([_|_] = Tables) -> 165 | qast:exp([qast:raw(" from "), tables_exp_(Tables)]). 166 | 167 | tables_exp_([_|_]=Tables) -> 168 | qast:join(lists:map(fun table_exp/1, Tables), qast:raw(",")). 169 | 170 | table_exp({real, Table, TRef}) -> 171 | qast:exp([ 172 | qast:raw([equery_utils:wrap(Table), " as "]), 173 | qast:alias(TRef) 174 | ]); 175 | table_exp({alias, AliasExp, _FeildsExp}) -> AliasExp. 176 | 177 | joins_exp(Joins) -> 178 | qast:exp(lists:map( 179 | fun({JoinType, JoinAst, Exp}) -> 180 | qast:exp([ 181 | qast:raw([" ", join_type(JoinType), " join "]), 182 | JoinAst, 183 | qast:raw(" on "), 184 | Exp 185 | ]) 186 | end, lists:reverse(Joins))). 187 | 188 | join_type(inner) -> <<"inner">>; 189 | join_type(left) -> <<"left">>; 190 | join_type(right) -> <<"right">>; 191 | join_type(full) -> <<"full">>; 192 | join_type({left, outer}) -> <<"left outer">>; 193 | join_type({right, outer}) -> <<"right outer">>; 194 | join_type({full, outer}) -> <<"full outer">>. 195 | 196 | where_exp(undefined) -> qast:raw([]); 197 | where_exp(WhereExp) -> qast:exp([qast:raw(" where "), WhereExp]). 198 | 199 | group_by_exp([]) -> qast:raw([]); 200 | group_by_exp(GroupBy) -> 201 | qast:exp([ 202 | qast:raw(" group by "), 203 | qast:join(GroupBy, qast:raw(",")) 204 | ]). 205 | 206 | order_by_exp([]) -> qast:raw([]); 207 | order_by_exp(OrderBy) -> 208 | OrderExps = lists:map(fun({OrderField,Direction}) -> 209 | qast:exp([ 210 | OrderField, 211 | qast:raw(case Direction of 212 | asc -> <<" ASC">>; 213 | desc -> <<" DESC">> 214 | end) 215 | ]) 216 | end, OrderBy), 217 | qast:exp([ 218 | qast:raw(" order by "), 219 | qast:join(OrderExps, qast:raw(",")) 220 | ]). 221 | 222 | limit_exp(undefined) -> qast:raw([]); 223 | limit_exp(Limit) -> 224 | qast:exp([ 225 | qast:raw(" limit "), 226 | qast:value(Limit, #{type => integer}) 227 | ]). 228 | 229 | offset_exp(undefined) -> qast:raw([]); 230 | offset_exp(Offset) -> 231 | qast:exp([ 232 | qast:raw(" offset "), 233 | qast:value(Offset, #{type => integer}) 234 | ]). 235 | 236 | type(Schema, FieldsList) -> 237 | Model = maps:get(model, Schema, undefined), 238 | {model, Model, [{F, qast:opts(Node)} || {F, Node} <- FieldsList]}. 239 | 240 | maybe_exp(undefined) -> qast:raw(""); 241 | maybe_exp(Exp) -> Exp. 242 | 243 | distinct_exp(undefined, _RFields) -> qast:raw(""); 244 | distinct_exp(all, _RFields) -> qast:raw("distinct "); 245 | distinct_exp([], RFields) -> distinct_exp(undefined, RFields); 246 | distinct_exp(DistinctOn, RFields) when is_list(DistinctOn), is_map(RFields) -> 247 | DistinctAliases = lists:map(fun(Field) -> 248 | FieldAst = maps:get(Field, RFields), 249 | qast:raw(equery_utils:field_name(Field), qast:opts(FieldAst)) 250 | end, DistinctOn), 251 | qast:exp([ 252 | qast:raw("distinct on ("), 253 | qast:join(DistinctAliases, qast:raw(",")), 254 | qast:raw(") ") 255 | ]). 256 | 257 | on_conflict_exp(Conflicts) -> 258 | lists:foldr(fun({ConflictTarget, ConflictAction}, Acc) -> 259 | qast:exp([ 260 | Acc, 261 | qast:raw(" on conflict"), 262 | conflict_target_exp(ConflictTarget), 263 | qast:raw("do "), 264 | conflict_action_exp(ConflictAction) 265 | ]) 266 | end, qast:raw(""), ?MAPS_TO_LIST(Conflicts)). 267 | 268 | conflict_target_exp(any) -> qast:raw(" "); 269 | conflict_target_exp(Columns) when is_list(Columns) -> 270 | qast:exp([ 271 | qast:raw(" ("), 272 | qast:join([ 273 | qast:raw(equery_utils:field_name(C)) || C <- Columns 274 | ], qast:raw(",")), 275 | qast:raw(") ") 276 | ]). 277 | 278 | conflict_action_exp(nothing) -> qast:raw("nothing"); 279 | conflict_action_exp(Set) when is_map(Set) -> 280 | qast:exp([ 281 | qast:raw("update set "), 282 | qast:join([ 283 | qast:exp([ 284 | qast:raw([equery_utils:field_name(F), " = "]), Node 285 | ]) || {F, Node} <- ?MAPS_TO_LIST(Set) 286 | ], qast:raw(",")) 287 | ]). 288 | 289 | lock(undefined) -> 290 | qast:raw(""); 291 | lock({RowLockLevel, Tables}) -> 292 | Aliases = lists:map(fun({real, _Table, TRef}) -> qast:alias(TRef) end, Tables), 293 | qast:exp([ 294 | qast:raw(" "), 295 | case RowLockLevel of 296 | for_update -> qast:raw("for update"); 297 | for_no_key_update -> qast:raw("for no key update"); 298 | for_share -> qast:raw("for share"); 299 | for_key_share -> qast:raw("for key share") 300 | end, 301 | qast:raw(" of "), 302 | qast:join(Aliases, qast:raw(",")) 303 | ]). 304 | -------------------------------------------------------------------------------- /src/q.erl: -------------------------------------------------------------------------------- 1 | -module(q). 2 | 3 | -include("query.hrl"). 4 | -include("ast_helpers.hrl"). 5 | 6 | -export([ 7 | pipe/2, 8 | 9 | get/2, 10 | lookup_tables/2 11 | ]). 12 | 13 | -export([ 14 | from/1, 15 | using/1, using/2, 16 | with/2, with/3, 17 | recursive/2, 18 | join/2, join/3, join/4, 19 | where/1, where/2, 20 | select/1, select/2, 21 | set/1, set/2, 22 | data/1, data/2, 23 | group_by/1, group_by/2, 24 | on_conflict/2, on_conflict/3, 25 | order_by/1, order_by/2, 26 | limit/1, limit/2, 27 | offset/1, offset/2, 28 | 29 | lock/1, lock/2, lock/3, 30 | for_update/0, for_update/1, 31 | 32 | distinct/0, distinct/1, 33 | distinct_on/1, distinct_on/2 34 | ]). 35 | 36 | -export([ 37 | compile/1 38 | ]). 39 | 40 | -type model() :: schema() | module(). 41 | -type query() :: #query{}. 42 | -type table() :: {alias, qast:ast_node(), #{atom() => term()}}. 43 | -type schema() :: #{fields => #{atom() => #{atom() => term()}}, table => binary(), atom() => any()}. 44 | -type data() :: [#{atom() => qast:ast_node()}]. 45 | -type select() :: #{atom() => qast:ast_node()} | qast:ast_node(). 46 | -type set() :: #{atom() => qast:ast_node()} | query(). 47 | -type order() :: [{qast:ast_node(), asc | desc}]. 48 | -type distinct() :: all | [atom()]. 49 | -type join_type() :: inner | left | right | full | {left, outer} | {right, outer} | {full, outer}. 50 | -type row_lock_level() :: for_update | for_no_key_update | for_share | for_key_share. 51 | -type qfun() :: fun((query()) -> query()). 52 | -type conflict_target() :: any | [atom()]. 53 | -type conflict_action() :: nothing | #{atom() => qast:ast_node()}. 54 | 55 | %% internal 56 | -type real_table() :: {real, iolist(), reference()}. 57 | 58 | -export_type([query/0]). 59 | 60 | -export_type([ 61 | model/0, 62 | table/0, 63 | real_table/0, 64 | schema/0, 65 | data/0, 66 | select/0, 67 | set/0, 68 | order/0, 69 | distinct/0, 70 | join_type/0, 71 | row_lock_level/0, 72 | qfun/0, 73 | conflict_target/0, 74 | conflict_action/0 75 | ]). 76 | 77 | %% = Flow ====================================================================== 78 | 79 | -spec pipe(Q, [qfun()]) -> Q when Q :: query(). 80 | pipe(Query, Funs) -> 81 | lists:foldl(fun(F, Q) -> F(Q) end, Query, Funs). 82 | 83 | -spec get(schema, query()) -> schema(); 84 | (data, query()) -> data(). 85 | get(schema, #query{schema=Schema}) -> Schema; 86 | get(data, #query{data=Data}) -> Data. 87 | 88 | %% = Query builders ============================================================ 89 | 90 | -spec from(model() | query() | table() | qast:ast_node()) -> query(). 91 | from(Info) when is_map(Info); is_atom(Info) -> 92 | Schema = get_schema(Info), 93 | {RealTable, Fields} = table_feilds(Schema), 94 | #query{ 95 | schema = Schema, 96 | data=[Fields], 97 | select=Fields, 98 | tables=[RealTable] 99 | }; 100 | from(#query{}=Query) -> 101 | from(braced(qsql:select(Query))); 102 | from({alias, _AliasExp, FieldsExp}=Alias) -> 103 | Fields = maps:map( 104 | fun(_N, Ast) -> qast:opts(Ast) end, 105 | FieldsExp), 106 | #query{ 107 | schema = #{ 108 | fields => Fields 109 | }, 110 | tables = [Alias], 111 | select = FieldsExp, 112 | data = [FieldsExp] 113 | }; 114 | from(Ast) -> 115 | #{type := {model, Model, FieldsList}} = qast:opts(Ast), 116 | TRef = make_ref(), 117 | Fields = maps:from_list(FieldsList), 118 | FieldsExp = aliased_fields(TRef, Fields), 119 | TableAst = as(Ast, qast:alias(TRef)), 120 | #query{ 121 | schema = #{ 122 | model => Model, 123 | fields => Fields 124 | }, 125 | data = [FieldsExp], 126 | select = FieldsExp, 127 | tables = [{alias, TableAst, FieldsExp}] 128 | }. 129 | 130 | as(VAst, AsAst) -> 131 | qast:exp([ 132 | VAst, qast:raw(" as "), AsAst 133 | ], qast:opts(VAst)). 134 | 135 | using(Info) -> fun(Q) -> using(Info, Q) end. 136 | using({alias, _AliasExp, FieldsExp}=Alias, #query{tables=[_|_]=Tables, data=Data}=Query) -> 137 | Query#query{ 138 | tables = Tables ++ [Alias], 139 | data = Data ++ [FieldsExp] 140 | }; 141 | using(Info, #query{tables=[_|_]=Tables, data=Data}=Query) when is_map(Info); is_atom(Info) -> 142 | Schema = get_schema(Info), 143 | {RealTable, Fields} = table_feilds(Schema), 144 | Query#query{ 145 | tables = Tables ++ [RealTable], 146 | data = Data ++ [Fields] 147 | }. 148 | 149 | table_feilds(#{table := Table}=Schema) -> 150 | SchemaFields = maps:get(fields, Schema, #{}), 151 | TRef = make_ref(), 152 | Fields = maps:map( 153 | fun(N, Opts) -> qast:field(TRef, N, Opts) end, 154 | SchemaFields), 155 | RealTable = {real, Table, TRef}, 156 | {RealTable, Fields}. 157 | 158 | 159 | %% = Recursive ================================================================= 160 | 161 | recursive(#query{select=RFields}=BaseQuery, UnionFun) when is_map(RFields) -> 162 | Schema = ?MODULE:get(schema, BaseQuery), 163 | TRef = make_ref(), 164 | Fields = maps:map( 165 | fun(_N, Ast) -> qast:opts(Ast) end, 166 | RFields), 167 | FieldsExp = maps:map( 168 | fun(N, Opts) -> qast:field(TRef, N, Opts) 169 | end, Fields), 170 | InternalQ = #query{ 171 | schema = (maps:with([model], Schema))#{ 172 | fields => Fields 173 | }, 174 | data = [FieldsExp], 175 | select = FieldsExp, 176 | tables = [{alias, qast:alias(TRef), FieldsExp}] 177 | }, 178 | WithExpression = qast:exp([ 179 | qast:raw("with recursive "), 180 | qast:alias(TRef), 181 | qast:raw(" as ("), 182 | qsql:select(BaseQuery), 183 | qast:raw(" union all "), 184 | qsql:select(call(UnionFun, [InternalQ])), 185 | qast:raw(") ") 186 | ]), 187 | InternalQ#query{with=WithExpression}. 188 | 189 | -spec with(model() | query() | qast:ast_node(), fun((table()) -> qfun())) -> qfun(). 190 | with(Info, Fun) -> fun(Q) -> with(Info, Fun, Q) end. 191 | 192 | -spec with(model() | query() | qast:ast_node(), fun((table()) -> qfun()), Q) -> Q when Q :: query(). 193 | with(Info, Fun, Q) when is_map(Info); is_atom(Info) -> 194 | with(from(Info), Fun, Q); 195 | with(#query{}=Query, Fun, Q) -> 196 | with(qsql:select(Query), Fun, Q); 197 | with(Ast, Fun, Q) -> 198 | #{type := {model, _Model, Fields}} = Opts = qast:opts(Ast), 199 | TRef = make_ref(), 200 | FieldsExp = lists:foldl(fun({N, O}, Acc) -> 201 | Acc#{N => qast:field(TRef, N, O)} 202 | end, #{}, Fields), 203 | Alias = qast:alias(TRef, Opts), 204 | WithExpression = qast:exp([ 205 | qast:raw("with "), 206 | Alias, 207 | qast:raw(" as ("), Ast, qast:raw(") ") 208 | ]), 209 | (call(Fun, [{alias, Alias, FieldsExp}]))(Q#query{with=WithExpression}). 210 | 211 | -spec join(model() | query() | table(), fun((data()) -> qast:ast_node())) -> qfun(). 212 | join(Info, Fun) -> 213 | join(inner, Info, Fun). 214 | 215 | -spec join(join_type(), model() | query() | table(), fun((data()) -> qast:ast_node())) -> qfun(). 216 | join(JoinType, Info, Fun) -> 217 | fun(Q) -> join(JoinType, Info, Fun, Q) end. 218 | 219 | -spec join(join_type(), model() | query() | table(), fun((data()) -> qast:ast_node()), Q) -> Q when Q :: query(). 220 | join(JoinType, #query{select=RFields}=JoinQ, Fun, #query{data=Data, joins=Joins}=Q) -> 221 | TRef = make_ref(), 222 | Fields = maps:map(fun(_, V) -> qast:opts(V) end, RFields), 223 | FieldsData = aliased_fields(TRef, Fields), 224 | NewData = Data ++ [FieldsData], 225 | JoinAst = qast:exp([ 226 | qast:raw("("), 227 | qsql:select(JoinQ), 228 | qast:raw(") as "), 229 | qast:alias(TRef) 230 | ]), 231 | Q#query{ 232 | data=NewData, 233 | joins=[{JoinType, JoinAst, call(Fun, [NewData])}|Joins] 234 | }; 235 | join(JoinType, {alias, TableAlias, FieldsExp}, Fun, #query{data=Data, joins=Joins}=Q) -> 236 | NewData = Data ++ [FieldsExp], 237 | Q#query{ 238 | data=NewData, 239 | joins=[{JoinType, TableAlias, call(Fun, [NewData])}|Joins] 240 | }; 241 | join(JoinType, Info, Fun, #query{data=Data, joins=Joins}=Q) -> 242 | JoinSchema = get_schema(Info), 243 | SchemaFields = maps:get(fields, JoinSchema, #{}), 244 | Table = maps:get(table, JoinSchema), 245 | TRef = make_ref(), 246 | Fields = maps:map( 247 | fun(N, O) -> qast:field(TRef, N, O) end, 248 | SchemaFields), 249 | NewData = Data ++ [Fields], 250 | JoinAst = qast:exp([ 251 | qast:raw([equery_utils:wrap(Table), " as "]), 252 | qast:alias(TRef) 253 | ]), 254 | Q#query{ 255 | data=NewData, 256 | joins=[{JoinType, JoinAst, call(Fun, [NewData])}|Joins] 257 | }. 258 | 259 | -spec where(fun((data()) -> qast:ast_node())) -> qfun(). 260 | where(Fun) -> fun(Q) -> where(Fun, Q) end. 261 | 262 | -spec where(fun((data()) -> qast:ast_node()), Q) -> Q when Q :: query(). 263 | where(Fun, #query{data=Data, where=OldWhere}=Q) -> 264 | Where = call(Fun, [Data]), 265 | NewWhere = 266 | case OldWhere of 267 | undefined -> Where; 268 | _ -> pg_sql:'andalso'(OldWhere, Where) 269 | end, 270 | Q#query{where = NewWhere}. 271 | 272 | 273 | -spec select(Fun) -> qfun() when 274 | Fun :: fun((data()) -> select()) | 275 | fun((select(), data()) -> select()). 276 | select(Fun) -> fun(Q) -> select(Fun, Q) end. 277 | 278 | -spec select(Fun, Q) -> Q when 279 | Fun :: fun((data()) -> select()) | 280 | fun((select(), data()) -> select()), 281 | Q :: query(). 282 | select(Fun, #query{data=Data}=Q) when is_function(Fun, 1) -> 283 | Q#query{select=call(Fun, [Data])}; 284 | select(Fun, #query{select=PrevSelect, data=Data}=Q) when is_function(Fun, 2) -> 285 | Q#query{select=call(Fun, [PrevSelect, Data])}. 286 | 287 | 288 | -spec set(Fun) -> qfun() when 289 | Fun :: fun((data()) -> set()) | 290 | fun((set(), data()) -> set()). 291 | set(Fun) -> fun(Q) -> set(Fun, Q) end. 292 | 293 | -spec set(Fun, Q) -> Q when 294 | Fun :: fun((data()) -> set()) | 295 | fun((set(), data()) -> set()), 296 | Q :: query(). 297 | set(Fun, #query{data=Data}=Q) when is_function(Fun, 1) -> 298 | Set = call(Fun, [Data]), 299 | check_set(Set), 300 | Q#query{set=Set}; 301 | set(Fun, #query{set=PrevSet, data=Data}=Q) when is_function(Fun, 2) -> 302 | Set = call(Fun, [PrevSet, Data]), 303 | check_set(Set), 304 | Q#query{set=Set}. 305 | 306 | check_set(#query{}) -> ok; 307 | check_set(Map) when is_map(Map) -> ok; 308 | check_set(_) -> error(bad_set). 309 | 310 | -spec group_by(fun((data()) -> qast:ast_node())) -> qfun(). 311 | group_by(Fun) -> fun(Q) -> group_by(Fun, Q) end. 312 | 313 | -spec group_by(fun((data()) -> qast:ast_node()), Q) -> Q when Q :: query(). 314 | group_by(Fun, #query{data=Data}=Q) -> 315 | Q#query{group_by=call(Fun, [Data])}. 316 | 317 | -spec on_conflict(conflict_target(), fun((data()) -> conflict_action())) -> qfun(). 318 | on_conflict(ConflictTarget, Fun) -> fun(Q) -> on_conflict(ConflictTarget, Fun, Q) end. 319 | 320 | -spec on_conflict(conflict_target(), fun((data()) -> conflict_action()), Q) -> Q when Q :: query(). 321 | on_conflict(ConflictTarget, Fun, #query{on_conflict=OnConflict, data=Data}=Q) -> 322 | Schema = get(schema, Q), 323 | SchemaFields = maps:get(fields, Schema, #{}), 324 | Table = qast:raw("EXCLUDED"), 325 | Fields = maps:map(fun(N, Opts) -> 326 | qast:exp([Table, qast:raw([".", equery_utils:field_name(N)])], Opts) 327 | end, SchemaFields), 328 | Q#query{on_conflict=maps:put(ConflictTarget, call(Fun, [Data ++ [Fields]]), OnConflict)}. 329 | 330 | -spec order_by(fun((data()) -> order())) -> qfun(). 331 | order_by(Fun) -> fun(Q) -> order_by(Fun, Q) end. 332 | 333 | -spec order_by(fun((data()) -> order()), Q) -> Q when Q :: query(). 334 | order_by(Fun, #query{data=Data}=Q) -> 335 | Q#query{order_by=call(Fun, [Data])}. 336 | 337 | -spec limit(non_neg_integer()) -> qfun(). 338 | limit(Value) -> fun(Q) -> limit(Value, Q) end. 339 | 340 | -spec limit(non_neg_integer(), Q) -> Q when Q :: query(). 341 | limit(Value, Q) -> 342 | Q#query{limit=Value}. 343 | 344 | -spec offset(non_neg_integer()) -> qfun(). 345 | offset(Value) -> fun(Q) -> offset(Value, Q) end. 346 | 347 | -spec offset(non_neg_integer(), Q) -> Q when Q :: query(). 348 | offset(Value, Q) -> 349 | Q#query{offset=Value}. 350 | 351 | -spec lock(row_lock_level()) -> qfun(). 352 | lock(RowLockLevel) -> 353 | lock(RowLockLevel, fun(RealTables) -> RealTables end). 354 | 355 | -spec lock(row_lock_level(), fun(([RealTable]) -> [RealTable])) -> qfun() when 356 | RealTable :: real_table(). 357 | lock(RowLockLevel, Fun) -> 358 | fun(Q) -> lock(RowLockLevel, Fun, Q) end. 359 | 360 | -spec lock(row_lock_level(), fun(([RealTable]) -> [RealTable]), query()) -> query() when 361 | RealTable :: real_table(). 362 | lock(RowLockLevel, Fun, #query{tables = AllTables} = Q) -> 363 | RealTables = [T || {real, _Table, _TRef} = T <- AllTables], 364 | Q#query{lock = {RowLockLevel, Fun(RealTables)}}. 365 | 366 | -spec lookup_tables(model() | [model()], [RealTable]) -> [RealTable] when 367 | RealTable :: real_table(). 368 | %% @THROWS {unknown_table, model()} 369 | lookup_tables(Models, Tables) when is_list(Models) -> 370 | lists:flatmap( 371 | fun(M) -> 372 | TableName = maps:get(table, get_schema(M)), 373 | RealTables = [T || {real, Table, _TRef} = T <- Tables, Table =:= TableName], 374 | case RealTables of 375 | [] -> error({unknown_table, M}); 376 | _ -> RealTables 377 | end 378 | end, 379 | Models); 380 | lookup_tables(Model, Tables) -> 381 | lookup_tables([Model], Tables). 382 | 383 | -spec for_update() -> qfun(). 384 | for_update() -> fun(Q) -> for_update(Q) end. 385 | 386 | -spec for_update(Q) -> Q when Q :: query(). 387 | for_update(Q) -> 388 | lock(for_update, fun(T) -> T end, Q). 389 | 390 | -spec data(fun((data()) -> data())) -> qfun(). 391 | data(Fun) -> fun(Q) -> data(Fun, Q) end. 392 | 393 | -spec data(fun((data()) -> data()), Q) -> Q when Q :: query(). 394 | data(Fun, #query{data=Data}=Q) -> 395 | Data2 = call(Fun, [Data]), 396 | is_list(Data2) orelse error(bad_list), 397 | Q#query{data=Data2}. 398 | 399 | -spec distinct() -> qfun(). 400 | distinct() -> fun(Q) -> distinct(Q) end. 401 | 402 | -spec distinct(Q) -> Q when Q :: query(). 403 | distinct(#query{}=Q) -> Q#query{distinct = all}. 404 | 405 | -spec distinct_on(fun((data()) -> [atom()])) -> qfun(). 406 | distinct_on(Fun) -> fun(Q) -> distinct_on(Fun, Q) end. 407 | 408 | -spec distinct_on(fun((data()) -> [atom()]), Q) -> Q when Q :: query(). 409 | distinct_on(Fun, #query{data=Data}=Q) -> 410 | Distinct = call(Fun, [Data]), 411 | is_list(Distinct) orelse error(bad_list), 412 | Q#query{distinct = Distinct}. 413 | 414 | compile(Fun) -> call(Fun, []). 415 | 416 | %% ============================================================================= 417 | %% Internal functions 418 | %% ============================================================================= 419 | 420 | call(Fun, Args) -> apply(equery_pt:transform_fun(Fun), Args). 421 | 422 | get_schema(Schema) when is_map(Schema) -> Schema; 423 | get_schema(Module) when is_atom(Module) -> (Module:schema())#{model => Module}. 424 | 425 | aliased_fields(TRef, Fields) -> 426 | maps:map(fun(F, Opts) -> 427 | qast:exp([ 428 | qast:alias(TRef), qast:raw([".", equery_utils:field_name(F)]) 429 | ], Opts) 430 | end, Fields). 431 | 432 | braced(QAst) -> 433 | qast:exp([ 434 | qast:raw("("), QAst, qast:raw(")") 435 | ], qast:opts(QAst)). 436 | -------------------------------------------------------------------------------- /test/q_tests.erl: -------------------------------------------------------------------------------- 1 | -module(q_tests). 2 | 3 | -export([schema/0]). 4 | 5 | -include_lib("eunit/include/eunit.hrl"). 6 | -include_lib("equery/include/equery.hrl"). 7 | -include_lib("equery/include/cth.hrl"). 8 | 9 | -define(USER_SCHEMA, #{ 10 | fields => #{ 11 | id => #{ type => integer, index => true, autoincrement => true }, 12 | name => #{type => {varchar, 60}, required => true}, 13 | password => #{type => {varchar, 60}, required => true}, 14 | salt => #{type => {varchar, 24}, required => true} 15 | }, 16 | table => <<"users">>, 17 | links => #{ 18 | comments => {has_many, ?COMMENT_SCHEMA, #{id => author}} 19 | } 20 | }). 21 | -define(USER_FIELDS, maps:get(fields, ?USER_SCHEMA)). 22 | -define(USER_FIELDS(L), maps:with(L, ?USER_FIELDS)). 23 | -define(USER_FIELDS_WITHOUT(L), maps:without(L, ?USER_FIELDS)). 24 | -define(USER_FIELDS_LIST, ?MAPS_TO_LIST(?USER_FIELDS)). 25 | -define(USER_FIELDS_LIST(L), ?MAPS_TO_LIST(?USER_FIELDS(L))). 26 | 27 | -define(COMMENT_SCHEMA, #{ 28 | fields => #{ 29 | id => #{type => serial}, 30 | author => #{type => integer}, 31 | text => #{type => text} 32 | }, 33 | table => <<"comments">>, 34 | links => #{ 35 | author => {belongs_to, ?MODULE, #{author => id}} 36 | } 37 | }). 38 | 39 | -define(TREE_FIELDS, maps:get(fields, tree_m:schema())). 40 | -define(TREE_FIELDS_LIST, ?MAPS_TO_LIST(?TREE_FIELDS)). 41 | 42 | schema() -> ?USER_SCHEMA. 43 | 44 | %% ============================================================================= 45 | %% tests 46 | %% ============================================================================= 47 | 48 | schema_test() -> 49 | ?assertEqual(?USER_SCHEMA, q:get(schema, q:from(?USER_SCHEMA))), 50 | ?assertEqual(maps:put(model, ?MODULE, ?USER_SCHEMA), q:get(schema, q:from(?MODULE))). 51 | 52 | data_test() -> 53 | ?assertEqual( 54 | maps:keys(?USER_FIELDS), 55 | maps:keys(hd(q:get(data, q:from(?USER_SCHEMA))))), 56 | ?assertEqual( 57 | maps:keys(?USER_FIELDS), 58 | maps:keys(hd(q:get(data, q:from(?MODULE))))). 59 | 60 | q_test() -> 61 | {Sql, Args, Feilds} = to_sql( 62 | qsql:select(q:pipe(q:from(?USER_SCHEMA), [ 63 | q:where( 64 | fun([#{name := Name}]) -> 65 | pg_sql:'=:='(Name, <<"test1">>) 66 | end), 67 | q:join(?COMMENT_SCHEMA, 68 | fun([#{id := UserId}, #{author := AuthorId}]) -> 69 | pg_sql:'=:='(UserId, AuthorId) 70 | end), 71 | q:where( 72 | fun([_,#{text := Name}]) -> 73 | pg_sql:'=:='(Name, <<"test2">>) 74 | end), 75 | q:order_by( 76 | fun([#{name := Name, id := Id}|_]) -> 77 | [{Name, asc}, {Id, desc}] 78 | end), 79 | q:for_update() 80 | ]))), 81 | ?assertEqual( 82 | <<"select " 83 | "\"__alias-0\".\"id\" as \"id\"," 84 | "\"__alias-0\".\"name\" as \"name\"," 85 | "\"__alias-0\".\"password\" as \"password\"," 86 | "\"__alias-0\".\"salt\" as \"salt\" " 87 | "from \"users\" as \"__alias-0\" " 88 | "inner join \"comments\" as \"__alias-1\" " 89 | "on (\"__alias-0\".\"id\" = \"__alias-1\".\"author\") " 90 | "where ((\"__alias-0\".\"name\" = $1) and (\"__alias-1\".\"text\" = $2)) " 91 | "order by \"__alias-0\".\"name\" ASC,\"__alias-0\".\"id\" DESC " 92 | "for update of \"__alias-0\"">>, 93 | Sql), 94 | ?assertEqual([<<"test1">>, <<"test2">>], Args), 95 | ?assertEqual({model, undefined, ?USER_FIELDS_LIST}, Feilds). 96 | 97 | q_lock_with_several_tables_test() -> 98 | {Sql, Args, Feilds} = to_sql( 99 | qsql:select(q:pipe(q:from(?USER_SCHEMA), [ 100 | q:where( 101 | fun([#{name := Name}]) -> 102 | pg_sql:'=:='(Name, <<"test1">>) 103 | end), 104 | q:using(?COMMENT_SCHEMA), 105 | q:where( 106 | fun([#{id := UserId}, #{author := AuthorId, text := Name}]) -> 107 | pg_sql:'andalso'( 108 | pg_sql:'=:='(UserId, AuthorId), 109 | pg_sql:'=:='(Name, <<"test2">>)) 110 | end), 111 | q:order_by( 112 | fun([#{name := Name, id := Id}|_]) -> 113 | [{Name, asc}, {Id, desc}] 114 | end), 115 | q:lock(for_update, fun(Tables) -> q:lookup_tables(?COMMENT_SCHEMA, Tables) end) 116 | ]))), 117 | ?assertEqual( 118 | <<"select " 119 | "\"__alias-0\".\"id\" as \"id\"," 120 | "\"__alias-0\".\"name\" as \"name\"," 121 | "\"__alias-0\".\"password\" as \"password\"," 122 | "\"__alias-0\".\"salt\" as \"salt\" " 123 | "from \"users\" as \"__alias-0\"," 124 | "\"comments\" as \"__alias-1\" " 125 | "where ((\"__alias-0\".\"name\" = $1) and ((\"__alias-0\".\"id\" = \"__alias-1\".\"author\") and (\"__alias-1\".\"text\" = $2))) " 126 | "order by \"__alias-0\".\"name\" ASC,\"__alias-0\".\"id\" DESC " 127 | "for update of \"__alias-1\"">>, 128 | Sql), 129 | ?assertEqual([<<"test1">>, <<"test2">>], Args), 130 | ?assertEqual({model, undefined, ?USER_FIELDS_LIST}, Feilds). 131 | 132 | q_lock_lookup_error_test() -> 133 | ?assertException( 134 | error, 135 | {unknown_table, #{table := <<"comments">>}}, 136 | qsql:select(q:pipe(q:from(?USER_SCHEMA), [ 137 | q:where( 138 | fun([#{name := Name}]) -> 139 | pg_sql:'=:='(Name, <<"test1">>) 140 | end), 141 | q:lock(for_update, fun(Tables) -> q:lookup_tables(?COMMENT_SCHEMA, Tables) end) 142 | ]))). 143 | 144 | q_lock_for_no_key_update_test() -> 145 | {Sql, _Args, _Feilds} = to_sql( 146 | qsql:select(q:pipe(q:from(?USER_SCHEMA), [ 147 | q:where( 148 | fun([#{name := Name}]) -> 149 | pg_sql:'=:='(Name, <<"test1">>) 150 | end), 151 | q:lock(for_no_key_update) 152 | ]))), 153 | ?assertEqual( 154 | <<"select " 155 | "\"__alias-0\".\"id\" as \"id\"," 156 | "\"__alias-0\".\"name\" as \"name\"," 157 | "\"__alias-0\".\"password\" as \"password\"," 158 | "\"__alias-0\".\"salt\" as \"salt\" " 159 | "from \"users\" as \"__alias-0\" " 160 | "where (\"__alias-0\".\"name\" = $1) " 161 | "for no key update of \"__alias-0\"">>, 162 | Sql). 163 | 164 | q_lock_for_share_test() -> 165 | {Sql, _Args, _Feilds} = to_sql( 166 | qsql:select(q:pipe(q:from(?USER_SCHEMA), [ 167 | q:where( 168 | fun([#{name := Name}]) -> 169 | pg_sql:'=:='(Name, <<"test1">>) 170 | end), 171 | q:lock(for_share) 172 | ]))), 173 | ?assertEqual( 174 | <<"select " 175 | "\"__alias-0\".\"id\" as \"id\"," 176 | "\"__alias-0\".\"name\" as \"name\"," 177 | "\"__alias-0\".\"password\" as \"password\"," 178 | "\"__alias-0\".\"salt\" as \"salt\" " 179 | "from \"users\" as \"__alias-0\" " 180 | "where (\"__alias-0\".\"name\" = $1) " 181 | "for share of \"__alias-0\"">>, 182 | Sql). 183 | 184 | q_lock_for_key_share_test() -> 185 | {Sql, _Args, _Feilds} = to_sql( 186 | qsql:select(q:pipe(q:from(?USER_SCHEMA), [ 187 | q:where( 188 | fun([#{name := Name}]) -> 189 | pg_sql:'=:='(Name, <<"test1">>) 190 | end), 191 | q:lock(for_key_share) 192 | ]))), 193 | ?assertEqual( 194 | <<"select " 195 | "\"__alias-0\".\"id\" as \"id\"," 196 | "\"__alias-0\".\"name\" as \"name\"," 197 | "\"__alias-0\".\"password\" as \"password\"," 198 | "\"__alias-0\".\"salt\" as \"salt\" " 199 | "from \"users\" as \"__alias-0\" " 200 | "where (\"__alias-0\".\"name\" = $1) " 201 | "for key share of \"__alias-0\"">>, 202 | Sql). 203 | 204 | q_from_query_test() -> 205 | BaseQuery = q:pipe(q:from(?USER_SCHEMA), [ 206 | q:select(fun([#{id := Id, name := Name}]) -> #{num => Id*2, name => Name} end) 207 | ]), 208 | {Sql, Args, Feilds} = to_sql( 209 | qsql:select(q:pipe(q:from(BaseQuery), [ 210 | q:where(fun([#{num := Num}]) -> Num > 10 end), 211 | q:select(fun([#{name := Name, num := N}]) -> #{user => Name, n => N} end) 212 | ]))), 213 | ?assertEqual( 214 | <<"select " 215 | "\"__alias-0\".\"num\" as \"n\"," 216 | "\"__alias-0\".\"name\" as \"user\" " 217 | "from (" 218 | "select " 219 | "\"__alias-1\".\"name\" as \"name\"," 220 | "(\"__alias-1\".\"id\" * $1) as \"num\" " 221 | "from \"users\" as \"__alias-1\"" 222 | ") as \"__alias-0\" " 223 | "where (\"__alias-0\".\"num\" > $2)">>, 224 | Sql), 225 | ?assertEqual([2,10], Args), 226 | ?assertEqual({model, undefined, lists:sort([ 227 | {n,#{}}, 228 | {user,#{required => true, type => {varchar, 60}}} 229 | ])}, Feilds). 230 | 231 | q_compile_test() -> 232 | WhereFun = q:compile(fun() -> 233 | fun([#{name := Name, filter := F}]) -> 234 | Name =:= <<"test2">> orelse F 235 | end 236 | end), 237 | {Sql, Args, Feilds} = to_sql( 238 | qsql:select(q:pipe(q:from(?MODULE), [ 239 | q:data( 240 | fun([#{name := Name}=TD]) -> 241 | [TD#{filter => Name =:= <<"test1">>}] 242 | end), 243 | q:where(WhereFun), 244 | q:join(?COMMENT_SCHEMA, 245 | fun([#{id := UserId}, #{author := AuthorId}]) -> 246 | UserId =:= AuthorId 247 | end), 248 | q:where( 249 | fun([_,#{text := Name}]) -> 250 | Name =:= <<"test2">> 251 | end), 252 | q:select( 253 | fun([#{id := Id}=U|_]) -> 254 | U#{'_id_gt' => Id > 3} 255 | end) 256 | ]))), 257 | ?assertEqual( 258 | <<"select " 259 | "(\"__alias-0\".\"id\" > $1) as \"_id_gt\"," 260 | "(\"__alias-0\".\"name\" = $2) as \"filter\"," 261 | "\"__alias-0\".\"id\" as \"id\"," 262 | "\"__alias-0\".\"name\" as \"name\"," 263 | "\"__alias-0\".\"password\" as \"password\"," 264 | "\"__alias-0\".\"salt\" as \"salt\" " 265 | "from \"users\" as \"__alias-0\" " 266 | "inner join \"comments\" as \"__alias-1\" on " 267 | "(\"__alias-0\".\"id\" = \"__alias-1\".\"author\") where " 268 | "(((\"__alias-0\".\"name\" = $3) or " 269 | "(\"__alias-0\".\"name\" = $4)) and " 270 | "(\"__alias-1\".\"text\" = $5))">>, 271 | Sql), 272 | ?assertEqual([3,<<"test1">>,<<"test2">>,<<"test1">>,<<"test2">>], Args), 273 | ?assertEqual({model, ?MODULE, lists:sort([ 274 | {'_id_gt',#{type => boolean}}, 275 | {filter,#{type => boolean}} 276 | | ?USER_FIELDS_LIST 277 | ])}, Feilds). 278 | 279 | q_insert_test() -> 280 | {Sql, _Args, ReturningFields} = to_sql( 281 | qsql:insert(q:set(fun(_) -> ?USER_FIELDS_WITHOUT([id]) end, q:from(?MODULE)))), 282 | ?assertEqual( 283 | <<"insert into \"users\" as \"__alias-0\" (\"name\",\"password\",\"salt\") " 284 | "values ($1,$2,$3) " 285 | "returning " 286 | "\"__alias-0\".\"id\" as \"id\"," 287 | "\"__alias-0\".\"name\" as \"name\"," 288 | "\"__alias-0\".\"password\" as \"password\"," 289 | "\"__alias-0\".\"salt\" as \"salt\"">>, 290 | Sql), 291 | ?assertEqual({model, ?MODULE, ?USER_FIELDS_LIST}, ReturningFields). 292 | 293 | q_insert_from_test() -> 294 | DeleteQ = qsql:delete(q:where(fun([#{id := Id}]) -> Id =:= 1 end, q:from(?MODULE))), 295 | {Sql, Args, ReturningFields} = to_sql( 296 | qsql:insert(q:with(DeleteQ, fun(Alias) -> 297 | q:set(fun(_) -> 298 | q:select(fun(S, _) -> 299 | S#{id => qast:raw(<<"15">>)} 300 | end, q:from(Alias)) 301 | end) 302 | end, q:from(?MODULE))) 303 | ), 304 | ?assertEqual( 305 | <<"with \"__alias-0\" as (" 306 | "delete from \"users\" as \"__alias-1\" " 307 | "where (\"__alias-1\".\"id\" = $1) " 308 | "returning " 309 | "\"__alias-1\".\"id\" as \"id\"," 310 | "\"__alias-1\".\"name\" as \"name\"," 311 | "\"__alias-1\".\"password\" as \"password\"," 312 | "\"__alias-1\".\"salt\" as \"salt\") " 313 | "insert into \"users\" as \"__alias-2\" (" 314 | "\"id\",\"name\",\"password\",\"salt\"" 315 | ") select " 316 | "15 as \"id\"," 317 | "\"__alias-0\"." 318 | "\"name\" as \"name\"," 319 | "\"__alias-0\".\"password\" as \"password\"," 320 | "\"__alias-0\".\"salt\" as \"salt\" " 321 | "from \"__alias-0\" " 322 | "returning " 323 | "\"__alias-2\".\"id\" as \"id\"," 324 | "\"__alias-2\".\"name\" as \"name\"," 325 | "\"__alias-2\".\"password\" as \"password\"," 326 | "\"__alias-2\".\"salt\" as \"salt\"">>, 327 | Sql), 328 | ?assertEqual([1], Args), 329 | ?assertEqual({model, ?MODULE, ?USER_FIELDS_LIST}, ReturningFields). 330 | 331 | q_upsert_test() -> 332 | {Sql, Args, ReturningFields} = to_sql( 333 | qsql:insert(q:pipe(q:from(?MODULE), [ 334 | q:set(fun(_) -> ?USER_FIELDS end), 335 | q:on_conflict([id], fun([_, #{id := Id}=Excluded]) -> Excluded#{id => Id + 1} end), 336 | q:on_conflict(any, fun(_) -> nothing end) 337 | ]))), 338 | ?assertEqual( 339 | <<"insert into \"users\" as \"__alias-0\" (\"id\",\"name\",\"password\",\"salt\") " 340 | "values ($1,$2,$3,$4) " 341 | "on conflict (\"id\") do update set " 342 | "\"id\" = (EXCLUDED.\"id\" + $5)," 343 | "\"name\" = EXCLUDED.\"name\"," 344 | "\"password\" = EXCLUDED.\"password\"," 345 | "\"salt\" = EXCLUDED.\"salt\" " 346 | "on conflict do nothing " 347 | "returning " 348 | "\"__alias-0\".\"id\" as \"id\"," 349 | "\"__alias-0\".\"name\" as \"name\"," 350 | "\"__alias-0\".\"password\" as \"password\"," 351 | "\"__alias-0\".\"salt\" as \"salt\"">>, 352 | Sql), 353 | ?assertEqual(1, lists:nth(5, Args)), 354 | ?assertEqual({model, ?MODULE, ?USER_FIELDS_LIST}, ReturningFields). 355 | 356 | q_with_test() -> 357 | {Sql, Args, ReturningFields} = to_sql( 358 | qsql:select(q:pipe(q:from(?MODULE), [ 359 | q:with(?COMMENT_SCHEMA, fun(Comments) -> 360 | q:using(Comments) 361 | end) 362 | ]))), 363 | ?assertEqual( 364 | <<"with \"__alias-0\" as (", 365 | "select ", 366 | "\"__alias-1\".\"author\" as \"author\",", 367 | "\"__alias-1\".\"id\" as \"id\",", 368 | "\"__alias-1\".\"text\" as \"text\" ", 369 | "from \"comments\" as \"__alias-1\"", 370 | ") select ", 371 | "\"__alias-2\".\"id\" as \"id\",", 372 | "\"__alias-2\".\"name\" as \"name\",", 373 | "\"__alias-2\".\"password\" as \"password\",", 374 | "\"__alias-2\".\"salt\" as \"salt\" ", 375 | "from \"users\" as \"__alias-2\",\"__alias-0\"">>, 376 | Sql), 377 | ?assertEqual([], Args), 378 | ?assertEqual({model, ?MODULE, ?USER_FIELDS_LIST}, ReturningFields). 379 | 380 | q_with_ast_test() -> 381 | {Sql, Args, ReturningFields} = to_sql( 382 | qsql:select(q:pipe(q:from(?MODULE), [ 383 | q:with( 384 | qsql:update(q:pipe(q:from(?MODULE), [ 385 | q:set(fun(_) -> #{name => <<"Sam">>} end), 386 | q:where(fun([#{id := Id}]) -> Id =:= 3 end) 387 | ])), 388 | fun(Updated) -> 389 | q:join(Updated, fun([#{id := OldId}, #{id := NewId}]) -> 390 | OldId =:= NewId 391 | end) 392 | end) 393 | ]))), 394 | ?assertEqual( 395 | <<"with \"__alias-0\" as (", 396 | "update \"users\" as \"__alias-1\" set ", 397 | "\"name\" = $1 ", 398 | "where (\"__alias-1\".\"id\" = $2) ", 399 | "returning ", 400 | "\"__alias-1\".\"id\" as \"id\",", 401 | "\"__alias-1\".\"name\" as \"name\",", 402 | "\"__alias-1\".\"password\" as \"password\",", 403 | "\"__alias-1\".\"salt\" as \"salt\"", 404 | ") select ", 405 | "\"__alias-2\".\"id\" as \"id\",", 406 | "\"__alias-2\".\"name\" as \"name\",", 407 | "\"__alias-2\".\"password\" as \"password\",", 408 | "\"__alias-2\".\"salt\" as \"salt\" ", 409 | "from \"users\" as \"__alias-2\" ", 410 | "inner join \"__alias-0\" ", 411 | "on (\"__alias-2\".\"id\" = \"__alias-0\".\"id\")">>, 412 | Sql), 413 | ?assertEqual([<<"Sam">>, 3], Args), 414 | ?assertEqual({model, ?MODULE, ?USER_FIELDS_LIST}, ReturningFields). 415 | 416 | 417 | q_update_test() -> 418 | {Sql, Args, ReturningFields} = to_sql( 419 | qsql:update(q:pipe(q:from(?MODULE), [ 420 | q:set(fun(_) -> #{name => <<"Sam">>} end), 421 | q:set(fun(Set, _) -> Set#{password => <<"pass">>} end), 422 | q:where(fun([#{id := Id}]) -> Id =:= 3 end), 423 | q:select(fun(_) -> #{} end) %% return nothing 424 | ]))), 425 | ?assertEqual( 426 | <<"update \"users\" as \"__alias-0\" set " 427 | "\"name\" = $1," 428 | "\"password\" = $2 " 429 | "where (\"__alias-0\".\"id\" = $3)">>, 430 | Sql), 431 | ?assertEqual([<<"Sam">>, <<"pass">>, 3], Args), 432 | ?assertEqual({model, ?MODULE, []}, ReturningFields). 433 | 434 | q_update_using_test() -> 435 | {Sql, Args, ReturningFields} = to_sql( 436 | qsql:update(q:pipe(q:from(?MODULE), [ 437 | q:set(fun(_) -> #{name => <<"Sam">>} end), 438 | q:set(fun(Set, _) -> Set#{password => <<"pass">>} end), 439 | q:where(fun([#{id := Id}]) -> Id =:= 3 end), 440 | q:using(?COMMENT_SCHEMA), 441 | q:select(fun(_) -> #{} end) %% return nothing 442 | ]))), 443 | ?assertEqual( 444 | <<"update \"users\" as \"__alias-0\" set " 445 | "\"name\" = $1," 446 | "\"password\" = $2 " 447 | "from \"comments\" as \"__alias-1\" " 448 | "where (\"__alias-0\".\"id\" = $3)">>, 449 | Sql), 450 | ?assertEqual([<<"Sam">>, <<"pass">>, 3], Args), 451 | ?assertEqual({model, ?MODULE, []}, ReturningFields). 452 | 453 | q_delete_test() -> 454 | {Sql, Args, ReturningFields} = to_sql( 455 | qsql:delete(q:pipe(q:from(?MODULE), [ 456 | q:where(fun([#{id := Id1}]) -> Id1 =:= 3 end) 457 | ]))), 458 | ?assertEqual( 459 | <<"delete from \"users\" as \"__alias-0\" " 460 | "where (\"__alias-0\".\"id\" = $1) " 461 | "returning " 462 | "\"__alias-0\".\"id\" as \"id\"," 463 | "\"__alias-0\".\"name\" as \"name\"," 464 | "\"__alias-0\".\"password\" as \"password\"," 465 | "\"__alias-0\".\"salt\" as \"salt\"">>, 466 | Sql), 467 | ?assertEqual([3], Args), 468 | ?assertEqual({model, ?MODULE, ?USER_FIELDS_LIST}, ReturningFields). 469 | 470 | q_delete_using_test() -> 471 | {Sql, Args, ReturningFields} = to_sql( 472 | qsql:delete(q:pipe(q:from(?MODULE), [ 473 | q:using(?MODULE), 474 | q:where(fun([#{id := Id1}, #{id := Id2}]) -> 475 | Id1 =:= Id2 andalso Id1 =:= 3 476 | end) 477 | ]))), 478 | ?assertEqual( 479 | <<"delete from \"users\" as \"__alias-0\" " 480 | "using \"users\" as \"__alias-1\" " 481 | "where ((\"__alias-0\".\"id\" = \"__alias-1\".\"id\") and (\"__alias-0\".\"id\" = $1)) " 482 | "returning " 483 | "\"__alias-0\".\"id\" as \"id\"," 484 | "\"__alias-0\".\"name\" as \"name\"," 485 | "\"__alias-0\".\"password\" as \"password\"," 486 | "\"__alias-0\".\"salt\" as \"salt\"">>, 487 | Sql), 488 | ?assertEqual([3], Args), 489 | ?assertEqual({model, ?MODULE, ?USER_FIELDS_LIST}, ReturningFields). 490 | 491 | q_data_test() -> 492 | {Sql, Args, ReturningFields} = to_sql( 493 | qsql:select(q:pipe(q:from(?USER_SCHEMA), [ 494 | q:data(fun([Tab]) -> [Tab#{f => 1}] end), 495 | q:select(fun([#{f := F}]) -> F end) 496 | ]))), 497 | ?assertEqual( 498 | <<"select $1 from \"users\" as \"__alias-0\"">>, 499 | Sql), 500 | ?assertEqual([1], Args), 501 | ?assertEqual(undefined, ReturningFields). 502 | 503 | 504 | q_group_by_test() -> 505 | {Sql, Args, Feilds} = to_sql( 506 | qsql:select(q:pipe(q:from(?MODULE), [ 507 | q:where( 508 | fun([#{name := Name}]) -> 509 | pg_sql:'=:='(Name, <<"test1">>) 510 | end), 511 | q:join(?COMMENT_SCHEMA, 512 | fun([#{id := UserId}, #{author := AuthorId}]) -> 513 | pg_sql:'=:='(UserId, AuthorId) 514 | end), 515 | q:where( 516 | fun([_,#{text := Name}]) -> 517 | pg_sql:'=:='(Name, <<"test2">>) 518 | end), 519 | q:group_by( 520 | fun([_, #{author := AuthorId}]) -> 521 | [AuthorId] 522 | end), 523 | q:select( 524 | fun([#{id := Id}|_]) -> 525 | #{cnt => pg_sql:count(Id)} 526 | end) 527 | ]))), 528 | ?assertEqual( 529 | <<"select " 530 | "count(\"__alias-0\".\"id\") as \"cnt\" " 531 | "from \"users\" as \"__alias-0\" " 532 | "inner join \"comments\" as \"__alias-1\" " 533 | "on (\"__alias-0\".\"id\" = \"__alias-1\".\"author\") " 534 | "where ((\"__alias-0\".\"name\" = $1) and (\"__alias-1\".\"text\" = $2)) " 535 | "group by \"__alias-1\".\"author\"">>, 536 | Sql), 537 | ?assertEqual([<<"test1">>, <<"test2">>], Args), 538 | ?assertEqual({model, ?MODULE, [{cnt, #{type => integer}}]}, Feilds). 539 | 540 | limit_offset_test() -> 541 | {Sql, Args, Feilds} = to_sql( 542 | qsql:select(q:pipe(q:from(?MODULE), [ 543 | q:limit(10), 544 | q:offset(3) 545 | ]))), 546 | ?assertEqual( 547 | <<"select " 548 | "\"__alias-0\".\"id\" as \"id\"," 549 | "\"__alias-0\".\"name\" as \"name\"," 550 | "\"__alias-0\".\"password\" as \"password\"," 551 | "\"__alias-0\".\"salt\" as \"salt\" " 552 | "from \"users\" as \"__alias-0\" " 553 | "limit $1 " 554 | "offset $2">>, Sql), 555 | ?assertEqual([10, 3], Args), 556 | ?assertEqual({model, ?MODULE, ?USER_FIELDS_LIST}, Feilds). 557 | 558 | complex_test() -> 559 | {Sql, Args, Feilds} = to_sql( 560 | qsql:select(q:pipe(q:from(?MODULE), [ 561 | q:where(fun([#{name := Name}]) -> Name =:= <<"user">> end), 562 | q:join( 563 | q:pipe(q:from(?COMMENT_SCHEMA), [ 564 | q:group_by(fun([#{author := Author}]) -> [Author] end), 565 | q:select(fun([#{author := Author}]) -> 566 | #{author => Author, comments_cnt => pg_sql:count(Author) } 567 | end) 568 | ]), 569 | fun([#{id := Id}, #{author := AuthorId}]) -> Id =:= AuthorId end), 570 | q:select(fun([#{name := Name}, #{comments_cnt := Cnt}]) -> 571 | #{name => Name, comments_cnt => Cnt} 572 | end) 573 | ]))), 574 | ?assertEqual( 575 | <<"select " 576 | "\"__alias-0\".\"comments_cnt\" as \"comments_cnt\"," 577 | "\"__alias-1\".\"name\" as \"name\" " 578 | "from \"users\" as \"__alias-1\" " 579 | "inner join (" 580 | "select " 581 | "\"__alias-2\".\"author\" as \"author\"," 582 | "count(\"__alias-2\".\"author\") as \"comments_cnt\" " 583 | "from \"comments\" as \"__alias-2\" " 584 | "group by \"__alias-2\".\"author\"" 585 | ") as \"__alias-0\" " 586 | "on (\"__alias-1\".\"id\" = \"__alias-0\".\"author\") " 587 | "where (\"__alias-1\".\"name\" = $1)">>, 588 | Sql), 589 | ?assertEqual([<<"user">>], Args), 590 | ?assertEqual({model, ?MODULE, [{comments_cnt, #{type => integer}}|?USER_FIELDS_LIST([name])]}, Feilds). 591 | 592 | single_item_select_test() -> 593 | {Sql, Args, Type} = to_sql( 594 | qsql:select(q:pipe(q:from(?USER_SCHEMA), [ 595 | q:select(fun([#{id := Id}]) -> Id end) 596 | ]))), 597 | ?assertEqual( 598 | <<"select \"__alias-0\".\"id\" from \"users\" as \"__alias-0\"">>, 599 | Sql), 600 | ?assertEqual([], Args), 601 | [{id, #{type := RType}}] = ?USER_FIELDS_LIST([id]), 602 | ?assertEqual(RType, Type). 603 | 604 | update_select_test() -> 605 | {Sql, Args, Feilds} = to_sql( 606 | qsql:select(q:pipe(q:from(?MODULE), [ 607 | q:select(fun([#{id := Id}]) -> #{id => Id} end), 608 | q:select(fun(S, [#{name := Name}]) -> S#{name => Name} end) 609 | ]))), 610 | ?assertEqual( 611 | <<"select " 612 | "\"__alias-0\".\"id\" as \"id\"," 613 | "\"__alias-0\".\"name\" as \"name\" " 614 | "from \"users\" as \"__alias-0\"">>, 615 | Sql), 616 | ?assertEqual([], Args), 617 | ?assertEqual({model, ?MODULE, ?USER_FIELDS_LIST([id,name])}, Feilds). 618 | 619 | operators_test() -> 620 | {Sql, Args, Feilds} = to_sql( 621 | qsql:select(q:pipe(q:from(?MODULE), [ 622 | q:where(fun([#{id := Id}]) -> 623 | Id < 3 andalso 624 | Id =< 4 orelse 625 | Id > 5 andalso 626 | Id >= 6 orelse 627 | Id =/= 7 andalso 628 | not Id * 1 + 2 - 3 / 4 orelse 629 | pg_sql:in(Id, [8,9,10]) orelse 630 | pg_sql:in(Id, [11]) orelse 631 | pg_sql:like(Id, <<"11%">>) orelse 632 | pg_sql:ilike(Id, <<"11%">>) orelse 633 | pg_sql:'~'(Id, <<"a">>) orelse 634 | pg_sql:'~*'(Id, <<"A">>) 635 | end), 636 | q:select(fun([T]) -> maps:with([name], T) end) 637 | ]))), 638 | ?assertEqual( 639 | <<"select \"__alias-0\".\"name\" as \"name\" from \"users\" as \"__alias-0\" where " 640 | "(((\"__alias-0\".\"id\" < $1) and " 641 | "(\"__alias-0\".\"id\" <= $2)) or " 642 | "(((\"__alias-0\".\"id\" > $3) and " 643 | "(\"__alias-0\".\"id\" >= $4)) or " 644 | "((not (\"__alias-0\".\"id\" = $5) and " 645 | "(((not \"__alias-0\".\"id\" * $6) + $7) - ($8 / $9))) or " 646 | "(\"__alias-0\".\"id\" = ANY($10) or " 647 | "((\"__alias-0\".\"id\" = $11) or " 648 | "(\"__alias-0\".\"id\" like $12 or " 649 | "(\"__alias-0\".\"id\" ilike $13 or " 650 | "((\"__alias-0\".\"id\" ~ $14) or " 651 | "(\"__alias-0\".\"id\" ~* $15)))))))))">>, 652 | Sql), 653 | ?assertEqual([3,4,5,6,7,1,2,3,4,[8,9,10],11,<<"11%">>,<<"11%">>,<<"a">>,<<"A">>], Args), 654 | ?assertEqual({model, ?MODULE, ?USER_FIELDS_LIST([name])}, Feilds). 655 | 656 | distinct_operation_test() -> 657 | {Sql, Args, Feilds} = to_sql( 658 | qsql:select(q:pipe(q:from(?MODULE), [ 659 | q:group_by(fun([#{name := Name}]) -> [Name] end), 660 | q:select(fun([#{id := Id}]) -> 661 | pg_sql:distinct(Id) 662 | end) 663 | ]))), 664 | ?assertEqual( 665 | <<"select distinct (\"__alias-0\".\"id\") from \"users\" as \"__alias-0\" " 666 | "group by \"__alias-0\".\"name\"">>, 667 | Sql), 668 | ?assertEqual([], Args), 669 | ?assertEqual(integer, Feilds). 670 | 671 | array_agg_operation_test() -> 672 | {Sql, Args, Feilds} = to_sql( 673 | qsql:select(q:pipe(q:from(?MODULE), [ 674 | q:group_by(fun([#{name := Name}]) -> [Name] end), 675 | q:select(fun([#{id := Id}]) -> 676 | pg_sql:array_agg(Id) 677 | end) 678 | ]))), 679 | ?assertEqual( 680 | <<"select array_agg(\"__alias-0\".\"id\") from \"users\" as \"__alias-0\" " 681 | "group by \"__alias-0\".\"name\"">>, 682 | Sql), 683 | ?assertEqual([], Args), 684 | ?assertEqual({array, integer}, Feilds). 685 | 686 | andalso_op_test() -> 687 | Node = qast:raw("a"), 688 | ?assertEqual(Node, pg_sql:'andalso'(true, Node)), 689 | ?assertEqual(Node, pg_sql:'andalso'(Node, true)), 690 | ?assertEqual(false, pg_sql:'andalso'(false, Node)), 691 | ?assertEqual(false, pg_sql:'andalso'(Node, false)), 692 | ?assertEqual( 693 | {<<"(a and a)">>, []}, 694 | qast:to_sql(pg_sql:'andalso'(Node, Node))). 695 | 696 | orelse_op_test() -> 697 | Node = qast:raw("b"), 698 | ?assertEqual(true, pg_sql:'orelse'(true, Node)), 699 | ?assertEqual(true, pg_sql:'orelse'(Node, true)), 700 | ?assertEqual(Node, pg_sql:'orelse'(false, Node)), 701 | ?assertEqual(Node, pg_sql:'orelse'(Node, false)), 702 | ?assertEqual( 703 | {<<"(b or b)">>, []}, 704 | qast:to_sql(pg_sql:'orelse'(Node, Node))). 705 | 706 | not_op_test() -> 707 | Node = qast:raw("c"), 708 | ?assertEqual(false, pg_sql:'not'(true)), 709 | ?assertEqual(true, pg_sql:'not'(false)), 710 | ?assertEqual( 711 | {<<"not c">>, []}, 712 | qast:to_sql(pg_sql:'not'(Node))). 713 | 714 | is_null_test() -> 715 | Node = qast:raw("d"), 716 | ?assertEqual( 717 | {<<"d is null">>, []}, 718 | qast:to_sql(pg_sql:'is_null'(Node))). 719 | 720 | coalesce_test() -> 721 | Node1 = qast:raw("e"), 722 | Node2 = qast:raw("f"), 723 | Node3 = qast:raw("g"), 724 | ?assertEqual( 725 | {<<"coalesce(e,f,g)">>, []}, 726 | qast:to_sql(pg_sql:'coalesce'([Node1,Node2,Node3]))). 727 | 728 | aggs_test_() -> 729 | Tests = [ 730 | {fun pg_sql:max/1, "max"}, 731 | {fun pg_sql:min/1, "min"}, 732 | {fun pg_sql:count/1, "count"}, 733 | {fun pg_sql:sum/1, "sum"} 734 | ], 735 | [{R, fun() -> 736 | ?assertEqual( 737 | {iolist_to_binary( 738 | ["select ",R,"(\"__alias-0\".\"id\") from \"users\" as \"__alias-0\""]), 739 | []}, 740 | qast:to_sql( 741 | qsql:select(q:pipe(q:from(?USER_SCHEMA), [ 742 | q:select(fun([#{id := Id}]) -> F(Id) end) 743 | ])))) 744 | end} || {F, R} <- Tests]. 745 | 746 | abs_test() -> 747 | Node = qast:raw("h"), 748 | ?assertEqual( 749 | {<<"abs(h)">>, []}, 750 | qast:to_sql(pg_sql:'abs'(Node))). 751 | 752 | trunc_test() -> 753 | Node = qast:raw("v"), 754 | ?assertEqual( 755 | {<<"trunc(v,2)">>, []}, 756 | qast:to_sql(pg_sql:'trunc'(Node, qast:raw("2")))), 757 | ?assertEqual( 758 | {<<"trunc(v,$1)">>, [2]}, 759 | qast:to_sql(pg_sql:'trunc'(Node, 2))). 760 | 761 | ops_test_() -> 762 | Tests = [ 763 | {fun pg_sql:max/2, "GREATEST"}, 764 | {fun pg_sql:min/2, "LEAST"} 765 | ], 766 | [{R, fun() -> 767 | ?assertEqual( 768 | {iolist_to_binary( 769 | ["select ",R,"(\"__alias-0\".\"id\",$1) from \"users\" as \"__alias-0\""]), 770 | [3]}, 771 | qast:to_sql( 772 | qsql:select(q:pipe(q:from(?USER_SCHEMA), [ 773 | q:select(fun([#{id := Id}]) -> F(Id, 3) end) 774 | ])))) 775 | end} || {F, R} <- Tests]. 776 | 777 | row_test() -> 778 | {Sql, Args, ReturningFields} = to_sql( 779 | qsql:select(q:pipe(q:from(?MODULE), [ 780 | q:select(fun([Tab]) -> pg_sql:row(Tab) end) 781 | ]))), 782 | ?assertEqual( 783 | <<"select row(" 784 | "\"__alias-0\".\"id\"," 785 | "\"__alias-0\".\"name\"," 786 | "\"__alias-0\".\"password\"," 787 | "\"__alias-0\".\"salt\"" 788 | ") from \"users\" as \"__alias-0\"">>, 789 | Sql), 790 | ?assertEqual([], Args), 791 | ?assertEqual({record, {model, undefined, ?USER_FIELDS_LIST}}, ReturningFields). 792 | 793 | in_test() -> 794 | {Sql, Args, Feilds} = to_sql( 795 | qsql:select(q:pipe(q:from(?MODULE), [ 796 | q:where(fun([#{id := Id}]) -> 797 | pg_sql:in(Id, q:pipe(q:from(?MODULE), [ 798 | q:select(fun([#{id := IId}]) -> pg_sql:max(IId) end) 799 | ])) 800 | end), 801 | q:select(fun([T]) -> maps:with([name], T) end) 802 | ]))), 803 | ?assertEqual( 804 | <<"select \"__alias-0\".\"name\" as \"name\" from \"users\" as \"__alias-0\" where " 805 | "\"__alias-0\".\"id\" in (" 806 | "select max(\"__alias-1\".\"id\") from \"users\" as \"__alias-1\"" 807 | ")">>, 808 | Sql), 809 | ?assertEqual([], Args), 810 | ?assertEqual({model, ?MODULE, ?USER_FIELDS_LIST([name])}, Feilds). 811 | 812 | in_query_test() -> 813 | {Sql, Args, ReturningFields} = to_sql( 814 | qsql:select(q:pipe(q:from(?MODULE), [ 815 | q:where(fun([#{id := Id}]) -> pg_sql:in(Id, [1,2]) end) 816 | ]))), 817 | ?assertEqual( 818 | <<"select " 819 | "\"__alias-0\".\"id\" as \"id\"," 820 | "\"__alias-0\".\"name\" as \"name\"," 821 | "\"__alias-0\".\"password\" as \"password\"," 822 | "\"__alias-0\".\"salt\" as \"salt\"" 823 | " from \"users\" as \"__alias-0\"" 824 | " where \"__alias-0\".\"id\" = ANY($1)">>, 825 | Sql), 826 | ?assertEqual([[1,2]], Args), 827 | ?assertEqual({model, ?MODULE, ?USER_FIELDS_LIST}, ReturningFields). 828 | 829 | exists_test() -> 830 | {Sql, Args, Feilds} = to_sql( 831 | qsql:select(q:pipe(q:from(?MODULE), [ 832 | q:where(fun([#{id := Id}]) -> 833 | pg_sql:exists(q:where( 834 | fun([#{author := AuthorId}]) -> 835 | AuthorId =:= Id 836 | end, 837 | q:pipe(q:from(?COMMENT_SCHEMA), [ 838 | q:select(fun(_) -> 839 | qast:raw(<<"1">>) 840 | end) 841 | ]) 842 | )) 843 | end), 844 | q:select(fun([T]) -> maps:with([name], T) end) 845 | ]))), 846 | ?assertEqual( 847 | <<"select \"__alias-0\".\"name\" as \"name\" from \"users\" as \"__alias-0\" where " 848 | "exists (" 849 | "select 1 from \"comments\" as \"__alias-1\" where " 850 | "(\"__alias-1\".\"author\" = \"__alias-0\".\"id\")" 851 | ")">>, 852 | Sql), 853 | ?assertEqual([], Args), 854 | ?assertEqual({model, ?MODULE, ?USER_FIELDS_LIST([name])}, Feilds). 855 | 856 | '@>_test'() -> 857 | {Sql, Args, ReturningFields} = to_sql( 858 | qsql:select(q:pipe(q:from(?MODULE), [ 859 | q:where(fun([#{id := Id}]) -> pg_sql:'@>'(Id, [1,2]) end) 860 | ]))), 861 | ?assertEqual( 862 | <<"select " 863 | "\"__alias-0\".\"id\" as \"id\"," 864 | "\"__alias-0\".\"name\" as \"name\"," 865 | "\"__alias-0\".\"password\" as \"password\"," 866 | "\"__alias-0\".\"salt\" as \"salt\"" 867 | " from \"users\" as \"__alias-0\"" 868 | " where \"__alias-0\".\"id\" @> $1">>, 869 | Sql), 870 | ?assertEqual([[1,2]], Args), 871 | ?assertEqual({model, ?MODULE, ?USER_FIELDS_LIST}, ReturningFields). 872 | 873 | pt_test() -> 874 | Path = "./test/test_m.tpl", 875 | {ok, Bin} = file:read_file(Path), 876 | {ok, M, ModuleBin} = compile_module_str(binary_to_list(Bin)), 877 | {module, M} = code:load_binary(M, "", ModuleBin), 878 | Q = q:from(M), 879 | ?assertEqual( 880 | {<<"select \"__alias-0\".\"id\" as \"id\" from \"test\" as \"__alias-0\" where " 881 | "(\"__alias-0\".\"id\" > $1)">>, 882 | [3]}, 883 | qast:to_sql(qsql:select(M:filter(3, Q)))), 884 | ?assertEqual( 885 | {<<"select \"__alias-0\".\"id\" as \"id\" from \"test\" as \"__alias-0\" where " 886 | "(\"__alias-0\".\"id\" = $1)">>, 887 | [3]}, 888 | qast:to_sql(qsql:select(M:filter(3, q:data(fun(D) -> D ++ D end, Q))))). 889 | 890 | transform_fun_test() -> 891 | FunS = "fun(A) -> not (A =:= 2) end.", 892 | Fun = compile_fun_str(FunS), 893 | false = Fun(2), 894 | true = Fun(3), 895 | TFun = equery_pt:transform_fun(Fun), 896 | {Sql, Args} = qast:to_sql(TFun(3)), 897 | ?assertEqual(<<"not ($1 = $2)">>, Sql), 898 | ?assertEqual([3,2], Args), 899 | 900 | %% check some erl_eval functions 901 | EvalStr = equery_pt:transform_fun(fun erl_eval:eval_str/1), 902 | {ok, TFunR} = EvalStr(FunS ++ "\r\n"), 903 | {SqlR, ArgsR} = qast:to_sql((equery_pt:transform_fun(TFunR))(3)), 904 | ?assertEqual(<<"not ($1 = $2)">>, SqlR), 905 | ?assertEqual([3,2], ArgsR). 906 | 907 | join_type_test_() -> 908 | Q = q:from(?USER_SCHEMA), 909 | JFun = fun([#{id := UserId}, #{author := AuthorId}]) -> 910 | pg_sql:'=:='(UserId, AuthorId) 911 | end, 912 | 913 | lists:map(fun({T, Exp}) -> 914 | F = fun() -> 915 | {Sql, []} = qast:to_sql(qsql:select(q:pipe(Q, [ 916 | q:join(T, ?COMMENT_SCHEMA, JFun), 917 | q:select(fun([#{id := UId}, #{id := CId}]) -> 918 | #{uid => UId, cid => CId} 919 | end) 920 | ]))), 921 | ?assertEqual( 922 | <<"select " 923 | "\"__alias-0\".\"id\" as \"cid\"," 924 | "\"__alias-1\".\"id\" as \"uid\" " 925 | "from \"users\" as \"__alias-1\" ", 926 | Exp/binary, " join \"comments\" as \"__alias-0\" on " 927 | "(\"__alias-1\".\"id\" = \"__alias-0\".\"author\")">>, Sql) 928 | end, 929 | {Exp, F} 930 | end, 931 | [ 932 | {inner, <<"inner">>}, 933 | {left, <<"left">>}, 934 | {right, <<"right">>}, 935 | {full, <<"full">>}, 936 | {{left, outer}, <<"left outer">>}, 937 | {{right, outer}, <<"right outer">>}, 938 | {{full, outer}, <<"full outer">>} 939 | ]). 940 | 941 | 942 | as_test() -> 943 | {Sql, Args, Type} = to_sql( 944 | qsql:select(q:pipe(q:from(?USER_SCHEMA), [ 945 | q:select(fun([#{id := Id}]) -> pg_sql:as(Id, text) end) 946 | ]))), 947 | ?assertEqual( 948 | <<"select (\"__alias-0\".\"id\")::text from \"users\" as \"__alias-0\"">>, 949 | Sql), 950 | ?assertEqual([], Args), 951 | ?assertEqual(text, Type). 952 | 953 | set_type_test() -> 954 | {Sql, Args, Type} = to_sql( 955 | qsql:select(q:pipe(q:from(?USER_SCHEMA), [ 956 | q:select(fun([#{id := Id}]) -> pg_sql:set_type(Id, text) end) 957 | ]))), 958 | ?assertEqual( 959 | <<"select \"__alias-0\".\"id\" from \"users\" as \"__alias-0\"">>, 960 | Sql), 961 | ?assertEqual([], Args), 962 | ?assertEqual(text, Type). 963 | 964 | recursive_test() -> 965 | {Sql, Args, Type} = to_sql( 966 | qsql:select( 967 | q:recursive( 968 | q:where(fun([#{id := Id}]) -> Id =:= 1 end, q:from(tree_m)), 969 | fun(Q) -> 970 | q:select(fun([_, T]) -> T end, 971 | (q:join(tree_m, fun([#{id := Id}, #{parentId := PId}]) -> Id =:= PId end))(Q)) 972 | end))), 973 | ?assertEqual( 974 | <<"with recursive \"__alias-0\" as (" 975 | "select \"__alias-1\".\"id\" as \"id\"," 976 | "\"__alias-1\".\"parentId\" as \"parentId\"," 977 | "\"__alias-1\".\"value\" as \"value\" " 978 | "from \"tree\" as \"__alias-1\" where (\"__alias-1\".\"id\" = $1) " 979 | "union all " 980 | "select \"__alias-2\".\"id\" as \"id\"," 981 | "\"__alias-2\".\"parentId\" as \"parentId\"," 982 | "\"__alias-2\".\"value\" as \"value\" " 983 | "from \"__alias-0\" " 984 | "inner join \"tree\" as \"__alias-2\" " 985 | "on (\"__alias-0\".\"id\" = \"__alias-2\".\"parentId\")" 986 | ") select " 987 | "\"__alias-0\".\"id\" as \"id\"," 988 | "\"__alias-0\".\"parentId\" as \"parentId\"," 989 | "\"__alias-0\".\"value\" as \"value\" " 990 | "from \"__alias-0\"">>, 991 | Sql), 992 | ?assertEqual([1], Args), 993 | ?assertEqual({model, tree_m, ?TREE_FIELDS_LIST}, Type). 994 | 995 | distinct_test() -> 996 | {Sql, Args, Feilds} = to_sql( 997 | qsql:select(q:pipe(q:from(?MODULE), [ 998 | q:distinct() 999 | ]))), 1000 | ?assertEqual( 1001 | <<"select distinct " 1002 | "\"__alias-0\".\"id\" as \"id\"," 1003 | "\"__alias-0\".\"name\" as \"name\"," 1004 | "\"__alias-0\".\"password\" as \"password\"," 1005 | "\"__alias-0\".\"salt\" as \"salt\" " 1006 | "from \"users\" as \"__alias-0\"">>, Sql), 1007 | ?assertEqual([], Args), 1008 | ?assertEqual({model, ?MODULE, ?USER_FIELDS_LIST}, Feilds). 1009 | 1010 | distinct_on_test() -> 1011 | {Sql, Args, Feilds} = to_sql( 1012 | qsql:select(q:pipe(q:from(?MODULE), [ 1013 | q:distinct_on(fun(_) -> [id, name] end) 1014 | ]))), 1015 | ?assertEqual( 1016 | <<"select distinct on (\"id\",\"name\") " 1017 | "\"__alias-0\".\"id\" as \"id\"," 1018 | "\"__alias-0\".\"name\" as \"name\"," 1019 | "\"__alias-0\".\"password\" as \"password\"," 1020 | "\"__alias-0\".\"salt\" as \"salt\" " 1021 | "from \"users\" as \"__alias-0\"">>, Sql), 1022 | ?assertEqual([], Args), 1023 | ?assertEqual({model, ?MODULE, ?USER_FIELDS_LIST}, Feilds). 1024 | 1025 | drop_distinct_on_test() -> 1026 | {Sql, Args, Feilds} = to_sql( 1027 | qsql:select(q:pipe(q:from(?MODULE), [ 1028 | q:distinct_on(fun(_) -> [id, name] end), 1029 | q:distinct_on(fun(_) -> [] end) %% Drop distinct 1030 | ]))), 1031 | ?assertEqual( 1032 | <<"select " 1033 | "\"__alias-0\".\"id\" as \"id\"," 1034 | "\"__alias-0\".\"name\" as \"name\"," 1035 | "\"__alias-0\".\"password\" as \"password\"," 1036 | "\"__alias-0\".\"salt\" as \"salt\" " 1037 | "from \"users\" as \"__alias-0\"">>, Sql), 1038 | ?assertEqual([], Args), 1039 | ?assertEqual({model, ?MODULE, ?USER_FIELDS_LIST}, Feilds). 1040 | 1041 | bad_set_test() -> 1042 | ?assertException(error, bad_set, q:set(fun(_) -> bad end, q:from(?MODULE))). 1043 | 1044 | %% ============================================================================= 1045 | %% Internal functions 1046 | %% ============================================================================= 1047 | 1048 | to_sql(QAst) -> 1049 | {Sql, Args} = qast:to_sql(QAst), 1050 | Type = maps:get(type, qast:opts(QAst), undefined), 1051 | {Sql, Args, Type}. 1052 | 1053 | compile_fun_str(FunS) -> 1054 | {ok, Tokens, _} = erl_scan:string(FunS), 1055 | {ok, Exprs} = erl_parse:parse_exprs(Tokens), 1056 | {value, Fun, _Bindings} = erl_eval:exprs(Exprs, []), 1057 | Fun. 1058 | 1059 | compile_module_str(ModuleS) -> 1060 | {ok, Tokens, _} = erl_scan:string(ModuleS), 1061 | Splited = split_dot(Tokens), 1062 | Forms = lists:map(fun(Ts) -> 1063 | {ok, Form} = erl_parse:parse_form(Ts), 1064 | Form 1065 | end, Splited), 1066 | Forms2 = equery_pt:parse_transform(Forms, []), 1067 | compile:forms(Forms2). 1068 | 1069 | split_dot(Tokens) -> 1070 | split_dot(Tokens, [], []). 1071 | split_dot([{dot, _}=C|Rest], A, R) -> 1072 | split_dot(Rest, [], [lists:reverse([C|A])|R]); 1073 | split_dot([C|Rest], A, R) -> 1074 | split_dot(Rest, [C|A], R); 1075 | split_dot([], _, R) -> 1076 | lists:reverse(R). 1077 | --------------------------------------------------------------------------------