1 |
|
%%============================================================================== |
2 |
|
%% Copyright 2016 Erlang Solutions Ltd. |
3 |
|
%% |
4 |
|
%% Licensed under the Apache License, Version 2.0 (the "License"); |
5 |
|
%% you may not use this file except in compliance with the License. |
6 |
|
%% You may obtain a copy of the License at |
7 |
|
%% |
8 |
|
%% http://www.apache.org/licenses/LICENSE-2.0 |
9 |
|
%% |
10 |
|
%% Unless required by applicable law or agreed to in writing, software |
11 |
|
%% distributed under the License is distributed on an "AS IS" BASIS, |
12 |
|
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
13 |
|
%% See the License for the specific language governing permissions and |
14 |
|
%% limitations under the License. |
15 |
|
%%============================================================================== |
16 |
|
|
17 |
|
-module(mongoose_rdbms_pgsql). |
18 |
|
-author('konrad.zemek@erlang-solutions.com'). |
19 |
|
-behaviour(mongoose_rdbms_backend). |
20 |
|
|
21 |
|
-include_lib("epgsql/include/epgsql.hrl"). |
22 |
|
|
23 |
|
-define(PGSQL_PORT, 5432). |
24 |
|
|
25 |
|
-export([escape_binary/1, unescape_binary/1, connect/2, disconnect/1, |
26 |
|
query/3, prepare/5, execute/4]). |
27 |
|
|
28 |
|
%% API |
29 |
|
|
30 |
|
-spec escape_binary(binary()) -> iodata(). |
31 |
|
escape_binary(Bin) when is_binary(Bin) -> |
32 |
:-( |
[<<"decode('">>, base64:encode(Bin), <<"','base64')">>]. |
33 |
|
|
34 |
|
-spec unescape_binary(binary()) -> binary(). |
35 |
|
unescape_binary(<<"\\x", Bin/binary>>) -> |
36 |
:-( |
base16:decode(Bin); |
37 |
|
unescape_binary(Bin) when is_binary(Bin) -> |
38 |
:-( |
Bin. |
39 |
|
|
40 |
|
-spec connect(Args :: any(), QueryTimeout :: non_neg_integer()) -> |
41 |
|
{ok, Connection :: term()} | {error, Reason :: any()}. |
42 |
|
connect(Settings, QueryTimeout) -> |
43 |
:-( |
case epgsql:connect(db_opts(Settings)) of |
44 |
|
{ok, Pid} -> |
45 |
:-( |
epgsql:squery(Pid, [<<"SET statement_timeout=">>, integer_to_binary(QueryTimeout)]), |
46 |
:-( |
epgsql:squery(Pid, <<"SET standard_conforming_strings=off">>), |
47 |
:-( |
{ok, Pid}; |
48 |
|
Error -> |
49 |
:-( |
Error |
50 |
|
end. |
51 |
|
|
52 |
|
-spec disconnect(Connection :: epgsql:connection()) -> ok. |
53 |
|
disconnect(Connection) -> |
54 |
:-( |
epgsql:close(Connection). |
55 |
|
|
56 |
|
-spec query(Connection :: term(), Query :: any(), |
57 |
|
Timeout :: infinity | non_neg_integer()) -> mongoose_rdbms:query_result(). |
58 |
|
query(Connection, Query, _Timeout) -> |
59 |
:-( |
pgsql_to_rdbms(epgsql:squery(Connection, Query)). |
60 |
|
|
61 |
|
-spec prepare(Connection :: term(), Name :: atom(), Table :: binary(), |
62 |
|
Fields :: [binary()], Statement :: iodata()) -> |
63 |
|
{ok, term()} | {error, any()}. |
64 |
|
prepare(Connection, Name, _Table, _Fields, Statement) -> |
65 |
:-( |
BinName = [atom_to_binary(Name, latin1)], |
66 |
:-( |
ReplacedStatement = replace_question_marks(Statement), |
67 |
:-( |
case epgsql:parse(Connection, BinName, ReplacedStatement, []) of |
68 |
:-( |
{ok, _} -> epgsql:describe(Connection, statement, BinName); |
69 |
:-( |
Error -> Error |
70 |
|
end. |
71 |
|
|
72 |
|
-spec execute(Connection :: term(), StatementRef :: term(), Params :: [term()], |
73 |
|
Timeout :: infinity | non_neg_integer()) -> mongoose_rdbms:query_result(). |
74 |
|
execute(Connection, StatementRef, Params, _Timeout) -> |
75 |
:-( |
pgsql_to_rdbms(epgsql:prepared_query(Connection, StatementRef, Params)). |
76 |
|
|
77 |
|
%% Helpers |
78 |
|
|
79 |
|
-spec db_opts(Settings :: term()) -> [term()]. |
80 |
|
db_opts({pgsql, Server, DB, User, Pass}) -> |
81 |
:-( |
db_opts({pgsql, Server, ?PGSQL_PORT, DB, User, Pass}); |
82 |
|
db_opts({pgsql, Server, Port, DB, User, Pass}) when is_integer(Port) -> |
83 |
:-( |
get_db_basic_opts({Server, Port, DB, User, Pass}); |
84 |
|
db_opts({pgsql, Server, DB, User, Pass, SSLConnOpts}) -> |
85 |
:-( |
db_opts({pgsql, Server, ?PGSQL_PORT, DB, User, Pass, SSLConnOpts}); |
86 |
|
db_opts({pgsql, Server, Port, DB, User, Pass, SSLConnOpts}) when is_integer(Port) -> |
87 |
:-( |
DBBasicOpts = get_db_basic_opts({Server, Port, DB, User, Pass}), |
88 |
:-( |
extend_db_opts_with_ssl(DBBasicOpts, SSLConnOpts). |
89 |
|
|
90 |
|
-spec get_db_basic_opts(Settings :: term()) -> [term()]. |
91 |
|
get_db_basic_opts({Server, Port, DB, User, Pass}) -> |
92 |
:-( |
[ |
93 |
|
{host, Server}, |
94 |
|
{port, Port}, |
95 |
|
{database, DB}, |
96 |
|
{username, User}, |
97 |
|
{password, Pass}, |
98 |
|
%% Encode 0 and 1 as booleans, as well as true and false |
99 |
|
{codecs, [{mongoose_rdbms_pgsql_codec_boolean, []}]} |
100 |
|
]. |
101 |
|
|
102 |
|
-spec extend_db_opts_with_ssl(Opts :: [term()], SSLConnOpts :: [term()]) -> [term()]. |
103 |
|
extend_db_opts_with_ssl(Opts, SSLConnOpts) -> |
104 |
:-( |
Opts ++ SSLConnOpts. |
105 |
|
|
106 |
|
-spec pgsql_to_rdbms(epgsql:reply(term())) -> mongoose_rdbms:query_result(). |
107 |
|
pgsql_to_rdbms(Items) when is_list(Items) -> |
108 |
:-( |
lists:reverse([pgsql_to_rdbms(Item) || Item <- Items]); |
109 |
|
pgsql_to_rdbms({error, #error{codename = unique_violation}}) -> |
110 |
:-( |
{error, duplicate_key}; |
111 |
|
pgsql_to_rdbms({error, #error{message = Message}}) -> |
112 |
:-( |
{error, unicode:characters_to_list(Message)}; |
113 |
|
pgsql_to_rdbms({ok, Count}) -> |
114 |
:-( |
{updated, Count}; |
115 |
|
pgsql_to_rdbms({ok, Count, _Column, Value}) -> |
116 |
:-( |
{updated, Count, Value}; |
117 |
|
pgsql_to_rdbms({ok, _Columns, Rows}) -> |
118 |
:-( |
{selected, Rows}. |
119 |
|
|
120 |
|
-spec replace_question_marks(Statement :: iodata()) -> iodata(). |
121 |
|
replace_question_marks(Statement) when is_list(Statement) -> |
122 |
:-( |
replace_question_marks(iolist_to_binary(Statement)); |
123 |
|
replace_question_marks(Statement) when is_binary(Statement) -> |
124 |
:-( |
[Head | Parts] = binary:split(Statement, <<"?">>, [global]), |
125 |
:-( |
Placeholders = [<<"$", (integer_to_binary(I))/binary>> || I <- lists:seq(1, length(Parts))], |
126 |
:-( |
PartsWithPlaceholders = lists:zipwith(fun(A, B) -> [A, B] end, Placeholders, Parts), |
127 |
:-( |
[Head | PartsWithPlaceholders]. |