%% @author Angel Herranz %% Manuel Cherep %% %% @copyright 2016 Coowry Ltd. - All rights reserved. %% %% See LICENSE.txt file for detailed information. %% %% @doc SQL Abstract Syntax Tree. %% %% Reference material: %% - USING: https://ronsavage.github.io/SQL/ (https://ronsavage.github.io/SQL/sql-2003-2.bnf.html) %% - As a reference: Module epgsql.erl %% - As a reference: http://www.postgresql.org/docs/current/static/sql.html %% - As a reference: https://docs.jboss.org/author/display/teiid81final/BNF+for+SQL+Grammar %% - As a reference: http://ns.inria.fr/ast/sql/index.html %% - To be reviewed: https://jakewheat.github.io/sql-overview/sql-2011-foundation-grammar.html -module(eesql). -include("include/eesql.hrl"). %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %% Non terminal symbols from https://ronsavage.github.io/SQL/sql-2003-2.bnf.html -export_type( [ commit_stmt/0, column_name/0, column_reference/0, delete_stmt/0, derived_column/0, from_clause/0, insert_stmt/0, join_type/0, literal/0, query_spec/0, rollback_stmt/0, row_value_expr/0, set_clause/0, set_quant/0, sql_stmt/0, start_trans_stmt/0, sort_spec/0, table_name/0, table_ref/0, joined_table/0, table_primary/0, update_stmt/0, value_expr/0 ] ). -export_type( [ pg_with_as/0, pg_call/0 ] ). %% TODO: convert to non terminal symbols from https://ronsavage.github.io/SQL/sql-2003-2.bnf.html -export_type( [id/0, identifier_chain/0, predicate/0, binop/0]). %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% -export([to_sql/1]). %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% -export([identifier_to_sql/1, identifier_chain_to_sql/1]). %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %% Precompiled regular expressions for checking proper identifiers and %% identifier chains %% io:format("~w~n", [re:compile("^(\"[a-zA-Z0-9_]+\"|^[a-zA-Z0-9_]+)$")]). -define(IDENTIFIER_MP, {re_pattern,1,0,0,<<69,82,67,80,157,0,0,0,16,0,0,0,1,0,0,0,255,255,255,255,255,255,255,255,0,0,0,0,0,0,1,0,0,0,64,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,125,0,89,25,127,0,43,0,1,29,34,106,0,0,0,0,0,0,255,3,254,255,255,135,254,255,255,7,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,100,29,34,113,0,38,25,106,0,0,0,0,0,0,255,3,254,255,255,135,254,255,255,7,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,100,114,0,81,27,114,0,89,0>>}). %% io:format("~w~n", [re:compile("^(\"[a-zA-Z0-9_]+\"|[a-zA-Z0-9_]+)(\.(\"[a-zA-Z0-9_]+\"|[a-zA-Z0-9_]+))*(\.\*)?$")]). -define(IDENTIFIER_CHAIN_MP, {re_pattern,4,0,0,<<69,82,67,80,4,1,0,0,16,0,0,0,1,0,0,0,255,255,255,255,255,255,255,255,0,0,0,0,0,0,4,0,0,0,64,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,125,0,192,25,127,0,43,0,1,29,34,106,0,0,0,0,0,0,255,3,254,255,255,135,254,255,255,7,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,100,29,34,113,0,37,106,0,0,0,0,0,0,255,3,254,255,255,135,254,255,255,7,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,100,114,0,80,140,127,0,89,0,2,12,127,0,43,0,3,29,34,106,0,0,0,0,0,0,255,3,254,255,255,135,254,255,255,7,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,100,29,34,113,0,37,106,0,0,0,0,0,0,255,3,254,255,255,135,254,255,255,7,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,100,114,0,80,115,0,89,140,127,0,7,0,4,85,12,114,0,7,27,114,0,192,0>>}). %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %% Guards to pre validate that an atom could be an identifier -define(IS_IDENTIFIER(X), is_atom(X), X /= null, X /= true, X /= false). %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %% The following types represents the PG SQL Abstract Syntax Tree. %% Key SQL statements and fragments -type sql_stmt() :: commit_stmt() | start_trans_stmt() | rollback_stmt() | query_spec() | insert_stmt() | update_stmt() | delete_stmt() | truncate_stmt() | pg_refresh_stmt() | union_stmt() | pg_with_as(). %% ::= %% ::= | %% ::= %% ::= [ ... ] %% ::= | %% ::= %% ::= ... %% ::= | -type id() :: atom(). %% ::= [ { }... ] -type identifier_chain() :: atom(). %% (any SQL literal, for the moment just inspired by epgsql:bind_param()) %% ::= | %% ::= | %% ::= %% %% | %% | %% | %% | %% | %% | -type literal() :: null | boolean() %% | string() %% Let's avoid confussion with arrays | binary() | integer() | float() %% | calendar:date() %% | calendar:time() %actualy, `Seconds' may be float() %% | calendar:datetime() %% | {calendar:time(), Days::non_neg_integer(), Months::non_neg_integer()} . %% ::= %% ::= [ ] %% ::= | MODULE %% ::= [ ] %% ::= ??? %% ::= -type table_name() :: identifier_chain(). %% ::= %% %% | MODULE %% ::= %% ::= -type column_reference() :: identifier_chain(). %% ::= -type column_name() :: id(). %% -type row_value_expr() :: nonempty_list(literal()). %% -type derived_column() :: value_expr() | {value_expr(), column_name()} %% AS %% TODO: Improve type | {count, value_expr() | all} | {count, {distinct, value_expr()}}. %%
%% Expressions for describing "tables" (eg. FROM in a SELECT statement) -type table_ref() :: table_primary() | joined_table(). %%
-type table_primary() :: id() | {id(), id()} %% AS | {query_spec(), id()} | pg_call() | {pg_call(), id}. %% -type joined_table() :: cross_join() | qualified_join() | natural_join() | union_join(). -type cross_join() :: #cross_join{}. -type qualified_join() :: #join{} | #qualified_join{}. -type natural_join() :: #natural_join{}. -type union_join() :: #union_join{}. -type join_type() :: inner | left | right | full. %% -type set_quant() :: all | distinct. %% Predicates -type predicate() :: boolean() | {'not', predicate()} | {'and', [predicate()]} | {'or', [predicate()]} | {value_expr(), binop(), value_expr()} | {is_null, column_reference()} | {exists, query_spec()} | {between, value_expr(), value_expr(), value_expr()} | {in, value_expr(), query_spec()}. %% | some, all, ... %% over-simplified for the moment. %% ::= | | %% ::= %% %% | %% | %% | %% | %% | %% | %% ::= %% ::= %% ::= | %% ::= | %% ::= | %% ::= %% ::= %% %% | %% | %% | %% | %% | %% | %% | %% | %% | %% | %% | %% | %% | %% | %% | %% | %% | %% | -type value_expr() :: identifier_chain() %% | {routine_invocation(), [value_expr()]} %% Represents function calls (a lot of clauses such us , , , ... | [value_expr()] % Array (maybe nested) | literal(). %% Supported function names -type routine_invocation() :: identifier_chain(). %% Function names such as UPPER, LOWER, POWER, ABS... %% Binary operators -type binop() :: '=' | '!=' | '<>' | '<' | '>' | '<=' | '>=' | like. %% COMMIT -type commit_stmt() :: commit | commit_and_chain | commit_and_no_chain. %% START TRANSACTION -type start_trans_stmt() :: start_transaction. %% ROLLBACK -type rollback_stmt() :: rollback. %% SELECT -type query_spec() :: #select{}. %% -type from_clause() :: nonempty_list(table_ref()). %% -type sort_spec() :: {value_expr(), asc | desc, last | first} | {value_expr(), asc | desc | last | first} | value_expr(). %% INSERT -type insert_stmt() :: #insert{}. %% UPDATE -type update_stmt() :: #update{}. %% -type set_clause() :: {column_reference(), value_expr()}. %% DELETE -type delete_stmt() :: #delete{}. %% TRUNCATE -type truncate_stmt() :: #truncate{}. %% TRUNCATE -type pg_refresh_stmt() :: #pg_refresh{}. %% UNION -type union_stmt() :: #union{}. %% WITH AS -type pg_with_as() :: #pg_with{}. %% Table function call -type pg_call() :: #pg_call{}. %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %% @doc X = [1,2,3], [1, x, 2, x, 3] = intersperse(X, x) -spec intersperse(list(), any()) -> list(). intersperse([], _) -> []; intersperse([X | Xs], I) -> [X | lists:foldr(fun(Y, Acc) -> [I, Y | Acc] end, [], Xs)]. -spec to_sql(sql_stmt()) -> {Equery, Params} when Equery :: iodata(), Params :: [literal()]. to_sql(Statement) -> Position = 1, {_Last_Pos, {Equery, Params}} = to_sql(Position, {sql_stmt, Statement}), {[Equery, ";"], Params}. %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %% @doc Serializes SQL (sub)sentences. -spec to_sql(Pos, {sql_stmt, sql_stmt()} | {where_clause, undefined | predicate()} | {predicate, predicate()} | {value_expr, value_expr()} | {value_expr_list, [value_expr()]} | {table_ref, table_ref()} | {table_primary, table_primary()} | {literal, literal()} | {offset, undefined | {pos_integer(), pos_integer()}} | {on_conflict_update_target, undefined | [column_reference()], [column_reference()]}) -> {Pos, {Equery, Params}} when Pos :: pos_integer(), Equery :: iodata(), Params :: [literal()]. %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %% Serialize SQL statement to_sql(Position, {sql_stmt, start_transaction}) -> {Position, {"BEGIN TRANSACTION", []}}; to_sql(Position, {sql_stmt, commit}) -> {Position, {"COMMIT", []}}; to_sql(Position, {sql_stmt, commit_and_chain}) -> {Position, {"COMMIT AND CHAIN", []}}; to_sql(Position, {sql_stmt, commit_and_no_chain}) -> {Position, {"COMMIT AND NO CHAIN", []}}; to_sql(Position, {sql_stmt, rollback}) -> {Position, {"ROLLBACK", []}}; to_sql(Position, {sql_stmt, #pg_refresh{materialized_view = View}}) -> {Position, {["REFRESH MATERIALIZED VIEW ", identifier_to_sql(View)], []}}; to_sql(Position, {sql_stmt, #truncate{table = Table, cascade = Cascade}}) -> case Cascade of false -> {Position, {["TRUNCATE ", identifier_to_sql(Table)], []}}; true -> {Position, {["TRUNCATE ", identifier_to_sql(Table), " CASCADE"], []}} end; to_sql(P, {sql_stmt, #union{type = Type, queries = Queries, order_by = Sort_Specs}}) -> {P2, {Clauses, Params}} = lists:foldl(fun(S, {P0, {Clauses0, Params0}}) -> {P1, {Clauses1, Params1}} = to_sql(P0, {sql_stmt, S}), %% Remove semicolon from select sql to avoid syntax error Clauses1_Without_Semicolon = lists:droplast(Clauses1), {P1, {Clauses0 ++ [Clauses1_Without_Semicolon], Params0 ++ Params1}} end, {P, {[], []}}, Queries), {P3, {Sort_Spec_Clauses, Sort_Specs_Params}} = to_sql_fold(P2, sort_spec, Sort_Specs), Order_By_Clause = case Sort_Spec_Clauses of [] -> ""; _ -> [" ORDER BY ", intersperse(Sort_Spec_Clauses, ", ")] end, Intersperse = case Type of all -> " UNION ALL "; null -> " UNION " end, {P3, {["(", intersperse(Clauses, Intersperse), ")", Order_By_Clause], Params ++ Sort_Specs_Params}}; to_sql(P0, {sql_stmt, #select{quantifier = Quant, columns = Columns, from = From, where = Where, group_by = Group_By, order_by = Sort_Specs, offset = Offset, for_update = For_Update}}) -> Quant_SQL = set_quant_to_sql(Quant), {P1, {Columns_SQL, Columns_Parameters}} = to_sql_fold(P0, derived_column, Columns), case Columns_SQL of [] -> Items = "*"; _ -> Items = intersperse(Columns_SQL, ", ") end, {P2, {Table_Ref_Clauses, Table_Ref_Parameters}} = to_sql_fold(P1, table_ref, From), {P3, {Where_Clause, Where_Parameters}} = to_sql(P2, {where_clause, Where}), {P4, {Sort_Spec_Clauses, Sort_Specs_Parameters}} = to_sql_fold(P3, sort_spec, Sort_Specs), %% TODO: Group By expression, not only a list of columns. {P5, {Group_By_Exprs_SQL, Group_By_Parameters}} = to_sql_fold(P4, derived_column, Group_By), case Group_By_Exprs_SQL of [] -> Group_By_Clause = ""; _ -> Group_By_Clause = [" GROUP BY ", intersperse(Group_By_Exprs_SQL, ", ")] end, Order_By_Clause = case Sort_Spec_Clauses of [] -> ""; _ -> [" ORDER BY ", intersperse(Sort_Spec_Clauses, ", ")] end, {P6, {Offset_Clause, Offset_Params}} = to_sql(P5, {offset, Offset}), case For_Update of false -> For_Update_Clause = ""; true -> For_Update_Clause = " FOR UPDATE" end, {P6, {["SELECT ", Quant_SQL, " ", Items, [" FROM ", intersperse(Table_Ref_Clauses, ", ")], Where_Clause, Group_By_Clause, Order_By_Clause, Offset_Clause, For_Update_Clause], Columns_Parameters ++ Table_Ref_Parameters ++ Where_Parameters ++ Group_By_Parameters ++ Sort_Specs_Parameters ++ Offset_Params}}; to_sql(P0, {sql_stmt, #insert{table = Table, columns = Columns, values = Rows, on_conflict_update_target = Conflict_Columns}}) -> {P1, {Values_Clause, Values_Parameters}} = to_sql_fold(P0, value_expr_list, Rows), {P2, {Conflict_Clause, Conflict_Params}} = to_sql(P1, {on_conflict_update_target, Conflict_Columns, Columns}), {P2, {["INSERT INTO ", identifier_to_sql(Table), " (", intersperse([identifier_to_sql(Column) || Column <- Columns], ", "), ")", " VALUES ", intersperse(Values_Clause, ", "), Conflict_Clause, " RETURNING *"], Values_Parameters ++ Conflict_Params}}; to_sql(P0, {sql_stmt, #update{table = Table, set = Set, where = Where}}) -> {P1, {Set_Clause, Set_Parameters}} = %% TODO: cannot be easily factored into to_sql_fold lists:foldl(fun({Column, Value}, {PI, {Accum_SQL, Accum_Params}}) -> {PJ, {Expr_SQL, Expr_Params}} = to_sql(PI, {value_expr, Value}), {PJ, {Accum_SQL ++ [[identifier_to_sql(Column), " = ", Expr_SQL]], Accum_Params ++ Expr_Params}} end, {P0, {[], []}}, Set), {P2, {Where_Clause, Where_Parameters}} = to_sql(P1, {where_clause, Where}), {P2, {["UPDATE ", identifier_to_sql(Table), " SET ", intersperse(Set_Clause, ", "), Where_Clause, " RETURNING *"], Set_Parameters ++ Where_Parameters}}; to_sql(P0, {sql_stmt, #delete{table = Table, where = Where}}) -> {P1, {Where_Clause, Where_Parameters}} = to_sql(P0, {where_clause, Where}), {P1, {["DELETE FROM ", identifier_to_sql(Table), Where_Clause, " RETURNING *"], Where_Parameters}}; %% Serialize %% Serialize a values clause to_sql(P0, {sql_stmt, #pg_with{definitions = Definitions, select = Select}}) -> {P1, {Query_Clause, Query_Params}} = lists:foldl(fun({Name, Query}, {PI, {Accum_SQL, Accum_Params}}) -> {PJ, {Expr_SQL, Expr_Params}} = to_sql(PI, {sql_stmt, Query}), {PJ, {Accum_SQL ++ [[identifier_chain_to_sql(Name), " AS (", Expr_SQL, ")"]], Accum_Params ++ Expr_Params}} end, {P0, {[], []}}, Definitions), {P2, {Select_Clause, Select_Params}} = to_sql(P1, {sql_stmt, Select}), {P2, {["WITH ", intersperse(Query_Clause, ", "), " ", Select_Clause], Query_Params ++ Select_Params}}; %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %% Serialize to_sql(P0, {derived_column, {count, all}}) -> {P0, {["COUNT(*)"],[]}}; to_sql(P0, {derived_column, {count, {distinct, Column}}}) -> {P1, {Value_Expr_SQL, Value_Expr_Parameters}} = to_sql(P0, {value_expr, Column}), {P1,{["COUNT(DISTINCT ", Value_Expr_SQL, ")"], Value_Expr_Parameters}}; to_sql(P0, {derived_column, {count, Column}}) -> {P1, {Value_Expr_SQL, Value_Expr_Parameters}} = to_sql(P0, {value_expr, Column}), {P1,{["COUNT(", Value_Expr_SQL, ")"], Value_Expr_Parameters}}; to_sql(P0, {derived_column, {Column, Alias}}) when ?IS_IDENTIFIER(Alias)-> {P1, {Value_Expr_SQL, Value_Expr_Parameters}} = to_sql(P0, {value_expr, Column}), {P1, {[Value_Expr_SQL, " AS ", identifier_to_sql(Alias)], Value_Expr_Parameters}}; to_sql(P0, {derived_column, Column}) -> to_sql(P0, {value_expr, Column}); %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %% Serialize a list of to_sql(P0, {value_expr_list, Row}) -> {P1, {Values_Clause, Values_Parameters}} = to_sql_fold(P0, value_expr, Row), {P1, {["(", intersperse(Values_Clause, ", "), ")"], Values_Parameters}}; %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %% Serialize
to_sql(P0, {table_ref, #join{type = no_join, table = Table, joins = Joins}}) -> {P1, {Table_Clauses, Table_Params}} = to_sql(P0, {table_ref, Table}), {P2, {Joins_Clauses, Joins_Params}} = to_sql_fold(P1, join, Joins), {P2, {[Table_Clauses, $ , intersperse(Joins_Clauses, " ")], Table_Params ++ Joins_Params}}; to_sql(P0, {join, #join{type = Type, table = Table, spec = Spec}}) -> {P1, {Table_Clauses, Table_Params}} = to_sql(P0, {table_ref, Table}), {P2, {Pred_SQL, Pred_Parameters}} = to_sql(P1, {predicate, Spec}), {P2, {[case Type of inner -> "INNER"; left -> "LEFT OUTER"; right -> "RIGHT OUTER"; full -> "FULL OUTER" end, $ , "JOIN", $ , Table_Clauses,$ , "ON",$ , Pred_SQL], Table_Params ++ Pred_Parameters}}; to_sql(P0, {table_ref, #cross_join{left = Left, right = Right}}) -> {P1, {Left_Clause, Left_Params}} = to_sql(P0, {table_ref, Left}), {P2, {Right_Clause, Right_Params}} = to_sql(P1, {table_primary, Right}), {P2, {["(", Left_Clause, $ , "CROSS JOIN", $ , Right_Clause, ")"], Left_Params ++ Right_Params}}; to_sql(P0, {table_ref, #natural_join{left = Left, right = Right}}) -> {P1, {Left_Clause, Left_Params}} = to_sql(P0, {table_ref, Left}), {P2, {Right_Clause, Right_Params}} = to_sql(P1, {table_primary, Right}), {P2, {["(", Left_Clause, $ , "NATURAL JOIN", $ , Right_Clause, ")"], Left_Params ++ Right_Params}}; to_sql(P0, {table_ref, #union_join{left = Left, right = Right}}) -> {P1, {Left_Clause, Left_Params}} = to_sql(P0, {table_ref, Left}), {P2, {Right_Clause, Right_Params}} = to_sql(P1, {table_primary, Right}), {P2, {["(", Left_Clause, $ , "UNION JOIN", $ , Right_Clause, ")"], Left_Params ++ Right_Params}}; to_sql(P0, {table_ref, #qualified_join{type = Type, left = Left, right = Right, on = On}}) -> {P1, {Left_Clause, Left_Params}} = to_sql(P0, {table_ref, Left}), {P2, {Right_Clause, Right_Params}} = to_sql(P1, {table_ref, Right}), {P3, {On_Clause, On_Params}} = to_sql(P2, {predicate, On}), {P3, {["(", Left_Clause, $ , case Type of inner -> "INNER"; left -> "LEFT OUTER"; right -> "RIGHT OUTER"; full -> "FULL OUTER" end, $ , "JOIN", $ , Right_Clause, $ , "ON", $ , On_Clause, ")" ], Left_Params ++ Right_Params ++ On_Params}}; to_sql(P0, {table_ref, Table_Primary}) -> to_sql(P0, {table_primary, Table_Primary}); %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %% Serialize
to_sql(P0, {table_primary, #pg_call{name = Name, args = Args}}) -> {P1, {Args_Clauses, Args_Params}} = to_sql_fold(P0, value_expr, Args), {P1, {[identifier_to_sql(Name), "(", intersperse(Args_Clauses, ", "), ")"], Args_Params}}; to_sql(P0, {table_primary, {#pg_call{} = Call, Correlation_Name}}) -> {P1, {Call_Clause, Call_Params}} = to_sql(P0, {table_primary, Call}), Correlation = identifier_to_sql(Correlation_Name), {P1, {[Call_Clause, $ , "AS", $ , Correlation], Call_Params}}; to_sql(P0, {table_primary, {#select{} = Select, Correlation_Name}}) -> {P1, {Clauses, Select_Params}} = to_sql(P0, {sql_stmt, Select}), Correlation = identifier_to_sql(Correlation_Name), %% Remove semicolon from select sql to avoid syntax error %% TODO: The select shouldn't have the semicolon, which sould be added %% at the end of any query, since semicolon can only happen once and at the end. Clauses_Without_Semicolon = lists:droplast(Clauses), {P1, {["(", Clauses_Without_Semicolon, ")", $ , "AS", $ , Correlation], Select_Params}}; to_sql(P0, {table_primary, {Table_Name, Correlation_Name}}) -> Table = identifier_chain_to_sql(Table_Name), Correlation = identifier_to_sql(Correlation_Name), {P0, {[Table, $ , "AS", $ , Correlation], []}}; to_sql(P0, {table_primary, Table_Name}) -> {P0, {identifier_chain_to_sql(Table_Name), []}}; %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %% Serialize to_sql(P0, {where_clause, undefined}) -> {P0, {"", []}}; to_sql(P0, {where_clause, Predicate}) -> {P1, {Pred_SQL, Pred_Params}} = to_sql(P0, {predicate, Predicate}), {P1, {[" WHERE ", Pred_SQL], Pred_Params}}; %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %% Serialize a sort specification () to_sql(P0, {sort_spec, {Value, Order, Nulls}}) -> {P1, {Value_SQL, Value_Params}} = to_sql(P0, {value_expr, Value}), {P1, {[Value_SQL, " ", case Order of asc -> "ASC"; desc -> "DESC" end, " NULLS ", case Nulls of last -> "LAST"; first -> "FIRST" end], Value_Params}}; to_sql(P0, {sort_spec, {Value, asc}}) -> {P1, {Value_SQL, Value_Params}} = to_sql(P0, {value_expr, Value}), {P1, {[Value_SQL, " ASC"], Value_Params}}; to_sql(P0, {sort_spec, {Value, desc}}) -> {P1, {Value_SQL, Value_Params}} = to_sql(P0, {value_expr, Value}), {P1, {[Value_SQL, " DESC"], Value_Params}}; to_sql(P0, {sort_spec, {Value, first}}) -> {P1, {Value_SQL, Value_Params}} = to_sql(P0, {value_expr, Value}), {P1, {[Value_SQL, " NULLS FIRST"], Value_Params}}; to_sql(P0, {sort_spec, {Value, last}}) -> {P1, {Value_SQL, Value_Params}} = to_sql(P0, {value_expr, Value}), {P1, {[Value_SQL, " NULLS LAST"], Value_Params}}; to_sql(P0, {sort_spec, Value}) -> to_sql(P0, {value_expr, Value}); %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %% Serialize offset/fetch () to_sql(P0, {offset, undefined}) -> {P0, {"", []}}; to_sql(P0, {offset, {Start, Count}}) -> {P1, {Start_SQL, Start_Params}} = to_sql(P0, {value_expr, Start}), {P2, {Count_SQL, Count_Params}} = to_sql(P1, {value_expr, Count}), {P2, {[" OFFSET (", Start_SQL, ") FETCH NEXT (", Count_SQL, ") ROWS ONLY"], Start_Params ++ Count_Params}}; to_sql(P0, {offset, {Value, Order, Nulls}}) -> {P1, {Value_SQL, Value_Params}} = to_sql(P0, {value_expr, Value}), {P1, {[Value_SQL, " ", case Order of asc -> "ASC"; desc -> "DESC" end, " NULLS ", case Nulls of last -> "LAST"; first -> "FIRST" end], Value_Params}}; %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %% Serialize on conflict to_sql(P0, {on_conflict_update_target, undefined, _}) -> {P0, {"", []}}; to_sql(P0, {on_conflict_update_target, [], _Columns}) -> %% Conflict columns is [] {P0, {[" ON CONFLICT DO NOTHING"], []}}; to_sql(P0, {on_conflict_update_target, Conflict_Columns, Columns}) -> Columns_To_Update = lists:subtract(Columns, Conflict_Columns), Set_Clauses = [ begin Column_SQL = identifier_to_sql(Column), [Column_SQL, " = EXCLUDED.", Column_SQL] end || Column <- Columns_To_Update ], {P0, {[" ON CONFLICT (", intersperse([identifier_to_sql(Column) || Column <- Conflict_Columns], ", "), ")", " DO UPDATE SET ", intersperse(Set_Clauses, ", ")], []}}; %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %% Serialize to_sql(P0, {predicate, true}) -> {P0, {"TRUE", []}}; to_sql(P0, {predicate, false}) -> {P0, {"FALSE", []}}; to_sql(P0, {predicate, {'not', Predicate}}) -> {P1, {Pred_SQL, Pred_Params}} = to_sql(P0, {predicate, Predicate}), {P1, {["NOT (", Pred_SQL, ")"], Pred_Params}}; to_sql(P0, {predicate, {Logic_Bin_Op, [Pred1 | Predicates]}}) when Logic_Bin_Op == 'and'; Logic_Bin_Op == 'or' -> Operator = case Logic_Bin_Op of 'and' -> " AND "; 'or' -> " OR " end, {P1, {Pred1_SQL, Pred1_Params}} = to_sql(P0, {predicate, Pred1}), %% TODO: cannot be easily factored into to_sql_fold lists:foldl(fun(Pred, {PI, {Accum_SQL, Accum_Params}}) -> {PJ, {Pred_SQL, Pred_Params}} = to_sql(PI, {predicate, Pred}), {PJ, {["(", Accum_SQL, Operator, Pred_SQL, ")"], Accum_Params ++ Pred_Params}} end, {P1, {Pred1_SQL, Pred1_Params}}, Predicates); to_sql(P0, {predicate, {is_null, Column}}) -> {P0, {[identifier_chain_to_sql(Column), " IS NULL"], []}}; to_sql(P0, {predicate, {Left, Bin_Op, Right}}) when Bin_Op == '='; Bin_Op == '!='; Bin_Op == '<>'; Bin_Op == '<'; Bin_Op == '>'; Bin_Op == '<='; Bin_Op == '>='; Bin_Op == like -> Bin_Op_SQL = if Bin_Op == like -> <<"LIKE">>; true -> atom_to_binary(Bin_Op, utf8) end, {P1, {Left_SQL, Left_Params}} = to_sql(P0, {value_expr, Left}), {P2, {Right_SQL, Right_Params}} = to_sql(P1, {value_expr, Right}), {P2, {[Left_SQL, " ", Bin_Op_SQL, " ", Right_SQL], Left_Params ++ Right_Params}}; to_sql(P0, {predicate, {exists, Select = #select{}}}) -> {P1, {Select_Clause, Select_Parameters}} = to_sql(P0, {sql_stmt, Select}), {P1, {["EXISTS ", "(", Select_Clause, ")"], Select_Parameters}}; to_sql(P0, {predicate, {between, Expr, Min, Max}}) -> {P1, {Expr_SQL, Expr_Params}} = to_sql(P0, {value_expr, Expr}), {P2, {Min_SQL, Min_Params}} = to_sql(P1, {value_expr, Min}), {P3, {Max_SQL, Max_Params}} = to_sql(P2, {value_expr, Max}), {P3, {[Expr_SQL, " BETWEEN ", Min_SQL, " AND ", Max_SQL], Expr_Params ++ Min_Params ++ Max_Params}}; to_sql(P0, {predicate, {in, Expr, Select = #select{}}}) -> {P1, {Expr_SQL, Expr_Params}} = to_sql(P0, {value_expr, Expr}), {P2, {Select_SQL, Select_Params}} = to_sql(P1, {sql_stmt, Select}), %% Remove semicolon from select sql to avoid syntax error Select_SQL_Without_Semicolon = lists:droplast(Select_SQL), {P2, {[Expr_SQL, " IN (", Select_SQL_Without_Semicolon, ")"], Expr_Params ++ Select_Params}}; %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %% Serialize a to_sql(P0, {value_expr, Column}) when ?IS_IDENTIFIER(Column) -> {P0, {identifier_chain_to_sql(Column), []}}; to_sql(P0, {value_expr, {cast, [Expr, Type]}}) -> {P1, {Expr_SQL, Expr_Params}} = to_sql(P0, {value_expr, Expr}), Type_SQL = identifier_chain_to_sql(Type), {P1, {["CAST(",Expr_SQL," AS ", Type_SQL, ")"], Expr_Params}}; to_sql(P0, {value_expr, {Function_Name, Actual_Args}}) -> Routine_Name = atom_to_binary(Function_Name, utf8), {P1, {Args_SQLs, Args_Params}} = to_sql_fold(P0, value_expr, Actual_Args), {P1, {[Routine_Name, "(", intersperse(Args_SQLs, ", "), ")"], Args_Params}}; to_sql(P0, {value_expr, Value_Exprs}) when is_list(Value_Exprs) -> {P1, {Values_SQLs, Values_Params}} = to_sql_fold(P0, value_expr, Value_Exprs), {P1, {["{", intersperse(Values_SQLs, ", "), "}"], Values_Params}}; to_sql(P0, {value_expr, Literal}) -> to_sql(P0, {literal, Literal}); %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %% Serialize a to_sql(P0, {literal, null}) -> {P0, {<<"NULL">>, []}}; to_sql(P0, {literal, true}) -> {P0, {<<"TRUE">>, []}}; to_sql(P0, {literal, false}) -> {P0, {<<"FALSE">>, []}}; to_sql(P0, {literal, Value}) when is_binary(Value); is_integer(Value); is_float(Value) -> {P0+1, {get_placeholder(P0), [Value]}}. %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %% Serialize a list of AST nodes to_sql_fold(P0, Node_Type, Nodes) -> lists:foldl(fun(Node, {PI, {Accum_SQL, Accum_Params}}) -> {PJ, {Node_SQL, Node_Params}} = to_sql(PI, {Node_Type, Node}), {PJ, {Accum_SQL ++ [Node_SQL], Accum_Params ++ Node_Params}} end, {P0, {[], []}}, Nodes). %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %% @doc Serialize set quantifier -spec set_quant_to_sql(set_quant()) -> iodata(). set_quant_to_sql(all) -> "ALL"; set_quant_to_sql(distinct) -> "DISTINCT". %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %% @doc Serialize an identifier -spec identifier_to_sql(id()) -> iodata(). identifier_to_sql(Id) -> Identifier = atom_to_binary(Id, utf8), %% case re:run(Identifier, ?IDENTIFIER_MP) of %% {match, [{0,0}]} -> %% throw({non_valid_identifier, Id}); %% {match, _Captured} -> %% Identifier; %% nomatch -> %% throw({non_valid_identifier, Id}) %% end. Identifier. %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %% @doc Serialize an identifier_chain -spec identifier_chain_to_sql(identifier_chain()) -> iodata(). identifier_chain_to_sql(Id) -> Identifier = atom_to_binary(Id, utf8), %% case re:run(Identifier, ?IDENTIFIER_CHAIN_MP) of %% {match, [{0,0}]} -> %% throw({non_valid_identifier, Id}); %% {match, _Captured} -> %% Identifier; %% nomatch -> %% throw({non_valid_identifier, Id}) %% end. Identifier. %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %% @doc Returns placeholder according to position -spec get_placeholder(Position :: integer()) -> string(). get_placeholder(Position) when is_integer(Position) -> "$" ++ integer_to_list(Position).