├── .gitignore ├── LICENSE ├── Makefile ├── README ├── ebin └── .empty ├── include └── pgsql.hrl ├── rebar ├── rebar.config ├── src ├── epgsql.app.src ├── pgsql.erl ├── pgsql_binary.erl ├── pgsql_connection.erl ├── pgsql_fdatetime.erl ├── pgsql_idatetime.erl ├── pgsql_sock.erl └── pgsql_types.erl ├── test_data ├── epgsql.crt ├── epgsql.key ├── root.crt ├── root.key └── test_schema.sql ├── test_ebin └── .empty └── test_src └── pgsql_tests.erl /.gitignore: -------------------------------------------------------------------------------- 1 | *.beam 2 | *.boot 3 | *.script 4 | ebin/*.app 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2008, Will Glozer 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 5 | 6 | * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 7 | * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 8 | * Neither the name of Will Glozer nor the names of his contributors may be used to endorse or promote products derived from this software without specific prior written permission. 9 | 10 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 11 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | NAME := epgsql 2 | VERSION := $(shell git describe --always --tags) 3 | 4 | ERL := erl 5 | ERLC := erlc 6 | 7 | # ------------------------------------------------------------------------ 8 | 9 | ERLC_FLAGS := -Wall -I include +debug_info 10 | 11 | SRC := $(wildcard src/*.erl) 12 | TESTS := $(wildcard test_src/*.erl) 13 | RELEASE := $(NAME)-$(VERSION).tar.gz 14 | 15 | APPDIR := $(NAME)-$(VERSION) 16 | BEAMS := $(SRC:src/%.erl=ebin/%.beam) 17 | 18 | compile: $(BEAMS) ebin/$(NAME).app 19 | 20 | app: compile 21 | @mkdir -p $(APPDIR)/ebin 22 | @cp -r ebin/* $(APPDIR)/ebin/ 23 | @cp -r include $(APPDIR) 24 | 25 | release: app 26 | @tar czvf $(RELEASE) $(APPDIR) 27 | 28 | clean: 29 | @rm -f ebin/*.beam 30 | @rm -f ebin/$(NAME).app 31 | @rm -rf $(NAME)-$(VERSION) $(NAME)-*.tar.gz 32 | 33 | test: $(TESTS:test_src/%.erl=test_ebin/%.beam) compile 34 | @dialyzer -n --src -c src 35 | $(ERL) -pa ebin/ -pa test_ebin/ -noshell -s pgsql_tests run_tests -s init stop 36 | 37 | # ------------------------------------------------------------------------ 38 | 39 | .SUFFIXES: .erl .beam 40 | .PHONY: app compile clean test 41 | 42 | ebin/%.beam : src/%.erl 43 | $(ERLC) $(ERLC_FLAGS) -o $(dir $@) $< 44 | 45 | ebin/%.app : src/%.app.src Makefile 46 | sed -e s/git/\"$(VERSION)\"/g $< > $@ 47 | 48 | test_ebin/%.beam : test_src/%.erl 49 | $(ERLC) $(ERLC_FLAGS) -o $(dir $@) $< 50 | -------------------------------------------------------------------------------- /README: -------------------------------------------------------------------------------- 1 | Erlang PostgreSQL Database Client 2 | 3 | * Connect 4 | 5 | {ok, C} = pgsql:connect(Host, [Username], [Password], Opts). 6 | 7 | Host - host to connect to. 8 | Username - username to connect as, defaults to $USER. 9 | Password - optional password to authenticate with. 10 | Opts - property list of extra options. Supported properties: 11 | 12 | + {database, String} 13 | + {port, Integer} 14 | + {ssl, Atom} true | false | required 15 | + {ssl_opts, List} see ssl application docs in OTP 16 | + {timeout, Integer} milliseconds, defaults to 5000 17 | + {async, Pid} see Asynchronous Messages section 18 | 19 | {ok, C} = pgsql:connect("localhost", "username", [{database, "test_db"}]). 20 | ok = pgsql:close(C). 21 | 22 | The timeout parameter will trigger an {error, timeout} result when the 23 | server fails to respond within Timeout milliseconds. This timeout applies 24 | to the initial connection attempt and any subsequent queries. 25 | 26 | * Simple Query 27 | 28 | {ok, Columns, Rows} = pgsql:squery(C, "select ..."). 29 | {ok, Count} = pgsql:squery(C, "update ..."). 30 | {ok, Count, Columns, Rows} = pgsql:squery(C, "insert ... returning ..."). 31 | 32 | {error, Error} = pgsql:squery(C, "invalid SQL"). 33 | 34 | Columns - list of column records, see pgsql.hrl for definition. 35 | Rows - list of tuples, one for each row. 36 | Count - integer count of rows inserted/updated/etc 37 | 38 | The simple query protocol returns all columns as text (Erlang binaries) 39 | and does not support binding parameters. 40 | 41 | * Extended Query 42 | 43 | {ok, Columns, Rows} = pgsql:equery(C, "select ...", [Parameters]). 44 | {ok, Count} = pgsql:equery(C, "update ...", [Parameters]). 45 | {ok, Count, Columns, Rows} = pgsql:equery(C, "insert ... returning ...", [Parameters]). 46 | 47 | {error, Error} = pgsql:equery(C, "invalid SQL", [Parameters]). 48 | 49 | Parameters - optional list of values to be bound to $1, $2, $3, etc. 50 | 51 | The extended query protocol combines parse, bind, and execute using 52 | the unnamed prepared statement and portal. A "select" statement returns 53 | {ok, Columns, Rows}, "insert/update/delete" returns {ok, Count} or 54 | {ok, Count, Columns, Rows} when a "returning" clause is present. When 55 | an error occurs, all statements result in {error, #error{}}. 56 | 57 | PostgreSQL's binary format is used to return integers as Erlang 58 | integers, floats as floats, bytea/text/varchar columns as binaries, 59 | bools as true/false, etc. For details see pgsql_binary.erl and the 60 | Data Representation section below. 61 | 62 | * Parse/Bind/Execute 63 | 64 | {ok, Statement} = pgsql:parse(C, [StatementName], Sql, [ParameterTypes]). 65 | 66 | StatementName - optional, reusable, name for the prepared statement. 67 | ParameterTypes - optional list of PostgreSQL types for each parameter. 68 | 69 | For valid type names see pgsql_types.erl. 70 | 71 | ok = pgsql:bind(C, Statement, [PortalName], ParameterValues). 72 | 73 | PortalName - optional name for the result portal. 74 | 75 | {ok | partial, Rows} = pgsql:execute(C, Statement, [PortalName], [MaxRows]). 76 | {ok, Count} = pgsql:execute(C, Statement, [PortalName]). 77 | {ok, Count, Rows} = pgsql:execute(C, Statement, [PortalName]). 78 | 79 | PortalName - optional portal name used in bind/4. 80 | MaxRows - maximum number of rows to return (0 for all rows). 81 | 82 | execute returns {partial, Rows} when more rows are available. 83 | 84 | ok = pgsql:close(C, Statement). 85 | ok = pgsql:close(C, statement | portal, Name). 86 | ok = pgsql:sync(C). 87 | 88 | All functions return {error, Error} when an error occurs. 89 | 90 | * Data Representation 91 | 92 | null = null 93 | bool = true | false 94 | char = $A | binary 95 | intX = 1 96 | floatX = 1.0 97 | date = {Year, Month, Day} 98 | time = {Hour, Minute, Second.Microsecond} 99 | timetz = {time, Timezone} 100 | timestamp = {date, time} 101 | timestamptz = {date, time} 102 | interval = {time, Days, Months} 103 | text = <<"a">> 104 | varchar = <<"a">> 105 | bytea = <<1, 2>> 106 | array = [1, 2, 3] 107 | 108 | record = {int2, time, text, ...} (decode only) 109 | 110 | * Errors 111 | 112 | Errors originating from the PostgreSQL backend are returned as {error, #error{}}, 113 | see pgsql.hrl for the record definition. epgsql functions may also return 114 | {error, What} where What is one of the following: 115 | 116 | {unsupported_auth_method, Method} - required auth method is unsupported 117 | timeout - request timed out 118 | closed - connection was closed 119 | sync_required - error occured and pgsql:sync must be called 120 | 121 | * Asynchronous Messages 122 | 123 | PostgreSQL may deliver two types of asynchronous message: "notices" in response 124 | to notice and warning messages generated by the server, and "notifications" which 125 | are generated by the LISTEN/NOTIFY mechanism. 126 | 127 | Passing the {async, Pid} option to pgsql:connect will result in these async 128 | messages being sent to the specified process, otherwise they will be dropped. 129 | 130 | Message formats: 131 | 132 | {pgsql, Connection, {notification, Channel, Pid, Payload}} 133 | 134 | Connection - connection the notification occured on 135 | 136 | Channel - channel the notification occured on 137 | Pid - database session pid that sent notification 138 | Payload - optional payload, only available from PostgreSQL >= 9.0 139 | 140 | {pgsql, Connection, {notice, Error}} 141 | 142 | Connection - connection the notice occured on 143 | Error - an #error{} record, see pgsql.hrl 144 | 145 | -------------------------------------------------------------------------------- /ebin/.empty: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wg/epgsql/3318bd5d646cad0623ae9dcc6df015bb85258a63/ebin/.empty -------------------------------------------------------------------------------- /include/pgsql.hrl: -------------------------------------------------------------------------------- 1 | -record(column, {name, type, size, modifier, format}). 2 | -record(statement, {name, columns, types}). 3 | 4 | -record(error, {severity, code, message, extra}). 5 | -------------------------------------------------------------------------------- /rebar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wg/epgsql/3318bd5d646cad0623ae9dcc6df015bb85258a63/rebar -------------------------------------------------------------------------------- /rebar.config: -------------------------------------------------------------------------------- 1 | {eunit_compile_opts, [{src_dirs, ["test_src"]}]}. 2 | -------------------------------------------------------------------------------- /src/epgsql.app.src: -------------------------------------------------------------------------------- 1 | {application, epgsql, 2 | [{description, "PostgreSQL Client"}, 3 | {vsn, git}, 4 | {modules, [pgsql, pgsql_binary, pgsql_connection, pgsql_fdatetime, 5 | pgsql_idatetime, pgsql_sock, pgsql_types]}, 6 | {registered, []}, 7 | {applications, [kernel, stdlib, crypto, ssl]}, 8 | {included_applications, []}]}. 9 | -------------------------------------------------------------------------------- /src/pgsql.erl: -------------------------------------------------------------------------------- 1 | %%% Copyright (C) 2008 - Will Glozer. All rights reserved. 2 | 3 | -module(pgsql). 4 | 5 | -export([connect/2, connect/3, connect/4, close/1]). 6 | -export([get_parameter/2, squery/2, equery/2, equery/3]). 7 | -export([parse/2, parse/3, parse/4, describe/2, describe/3]). 8 | -export([bind/3, bind/4, execute/2, execute/3, execute/4]). 9 | -export([close/2, close/3, sync/1]). 10 | -export([with_transaction/2]). 11 | 12 | -include("pgsql.hrl"). 13 | 14 | %% -- client interface -- 15 | 16 | connect(Host, Opts) -> 17 | connect(Host, os:getenv("USER"), "", Opts). 18 | 19 | connect(Host, Username, Opts) -> 20 | connect(Host, Username, "", Opts). 21 | 22 | connect(Host, Username, Password, Opts) -> 23 | {ok, C} = pgsql_connection:start_link(), 24 | pgsql_connection:connect(C, Host, Username, Password, Opts). 25 | 26 | close(C) when is_pid(C) -> 27 | catch pgsql_connection:stop(C), 28 | ok. 29 | 30 | get_parameter(C, Name) -> 31 | pgsql_connection:get_parameter(C, Name). 32 | 33 | squery(C, Sql) -> 34 | ok = pgsql_connection:squery(C, Sql), 35 | case receive_results(C, []) of 36 | [Result] -> Result; 37 | Results -> Results 38 | end. 39 | 40 | equery(C, Sql) -> 41 | equery(C, Sql, []). 42 | 43 | equery(C, Sql, Parameters) -> 44 | case pgsql_connection:parse(C, "", Sql, []) of 45 | {ok, #statement{types = Types} = S} -> 46 | Typed_Parameters = lists:zip(Types, Parameters), 47 | ok = pgsql_connection:equery(C, S, Typed_Parameters), 48 | receive_result(C, undefined); 49 | Error -> 50 | Error 51 | end. 52 | 53 | %% parse 54 | 55 | parse(C, Sql) -> 56 | parse(C, "", Sql, []). 57 | 58 | parse(C, Sql, Types) -> 59 | parse(C, "", Sql, Types). 60 | 61 | parse(C, Name, Sql, Types) -> 62 | pgsql_connection:parse(C, Name, Sql, Types). 63 | 64 | %% bind 65 | 66 | bind(C, Statement, Parameters) -> 67 | bind(C, Statement, "", Parameters). 68 | 69 | bind(C, Statement, PortalName, Parameters) -> 70 | pgsql_connection:bind(C, Statement, PortalName, Parameters). 71 | 72 | %% execute 73 | 74 | execute(C, S) -> 75 | execute(C, S, "", 0). 76 | 77 | execute(C, S, N) -> 78 | execute(C, S, "", N). 79 | 80 | execute(C, S, PortalName, N) -> 81 | pgsql_connection:execute(C, S, PortalName, N), 82 | receive_extended_result(C). 83 | 84 | %% statement/portal functions 85 | 86 | describe(C, #statement{name = Name}) -> 87 | pgsql_connection:describe(C, statement, Name). 88 | 89 | describe(C, Type, Name) -> 90 | pgsql_connection:describe(C, Type, Name). 91 | 92 | close(C, #statement{name = Name}) -> 93 | pgsql_connection:close(C, statement, Name). 94 | 95 | close(C, Type, Name) -> 96 | pgsql_connection:close(C, Type, Name). 97 | 98 | sync(C) -> 99 | pgsql_connection:sync(C). 100 | 101 | %% misc helper functions 102 | with_transaction(C, F) -> 103 | try {ok, [], []} = squery(C, "BEGIN"), 104 | R = F(C), 105 | {ok, [], []} = squery(C, "COMMIT"), 106 | R 107 | catch 108 | _:Why -> 109 | squery(C, "ROLLBACK"), 110 | {rollback, Why} 111 | end. 112 | 113 | %% -- internal functions -- 114 | 115 | receive_result(C, Result) -> 116 | try receive_result(C, [], []) of 117 | done -> Result; 118 | R -> receive_result(C, R) 119 | catch 120 | throw:E -> E 121 | end. 122 | 123 | receive_results(C, Results) -> 124 | try receive_result(C, [], []) of 125 | done -> lists:reverse(Results); 126 | R -> receive_results(C, [R | Results]) 127 | catch 128 | throw:E -> E 129 | end. 130 | 131 | receive_result(C, Cols, Rows) -> 132 | receive 133 | {pgsql, C, {columns, Cols2}} -> 134 | receive_result(C, Cols2, Rows); 135 | {pgsql, C, {data, Row}} -> 136 | receive_result(C, Cols, [Row | Rows]); 137 | {pgsql, C, {error, _E} = Error} -> 138 | Error; 139 | {pgsql, C, {complete, {_Type, Count}}} -> 140 | case Rows of 141 | [] -> {ok, Count}; 142 | _L -> {ok, Count, Cols, lists:reverse(Rows)} 143 | end; 144 | {pgsql, C, {complete, _Type}} -> 145 | {ok, Cols, lists:reverse(Rows)}; 146 | {pgsql, C, done} -> 147 | done; 148 | {pgsql, C, timeout} -> 149 | throw({error, timeout}); 150 | {'EXIT', C, _Reason} -> 151 | throw({error, closed}) 152 | end. 153 | 154 | receive_extended_result(C)-> 155 | receive_extended_result(C, []). 156 | 157 | receive_extended_result(C, Rows) -> 158 | receive 159 | {pgsql, C, {data, Row}} -> 160 | receive_extended_result(C, [Row | Rows]); 161 | {pgsql, C, {error, _E} = Error} -> 162 | Error; 163 | {pgsql, C, suspended} -> 164 | {partial, lists:reverse(Rows)}; 165 | {pgsql, C, {complete, {_Type, Count}}} -> 166 | case Rows of 167 | [] -> {ok, Count}; 168 | _L -> {ok, Count, lists:reverse(Rows)} 169 | end; 170 | {pgsql, C, {complete, _Type}} -> 171 | {ok, lists:reverse(Rows)}; 172 | {pgsql, C, timeout} -> 173 | {error, timeout}; 174 | {'EXIT', C, _Reason} -> 175 | {error, closed} 176 | end. 177 | -------------------------------------------------------------------------------- /src/pgsql_binary.erl: -------------------------------------------------------------------------------- 1 | %%% Copyright (C) 2008 - Will Glozer. All rights reserved. 2 | 3 | -module(pgsql_binary). 4 | 5 | -export([encode/2, decode/2, supports/1]). 6 | 7 | -define(int32, 1/big-signed-unit:32). 8 | -define(datetime, (get(datetime_mod))). 9 | 10 | encode(_Any, null) -> <<-1:?int32>>; 11 | encode(bool, true) -> <<1:?int32, 1:1/big-signed-unit:8>>; 12 | encode(bool, false) -> <<1:?int32, 0:1/big-signed-unit:8>>; 13 | encode(int2, N) -> <<2:?int32, N:1/big-signed-unit:16>>; 14 | encode(int4, N) -> <<4:?int32, N:1/big-signed-unit:32>>; 15 | encode(int8, N) -> <<8:?int32, N:1/big-signed-unit:64>>; 16 | encode(float4, N) -> <<4:?int32, N:1/big-float-unit:32>>; 17 | encode(float8, N) -> <<8:?int32, N:1/big-float-unit:64>>; 18 | encode(bpchar, C) when is_integer(C) -> <<1:?int32, C:1/big-unsigned-unit:8>>; 19 | encode(bpchar, B) when is_binary(B) -> <<(byte_size(B)):?int32, B/binary>>; 20 | encode(time = Type, B) -> ?datetime:encode(Type, B); 21 | encode(timetz = Type, B) -> ?datetime:encode(Type, B); 22 | encode(date = Type, B) -> ?datetime:encode(Type, B); 23 | encode(timestamp = Type, B) -> ?datetime:encode(Type, B); 24 | encode(timestamptz = Type, B) -> ?datetime:encode(Type, B); 25 | encode(interval = Type, B) -> ?datetime:encode(Type, B); 26 | encode(bytea, B) when is_binary(B) -> <<(byte_size(B)):?int32, B/binary>>; 27 | encode(text, B) when is_binary(B) -> <<(byte_size(B)):?int32, B/binary>>; 28 | encode(varchar, B) when is_binary(B) -> <<(byte_size(B)):?int32, B/binary>>; 29 | encode(boolarray, L) when is_list(L) -> encode_array(bool, L); 30 | encode(int2array, L) when is_list(L) -> encode_array(int2, L); 31 | encode(int4array, L) when is_list(L) -> encode_array(int4, L); 32 | encode(int8array, L) when is_list(L) -> encode_array(int8, L); 33 | encode(float4array, L) when is_list(L) -> encode_array(float4, L); 34 | encode(float8array, L) when is_list(L) -> encode_array(float8, L); 35 | encode(chararray, L) when is_list(L) -> encode_array(bpchar, L); 36 | encode(textarray, L) when is_list(L) -> encode_array(text, L); 37 | encode(Type, L) when is_list(L) -> encode(Type, list_to_binary(L)); 38 | encode(_Type, _Value) -> {error, unsupported}. 39 | 40 | decode(bool, <<1:1/big-signed-unit:8>>) -> true; 41 | decode(bool, <<0:1/big-signed-unit:8>>) -> false; 42 | decode(bpchar, <>) -> C; 43 | decode(int2, <>) -> N; 44 | decode(int4, <>) -> N; 45 | decode(int8, <>) -> N; 46 | decode(float4, <>) -> N; 47 | decode(float8, <>) -> N; 48 | decode(record, <<_:?int32, Rest/binary>>) -> list_to_tuple(decode_record(Rest, [])); 49 | decode(time = Type, B) -> ?datetime:decode(Type, B); 50 | decode(timetz = Type, B) -> ?datetime:decode(Type, B); 51 | decode(date = Type, B) -> ?datetime:decode(Type, B); 52 | decode(timestamp = Type, B) -> ?datetime:decode(Type, B); 53 | decode(timestamptz = Type, B) -> ?datetime:decode(Type, B); 54 | decode(interval = Type, B) -> ?datetime:decode(Type, B); 55 | decode(boolarray, B) -> decode_array(B); 56 | decode(int2array, B) -> decode_array(B); 57 | decode(int4array, B) -> decode_array(B); 58 | decode(int8array, B) -> decode_array(B); 59 | decode(float4array, B) -> decode_array(B); 60 | decode(float8array, B) -> decode_array(B); 61 | decode(chararray, B) -> decode_array(B); 62 | decode(textarray, B) -> decode_array(B); 63 | decode(_Other, Bin) -> Bin. 64 | 65 | encode_array(Type, A) -> 66 | {Data, {NDims, Lengths}} = encode_array(Type, A, 0, []), 67 | Oid = pgsql_types:type2oid(Type), 68 | Lens = [<> || N <- lists:reverse(Lengths)], 69 | Hdr = <>, 70 | Bin = iolist_to_binary([Hdr, Lens, Data]), 71 | <<(byte_size(Bin)):?int32, Bin/binary>>. 72 | 73 | encode_array(_Type, [], NDims, Lengths) -> 74 | {<<>>, {NDims, Lengths}}; 75 | encode_array(Type, [H | _] = Array, NDims, Lengths) when not is_list(H) -> 76 | F = fun(E, Len) -> {encode(Type, E), Len + 1} end, 77 | {Data, Len} = lists:mapfoldl(F, 0, Array), 78 | {Data, {NDims + 1, [Len | Lengths]}}; 79 | encode_array(Type, Array, NDims, Lengths) -> 80 | Lengths2 = [length(Array) | Lengths], 81 | F = fun(A2, {_NDims, _Lengths}) -> encode_array(Type, A2, NDims, Lengths2) end, 82 | {Data, {NDims2, Lengths3}} = lists:mapfoldl(F, {NDims, Lengths2}, Array), 83 | {Data, {NDims2 + 1, Lengths3}}. 84 | 85 | decode_array(<>) -> 86 | {Dims, Data} = erlang:split_binary(Rest, NDims * 2 * 4), 87 | Lengths = [Len || <> <= Dims], 88 | Type = pgsql_types:oid2type(Oid), 89 | {Array, <<>>} = decode_array(Data, Type, Lengths), 90 | Array. 91 | 92 | decode_array(Data, _Type, []) -> 93 | {[], Data}; 94 | decode_array(Data, Type, [Len]) -> 95 | decode_elements(Data, Type, [], Len); 96 | decode_array(Data, Type, [Len | T]) -> 97 | F = fun(_N, Rest) -> decode_array(Rest, Type, T) end, 98 | lists:mapfoldl(F, Data, lists:seq(1, Len)). 99 | 100 | decode_elements(Rest, _Type, Acc, 0) -> 101 | {lists:reverse(Acc), Rest}; 102 | decode_elements(<<-1:?int32, Rest/binary>>, Type, Acc, N) -> 103 | decode_elements(Rest, Type, [null | Acc], N - 1); 104 | decode_elements(<>, Type, Acc, N) -> 105 | Value2 = decode(Type, Value), 106 | decode_elements(Rest, Type, [Value2 | Acc], N - 1). 107 | 108 | decode_record(<<>>, Acc) -> 109 | lists:reverse(Acc); 110 | decode_record(<<_Type:?int32, -1:?int32, Rest/binary>>, Acc) -> 111 | decode_record(Rest, [null | Acc]); 112 | decode_record(<>, Acc) -> 113 | Value2 = decode(pgsql_types:oid2type(Type), Value), 114 | decode_record(Rest, [Value2 | Acc]). 115 | 116 | supports(bool) -> true; 117 | supports(bpchar) -> true; 118 | supports(int2) -> true; 119 | supports(int4) -> true; 120 | supports(int8) -> true; 121 | supports(float4) -> true; 122 | supports(float8) -> true; 123 | supports(bytea) -> true; 124 | supports(text) -> true; 125 | supports(varchar) -> true; 126 | supports(record) -> true; 127 | supports(date) -> true; 128 | supports(time) -> true; 129 | supports(timetz) -> true; 130 | supports(timestamp) -> true; 131 | supports(timestamptz) -> true; 132 | supports(interval) -> true; 133 | supports(boolarray) -> true; 134 | supports(int2array) -> true; 135 | supports(int4array) -> true; 136 | supports(int8array) -> true; 137 | supports(float4array) -> true; 138 | supports(float8array) -> true; 139 | supports(chararray) -> true; 140 | supports(textarray) -> true; 141 | supports(_Type) -> false. 142 | -------------------------------------------------------------------------------- /src/pgsql_connection.erl: -------------------------------------------------------------------------------- 1 | %%% Copyright (C) 2008 - Will Glozer. All rights reserved. 2 | 3 | -module(pgsql_connection). 4 | 5 | -behavior(gen_fsm). 6 | 7 | -export([start_link/0, stop/1, connect/5, get_parameter/2]). 8 | -export([squery/2, equery/3]). 9 | -export([parse/4, bind/4, execute/4, describe/3]). 10 | -export([close/3, sync/1]). 11 | 12 | -export([init/1, handle_event/3, handle_sync_event/4]). 13 | -export([handle_info/3, terminate/3, code_change/4]). 14 | 15 | -export([startup/3, auth/2, initializing/2, ready/2, ready/3]). 16 | -export([querying/2, parsing/2, binding/2, describing/2]). 17 | -export([executing/2, closing/2, synchronizing/2, timeout/2]). 18 | -export([aborted/3]). 19 | 20 | -include("pgsql.hrl"). 21 | 22 | -record(state, { 23 | reader, 24 | sock, 25 | timeout, 26 | parameters = [], 27 | reply, 28 | reply_to, 29 | async, 30 | backend, 31 | statement, 32 | txstatus}). 33 | 34 | -define(int16, 1/big-signed-unit:16). 35 | -define(int32, 1/big-signed-unit:32). 36 | 37 | %% -- client interface -- 38 | 39 | start_link() -> 40 | gen_fsm:start_link(?MODULE, [], []). 41 | 42 | stop(C) -> 43 | gen_fsm:send_all_state_event(C, stop). 44 | 45 | connect(C, Host, Username, Password, Opts) -> 46 | gen_fsm:sync_send_event(C, {connect, Host, Username, Password, Opts}, infinity). 47 | 48 | get_parameter(C, Name) -> 49 | gen_fsm:sync_send_event(C, {get_parameter, to_binary(Name)}). 50 | 51 | squery(C, Sql) -> 52 | gen_fsm:sync_send_event(C, {squery, Sql}, infinity). 53 | 54 | equery(C, Statement, Parameters) -> 55 | gen_fsm:sync_send_event(C, {equery, Statement, Parameters}, infinity). 56 | 57 | parse(C, Name, Sql, Types) -> 58 | gen_fsm:sync_send_event(C, {parse, Name, Sql, Types}, infinity). 59 | 60 | bind(C, Statement, PortalName, Parameters) -> 61 | gen_fsm:sync_send_event(C, {bind, Statement, PortalName, Parameters}, infinity). 62 | 63 | execute(C, Statement, PortalName, MaxRows) -> 64 | gen_fsm:sync_send_event(C, {execute, Statement, PortalName, MaxRows}, infinity). 65 | 66 | describe(C, Type, Name) -> 67 | gen_fsm:sync_send_event(C, {describe, Type, Name}, infinity). 68 | 69 | close(C, Type, Name) -> 70 | gen_fsm:sync_send_event(C, {close, Type, Name}, infinity). 71 | 72 | sync(C) -> 73 | gen_fsm:sync_send_event(C, sync, infinity). 74 | 75 | %% -- gen_fsm implementation -- 76 | 77 | init([]) -> 78 | process_flag(trap_exit, true), 79 | {ok, startup, #state{}}. 80 | 81 | handle_event({notice, _Notice} = Msg, State_Name, State) -> 82 | notify_async(State, Msg), 83 | {next_state, State_Name, State}; 84 | 85 | handle_event({notification, _Channel, _Pid, _Payload} = Msg, State_Name, State) -> 86 | notify_async(State, Msg), 87 | {next_state, State_Name, State}; 88 | 89 | handle_event({parameter_status, Name, Value}, State_Name, State) -> 90 | Parameters2 = lists:keystore(Name, 1, State#state.parameters, {Name, Value}), 91 | {next_state, State_Name, State#state{parameters = Parameters2}}; 92 | 93 | handle_event(stop, _State_Name, State) -> 94 | {stop, normal, State}; 95 | 96 | handle_event(Event, _State_Name, State) -> 97 | {stop, {unsupported_event, Event}, State}. 98 | 99 | handle_sync_event(Event, _From, _State_Name, State) -> 100 | {stop, {unsupported_sync_event, Event}, State}. 101 | 102 | handle_info({'EXIT', Pid, Reason}, _State_Name, State = #state{sock = Pid}) -> 103 | {stop, Reason, State}; 104 | 105 | handle_info(Info, _State_Name, State) -> 106 | {stop, {unsupported_info, Info}, State}. 107 | 108 | terminate(_Reason, _State_Name, State = #state{sock = Sock}) 109 | when Sock =/= undefined -> 110 | send(State, $X, []); 111 | 112 | terminate(_Reason, _State_Name, _State) -> 113 | ok. 114 | 115 | code_change(_Old_Vsn, State_Name, State, _Extra) -> 116 | {ok, State_Name, State}. 117 | 118 | %% -- states -- 119 | 120 | startup({connect, Host, Username, Password, Opts}, From, State) -> 121 | Timeout = proplists:get_value(timeout, Opts, 5000), 122 | Async = proplists:get_value(async, Opts, undefined), 123 | case pgsql_sock:start_link(self(), Host, Username, Opts) of 124 | {ok, Sock} -> 125 | put(username, Username), 126 | put(password, Password), 127 | State2 = State#state{ 128 | sock = Sock, 129 | timeout = Timeout, 130 | reply_to = From, 131 | async = Async}, 132 | {next_state, auth, State2, Timeout}; 133 | Error -> 134 | {stop, normal, Error, State} 135 | end. 136 | 137 | %% AuthenticationOk 138 | auth({$R, <<0:?int32>>}, State) -> 139 | #state{timeout = Timeout} = State, 140 | {next_state, initializing, State, Timeout}; 141 | 142 | %% AuthenticationCleartextPassword 143 | auth({$R, <<3:?int32>>}, State) -> 144 | #state{timeout = Timeout} = State, 145 | send(State, $p, [get(password), 0]), 146 | {next_state, auth, State, Timeout}; 147 | 148 | %% AuthenticationMD5Password 149 | auth({$R, <<5:?int32, Salt:4/binary>>}, State) -> 150 | #state{timeout = Timeout} = State, 151 | Digest1 = hex(erlang:md5([get(password), get(username)])), 152 | Str = ["md5", hex(erlang:md5([Digest1, Salt])), 0], 153 | send(State, $p, Str), 154 | {next_state, auth, State, Timeout}; 155 | 156 | auth({$R, <>}, State) -> 157 | case M of 158 | 2 -> Method = kerberosV5; 159 | 4 -> Method = crypt; 160 | 6 -> Method = scm; 161 | 7 -> Method = gss; 162 | 8 -> Method = sspi; 163 | _ -> Method = unknown 164 | end, 165 | Error = {error, {unsupported_auth_method, Method}}, 166 | gen_fsm:reply(State#state.reply_to, Error), 167 | {stop, normal, State}; 168 | 169 | %% ErrorResponse 170 | auth({error, E}, State) -> 171 | case E#error.code of 172 | <<"28000">> -> Why = invalid_authorization_specification; 173 | <<"28P01">> -> Why = invalid_password; 174 | Any -> Why = Any 175 | end, 176 | gen_fsm:reply(State#state.reply_to, {error, Why}), 177 | {stop, normal, State}; 178 | 179 | auth(timeout, State) -> 180 | gen_fsm:reply(State#state.reply_to, {error, timeout}), 181 | {stop, normal, State}. 182 | 183 | %% BackendKeyData 184 | initializing({$K, <>}, State) -> 185 | #state{timeout = Timeout} = State, 186 | State2 = State#state{backend = {Pid, Key}}, 187 | {next_state, initializing, State2, Timeout}; 188 | 189 | %% ErrorResponse 190 | initializing({error, E}, State) -> 191 | case E#error.code of 192 | <<"28000">> -> Why = invalid_authorization_specification; 193 | Any -> Why = Any 194 | end, 195 | gen_fsm:reply(State#state.reply_to, {error, Why}), 196 | {stop, normal, State}; 197 | 198 | initializing(timeout, State) -> 199 | gen_fsm:reply(State#state.reply_to, {error, timeout}), 200 | {stop, normal, State}; 201 | 202 | %% ReadyForQuery 203 | initializing({$Z, <>}, State) -> 204 | #state{parameters = Parameters, reply_to = Reply_To} = State, 205 | erase(username), 206 | erase(password), 207 | case lists:keysearch(<<"integer_datetimes">>, 1, Parameters) of 208 | {value, {_, <<"on">>}} -> put(datetime_mod, pgsql_idatetime); 209 | {value, {_, <<"off">>}} -> put(datetime_mod, pgsql_fdatetime) 210 | end, 211 | gen_fsm:reply(Reply_To, {ok, self()}), 212 | {next_state, ready, State#state{txstatus = Status}}. 213 | 214 | ready(_Msg, State) -> 215 | {next_state, ready, State}. 216 | 217 | %% execute simple query 218 | ready({squery, Sql}, From, State) -> 219 | #state{timeout = Timeout} = State, 220 | send(State, $Q, [Sql, 0]), 221 | State2 = State#state{statement = #statement{}, reply_to = From}, 222 | {reply, ok, querying, State2, Timeout}; 223 | 224 | %% execute extended query 225 | ready({equery, Statement, Parameters}, From, State) -> 226 | #state{timeout = Timeout} = State, 227 | #statement{name = StatementName, columns = Columns} = Statement, 228 | Bin1 = encode_parameters(Parameters), 229 | Bin2 = encode_formats(Columns), 230 | send(State, $B, ["", 0, StatementName, 0, Bin1, Bin2]), 231 | send(State, $E, ["", 0, <<0:?int32>>]), 232 | send(State, $C, [$S, "", 0]), 233 | send(State, $S, []), 234 | State2 = State#state{statement = Statement, reply_to = From}, 235 | {reply, ok, querying, State2, Timeout}; 236 | 237 | ready({get_parameter, Name}, _From, State) -> 238 | case lists:keysearch(Name, 1, State#state.parameters) of 239 | {value, {Name, Value}} -> Value; 240 | false -> Value = undefined 241 | end, 242 | {reply, {ok, Value}, ready, State}; 243 | 244 | ready({parse, Name, Sql, Types}, From, State) -> 245 | #state{timeout = Timeout} = State, 246 | Bin = encode_types(Types), 247 | send(State, $P, [Name, 0, Sql, 0, Bin]), 248 | send(State, $D, [$S, Name, 0]), 249 | send(State, $H, []), 250 | S = #statement{name = Name}, 251 | State2 = State#state{statement = S, reply_to = From}, 252 | {next_state, parsing, State2, Timeout}; 253 | 254 | ready({bind, Statement, PortalName, Parameters}, From, State) -> 255 | #state{timeout = Timeout} = State, 256 | #statement{name = StatementName, columns = Columns, types = Types} = Statement, 257 | Typed_Parameters = lists:zip(Types, Parameters), 258 | Bin1 = encode_parameters(Typed_Parameters), 259 | Bin2 = encode_formats(Columns), 260 | send(State, $B, [PortalName, 0, StatementName, 0, Bin1, Bin2]), 261 | send(State, $H, []), 262 | State2 = State#state{statement = Statement, reply_to = From}, 263 | {next_state, binding, State2, Timeout}; 264 | 265 | ready({execute, Statement, PortalName, MaxRows}, From, State) -> 266 | #state{timeout = Timeout} = State, 267 | send(State, $E, [PortalName, 0, <>]), 268 | send(State, $H, []), 269 | State2 = State#state{statement = Statement, reply_to = From}, 270 | {reply, ok, executing, State2, Timeout}; 271 | 272 | ready({describe, Type, Name}, From, State) -> 273 | #state{timeout = Timeout} = State, 274 | case Type of 275 | statement -> Type2 = $S; 276 | portal -> Type2 = $P 277 | end, 278 | send(State, $D, [Type2, Name, 0]), 279 | send(State, $H, []), 280 | {next_state, describing, State#state{reply_to = From}, Timeout}; 281 | 282 | ready({close, Type, Name}, From, State) -> 283 | #state{timeout = Timeout} = State, 284 | case Type of 285 | statement -> Type2 = $S; 286 | portal -> Type2 = $P 287 | end, 288 | send(State, $C, [Type2, Name, 0]), 289 | send(State, $H, []), 290 | {next_state, closing, State#state{reply_to = From}, Timeout}; 291 | 292 | ready(sync, From, State) -> 293 | #state{timeout = Timeout} = State, 294 | send(State, $S, []), 295 | State2 = State#state{reply = ok, reply_to = From}, 296 | {next_state, synchronizing, State2, Timeout}. 297 | 298 | %% BindComplete 299 | querying({$2, <<>>}, State) -> 300 | #state{timeout = Timeout, statement = #statement{columns = Columns}} = State, 301 | notify(State, {columns, Columns}), 302 | {next_state, querying, State, Timeout}; 303 | 304 | %% CloseComplete 305 | querying({$3, <<>>}, State) -> 306 | #state{timeout = Timeout} = State, 307 | {next_state, querying, State, Timeout}; 308 | 309 | %% RowDescription 310 | querying({$T, <>}, State) -> 311 | #state{timeout = Timeout} = State, 312 | Columns = decode_columns(Count, Bin), 313 | S2 = (State#state.statement)#statement{columns = Columns}, 314 | notify(State, {columns, Columns}), 315 | {next_state, querying, State#state{statement = S2}, Timeout}; 316 | 317 | %% DataRow 318 | querying({$D, <<_Count:?int16, Bin/binary>>}, State) -> 319 | #state{timeout = Timeout, statement = #statement{columns = Columns}} = State, 320 | Data = decode_data(Columns, Bin), 321 | notify(State, {data, Data}), 322 | {next_state, querying, State, Timeout}; 323 | 324 | %% CommandComplete 325 | querying({$C, Bin}, State) -> 326 | #state{timeout = Timeout} = State, 327 | Complete = decode_complete(Bin), 328 | notify(State, {complete, Complete}), 329 | {next_state, querying, State, Timeout}; 330 | 331 | %% EmptyQueryResponse 332 | querying({$I, _Bin}, State) -> 333 | #state{timeout = Timeout} = State, 334 | notify(State, {complete, empty}), 335 | {next_state, querying, State, Timeout}; 336 | 337 | querying(timeout, State) -> 338 | #state{sock = Sock, timeout = Timeout, backend = {Pid, Key}} = State, 339 | pgsql_sock:cancel(Sock, Pid, Key), 340 | {next_state, timeout, State, Timeout}; 341 | 342 | %% ErrorResponse 343 | querying({error, E}, State) -> 344 | #state{timeout = Timeout} = State, 345 | notify(State, {error, E}), 346 | {next_state, querying, State, Timeout}; 347 | 348 | %% ReadyForQuery 349 | querying({$Z, <<_Status:8>>}, State) -> 350 | notify(State, done), 351 | {next_state, ready, State#state{reply_to = undefined}}. 352 | 353 | %% ParseComplete 354 | parsing({$1, <<>>}, State) -> 355 | #state{timeout = Timeout} = State, 356 | {next_state, describing, State, Timeout}; 357 | 358 | parsing(timeout, State) -> 359 | #state{timeout = Timeout} = State, 360 | Reply = {error, timeout}, 361 | send(State, $S, []), 362 | {next_state, parsing, State#state{reply = Reply}, Timeout}; 363 | 364 | %% ErrorResponse 365 | parsing({error, E}, State) -> 366 | #state{timeout = Timeout} = State, 367 | Reply = {error, E}, 368 | send(State, $S, []), 369 | {next_state, parsing, State#state{reply = Reply}, Timeout}; 370 | 371 | %% ReadyForQuery 372 | parsing({$Z, <>}, State) -> 373 | #state{reply = Reply, reply_to = Reply_To} = State, 374 | gen_fsm:reply(Reply_To, Reply), 375 | {next_state, ready, State#state{reply = undefined, txstatus = Status}}. 376 | 377 | %% BindComplete 378 | binding({$2, <<>>}, State) -> 379 | gen_fsm:reply(State#state.reply_to, ok), 380 | {next_state, ready, State}; 381 | 382 | binding(timeout, State) -> 383 | #state{timeout = Timeout} = State, 384 | Reply = {error, timeout}, 385 | send(State, $S, []), 386 | {next_state, binding, State#state{reply = Reply}, Timeout}; 387 | 388 | %% ErrorResponse 389 | binding({error, E}, State) -> 390 | #state{timeout = Timeout} = State, 391 | Reply = {error, E}, 392 | send(State, $S, []), 393 | {next_state, binding, State#state{reply = Reply}, Timeout}; 394 | 395 | %% ReadyForQuery 396 | binding({$Z, <>}, State) -> 397 | #state{reply = Reply, reply_to = Reply_To} = State, 398 | gen_fsm:reply(Reply_To, Reply), 399 | {next_state, ready, State#state{reply = undefined, txstatus = Status}}. 400 | 401 | %% ParameterDescription 402 | describing({$t, <<_Count:?int16, Bin/binary>>}, State) -> 403 | #state{timeout = Timeout} = State, 404 | Types = [pgsql_types:oid2type(Oid) || <> <= Bin], 405 | S2 = (State#state.statement)#statement{types = Types}, 406 | {next_state, describing, State#state{statement = S2}, Timeout}; 407 | 408 | %% RowDescription 409 | describing({$T, <>}, State) -> 410 | Columns = decode_columns(Count, Bin), 411 | Columns2 = [C#column{format = format(C#column.type)} || C <- Columns], 412 | S2 = (State#state.statement)#statement{columns = Columns2}, 413 | gen_fsm:reply(State#state.reply_to, {ok, S2}), 414 | {next_state, ready, State}; 415 | 416 | %% NoData 417 | describing({$n, <<>>}, State) -> 418 | S2 = (State#state.statement)#statement{columns = []}, 419 | gen_fsm:reply(State#state.reply_to, {ok, S2}), 420 | {next_state, ready, State}; 421 | 422 | describing(timeout, State) -> 423 | #state{timeout = Timeout} = State, 424 | Reply = {error, timeout}, 425 | send(State, $S, []), 426 | {next_state, describing, State#state{reply = Reply}, Timeout}; 427 | 428 | %% ErrorResponse 429 | describing({error, E}, State) -> 430 | #state{timeout = Timeout} = State, 431 | Reply = {error, E}, 432 | send(State, $S, []), 433 | {next_state, describing, State#state{reply = Reply}, Timeout}; 434 | 435 | %% ReadyForQuery 436 | describing({$Z, <>}, State) -> 437 | #state{reply = Reply, reply_to = Reply_To} = State, 438 | gen_fsm:reply(Reply_To, Reply), 439 | {next_state, ready, State#state{reply = undefined, txstatus = Status}}. 440 | 441 | %% DataRow 442 | executing({$D, <<_Count:?int16, Bin/binary>>}, State) -> 443 | #state{timeout = Timeout, statement = #statement{columns = Columns}} = State, 444 | Data = decode_data(Columns, Bin), 445 | notify(State, {data, Data}), 446 | {next_state, executing, State, Timeout}; 447 | 448 | %% PortalSuspended 449 | executing({$s, <<>>}, State) -> 450 | notify(State, suspended), 451 | {next_state, ready, State}; 452 | 453 | %% CommandComplete 454 | executing({$C, Bin}, State) -> 455 | notify(State, {complete, decode_complete(Bin)}), 456 | {next_state, ready, State}; 457 | 458 | %% EmptyQueryResponse 459 | executing({$I, _Bin}, State) -> 460 | notify(State, {complete, empty}), 461 | {next_state, ready, State}; 462 | 463 | executing(timeout, State) -> 464 | #state{sock = Sock, timeout = Timeout, backend = {Pid, Key}} = State, 465 | pgsql_sock:cancel(Sock, Pid, Key), 466 | send(State, $S, []), 467 | {next_state, timeout, State, Timeout}; 468 | 469 | %% ErrorResponse 470 | executing({error, E}, State) -> 471 | #state{timeout = Timeout} = State, 472 | notify(State, {error, E}), 473 | {next_state, aborted, State, Timeout}. 474 | 475 | %% CloseComplete 476 | closing({$3, <<>>}, State) -> 477 | gen_fsm:reply(State#state.reply_to, ok), 478 | {next_state, ready, State}; 479 | 480 | closing(timeout, State) -> 481 | gen_fsm:reply(State#state.reply_to, {error, timeout}), 482 | {next_state, ready, State}; 483 | 484 | %% ErrorResponse 485 | closing({error, E}, State) -> 486 | Error = {error, E}, 487 | gen_fsm:reply(State#state.reply_to, Error), 488 | {next_state, ready, State}. 489 | 490 | %% ErrorResponse 491 | synchronizing({error, E}, State) -> 492 | #state{timeout = Timeout} = State, 493 | Reply = {error, E}, 494 | {next_state, synchronizing, State#state{reply = Reply}, Timeout}; 495 | 496 | synchronizing(timeout, State) -> 497 | #state{timeout = Timeout} = State, 498 | Reply = {error, timeout}, 499 | {next_state, synchronizing, State#state{reply = Reply}, Timeout}; 500 | 501 | %% ReadyForQuery 502 | synchronizing({$Z, <>}, State) -> 503 | #state{reply = Reply, reply_to = Reply_To} = State, 504 | gen_fsm:reply(Reply_To, Reply), 505 | {next_state, ready, State#state{reply = undefined, txstatus = Status}}. 506 | 507 | timeout({$Z, <>}, State) -> 508 | notify(State, timeout), 509 | {next_state, ready, State#state{txstatus = Status}}; 510 | 511 | timeout(timeout, State) -> 512 | {stop, timeout, State}; 513 | 514 | %% ignore events that occur after timeout 515 | timeout(_Event, State) -> 516 | #state{timeout = Timeout} = State, 517 | {next_state, timeout, State, Timeout}. 518 | 519 | aborted(sync, From, State) -> 520 | #state{timeout = Timeout} = State, 521 | send(State, $S, []), 522 | State2 = State#state{reply = ok, reply_to = From}, 523 | {next_state, synchronizing, State2, Timeout}; 524 | 525 | aborted(_Msg, _From, State) -> 526 | #state{timeout = Timeout} = State, 527 | {reply, {error, sync_required}, aborted, State, Timeout}. 528 | 529 | %% -- internal functions -- 530 | 531 | %% decode data 532 | decode_data(Columns, Bin) -> 533 | decode_data(Columns, Bin, []). 534 | 535 | decode_data([], _Bin, Acc) -> 536 | list_to_tuple(lists:reverse(Acc)); 537 | decode_data([_C | T], <<-1:?int32, Rest/binary>>, Acc) -> 538 | decode_data(T, Rest, [null | Acc]); 539 | decode_data([C | T], <>, Acc) -> 540 | case C of 541 | #column{type = Type, format = 1} -> Value2 = pgsql_binary:decode(Type, Value); 542 | #column{} -> Value2 = Value 543 | end, 544 | decode_data(T, Rest, [Value2 | Acc]). 545 | 546 | %% decode column information 547 | decode_columns(Count, Bin) -> 548 | decode_columns(Count, Bin, []). 549 | 550 | decode_columns(0, _Bin, Acc) -> 551 | lists:reverse(Acc); 552 | decode_columns(N, Bin, Acc) -> 553 | {Name, Rest} = pgsql_sock:decode_string(Bin), 554 | <<_Table_Oid:?int32, _Attrib_Num:?int16, Type_Oid:?int32, 555 | Size:?int16, Modifier:?int32, Format:?int16, Rest2/binary>> = Rest, 556 | Desc = #column{ 557 | name = Name, 558 | type = pgsql_types:oid2type(Type_Oid), 559 | size = Size, 560 | modifier = Modifier, 561 | format = Format}, 562 | decode_columns(N - 1, Rest2, [Desc | Acc]). 563 | 564 | %% decode command complete msg 565 | decode_complete(<<"SELECT", 0>>) -> select; 566 | decode_complete(<<"SELECT", _/binary>>) -> select; 567 | decode_complete(<<"BEGIN", 0>>) -> 'begin'; 568 | decode_complete(<<"ROLLBACK", 0>>) -> rollback; 569 | decode_complete(Bin) -> 570 | {Str, _} = pgsql_sock:decode_string(Bin), 571 | case string:tokens(binary_to_list(Str), " ") of 572 | ["INSERT", _Oid, Rows] -> {insert, list_to_integer(Rows)}; 573 | ["UPDATE", Rows] -> {update, list_to_integer(Rows)}; 574 | ["DELETE", Rows] -> {delete, list_to_integer(Rows)}; 575 | ["MOVE", Rows] -> {move, list_to_integer(Rows)}; 576 | ["FETCH", Rows] -> {fetch, list_to_integer(Rows)}; 577 | [Type | _Rest] -> pgsql_sock:lower_atom(Type) 578 | end. 579 | 580 | %% encode types 581 | encode_types(Types) -> 582 | encode_types(Types, 0, <<>>). 583 | 584 | encode_types([], Count, Acc) -> 585 | <>; 586 | 587 | encode_types([Type | T], Count, Acc) -> 588 | case Type of 589 | undefined -> Oid = 0; 590 | _Any -> Oid = pgsql_types:type2oid(Type) 591 | end, 592 | encode_types(T, Count + 1, <>). 593 | 594 | %% encode column formats 595 | encode_formats(Columns) -> 596 | encode_formats(Columns, 0, <<>>). 597 | 598 | encode_formats([], Count, Acc) -> 599 | <>; 600 | 601 | encode_formats([#column{format = Format} | T], Count, Acc) -> 602 | encode_formats(T, Count + 1, <>). 603 | 604 | format(Type) -> 605 | case pgsql_binary:supports(Type) of 606 | true -> 1; 607 | false -> 0 608 | end. 609 | 610 | %% encode parameters 611 | encode_parameters(Parameters) -> 612 | encode_parameters(Parameters, 0, <<>>, <<>>). 613 | 614 | encode_parameters([], Count, Formats, Values) -> 615 | <>; 616 | 617 | encode_parameters([P | T], Count, Formats, Values) -> 618 | {Format, Value} = encode_parameter(P), 619 | Formats2 = <>, 620 | Values2 = <>, 621 | encode_parameters(T, Count + 1, Formats2, Values2). 622 | 623 | %% encode parameter 624 | 625 | encode_parameter({Type, Value}) -> 626 | case pgsql_binary:encode(Type, Value) of 627 | Bin when is_binary(Bin) -> {1, Bin}; 628 | {error, unsupported} -> encode_parameter(Value) 629 | end; 630 | encode_parameter(A) when is_atom(A) -> {0, encode_list(atom_to_list(A))}; 631 | encode_parameter(B) when is_binary(B) -> {0, <<(byte_size(B)):?int32, B/binary>>}; 632 | encode_parameter(I) when is_integer(I) -> {0, encode_list(integer_to_list(I))}; 633 | encode_parameter(F) when is_float(F) -> {0, encode_list(float_to_list(F))}; 634 | encode_parameter(L) when is_list(L) -> {0, encode_list(L)}. 635 | 636 | encode_list(L) -> 637 | Bin = list_to_binary(L), 638 | <<(byte_size(Bin)):?int32, Bin/binary>>. 639 | 640 | notify(#state{reply_to = {Pid, _Tag}}, Msg) -> 641 | Pid ! {pgsql, self(), Msg}. 642 | 643 | notify_async(#state{async = Pid}, Msg) -> 644 | case is_pid(Pid) of 645 | true -> Pid ! {pgsql, self(), Msg}; 646 | false -> false 647 | end. 648 | 649 | to_binary(B) when is_binary(B) -> B; 650 | to_binary(L) when is_list(L) -> list_to_binary(L). 651 | 652 | hex(Bin) -> 653 | HChar = fun(N) when N < 10 -> $0 + N; 654 | (N) when N < 16 -> $W + N 655 | end, 656 | <<<<(HChar(H)), (HChar(L))>> || <> <= Bin>>. 657 | 658 | %% send data to server 659 | 660 | send(#state{sock = Sock}, Type, Data) -> 661 | pgsql_sock:send(Sock, Type, Data). 662 | -------------------------------------------------------------------------------- /src/pgsql_fdatetime.erl: -------------------------------------------------------------------------------- 1 | %%% Copyright (C) 2008 - Will Glozer. All rights reserved. 2 | 3 | -module(pgsql_fdatetime). 4 | 5 | -export([decode/2, encode/2]). 6 | 7 | -define(int32, 1/big-signed-unit:32). 8 | 9 | -define(postgres_epoc_jdate, 2451545). 10 | 11 | -define(mins_per_hour, 60). 12 | -define(secs_per_day, 86400.0). 13 | -define(secs_per_hour, 3600.0). 14 | -define(secs_per_minute, 60.0). 15 | 16 | decode(date, <>) -> j2date(?postgres_epoc_jdate + J); 17 | decode(time, <>) -> f2time(N); 18 | decode(timetz, <>) -> {f2time(N), TZ}; 19 | decode(timestamp, <>) -> f2timestamp(N); 20 | decode(timestamptz, <>) -> f2timestamp(N); 21 | decode(interval, <>) -> {f2time(N), D, M}. 22 | 23 | encode(date, D) -> <<4:?int32, (date2j(D) - ?postgres_epoc_jdate):1/big-signed-unit:32>>; 24 | encode(time, T) -> <<8:?int32, (time2f(T)):1/big-float-unit:64>>; 25 | encode(timetz, {T, TZ}) -> <<12:?int32, (time2f(T)):1/big-float-unit:64, TZ:?int32>>; 26 | encode(timestamp, TS) -> <<8:?int32, (timestamp2f(TS)):1/big-float-unit:64>>; 27 | encode(timestamptz, TS) -> <<8:?int32, (timestamp2f(TS)):1/big-float-unit:64>>; 28 | encode(interval, {T, D, M}) -> <<16:?int32, (time2f(T)):1/big-float-unit:64, D:?int32, M:?int32>>. 29 | 30 | j2date(N) -> 31 | J = N + 32044, 32 | Q1 = J div 146097, 33 | Extra = (J - Q1 * 146097) * 4 + 3, 34 | J2 = J + 60 + Q1 * 3 + Extra div 146097, 35 | Q2 = J2 div 1461, 36 | J3 = J2 - Q2 * 1461, 37 | Y = J3 * 4 div 1461, 38 | case Y of 39 | 0 -> J4 = ((J3 + 306) rem 366) + 123; 40 | _ -> J4 = ((J3 + 305) rem 365) + 123 41 | end, 42 | Year = (Y + Q2 * 4) - 4800, 43 | Q3 = J4 * 2141 div 65536, 44 | Day = J4 - 7834 * Q3 div 256, 45 | Month = (Q3 + 10) rem 12 + 1, 46 | {Year, Month, Day}. 47 | 48 | date2j({Y, M, D}) -> 49 | case M > 2 of 50 | true -> 51 | M2 = M + 1, 52 | Y2 = Y + 4800; 53 | false -> 54 | M2 = M + 13, 55 | Y2 = Y + 4799 56 | end, 57 | C = Y2 div 100, 58 | J1 = Y2 * 365 - 32167, 59 | J2 = J1 + (Y2 div 4 - C + C div 4), 60 | J2 + 7834 * M2 div 256 + D. 61 | 62 | f2time(N) -> 63 | {R1, Hour} = tmodulo(N, ?secs_per_hour), 64 | {R2, Min} = tmodulo(R1, ?secs_per_minute), 65 | {R3, Sec} = tmodulo(R2, 1.0), 66 | case timeround(R3) of 67 | US when US >= 1.0 -> f2time(ceiling(N)); 68 | US -> {Hour, Min, Sec + US} 69 | end. 70 | 71 | time2f({H, M, S}) -> 72 | ((H * ?mins_per_hour + M) * ?secs_per_minute) + S. 73 | 74 | f2timestamp(N) -> 75 | case tmodulo(N, ?secs_per_day) of 76 | {T, D} when T < 0 -> f2timestamp2(D - 1 + ?postgres_epoc_jdate, T + ?secs_per_day); 77 | {T, D} -> f2timestamp2(D + ?postgres_epoc_jdate, T) 78 | end. 79 | 80 | f2timestamp2(D, T) -> 81 | {_H, _M, S} = Time = f2time(T), 82 | Date = j2date(D), 83 | case tsround(S - trunc(S)) of 84 | N when N >= 1.0 -> 85 | case ceiling(T) of 86 | T2 when T2 > ?secs_per_day -> f2timestamp2(D + 1, 0.0); 87 | T2 -> f2timestamp2(T2, D) 88 | end; 89 | _ -> ok 90 | end, 91 | {Date, Time}. 92 | 93 | timestamp2f({Date, Time}) -> 94 | D = date2j(Date) - ?postgres_epoc_jdate, 95 | D * ?secs_per_day + time2f(Time). 96 | 97 | tmodulo(T, U) -> 98 | case T < 0 of 99 | true -> Q = ceiling(T / U); 100 | false -> Q = floor(T / U) 101 | end, 102 | case Q of 103 | 0 -> {T, Q}; 104 | _ -> {T - rint(Q * U), Q} 105 | end. 106 | 107 | rint(N) -> round(N) * 1.0. 108 | timeround(J) -> rint(J * 10000000000.0) / 10000000000.0. 109 | tsround(J) -> rint(J * 1000000.0) / 1000000.0. 110 | 111 | floor(X) -> 112 | T = erlang:trunc(X), 113 | case (X - T) of 114 | N when N < 0 -> T - 1; 115 | N when N > 0 -> T; 116 | _ -> T 117 | end. 118 | 119 | ceiling(X) -> 120 | T = erlang:trunc(X), 121 | case (X - T) of 122 | N when N < 0 -> T; 123 | N when N > 0 -> T + 1; 124 | _ -> T 125 | end. 126 | -------------------------------------------------------------------------------- /src/pgsql_idatetime.erl: -------------------------------------------------------------------------------- 1 | %%% Copyright (C) 2008 - Will Glozer. All rights reserved. 2 | 3 | -module(pgsql_idatetime). 4 | 5 | -export([decode/2, encode/2]). 6 | 7 | -define(int32, 1/big-signed-unit:32). 8 | -define(int64, 1/big-signed-unit:64). 9 | 10 | -define(postgres_epoc_jdate, 2451545). 11 | 12 | -define(mins_per_hour, 60). 13 | -define(secs_per_minute, 60). 14 | 15 | -define(usecs_per_day, 86400000000). 16 | -define(usecs_per_hour, 3600000000). 17 | -define(usecs_per_minute, 60000000). 18 | -define(usecs_per_sec, 1000000). 19 | 20 | decode(date, <>) -> j2date(?postgres_epoc_jdate + J); 21 | decode(time, <>) -> i2time(N); 22 | decode(timetz, <>) -> {i2time(N), TZ}; 23 | decode(timestamp, <>) -> i2timestamp(N); 24 | decode(timestamptz, <>) -> i2timestamp(N); 25 | decode(interval, <>) -> {i2time(N), D, M}. 26 | 27 | encode(date, D) -> <<4:?int32, (date2j(D) - ?postgres_epoc_jdate):?int32>>; 28 | encode(time, T) -> <<8:?int32, (time2i(T)):?int64>>; 29 | encode(timetz, {T, TZ}) -> <<12:?int32, (time2i(T)):?int64, TZ:?int32>>; 30 | encode(timestamp, TS) -> <<8:?int32, (timestamp2i(TS)):?int64>>; 31 | encode(timestamptz, TS) -> <<8:?int32, (timestamp2i(TS)):?int64>>; 32 | encode(interval, {T, D, M}) -> <<16:?int32, (time2i(T)):?int64, D:?int32, M:?int32>>. 33 | 34 | j2date(N) -> 35 | J = N + 32044, 36 | Q1 = J div 146097, 37 | Extra = (J - Q1 * 146097) * 4 + 3, 38 | J2 = J + 60 + Q1 * 3 + Extra div 146097, 39 | Q2 = J2 div 1461, 40 | J3 = J2 - Q2 * 1461, 41 | Y = J3 * 4 div 1461, 42 | case Y of 43 | 0 -> J4 = ((J3 + 306) rem 366) + 123; 44 | _ -> J4 = ((J3 + 305) rem 365) + 123 45 | end, 46 | Year = (Y + Q2 * 4) - 4800, 47 | Q3 = J4 * 2141 div 65536, 48 | Day = J4 - 7834 * Q3 div 256, 49 | Month = (Q3 + 10) rem 12 + 1, 50 | {Year, Month, Day}. 51 | 52 | date2j({Y, M, D}) -> 53 | case M > 2 of 54 | true -> 55 | M2 = M + 1, 56 | Y2 = Y + 4800; 57 | false -> 58 | M2 = M + 13, 59 | Y2 = Y + 4799 60 | end, 61 | C = Y2 div 100, 62 | J1 = Y2 * 365 - 32167, 63 | J2 = J1 + (Y2 div 4 - C + C div 4), 64 | J2 + 7834 * M2 div 256 + D. 65 | 66 | i2time(N) -> 67 | Hour = N div ?usecs_per_hour, 68 | R1 = N - Hour * ?usecs_per_hour, 69 | Min = R1 div ?usecs_per_minute, 70 | R2 = R1 - Min * ?usecs_per_minute, 71 | Sec = R2 div ?usecs_per_sec, 72 | US = R2 - Sec * ?usecs_per_sec, 73 | {Hour, Min, Sec + US / ?usecs_per_sec}. 74 | 75 | time2i({H, M, S}) -> 76 | US = trunc(round(S * ?usecs_per_sec)), 77 | ((H * ?mins_per_hour + M) * ?secs_per_minute) * ?usecs_per_sec + US. 78 | 79 | i2timestamp(N) -> 80 | case tmodulo(N, ?usecs_per_day) of 81 | {T, D} when T < 0 -> i2timestamp2(D - 1 + ?postgres_epoc_jdate, T + ?usecs_per_day); 82 | {T, D} -> i2timestamp2(D + ?postgres_epoc_jdate, T) 83 | end. 84 | 85 | i2timestamp2(D, T) -> 86 | {j2date(D), i2time(T)}. 87 | 88 | timestamp2i({Date, Time}) -> 89 | D = date2j(Date) - ?postgres_epoc_jdate, 90 | D * ?usecs_per_day + time2i(Time). 91 | 92 | tmodulo(T, U) -> 93 | case T div U of 94 | 0 -> {T, 0}; 95 | Q -> {T - (Q * U), Q} 96 | end. 97 | -------------------------------------------------------------------------------- /src/pgsql_sock.erl: -------------------------------------------------------------------------------- 1 | %%% Copyright (C) 2009 - Will Glozer. All rights reserved. 2 | 3 | -module(pgsql_sock). 4 | 5 | -behavior(gen_server). 6 | 7 | -export([start_link/4, send/2, send/3, cancel/3]). 8 | -export([decode_string/1, lower_atom/1]). 9 | 10 | -export([handle_call/3, handle_cast/2, handle_info/2]). 11 | -export([init/1, code_change/3, terminate/2]). 12 | 13 | -include("pgsql.hrl"). 14 | 15 | -record(state, {c, mod, sock, tail}). 16 | 17 | -define(int16, 1/big-signed-unit:16). 18 | -define(int32, 1/big-signed-unit:32). 19 | 20 | %% -- client interface -- 21 | 22 | start_link(C, Host, Username, Opts) -> 23 | gen_server:start_link(?MODULE, [C, Host, Username, Opts], []). 24 | 25 | send(S, Type, Data) -> 26 | Bin = iolist_to_binary(Data), 27 | Msg = <>, 28 | gen_server:cast(S, {send, Msg}). 29 | 30 | send(S, Data) -> 31 | Bin = iolist_to_binary(Data), 32 | Msg = <<(byte_size(Bin) + 4):?int32, Bin/binary>>, 33 | gen_server:cast(S, {send, Msg}). 34 | 35 | cancel(S, Pid, Key) -> 36 | gen_server:cast(S, {cancel, Pid, Key}). 37 | 38 | %% -- gen_server implementation -- 39 | 40 | init([C, Host, Username, Opts]) -> 41 | process_flag(trap_exit, true), 42 | 43 | Opts2 = ["user", 0, Username, 0], 44 | case proplists:get_value(database, Opts, undefined) of 45 | undefined -> Opts3 = Opts2; 46 | Database -> Opts3 = [Opts2 | ["database", 0, Database, 0]] 47 | end, 48 | 49 | Port = proplists:get_value(port, Opts, 5432), 50 | SockOpts = [{active, false}, {packet, raw}, binary, {nodelay, true}], 51 | {ok, S} = gen_tcp:connect(Host, Port, SockOpts), 52 | 53 | State = #state{ 54 | c = C, 55 | mod = gen_tcp, 56 | sock = S, 57 | tail = <<>>}, 58 | 59 | case proplists:get_value(ssl, Opts) of 60 | T when T == true; T == required -> 61 | ok = gen_tcp:send(S, <<8:?int32, 80877103:?int32>>), 62 | {ok, <>} = gen_tcp:recv(S, 1), 63 | State2 = start_ssl(Code, T, Opts, State); 64 | _ -> 65 | State2 = State 66 | end, 67 | 68 | setopts(State2, [{active, true}]), 69 | send(self(), [<<196608:32>>, Opts3, 0]), 70 | {ok, State2}. 71 | 72 | handle_call(Call, _From, State) -> 73 | {stop, {unsupported_call, Call}, State}. 74 | 75 | handle_cast({send, Data}, State) -> 76 | #state{mod = Mod, sock = Sock} = State, 77 | ok = Mod:send(Sock, Data), 78 | {noreply, State}; 79 | 80 | handle_cast({cancel, Pid, Key}, State) -> 81 | {ok, {Addr, Port}} = inet:peername(State#state.sock), 82 | SockOpts = [{active, false}, {packet, raw}, binary], 83 | {ok, Sock} = gen_tcp:connect(Addr, Port, SockOpts), 84 | Msg = <<16:?int32, 80877102:?int32, Pid:?int32, Key:?int32>>, 85 | ok = gen_tcp:send(Sock, Msg), 86 | gen_tcp:close(Sock), 87 | {noreply, State}; 88 | 89 | handle_cast(Cast, State) -> 90 | {stop, {unsupported_cast, Cast}, State}. 91 | 92 | handle_info({_, _Sock, Data}, #state{tail = Tail} = State) -> 93 | State2 = decode(<>, State), 94 | {noreply, State2}; 95 | 96 | handle_info({Closed, _Sock}, State) 97 | when Closed == tcp_closed; Closed == ssl_closed -> 98 | {stop, sock_closed, State}; 99 | 100 | handle_info({Error, _Sock, Reason}, State) 101 | when Error == tcp_error; Error == ssl_error -> 102 | {stop, {sock_error, Reason}, State}; 103 | 104 | handle_info({'EXIT', _Pid, Reason}, State) -> 105 | {stop, Reason, State}; 106 | 107 | handle_info(Info, State) -> 108 | {stop, {unsupported_info, Info}, State}. 109 | 110 | terminate(_Reason, _State) -> 111 | ok. 112 | 113 | code_change(_OldVsn, State, _Extra) -> 114 | {ok, State}. 115 | 116 | %% -- internal functions -- 117 | 118 | start_ssl($S, _Flag, Opts, State) -> 119 | #state{sock = S1} = State, 120 | case ssl:connect(S1, Opts) of 121 | {ok, S2} -> State#state{mod = ssl, sock = S2}; 122 | {error, Reason} -> exit({ssl_negotiation_failed, Reason}) 123 | end; 124 | 125 | start_ssl($N, Flag, _Opts, State) -> 126 | case Flag of 127 | true -> State; 128 | required -> exit(ssl_not_available) 129 | end. 130 | 131 | setopts(#state{mod = Mod, sock = Sock}, Opts) -> 132 | case Mod of 133 | gen_tcp -> inet:setopts(Sock, Opts); 134 | ssl -> ssl:setopts(Sock, Opts) 135 | end. 136 | 137 | decode(<> = Bin, #state{c = C} = State) -> 138 | Len2 = Len - 4, 139 | case Rest of 140 | <> when Type == $N -> 141 | gen_fsm:send_all_state_event(C, {notice, decode_error(Data)}), 142 | decode(Tail, State); 143 | <> when Type == $S -> 144 | [Name, Value] = decode_strings(Data), 145 | gen_fsm:send_all_state_event(C, {parameter_status, Name, Value}), 146 | decode(Tail, State); 147 | <> when Type == $E -> 148 | gen_fsm:send_event(C, {error, decode_error(Data)}), 149 | decode(Tail, State); 150 | <> when Type == $A -> 151 | <> = Data, 152 | case decode_strings(Strings) of 153 | [Channel, Payload] -> ok; 154 | [Channel] -> Payload = <<>> 155 | end, 156 | gen_fsm:send_all_state_event(C, {notification, Channel, Pid, Payload}), 157 | decode(Tail, State); 158 | <> -> 159 | gen_fsm:send_event(C, {Type, Data}), 160 | decode(Tail, State); 161 | _Other -> 162 | State#state{tail = Bin} 163 | end; 164 | decode(Bin, State) -> 165 | State#state{tail = Bin}. 166 | 167 | %% decode a single null-terminated string 168 | decode_string(Bin) -> 169 | decode_string(Bin, <<>>). 170 | 171 | decode_string(<<0, Rest/binary>>, Str) -> 172 | {Str, Rest}; 173 | decode_string(<>, Str) -> 174 | decode_string(Rest, <>). 175 | 176 | %% decode multiple null-terminated string 177 | decode_strings(Bin) -> 178 | decode_strings(Bin, []). 179 | 180 | decode_strings(<<>>, Acc) -> 181 | lists:reverse(Acc); 182 | decode_strings(Bin, Acc) -> 183 | {Str, Rest} = decode_string(Bin), 184 | decode_strings(Rest, [Str | Acc]). 185 | 186 | %% decode field 187 | decode_fields(Bin) -> 188 | decode_fields(Bin, []). 189 | 190 | decode_fields(<<0>>, Acc) -> 191 | Acc; 192 | decode_fields(<>, Acc) -> 193 | {Str, Rest2} = decode_string(Rest), 194 | decode_fields(Rest2, [{Type, Str} | Acc]). 195 | 196 | %% decode ErrorResponse 197 | decode_error(Bin) -> 198 | Fields = decode_fields(Bin), 199 | Error = #error{ 200 | severity = lower_atom(proplists:get_value($S, Fields)), 201 | code = proplists:get_value($C, Fields), 202 | message = proplists:get_value($M, Fields), 203 | extra = decode_error_extra(Fields)}, 204 | Error. 205 | 206 | decode_error_extra(Fields) -> 207 | Types = [{$D, detail}, {$H, hint}, {$P, position}], 208 | decode_error_extra(Types, Fields, []). 209 | 210 | decode_error_extra([], _Fields, Extra) -> 211 | Extra; 212 | decode_error_extra([{Type, Name} | T], Fields, Extra) -> 213 | case proplists:get_value(Type, Fields) of 214 | undefined -> decode_error_extra(T, Fields, Extra); 215 | Value -> decode_error_extra(T, Fields, [{Name, Value} | Extra]) 216 | end. 217 | 218 | lower_atom(Str) when is_binary(Str) -> 219 | lower_atom(binary_to_list(Str)); 220 | lower_atom(Str) when is_list(Str) -> 221 | list_to_atom(string:to_lower(Str)). 222 | -------------------------------------------------------------------------------- /src/pgsql_types.erl: -------------------------------------------------------------------------------- 1 | -module(pgsql_types). 2 | 3 | -export([oid2type/1, type2oid/1]). 4 | 5 | oid2type(16) -> bool; 6 | oid2type(17) -> bytea; 7 | oid2type(18) -> char; 8 | oid2type(19) -> name; 9 | oid2type(20) -> int8; 10 | oid2type(21) -> int2; 11 | oid2type(22) -> int2vector; 12 | oid2type(23) -> int4; 13 | oid2type(24) -> regproc; 14 | oid2type(25) -> text; 15 | oid2type(26) -> oid; 16 | oid2type(27) -> tid; 17 | oid2type(28) -> xid; 18 | oid2type(29) -> cid; 19 | oid2type(30) -> oidvector; 20 | oid2type(71) -> pg_type_reltype; 21 | oid2type(75) -> pg_attribute_reltype; 22 | oid2type(81) -> pg_proc_reltype; 23 | oid2type(83) -> pg_class_reltype; 24 | oid2type(142) -> xml; 25 | oid2type(600) -> point; 26 | oid2type(601) -> lseg; 27 | oid2type(602) -> path; 28 | oid2type(603) -> box; 29 | oid2type(604) -> polygon; 30 | oid2type(628) -> line; 31 | oid2type(700) -> float4; 32 | oid2type(701) -> float8; 33 | oid2type(702) -> abstime; 34 | oid2type(703) -> reltime; 35 | oid2type(704) -> tinterval; 36 | oid2type(705) -> unknown; 37 | oid2type(718) -> circle; 38 | oid2type(790) -> cash; 39 | oid2type(829) -> macaddr; 40 | oid2type(869) -> inet; 41 | oid2type(650) -> cidr; 42 | oid2type(1000) -> boolarray; 43 | oid2type(1005) -> int2array; 44 | oid2type(1007) -> int4array; 45 | oid2type(1009) -> textarray; 46 | oid2type(1014) -> chararray; 47 | oid2type(1016) -> int8array; 48 | oid2type(1021) -> float4array; 49 | oid2type(1022) -> float8array; 50 | oid2type(1033) -> aclitem; 51 | oid2type(1263) -> cstringarray; 52 | oid2type(1042) -> bpchar; 53 | oid2type(1043) -> varchar; 54 | oid2type(1082) -> date; 55 | oid2type(1083) -> time; 56 | oid2type(1114) -> timestamp; 57 | oid2type(1184) -> timestamptz; 58 | oid2type(1186) -> interval; 59 | oid2type(1266) -> timetz; 60 | oid2type(1560) -> bit; 61 | oid2type(1562) -> varbit; 62 | oid2type(1700) -> numeric; 63 | oid2type(1790) -> refcursor; 64 | oid2type(2202) -> regprocedure; 65 | oid2type(2203) -> regoper; 66 | oid2type(2204) -> regoperator; 67 | oid2type(2205) -> regclass; 68 | oid2type(2206) -> regtype; 69 | oid2type(2211) -> regtypearray; 70 | oid2type(3614) -> tsvector; 71 | oid2type(3642) -> gtsvector; 72 | oid2type(3615) -> tsquery; 73 | oid2type(3734) -> regconfig; 74 | oid2type(3769) -> regdictionary; 75 | oid2type(2249) -> record; 76 | oid2type(2275) -> cstring; 77 | oid2type(2276) -> any; 78 | oid2type(2277) -> anyarray; 79 | oid2type(2278) -> void; 80 | oid2type(2279) -> trigger; 81 | oid2type(2280) -> language_handler; 82 | oid2type(2281) -> internal; 83 | oid2type(2282) -> opaque; 84 | oid2type(2283) -> anyelement; 85 | oid2type(2776) -> anynonarray; 86 | oid2type(3500) -> anyenum; 87 | oid2type(Oid) -> {unknown_oid, Oid}. 88 | 89 | type2oid(bool) -> 16; 90 | type2oid(bytea) -> 17; 91 | type2oid(char) -> 18; 92 | type2oid(name) -> 19; 93 | type2oid(int8) -> 20; 94 | type2oid(int2) -> 21; 95 | type2oid(int2vector) -> 22; 96 | type2oid(int4) -> 23; 97 | type2oid(regproc) -> 24; 98 | type2oid(text) -> 25; 99 | type2oid(oid) -> 26; 100 | type2oid(tid) -> 27; 101 | type2oid(xid) -> 28; 102 | type2oid(cid) -> 29; 103 | type2oid(oidvector) -> 30; 104 | type2oid(pg_type_reltype) -> 71; 105 | type2oid(pg_attribute_reltype) -> 75; 106 | type2oid(pg_proc_reltype) -> 81; 107 | type2oid(pg_class_reltype) -> 83; 108 | type2oid(xml) -> 142; 109 | type2oid(point) -> 600; 110 | type2oid(lseg) -> 601; 111 | type2oid(path) -> 602; 112 | type2oid(box) -> 603; 113 | type2oid(polygon) -> 604; 114 | type2oid(line) -> 628; 115 | type2oid(float4) -> 700; 116 | type2oid(float8) -> 701; 117 | type2oid(abstime) -> 702; 118 | type2oid(reltime) -> 703; 119 | type2oid(tinterval) -> 704; 120 | type2oid(unknown) -> 705; 121 | type2oid(circle) -> 718; 122 | type2oid(cash) -> 790; 123 | type2oid(macaddr) -> 829; 124 | type2oid(inet) -> 869; 125 | type2oid(cidr) -> 650; 126 | type2oid(boolarray) -> 1000; 127 | type2oid(int2array) -> 1005; 128 | type2oid(int4array) -> 1007; 129 | type2oid(textarray) -> 1009; 130 | type2oid(chararray) -> 1014; 131 | type2oid(int8array) -> 1016; 132 | type2oid(float4array) -> 1021; 133 | type2oid(float8array) -> 1022; 134 | type2oid(aclitem) -> 1033; 135 | type2oid(cstringarray) -> 1263; 136 | type2oid(bpchar) -> 1042; 137 | type2oid(varchar) -> 1043; 138 | type2oid(date) -> 1082; 139 | type2oid(time) -> 1083; 140 | type2oid(timestamp) -> 1114; 141 | type2oid(timestamptz) -> 1184; 142 | type2oid(interval) -> 1186; 143 | type2oid(timetz) -> 1266; 144 | type2oid(bit) -> 1560; 145 | type2oid(varbit) -> 1562; 146 | type2oid(numeric) -> 1700; 147 | type2oid(refcursor) -> 1790; 148 | type2oid(regprocedure) -> 2202; 149 | type2oid(regoper) -> 2203; 150 | type2oid(regoperator) -> 2204; 151 | type2oid(regclass) -> 2205; 152 | type2oid(regtype) -> 2206; 153 | type2oid(regtypearray) -> 2211; 154 | type2oid(tsvector) -> 3614; 155 | type2oid(gtsvector) -> 3642; 156 | type2oid(tsquery) -> 3615; 157 | type2oid(regconfig) -> 3734; 158 | type2oid(regdictionary) -> 3769; 159 | type2oid(record) -> 2249; 160 | type2oid(cstring) -> 2275; 161 | type2oid(any) -> 2276; 162 | type2oid(anyarray) -> 2277; 163 | type2oid(void) -> 2278; 164 | type2oid(trigger) -> 2279; 165 | type2oid(language_handler) -> 2280; 166 | type2oid(internal) -> 2281; 167 | type2oid(opaque) -> 2282; 168 | type2oid(anyelement) -> 2283; 169 | type2oid(anynonarray) -> 2776; 170 | type2oid(anyenum) -> 3500; 171 | type2oid(Type) -> {unknown_type, Type}. 172 | -------------------------------------------------------------------------------- /test_data/epgsql.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIC6jCCAlOgAwIBAgIJAKwIWpOFC5EWMA0GCSqGSIb3DQEBBQUAMGQxCzAJBgNV 3 | BAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlhMRYwFAYDVQQHEw1TYW4gRnJhbmNp 4 | c2NvMRQwEgYDVQQKEwtsYW1iZGFXb3JrczESMBAGA1UEAxMJZXBnc3FsIENBMB4X 5 | DTA5MDMyNzA0MzAxM1oXDTE5MDIwMzA0MzAxM1owazELMAkGA1UEBhMCVVMxEzAR 6 | BgNVBAgTCkNhbGlmb3JuaWExFjAUBgNVBAcTDVNhbiBGcmFuY2lzY28xFDASBgNV 7 | BAoTC2xhbWJkYVdvcmtzMRkwFwYDVQQDFBBlcGdzcWxfdGVzdF9jZXJ0MIGfMA0G 8 | CSqGSIb3DQEBAQUAA4GNADCBiQKBgQCxnVk30edKqkIWWzTeWJHlc5Zco5MASAqz 9 | J6fC4HmR4y+StFB88NZE/ESKbWXNOD464fku72m5i4DI1NftGgZLVjjrFmLEE05S 10 | hymkqWtVb+H9RBD2SHl4VjwDUsMDHZGiXL3n02uWF+NtpeQHkacfav10ZQO0nnub 11 | njCUV3EHoQIDAQABo4GcMIGZMIGWBgNVHSMEgY4wgYuAFO5OzTWlO3ao7YAytIBW 12 | A3A1GXDaoWikZjBkMQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEW 13 | MBQGA1UEBxMNU2FuIEZyYW5jaXNjbzEUMBIGA1UEChMLbGFtYmRhV29ya3MxEjAQ 14 | BgNVBAMTCWVwZ3NxbCBDQYIJAIFM8k/soL/qMA0GCSqGSIb3DQEBBQUAA4GBAHhy 15 | nE3/U1DiqHNf+AaCktVoHlsjfXvfrhouqR7T84kxCjAHzM6q183ga4S9NhLeyhCT 16 | oiDnIoUlUg7v1h9wTvXAoO2NwkCpSbIUncj6Q2h6J8obWq/BDSJwbdPcCHe4un8P 17 | hEpkiEK1QMMm5h9d+jgE8mrgGZXMnvzw40ovJHON 18 | -----END CERTIFICATE----- 19 | -------------------------------------------------------------------------------- /test_data/epgsql.key: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIICXgIBAAKBgQCxnVk30edKqkIWWzTeWJHlc5Zco5MASAqzJ6fC4HmR4y+StFB8 3 | 8NZE/ESKbWXNOD464fku72m5i4DI1NftGgZLVjjrFmLEE05ShymkqWtVb+H9RBD2 4 | SHl4VjwDUsMDHZGiXL3n02uWF+NtpeQHkacfav10ZQO0nnubnjCUV3EHoQIDAQAB 5 | AoGARXSemvF+XPhPd6aa+gfwpaWZuwhMR+PkK0Lqm45ke+Q3ikrw3qrfX4K22tsE 6 | 4EeKLkSHyQ7ebSxcZCy3c4SlyNES88wk7epGYbui4L0Iv/1WXfg1zIRqdNgBMr6M 7 | ZUZoUJx1gyRY2S3zGjTBn8b4Wh9EwsD0KTluvtH74DtLPQECQQDiLhIVasTsgKpn 8 | SoLVJ+UqpQ8oe17m4gHbwMOK2s+o479oKuAbmwgUX8U2waoncq06vG+x3gziVIOF 9 | Qkj6s6rZAkEAyQgfN01SoNOwp61Nis8TWeltqZdh0VHYqpu/ARfUpsTAWHGhc4eK 10 | Ee+J1DmxrUAP+s25Z640Ps9jNTugrWB2CQJBAJ+XyHTKQKdsZlC517VWEDLWAusa 11 | mi0pvgv0aUW5/Zr7EJ0M29M+wiW2Ke7oGgr5tNfkDKAhwU+WOLM3wUz8p4kCQQCr 12 | 5zcSShtzDTINYCNjpElO5E3y7FEn9g4Jbd7550/fP3We66P8r5VAWw4IHUGy/Yns 13 | lIiLgSqJ3ztdZNy0BT1JAkEAhPz9yMZN7NBfdTjM1ebp4VirL8uQAdod/d3oRN87 14 | dqVxywRm4dZ4hMD2Fr6CuLsZylCQKEt1jNEfjZzRC4hR2g== 15 | -----END RSA PRIVATE KEY----- 16 | -------------------------------------------------------------------------------- /test_data/root.crt: -------------------------------------------------------------------------------- 1 | Certificate: 2 | Data: 3 | Version: 3 (0x2) 4 | Serial Number: 5 | 81:4c:f2:4f:ec:a0:bf:ea 6 | Signature Algorithm: sha1WithRSAEncryption 7 | Issuer: C=US, ST=California, L=San Francisco, O=lambdaWorks, CN=epgsql CA 8 | Validity 9 | Not Before: Mar 27 03:52:34 2009 GMT 10 | Not After : Feb 3 03:52:34 2019 GMT 11 | Subject: C=US, ST=California, L=San Francisco, O=lambdaWorks, CN=epgsql CA 12 | Subject Public Key Info: 13 | Public Key Algorithm: rsaEncryption 14 | RSA Public Key: (1024 bit) 15 | Modulus (1024 bit): 16 | 00:bd:03:59:e5:ce:5d:24:45:ae:bf:cd:a4:4a:d4: 17 | 33:7a:48:08:79:8a:20:4c:b6:28:51:f8:f0:9a:1d: 18 | 1e:fb:b8:de:a3:b7:10:95:d7:a3:58:b7:94:b4:7e: 19 | 36:0a:0c:68:1c:e8:21:a5:5d:9d:0a:3a:5d:26:dd: 20 | bb:5b:62:59:e0:1f:b8:48:a7:3d:28:dd:f3:b9:de: 21 | 27:d7:25:4b:f6:8a:ac:ef:a3:0e:b3:fb:1b:b8:dd: 22 | db:01:72:01:1f:79:5b:f8:c3:54:7e:1a:94:68:1d: 23 | 81:2c:05:11:05:2c:5b:81:05:21:19:c0:c7:94:4f: 24 | 77:f5:76:4c:98:8d:ab:68:5b 25 | Exponent: 65537 (0x10001) 26 | X509v3 extensions: 27 | X509v3 Subject Key Identifier: 28 | EE:4E:CD:35:A5:3B:76:A8:ED:80:32:B4:80:56:03:70:35:19:70:DA 29 | X509v3 Authority Key Identifier: 30 | keyid:EE:4E:CD:35:A5:3B:76:A8:ED:80:32:B4:80:56:03:70:35:19:70:DA 31 | DirName:/C=US/ST=California/L=San Francisco/O=lambdaWorks/CN=epgsql CA 32 | serial:81:4C:F2:4F:EC:A0:BF:EA 33 | 34 | X509v3 Basic Constraints: 35 | CA:TRUE 36 | Signature Algorithm: sha1WithRSAEncryption 37 | 27:4c:04:ee:27:46:23:9b:6f:7c:8f:5b:9e:c6:65:74:33:40: 38 | 06:be:ca:e0:55:91:1c:9e:1c:77:27:82:03:4e:67:91:5d:14: 39 | e4:74:b7:88:9e:49:d6:02:5b:71:94:b3:62:2a:5e:58:00:7d: 40 | 8c:42:09:db:ca:27:20:71:33:16:09:d2:17:36:d4:4f:63:09: 41 | 0a:48:80:d7:36:13:24:57:e3:7a:7e:25:4e:b8:f0:71:c6:34: 42 | 69:4e:e1:4b:5a:ec:b3:be:14:78:1e:af:85:b2:56:91:62:03: 43 | 6b:b2:85:2e:8e:ef:4b:5a:bf:ac:54:43:24:cb:0e:c6:f8:58: 44 | b5:a1 45 | -----BEGIN CERTIFICATE----- 46 | MIIDEDCCAnmgAwIBAgIJAIFM8k/soL/qMA0GCSqGSIb3DQEBBQUAMGQxCzAJBgNV 47 | BAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlhMRYwFAYDVQQHEw1TYW4gRnJhbmNp 48 | c2NvMRQwEgYDVQQKEwtsYW1iZGFXb3JrczESMBAGA1UEAxMJZXBnc3FsIENBMB4X 49 | DTA5MDMyNzAzNTIzNFoXDTE5MDIwMzAzNTIzNFowZDELMAkGA1UEBhMCVVMxEzAR 50 | BgNVBAgTCkNhbGlmb3JuaWExFjAUBgNVBAcTDVNhbiBGcmFuY2lzY28xFDASBgNV 51 | BAoTC2xhbWJkYVdvcmtzMRIwEAYDVQQDEwllcGdzcWwgQ0EwgZ8wDQYJKoZIhvcN 52 | AQEBBQADgY0AMIGJAoGBAL0DWeXOXSRFrr/NpErUM3pICHmKIEy2KFH48JodHvu4 53 | 3qO3EJXXo1i3lLR+NgoMaBzoIaVdnQo6XSbdu1tiWeAfuEinPSjd87neJ9clS/aK 54 | rO+jDrP7G7jd2wFyAR95W/jDVH4alGgdgSwFEQUsW4EFIRnAx5RPd/V2TJiNq2hb 55 | AgMBAAGjgckwgcYwHQYDVR0OBBYEFO5OzTWlO3ao7YAytIBWA3A1GXDaMIGWBgNV 56 | HSMEgY4wgYuAFO5OzTWlO3ao7YAytIBWA3A1GXDaoWikZjBkMQswCQYDVQQGEwJV 57 | UzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNU2FuIEZyYW5jaXNjbzEU 58 | MBIGA1UEChMLbGFtYmRhV29ya3MxEjAQBgNVBAMTCWVwZ3NxbCBDQYIJAIFM8k/s 59 | oL/qMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADgYEAJ0wE7idGI5tvfI9b 60 | nsZldDNABr7K4FWRHJ4cdyeCA05nkV0U5HS3iJ5J1gJbcZSzYipeWAB9jEIJ28on 61 | IHEzFgnSFzbUT2MJCkiA1zYTJFfjen4lTrjwccY0aU7hS1rss74UeB6vhbJWkWID 62 | a7KFLo7vS1q/rFRDJMsOxvhYtaE= 63 | -----END CERTIFICATE----- 64 | -------------------------------------------------------------------------------- /test_data/root.key: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIICXQIBAAKBgQC9A1nlzl0kRa6/zaRK1DN6SAh5iiBMtihR+PCaHR77uN6jtxCV 3 | 16NYt5S0fjYKDGgc6CGlXZ0KOl0m3btbYlngH7hIpz0o3fO53ifXJUv2iqzvow6z 4 | +xu43dsBcgEfeVv4w1R+GpRoHYEsBREFLFuBBSEZwMeUT3f1dkyYjatoWwIDAQAB 5 | AoGBAKg3UUyayn47nfiJtgv6qw3LCe/RJEnhXCUIHmmqPSbeMxcVF6ej0HZme+ve 6 | 34012XrQhRE9LUQrCThL4jDEaSLsZ64PY+XL0ZdNCS4RX6OHGp6EyHC1HNSHn8a2 7 | zQuAzBsBHM39h/EVid9m0acfcEuN7TYAKF+sH6qjEBiSAWdRAkEA5hnwRsecGht6 8 | ViW4uuwHadNrc19mMPpXxFtIb79ONH+FmUkSQ0pRNOkEVICC7yokZbnhxcxSb76k 9 | r3S7rDa8xQJBANJJgpzuxbF0/NTXl5aH1gcucpIp6XBJfRmn1DpFq3Y20qGPr+Ez 10 | SiiDaqxoYjYRQ6FJg26kWnonWPawsiXSIp8CQQDQuQazra10ISi/rEf9hszSuezm 11 | IstX8j5a51K1yxrtlB9kBFyEnY08KYK8BDbBK8EIZaze95BvvMc2QPVcKerhAkB+ 12 | Qh7HBOHz827eiHd+rR5Hf47QzZNYlPck0UyulCgnuTDsSi5qw3XSL118GMxm9CSs 13 | EUx1wP6F+1wB+gNsi+e3AkAR39uESbaaVOZmh1Uvvz0RVckXlJOEPY8Rp6kxhFS2 14 | QBsWbMrb5jraFy54iCmj8o3stp+LjBBv4PFA0LKq4vIa 15 | -----END RSA PRIVATE KEY----- 16 | -------------------------------------------------------------------------------- /test_data/test_schema.sql: -------------------------------------------------------------------------------- 1 | -- script to create test schema for epgsql unit tests -- 2 | -- 3 | -- this script should be run as the same user the tests will be run as, 4 | -- so that the test for connecting as the 'current user' succeeds 5 | -- 6 | -- the following lines must be added to pg_hba.conf for all tests to 7 | -- succeed: 8 | -- 9 | -- host epgsql_test_db1 epgsql_test 127.0.0.1/32 trust 10 | -- host epgsql_test_db1 epgsql_test_md5 127.0.0.1/32 md5 11 | -- host epgsql_test_db1 epgsql_test_cleartext 127.0.0.1/32 password 12 | -- hostssl epgsql_test_db1 epgsql_test_cert 127.0.0.1/32 cert 13 | -- 14 | -- any 'trust all' must be commented out for the invalid password test 15 | -- to succeed. 16 | -- 17 | -- ssl support must be configured, and the sslinfo contrib module 18 | -- loaded for the ssl tests to succeed. 19 | 20 | 21 | CREATE USER epgsql_test; 22 | CREATE USER epgsql_test_md5 WITH PASSWORD 'epgsql_test_md5'; 23 | CREATE USER epgsql_test_cleartext WITH PASSWORD 'epgsql_test_cleartext'; 24 | CREATE USER epgsql_test_cert; 25 | 26 | CREATE DATABASE epgsql_test_db1 WITH ENCODING 'UTF8'; 27 | CREATE DATABASE epgsql_test_db2 WITH ENCODING 'UTF8'; 28 | 29 | GRANT ALL ON DATABASE epgsql_test_db1 to epgsql_test; 30 | GRANT ALL ON DATABASE epgsql_test_db1 to epgsql_test_md5; 31 | GRANT ALL ON DATABASE epgsql_test_db1 to epgsql_test_cleartext; 32 | GRANT ALL ON DATABASE epgsql_test_db2 to epgsql_test; 33 | 34 | \c epgsql_test_db1; 35 | 36 | CREATE TABLE test_table1 (id integer primary key, value text); 37 | 38 | INSERT INTO test_table1 (id, value) VALUES (1, 'one'); 39 | INSERT INTO test_table1 (id, value) VALUES (2, 'two'); 40 | 41 | CREATE TABLE test_table2 ( 42 | c_bool bool, 43 | c_char char, 44 | c_int2 int2, 45 | c_int4 int4, 46 | c_int8 int8, 47 | c_float4 float4, 48 | c_float8 float8, 49 | c_bytea bytea, 50 | c_text text, 51 | c_varchar varchar(64), 52 | c_date date, 53 | c_time time, 54 | c_timetz timetz, 55 | c_timestamp timestamp, 56 | c_timestamptz timestamptz, 57 | c_interval interval); 58 | 59 | CREATE LANGUAGE plpgsql; 60 | 61 | CREATE OR REPLACE FUNCTION insert_test1(_id integer, _value text) 62 | returns integer 63 | as $$ 64 | begin 65 | insert into test_table1 (id, value) values (_id, _value); 66 | return _id; 67 | end 68 | $$ language plpgsql; 69 | 70 | CREATE OR REPLACE FUNCTION do_nothing() 71 | returns void 72 | as $$ 73 | begin 74 | end 75 | $$ language plpgsql; 76 | 77 | GRANT ALL ON TABLE test_table1 TO epgsql_test; 78 | GRANT ALL ON TABLE test_table2 TO epgsql_test; 79 | GRANT ALL ON FUNCTION insert_test1(integer, text) TO epgsql_test; 80 | GRANT ALL ON FUNCTION do_nothing() TO epgsql_test; -------------------------------------------------------------------------------- /test_ebin/.empty: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wg/epgsql/3318bd5d646cad0623ae9dcc6df015bb85258a63/test_ebin/.empty -------------------------------------------------------------------------------- /test_src/pgsql_tests.erl: -------------------------------------------------------------------------------- 1 | -module(pgsql_tests). 2 | 3 | -export([run_tests/0]). 4 | 5 | -include_lib("eunit/include/eunit.hrl"). 6 | -include_lib("public_key/include/public_key.hrl"). 7 | -include("pgsql.hrl"). 8 | 9 | -define(host, "localhost"). 10 | -define(port, 5432). 11 | 12 | -define(ssl_apps, [crypto, public_key, ssl]). 13 | 14 | connect_test() -> 15 | connect_only([]). 16 | 17 | connect_to_db_test() -> 18 | connect_only([{database, "epgsql_test_db1"}]). 19 | 20 | connect_as_test() -> 21 | connect_only(["epgsql_test", [{database, "epgsql_test_db1"}]]). 22 | 23 | connect_with_cleartext_test() -> 24 | connect_only(["epgsql_test_cleartext", 25 | "epgsql_test_cleartext", 26 | [{database, "epgsql_test_db1"}]]). 27 | 28 | connect_with_md5_test() -> 29 | connect_only(["epgsql_test_md5", 30 | "epgsql_test_md5", 31 | [{database, "epgsql_test_db1"}]]). 32 | 33 | connect_with_invalid_user_test() -> 34 | {error, invalid_authorization_specification} = 35 | pgsql:connect(?host, 36 | "epgsql_test_invalid", 37 | "epgsql_test_invalid", 38 | [{port, ?port}, {database, "epgsql_test_db1"}]). 39 | 40 | connect_with_invalid_password_test() -> 41 | {error, Why} = 42 | pgsql:connect(?host, 43 | "epgsql_test_md5", 44 | "epgsql_test_invalid", 45 | [{port, ?port}, {database, "epgsql_test_db1"}]), 46 | case Why of 47 | invalid_authorization_specification -> ok; % =< 8.4 48 | invalid_password -> ok % >= 9.0 49 | end. 50 | 51 | 52 | connect_with_ssl_test() -> 53 | lists:foreach(fun application:start/1, ?ssl_apps), 54 | with_connection( 55 | fun(C) -> 56 | {ok, _Cols, [{true}]} = pgsql:equery(C, "select ssl_is_used()") 57 | end, 58 | "epgsql_test", 59 | [{ssl, true}]). 60 | 61 | connect_with_client_cert_test() -> 62 | lists:foreach(fun application:start/1, ?ssl_apps), 63 | Dir = filename:join(filename:dirname(code:which(pgsql_tests)), "../test_data"), 64 | File = fun(Name) -> filename:join(Dir, Name) end, 65 | {ok, Pem} = file:read_file(File("epgsql.crt")), 66 | [{'Certificate', Der, not_encrypted}] = public_key:pem_decode(Pem), 67 | Cert = public_key:pkix_decode_cert(Der, plain), 68 | #'TBSCertificate'{serialNumber = Serial} = Cert#'Certificate'.tbsCertificate, 69 | Serial2 = list_to_binary(integer_to_list(Serial)), 70 | 71 | with_connection( 72 | fun(C) -> 73 | {ok, _, [{true}]} = pgsql:equery(C, "select ssl_is_used()"), 74 | {ok, _, [{Serial2}]} = pgsql:equery(C, "select ssl_client_serial()") 75 | end, 76 | "epgsql_test_cert", 77 | [{ssl, true}, {keyfile, File("epgsql.key")}, {certfile, File("epgsql.crt")}]). 78 | 79 | select_test() -> 80 | with_connection( 81 | fun(C) -> 82 | {ok, Cols, Rows} = pgsql:squery(C, "select * from test_table1"), 83 | [#column{name = <<"id">>, type = int4, size = 4}, 84 | #column{name = <<"value">>, type = text, size = -1}] = Cols, 85 | [{<<"1">>, <<"one">>}, {<<"2">>, <<"two">>}] = Rows 86 | end). 87 | 88 | insert_test() -> 89 | with_rollback( 90 | fun(C) -> 91 | {ok, 1} = pgsql:squery(C, "insert into test_table1 (id, value) values (3, 'three')") 92 | end). 93 | 94 | update_test() -> 95 | with_rollback( 96 | fun(C) -> 97 | {ok, 1} = pgsql:squery(C, "insert into test_table1 (id, value) values (3, 'three')"), 98 | {ok, 1} = pgsql:squery(C, "insert into test_table1 (id, value) values (4, 'four')"), 99 | {ok, 2} = pgsql:squery(C, "update test_table1 set value = 'foo' where id > 2"), 100 | {ok, _, [{<<"2">>}]} = pgsql:squery(C, "select count(*) from test_table1 where value = 'foo'") 101 | end). 102 | 103 | delete_test() -> 104 | with_rollback( 105 | fun(C) -> 106 | {ok, 1} = pgsql:squery(C, "insert into test_table1 (id, value) values (3, 'three')"), 107 | {ok, 1} = pgsql:squery(C, "insert into test_table1 (id, value) values (4, 'four')"), 108 | {ok, 2} = pgsql:squery(C, "delete from test_table1 where id > 2"), 109 | {ok, _, [{<<"2">>}]} = pgsql:squery(C, "select count(*) from test_table1") 110 | end). 111 | 112 | create_and_drop_table_test() -> 113 | with_rollback( 114 | fun(C) -> 115 | {ok, [], []} = pgsql:squery(C, "create table test_table3 (id int4)"), 116 | {ok, [#column{type = int4}], []} = pgsql:squery(C, "select * from test_table3"), 117 | {ok, [], []} = pgsql:squery(C, "drop table test_table3") 118 | end). 119 | 120 | cursor_test() -> 121 | with_connection( 122 | fun(C) -> 123 | {ok, [], []} = pgsql:squery(C, "begin"), 124 | {ok, [], []} = pgsql:squery(C, "declare c cursor for select id from test_table1"), 125 | {ok, 2} = pgsql:squery(C, "move forward 2 from c"), 126 | {ok, 1} = pgsql:squery(C, "move backward 1 from c"), 127 | {ok, 1, _Cols, [{<<"2">>}]} = pgsql:squery(C, "fetch next from c"), 128 | {ok, [], []} = pgsql:squery(C, "close c") 129 | end). 130 | 131 | multiple_result_test() -> 132 | with_connection( 133 | fun(C) -> 134 | [{ok, _, [{<<"1">>}]}, {ok, _, [{<<"2">>}]}] = pgsql:squery(C, "select 1; select 2"), 135 | [{ok, _, [{<<"1">>}]}, {error, #error{}}] = pgsql:squery(C, "select 1; select foo;") 136 | end). 137 | 138 | extended_select_test() -> 139 | with_connection( 140 | fun(C) -> 141 | {ok, Cols, Rows} = pgsql:equery(C, "select * from test_table1", []), 142 | [#column{name = <<"id">>, type = int4, size = 4}, 143 | #column{name = <<"value">>, type = text, size = -1}] = Cols, 144 | [{1, <<"one">>}, {2, <<"two">>}] = Rows 145 | end). 146 | 147 | extended_sync_ok_test() -> 148 | with_connection( 149 | fun(C) -> 150 | {ok, _Cols, [{<<"one">>}]} = pgsql:equery(C, "select value from test_table1 where id = $1", [1]), 151 | {ok, _Cols, [{<<"two">>}]} = pgsql:equery(C, "select value from test_table1 where id = $1", [2]) 152 | end). 153 | 154 | extended_sync_error_test() -> 155 | with_connection( 156 | fun(C) -> 157 | {error, #error{}} = pgsql:equery(C, "select _alue from test_table1 where id = $1", [1]), 158 | {ok, _Cols, [{<<"one">>}]} = pgsql:equery(C, "select value from test_table1 where id = $1", [1]) 159 | end). 160 | 161 | returning_from_insert_test() -> 162 | with_rollback( 163 | fun(C) -> 164 | {ok, 1, _Cols, [{3}]} = pgsql:equery(C, "insert into test_table1 (id) values (3) returning id") 165 | end). 166 | 167 | returning_from_update_test() -> 168 | with_rollback( 169 | fun(C) -> 170 | {ok, 2, _Cols, [{1}, {2}]} = pgsql:equery(C, "update test_table1 set value = 'hi' returning id") 171 | end). 172 | 173 | returning_from_delete_test() -> 174 | with_rollback( 175 | fun(C) -> 176 | {ok, 2, _Cols, [{1}, {2}]} = pgsql:equery(C, "delete from test_table1 returning id") 177 | end). 178 | 179 | parse_test() -> 180 | with_connection( 181 | fun(C) -> 182 | {ok, S} = pgsql:parse(C, "select * from test_table1"), 183 | [#column{name = <<"id">>}, #column{name = <<"value">>}] = S#statement.columns, 184 | ok = pgsql:close(C, S), 185 | ok = pgsql:sync(C) 186 | end). 187 | 188 | parse_column_format_test() -> 189 | with_connection( 190 | fun(C) -> 191 | {ok, S} = pgsql:parse(C, "select 1::int4, false::bool, 2.0::float4"), 192 | [#column{type = int4}, 193 | #column{type = bool}, 194 | #column{type = float4}] = S#statement.columns, 195 | ok = pgsql:bind(C, S, []), 196 | {ok, [{1, false, 2.0}]} = pgsql:execute(C, S, 0), 197 | ok = pgsql:close(C, S), 198 | ok = pgsql:sync(C) 199 | end). 200 | 201 | parse_error_test() -> 202 | with_connection( 203 | fun(C) -> 204 | {error, #error{}} = pgsql:parse(C, "select _ from test_table1"), 205 | {ok, S} = pgsql:parse(C, "select * from test_table1"), 206 | [#column{name = <<"id">>}, #column{name = <<"value">>}] = S#statement.columns, 207 | ok = pgsql:close(C, S), 208 | ok = pgsql:sync(C) 209 | end). 210 | 211 | parse_and_close_test() -> 212 | with_connection( 213 | fun(C) -> 214 | Parse = fun() -> pgsql:parse(C, "test", "select * from test_table1", []) end, 215 | {ok, S} = Parse(), 216 | {error, #error{code = <<"42P05">>}} = Parse(), 217 | pgsql:close(C, S), 218 | {ok, S} = Parse(), 219 | ok = pgsql:sync(C) 220 | end). 221 | 222 | bind_test() -> 223 | with_connection( 224 | fun(C) -> 225 | {ok, S} = pgsql:parse(C, "select value from test_table1 where id = $1"), 226 | ok = pgsql:bind(C, S, [1]), 227 | ok = pgsql:close(C, S), 228 | ok = pgsql:sync(C) 229 | end). 230 | 231 | bind_parameter_format_test() -> 232 | with_connection( 233 | fun(C) -> 234 | {ok, S} = pgsql:parse(C, "select $1, $2, $3", [int2, text, bool]), 235 | [int2, text, bool] = S#statement.types, 236 | ok = pgsql:bind(C, S, [1, "hi", true]), 237 | {ok, [{1, <<"hi">>, true}]} = pgsql:execute(C, S, 0), 238 | ok = pgsql:close(C, S), 239 | ok = pgsql:sync(C) 240 | end). 241 | 242 | bind_error_test() -> 243 | with_connection( 244 | fun(C) -> 245 | {ok, S} = pgsql:parse(C, "select $1::char"), 246 | {error, #error{}} = pgsql:bind(C, S, [0]), 247 | ok = pgsql:bind(C, S, [$A]), 248 | ok = pgsql:close(C, S), 249 | ok = pgsql:sync(C) 250 | end). 251 | 252 | bind_and_close_test() -> 253 | with_connection( 254 | fun(C) -> 255 | {ok, S} = pgsql:parse(C, "select * from test_table1"), 256 | ok = pgsql:bind(C, S, "one", []), 257 | {error, #error{code = <<"42P03">>}} = pgsql:bind(C, S, "one", []), 258 | ok = pgsql:close(C, portal, "one"), 259 | ok = pgsql:bind(C, S, "one", []), 260 | ok = pgsql:sync(C) 261 | end). 262 | 263 | execute_error_test() -> 264 | with_connection( 265 | fun(C) -> 266 | {ok, S} = pgsql:parse(C, "insert into test_table1 (id, value) values ($1, $2)"), 267 | ok = pgsql:bind(C, S, [1, <<"foo">>]), 268 | {error, #error{code = <<"23505">>}} = pgsql:execute(C, S, 0), 269 | {error, sync_required} = pgsql:bind(C, S, [3, <<"quux">>]), 270 | ok = pgsql:sync(C), 271 | ok = pgsql:bind(C, S, [3, <<"quux">>]), 272 | {ok, _} = pgsql:execute(C, S, 0), 273 | {ok, 1} = pgsql:squery(C, "delete from test_table1 where id = 3") 274 | end). 275 | 276 | describe_test() -> 277 | with_connection( 278 | fun(C) -> 279 | {ok, S} = pgsql:parse(C, "select * from test_table1"), 280 | [#column{name = <<"id">>}, #column{name = <<"value">>}] = S#statement.columns, 281 | {ok, S} = pgsql:describe(C, S), 282 | ok = pgsql:close(C, S), 283 | ok = pgsql:sync(C) 284 | end). 285 | 286 | describe_with_param_test() -> 287 | with_connection( 288 | fun(C) -> 289 | {ok, S} = pgsql:parse(C, "select id from test_table1 where id = $1"), 290 | [int4] = S#statement.types, 291 | [#column{name = <<"id">>}] = S#statement.columns, 292 | {ok, S} = pgsql:describe(C, S), 293 | ok = pgsql:close(C, S), 294 | ok = pgsql:sync(C) 295 | end). 296 | 297 | describe_named_test() -> 298 | with_connection( 299 | fun(C) -> 300 | {ok, S} = pgsql:parse(C, "name", "select * from test_table1", []), 301 | [#column{name = <<"id">>}, #column{name = <<"value">>}] = S#statement.columns, 302 | {ok, S} = pgsql:describe(C, S), 303 | ok = pgsql:close(C, S), 304 | ok = pgsql:sync(C) 305 | end). 306 | 307 | describe_error_test() -> 308 | with_connection( 309 | fun(C) -> 310 | {error, #error{}} = pgsql:describe(C, statement, ""), 311 | {ok, S} = pgsql:parse(C, "select * from test_table1"), 312 | {ok, S} = pgsql:describe(C, statement, ""), 313 | ok = pgsql:sync(C) 314 | 315 | end). 316 | 317 | portal_test() -> 318 | with_connection( 319 | fun(C) -> 320 | {ok, S} = pgsql:parse(C, "select value from test_table1"), 321 | ok = pgsql:bind(C, S, []), 322 | {partial, [{<<"one">>}]} = pgsql:execute(C, S, 1), 323 | {partial, [{<<"two">>}]} = pgsql:execute(C, S, 1), 324 | {ok, []} = pgsql:execute(C, S,1), 325 | ok = pgsql:close(C, S), 326 | ok = pgsql:sync(C) 327 | end). 328 | 329 | returning_test() -> 330 | with_rollback( 331 | fun(C) -> 332 | {ok, S} = pgsql:parse(C, "update test_table1 set value = $1 returning id"), 333 | ok = pgsql:bind(C, S, ["foo"]), 334 | {ok, 2, [{1}, {2}]} = pgsql:execute(C, S), 335 | ok = pgsql:sync(C) 336 | end). 337 | 338 | multiple_statement_test() -> 339 | with_connection( 340 | fun(C) -> 341 | {ok, S1} = pgsql:parse(C, "one", "select value from test_table1 where id = 1", []), 342 | ok = pgsql:bind(C, S1, []), 343 | {partial, [{<<"one">>}]} = pgsql:execute(C, S1, 1), 344 | {ok, S2} = pgsql:parse(C, "two", "select value from test_table1 where id = 2", []), 345 | ok = pgsql:bind(C, S2, []), 346 | {partial, [{<<"two">>}]} = pgsql:execute(C, S2, 1), 347 | {ok, []} = pgsql:execute(C, S1, 1), 348 | {ok, []} = pgsql:execute(C, S2, 1), 349 | ok = pgsql:close(C, S1), 350 | ok = pgsql:close(C, S2), 351 | ok = pgsql:sync(C) 352 | end). 353 | 354 | multiple_portal_test() -> 355 | with_connection( 356 | fun(C) -> 357 | {ok, S} = pgsql:parse(C, "select value from test_table1 where id = $1"), 358 | ok = pgsql:bind(C, S, "one", [1]), 359 | ok = pgsql:bind(C, S, "two", [2]), 360 | {ok, [{<<"one">>}]} = pgsql:execute(C, S, "one", 0), 361 | {ok, [{<<"two">>}]} = pgsql:execute(C, S, "two", 0), 362 | ok = pgsql:close(C, S), 363 | ok = pgsql:sync(C) 364 | end). 365 | 366 | execute_function_test() -> 367 | with_rollback( 368 | fun(C) -> 369 | {ok, _Cols1, [{3}]} = pgsql:equery(C, "select insert_test1(3, 'three')"), 370 | {ok, _Cols2, [{<<>>}]} = pgsql:equery(C, "select do_nothing()") 371 | end). 372 | 373 | parameter_get_test() -> 374 | with_connection( 375 | fun(C) -> 376 | {ok, <<"off">>} = pgsql:get_parameter(C, "is_superuser") 377 | end). 378 | 379 | parameter_set_test() -> 380 | with_connection( 381 | fun(C) -> 382 | {ok, [], []} = pgsql:squery(C, "set DateStyle = 'ISO, MDY'"), 383 | {ok, <<"ISO, MDY">>} = pgsql:get_parameter(C, "DateStyle"), 384 | {ok, _Cols, [{<<"2000-01-02">>}]} = pgsql:squery(C, "select '2000-01-02'::date"), 385 | {ok, [], []} = pgsql:squery(C, "set DateStyle = 'German'"), 386 | {ok, <<"German, DMY">>} = pgsql:get_parameter(C, "DateStyle"), 387 | {ok, _Cols, [{<<"02.01.2000">>}]} = pgsql:squery(C, "select '2000-01-02'::date") 388 | end). 389 | 390 | numeric_type_test() -> 391 | check_type(int2, "1", 1, [0, 256, -32768, +32767]), 392 | check_type(int4, "1", 1, [0, 512, -2147483648, +2147483647]), 393 | check_type(int8, "1", 1, [0, 1024, -9223372036854775808, +9223372036854775807]), 394 | check_type(float4, "1.0", 1.0, [0.0, 1.23456, -1.23456]), 395 | check_type(float8, "1.0", 1.0, [0.0, 1.23456789012345, -1.23456789012345]). 396 | 397 | character_type_test() -> 398 | Alpha = unicode:characters_to_binary([16#03B1]), 399 | Ka = unicode:characters_to_binary([16#304B]), 400 | One = unicode:characters_to_binary([16#10D360]), 401 | check_type(bpchar, "'A'", $A, [1, $1, 16#7F, Alpha, Ka, One], "c_char"), 402 | check_type(text, "'hi'", <<"hi">>, [<<"">>, <<"hi">>]), 403 | check_type(varchar, "'hi'", <<"hi">>, [<<"">>, <<"hi">>]). 404 | 405 | date_time_type_test() -> 406 | with_connection( 407 | fun(C) -> 408 | case pgsql:get_parameter(C, "integer_datetimes") of 409 | {ok, <<"on">>} -> MaxTsDate = 294276; 410 | {ok, <<"off">>} -> MaxTsDate = 5874897 411 | end, 412 | 413 | check_type(date, "'2008-01-02'", {2008,1,2}, [{-4712,1,1}, {5874897,1,1}]), 414 | check_type(time, "'00:01:02'", {0,1,2.0}, [{0,0,0.0}, {24,0,0.0}]), 415 | check_type(timetz, "'00:01:02-01'", {{0,1,2.0},1*60*60}, 416 | [{{0,0,0.0},0}, {{24,0,0.0},-13*60*60}]), 417 | check_type(timestamp, "'2008-01-02 03:04:05'", {{2008,1,2},{3,4,5.0}}, 418 | [{{-4712,1,1},{0,0,0.0}}, {{MaxTsDate,12,31}, {23,59,59.0}}]), 419 | check_type(interval, "'1 hour 2 minutes 3.1 seconds'", {{1,2,3.1},0,0}, 420 | [{{0,0,0.0},0,-178000000 * 12}, {{0,0,0.0},0,178000000 * 12}]) 421 | end). 422 | 423 | misc_type_test() -> 424 | check_type(bool, "true", true, [true, false]), 425 | check_type(bytea, "E'\001\002'", <<1,2>>, [<<>>, <<0,128,255>>]). 426 | 427 | array_type_test() -> 428 | with_connection( 429 | fun(C) -> 430 | {ok, _, [{[1, 2]}]} = pgsql:equery(C, "select ($1::int[])[1:2]", [[1, 2, 3]]), 431 | Select = fun(Type, A) -> 432 | Query = "select $1::" ++ atom_to_list(Type) ++ "[]", 433 | {ok, _Cols, [{A2}]} = pgsql:equery(C, Query, [A]), 434 | case lists:all(fun({V, V2}) -> compare(Type, V, V2) end, lists:zip(A, A2)) of 435 | true -> ok; 436 | false -> ?assertMatch(A, A2) 437 | end 438 | end, 439 | Select(int2, []), 440 | Select(int2, [1, 2, 3, 4]), 441 | Select(int2, [[1], [2], [3], [4]]), 442 | Select(int2, [[[[[[1, 2]]]]]]), 443 | Select(bool, [true]), 444 | Select(char, [$a, $b, $c]), 445 | Select(int4, [[1, 2]]), 446 | Select(int8, [[[[1, 2]], [[3, 4]]]]), 447 | Select(text, [<<"one">>, <<"two>">>]), 448 | Select(float4, [0.0, 1.0, 0.123]), 449 | Select(float8, [0.0, 1.0, 0.123]) 450 | end). 451 | 452 | text_format_test() -> 453 | with_connection( 454 | fun(C) -> 455 | Select = fun(Type, V) -> 456 | V2 = list_to_binary(V), 457 | Query = "select $1::" ++ Type, 458 | {ok, _Cols, [{V2}]} = pgsql:equery(C, Query, [V]), 459 | {ok, _Cols, [{V2}]} = pgsql:equery(C, Query, [V2]) 460 | end, 461 | Select("inet", "127.0.0.1"), 462 | Select("numeric", "123456") 463 | end). 464 | 465 | connect_timeout_test() -> 466 | {error, timeout} = pgsql:connect(?host, [{port, ?port}, {timeout, 0}]). 467 | 468 | query_timeout_test() -> 469 | with_connection( 470 | fun(C) -> 471 | {error, timeout} = pgsql:squery(C, "select pg_sleep(1)"), 472 | {error, timeout} = pgsql:equery(C, "select pg_sleep(2)"), 473 | {ok, _Cols, [{1}]} = pgsql:equery(C, "select 1") 474 | end, 475 | [{timeout, 10}]). 476 | 477 | execute_timeout_test() -> 478 | with_connection( 479 | fun(C) -> 480 | {ok, S} = pgsql:parse(C, "select pg_sleep($1)"), 481 | ok = pgsql:bind(C, S, [2]), 482 | {error, timeout} = pgsql:execute(C, S, 0), 483 | ok = pgsql:bind(C, S, [0]), 484 | {ok, [{<<>>}]} = pgsql:execute(C, S, 0), 485 | ok = pgsql:close(C, S), 486 | ok = pgsql:sync(C) 487 | end, 488 | [{timeout, 10}]). 489 | 490 | connection_closed_test() -> 491 | P = self(), 492 | F = fun() -> 493 | process_flag(trap_exit, true), 494 | {ok, C} = pgsql:connect(?host, [{port, ?port}]), 495 | P ! {connected, C}, 496 | receive 497 | Any -> P ! Any 498 | end 499 | end, 500 | spawn_link(F), 501 | receive 502 | {connected, C} -> 503 | timer:sleep(100), 504 | pgsql:close(C), 505 | {'EXIT', C, _} = receive R -> R end 506 | end, 507 | flush(). 508 | 509 | active_connection_closed_test() -> 510 | P = self(), 511 | F = fun() -> 512 | process_flag(trap_exit, true), 513 | {ok, C} = pgsql:connect(?host, [{port, ?port}]), 514 | P ! {connected, C}, 515 | R = pgsql:squery(C, "select pg_sleep(10)"), 516 | P ! R 517 | end, 518 | spawn_link(F), 519 | receive 520 | {connected, C} -> 521 | timer:sleep(100), 522 | pgsql:close(C), 523 | {error, closed} = receive R -> R end 524 | end, 525 | flush(). 526 | 527 | warning_notice_test() -> 528 | with_connection( 529 | fun(C) -> 530 | Q = "create function pg_temp.raise() returns void as $$ 531 | begin 532 | raise warning 'oops'; 533 | end; 534 | $$ language plpgsql; 535 | select pg_temp.raise()", 536 | [{ok, _, _}, _] = pgsql:squery(C, Q), 537 | receive 538 | {pgsql, C, {notice, #error{message = <<"oops">>}}} -> ok 539 | after 540 | 100 -> erlang:error(didnt_receive_notice) 541 | end 542 | end, 543 | [{async, self()}]). 544 | 545 | listen_notify_test() -> 546 | with_connection( 547 | fun(C) -> 548 | {ok, [], []} = pgsql:squery(C, "listen epgsql_test"), 549 | {ok, _, [{Pid}]} = pgsql:equery(C, "select pg_backend_pid()"), 550 | {ok, [], []} = pgsql:squery(C, "notify epgsql_test"), 551 | receive 552 | {pgsql, C, {notification, <<"epgsql_test">>, Pid, <<>>}} -> ok 553 | after 554 | 100 -> erlang:error(didnt_receive_notification) 555 | end 556 | end, 557 | [{async, self()}]). 558 | 559 | listen_notify_payload_test() -> 560 | with_min_version( 561 | 9.0, 562 | fun(C) -> 563 | {ok, [], []} = pgsql:squery(C, "listen epgsql_test"), 564 | {ok, _, [{Pid}]} = pgsql:equery(C, "select pg_backend_pid()"), 565 | {ok, [], []} = pgsql:squery(C, "notify epgsql_test, 'test!'"), 566 | receive 567 | {pgsql, C, {notification, <<"epgsql_test">>, Pid, <<"test!">>}} -> ok 568 | after 569 | 100 -> erlang:error(didnt_receive_notification) 570 | end 571 | end, 572 | [{async, self()}]). 573 | 574 | application_test() -> 575 | lists:foreach(fun application:start/1, ?ssl_apps), 576 | ok = application:start(epgsql). 577 | 578 | %% -- run all tests -- 579 | 580 | run_tests() -> 581 | Files = filelib:wildcard("test_ebin/*tests.beam"), 582 | Mods = [list_to_atom(filename:basename(F, ".beam")) || F <- Files], 583 | eunit:test(Mods, []). 584 | 585 | %% -- internal functions -- 586 | 587 | connect_only(Args) -> 588 | TestOpts = [{port, ?port}], 589 | case Args of 590 | [User, Opts] -> Args2 = [User, TestOpts ++ Opts]; 591 | [User, Pass, Opts] -> Args2 = [User, Pass, TestOpts ++ Opts]; 592 | Opts -> Args2 = [TestOpts ++ Opts] 593 | end, 594 | {ok, C} = apply(pgsql, connect, [?host | Args2]), 595 | pgsql:close(C), 596 | flush(). 597 | 598 | with_connection(F) -> 599 | with_connection(F, "epgsql_test", []). 600 | 601 | with_connection(F, Args) -> 602 | with_connection(F, "epgsql_test", Args). 603 | 604 | with_connection(F, Username, Args) -> 605 | Args2 = [{port, ?port}, {database, "epgsql_test_db1"} | Args], 606 | {ok, C} = pgsql:connect(?host, Username, Args2), 607 | try 608 | F(C) 609 | after 610 | pgsql:close(C) 611 | end, 612 | flush(). 613 | 614 | with_rollback(F) -> 615 | with_connection( 616 | fun(C) -> 617 | try 618 | pgsql:squery(C, "begin"), 619 | F(C) 620 | after 621 | pgsql:squery(C, "rollback") 622 | end 623 | end). 624 | 625 | with_min_version(Min, F, Args) -> 626 | with_connection( 627 | fun(C) -> 628 | {ok, Bin} = pgsql:get_parameter(C, <<"server_version">>), 629 | {ok, [{float, 1, Ver} | _], _} = erl_scan:string(binary_to_list(Bin)), 630 | case Ver >= Min of 631 | true -> F(C); 632 | false -> ?debugFmt("skipping test requiring PostgreSQL >= ~.2f~n", [Min]) 633 | end 634 | end, 635 | Args). 636 | 637 | check_type(Type, In, Out, Values) -> 638 | Column = "c_" ++ atom_to_list(Type), 639 | check_type(Type, In, Out, Values, Column). 640 | 641 | check_type(Type, In, Out, Values, Column) -> 642 | with_connection( 643 | fun(C) -> 644 | Select = io_lib:format("select ~s::~w", [In, Type]), 645 | {ok, [#column{type = Type}], [{Out}]} = pgsql:equery(C, Select), 646 | Sql = io_lib:format("insert into test_table2 (~s) values ($1) returning ~s", [Column, Column]), 647 | {ok, #statement{columns = [#column{type = Type}]} = S} = pgsql:parse(C, Sql), 648 | Insert = fun(V) -> 649 | pgsql:bind(C, S, [V]), 650 | {ok, 1, [{V2}]} = pgsql:execute(C, S), 651 | case compare(Type, V, V2) of 652 | true -> ok; 653 | false -> ?debugFmt("~p =/= ~p~n", [V, V2]), ?assert(false) 654 | end, 655 | ok = pgsql:sync(C) 656 | end, 657 | lists:foreach(Insert, [null | Values]) 658 | end). 659 | 660 | compare(_Type, null, null) -> true; 661 | compare(float4, V1, V2) -> abs(V2 - V1) < 0.000001; 662 | compare(float8, V1, V2) -> abs(V2 - V1) < 0.000000000000001; 663 | compare(_Type, V1, V2) -> V1 =:= V2. 664 | 665 | %% flush mailbox 666 | flush() -> 667 | ?assertEqual([], flush([])). 668 | 669 | flush(Acc) -> 670 | receive 671 | {'EXIT', _Pid, normal} -> flush(Acc); 672 | M -> flush([M | Acc]) 673 | after 674 | 0 -> lists:reverse(Acc) 675 | end. 676 | --------------------------------------------------------------------------------