Просмотр исходного кода

[Doc][Minor] Improve README.md

Alexander Burkov 10 лет назад
Родитель
Сommit
b26786293b
3 измененных файлов с 434 добавлено и 319 удалено
  1. 382 282
      README.md
  2. 18 14
      include/epgsql.hrl
  3. 34 23
      src/epgsql.erl

+ 382 - 282
README.md

@@ -1,325 +1,425 @@
 # Erlang PostgreSQL Database Client
 
-Asynchronous fork of https://github.com/wg/epgsql originally here:
-https://github.com/mabrek/epgsql and subsequently forked in order to
+Asynchronous fork of [wg/epgsql](https://github.com/wg/epgsql) originally here:
+[mabrek/epgsql](https://github.com/mabrek/epgsql) and subsequently forked in order to
 provide a common fork for community development.
 
-* Motivation
+## Motivation
 
-  When you need to execute several queries, it involves a number network
-  round-trips between the application and the database.
-  The PostgreSQL frontend/backend protocol supports request pipelining.
-  This means that you don't need to wait for the previous command to finish
-  before sending the next command. This version of the driver makes full use
-  of the protocol feature that allows faster execution.
+When you need to execute several queries, it involves a number network
+round-trips between the application and the database.
+The PostgreSQL frontend/backend protocol supports request pipelining.
+This means that you don't need to wait for the previous command to finish
+before sending the next command. This version of the driver makes full use
+of the protocol feature that allows faster execution.
 
 
-* Difference highlights
+## Difference highlights
 
-  + 3 API sets: epgsql, epgsqla and epgsqli:
-    epgsql maintains backwards compatibility with the original driver API,
-    epgsqla delivers complete results as regular erlang messages,
-    epgsqli delivers results as messages incrementally (row by row)
-  + internal queue of client requests, so you don't need to wait for the
-    response to send the next request
-  + single process to hold driver state and receive socket data
-  + execution of several parsed statements as a batch
-  + binding timestamps in `erlang:now()` format
-  see CHANGES for full list.
+- 3 API sets:
+  - **epgsql** maintains backwards compatibility with the original driver API
+  - **epgsqla** delivers complete results as regular erlang messages
+  - **epgsqli** delivers results as messages incrementally (row by row)
+- internal queue of client requests, so you don't need to wait for the response
+  to send the next request
+- single process to hold driver state and receive socket data
+- execution of several parsed statements as a batch
+- binding timestamps in `erlang:now()` format
 
-* Differences between devel branch and mabrek's original async fork:
+see `CHANGES` for full list.
 
-  + Unnamed statements are used unless specified otherwise.  This may
-    cause problems for people attempting to use the same connection
-    concurrently, which will no longer work.
+### Differences between devel branch and mabrek's original async fork:
 
-* Known problems
+- Unnamed statements are used unless specified otherwise. This may
+  cause problems for people attempting to use the same connection
+  concurrently, which will no longer work.
 
-  A timeout supplied at connect time works as a socket connect timeout,
+## Known problems
+
+- A timeout supplied at connect time works as a socket connect timeout,
   not a query timeout. It passes all tests from original driver except for
   the 3 failing timeout tests.
-  SSL performance can degrade if the driver process has a large inbox
+- SSL performance can degrade if the driver process has a large inbox
   (thousands of messages).
-  Usage of unnamed prepared statement and portals leads to unpredicted results
+- Usage of unnamed prepared statement and portals leads to unpredicted results
   in case of concurrent access to same connection.
 
+## Usage
+### Connect
+
+```erlang
+-type host() :: inet:ip_address() | inet:hostname().
+
+-type connect_option() ::
+    {database, DBName     :: string()}             |
+    {port,     PortNum    :: inet:port_number()}   |
+    {ssl,      IsEnabled  :: boolean() | required} |
+    {ssl_opts, SslOptions :: [ssl:ssl_option()]}   | % @see OTP ssl app, ssl_api.hrl
+    {timeout,  TimeoutMs  :: timeout()}            | % default: 5000 ms
+    {async,    Receiver   :: pid()}. % process to receive LISTEN/NOTIFY msgs
+    
+-spec connect(host(), string(), string(), [connect_option()])
+        -> {ok, Connection :: connection()} | {error, Reason :: connect_error()}.    
+%% @doc connects to Postgres
+%% where
+%% `Host'     - host to connect to
+%% `Username' - username to connect as, defaults to `$USER'
+%% `Password' - optional password to authenticate with
+%% `Opts'     - proplist of extra options
+%% returns `{ok, Connection}' otherwise `{error, Reason}'
+connect(Host, Username, Password, Opts) -> ...
+```
+example:
+```erlang
+{ok, C} = epgsql:connect("localhost", "username", "psss", [
+    {database, "test_db"},
+    {timeout, 4000}
+]),
+...
+ok = epgsql:close(C).
+```
+
+The `{timeout, TimeoutMs}` parameter will trigger an `{error, timeout}` result when the
+socket fails to connect within `TimeoutMs` milliseconds.
+
+Asynchronous connect example (applies to **epgsqli** too):
+
+```erlang
+  {ok, C} = epgsqla:start_link(),
+  Ref = epgsqla:connect(C, "localhost", "username", "psss", [{database, "test_db"}]),
+  receive
+    {C, Ref, connected} ->
+        {ok, C};
+    {C, Ref, Error = {error, _}} ->
+        Error;
+    {'EXIT', C, _Reason} ->
+        {error, closed}
+  end.
+```
+
+### Simple Query
+
+```erlang
+-type query() :: string() | iodata().
+-type squery_row() :: {binary()}.
+
+-record(column, {
+    name :: binary(),
+    type :: epgsql_type(),
+    size :: -1 | pos_integer(),
+    modifier :: -1 | pos_integer(),
+    format :: integer()
+}).
+
+-type ok_reply(RowType) ::
+    {ok, Count :: non_neg_integer()} |                                                            % select
+    {ok, ColumnsDescription :: [#column{}], RowsValues :: [RowType]} |                            % update/insert
+    {ok, Count :: non_neg_integer(), ColumnsDescription :: [#column{}], RowsValues :: [RowType]}. % update/insert + returning
+-type error_reply() :: {error, query_error()}.
+-type reply(RowType) :: ok_reply() | error_reply().
+
+-spec squery(connection(), query()) -> reply(squery_row()) | [reply(squery_row())].
+%% @doc runs simple `SqlQuery' via given `Connection'
+squery(Connection, SqlQuery) -> ...
+```
+examples:
+```erlang
+InsertRes = epgsql:squery(C, "insert into account (name) values  ('alice'), ('bob')"),
+io:format("~p~n", [InsertRes]),
+```
+> ```
+{ok,2}
+```
+
+```erlang
+SelectRes = epgsql:squery(C, "select * from account"),
+io:format("~p~n", [SelectRes]).
+```
+> ```
+{ok,
+    [{column,<<"id">>,int4,4,-1,0},{column,<<"name">>,text,-1,-1,0}],
+    [{<<"1">>,<<"alice">>},{<<"2">>,<<"bob">>}]
+}
+```
+
+```erlang
+InsertReturningRes = epgsql:squery(C, 
+    "insert into account(name)"
+    "    values ('joe'), (null)"
+    "    returning *"),
+io:format("~p~n", [InsertReturningRes]).
+```
+> ```
+{ok,2,
+    [{column,<<"id">>,int4,4,-1,0}, {column,<<"name">>,text,-1,-1,0}],
+    [{<<"3">>,<<"joe">>},{<<"4">>,null}]
+}
+```
+
+```erlang
+{error, Reason} = epgsql:squery(C, "insert into account values (1, 'bad_pkey')"),
+io:format("~p~n", [Reason]).
+```
+> ```
+{error,
+    error,
+    <<"23505">>,
+    <<"duplicate key value violates unique constraint \"account_pkey\"">>,
+    [{detail,<<"Key (id)=(1) already exists.">>}]
+}
+```
+
+The simple query protocol returns all columns as binary strings
+and does not support parameters binding.
+
+Several queries separated by semicolon can be executed by squery.
+
+```erlang
+  [{ok, _, [{<<"1">>}]}, {ok, _, [{<<"2">>}]}] = epgsql:squery(C, "select 1; select 2").
+```
+
+`epgsqla:squery/2` returns result as a single message:
+
+```erlang
+  Ref = epgsqla:squery(C, Sql),
+  receive
+    {C, Ref, Result} -> Result
+  end.
+```
+
+Result has the same format as return value of `epgsql:squery/2`.
+
+`epgsqli:squery/2` returns results incrementally for each query inside Sql and for each row:
+
+```erlang
+Ref = epgsqli:squery(C, Sql),
+receive
+  {C, Ref, {columns, Columns}} ->
+      %% columns description
+      Columns;
+  {C, Ref, {data, Row}} ->
+      %% single data row
+      Row;
+  {C, Ref, {error, _E} = Error} ->
+      Error;
+  {C, Ref, {complete, {_Type, Count}}} ->
+      %% execution of one insert/update/delete has finished
+      {ok, Count}; % affected rows count
+  {C, Ref, {complete, _Type}} ->
+      %% execution of one select has finished
+      ok;
+  {C, Ref, done} ->
+      %% execution of all queries from Sql has been finished
+      done;
+end.
+```
+
+## Extended Query
+
+```erlang
+{ok, Columns, Rows}        = epgsql:equery(C, "select ...", [Parameters]).
+{ok, Count}                = epgsql:equery(C, "update ...", [Parameters]).
+{ok, Count, Columns, Rows} = epgsql:equery(C, "insert ... returning ...", [Parameters]).
+{error, Error}             = epgsql:equery(C, "invalid SQL", [Parameters]).
+```
+`Parameters` - optional list of values to be bound to `$1`, `$2`, `$3`, etc.
+
+The extended query protocol combines parse, bind, and execute using
+the unnamed prepared statement and portal. A `select` statement returns
+`{ok, Columns, Rows}`, `insert/update/delete` returns `{ok, Count}` or
+`{ok, Count, Columns, Rows}` when a `returning` clause is present. When
+an error occurs, all statements result in `{error, #error{}}`.
+
+```erlang
+SelectRes = epgsql:equery(C, "select id from account where name = $1", ["alice"]),
+io:format("~p~n", [SelectRes]).
+```
+> ```
+{ok,
+    [{column,<<"id">>,int4,4,-1,1}],
+    [{1}]
+}
+```
+
+PostgreSQL's binary format is used to return integers as Erlang
+integers, floats as floats, bytes/text/varchar columns as binaries,
+bools as true/false, etc. For details see `pgsql_binary.erl` and the
+Data Representation section below.
+
+Asynchronous API `epgsqla:equery/3` requires you to parse statement beforehand
+
+```erlang
+Ref = epgsqla:equery(C, Statement, [Parameters]),
+receive
+  {C, Ref, Res} -> Res
+end.
+```
+
+- `Statement` - parsed statement (see parse below)
+- `Res` has same format as return value of `epgsql:equery/3`.
+
+`epgsqli:equery(C, Statement, [Parameters])` sends same set of messages as
+squery including final `{C, Ref, done}`.
+
+
+## Parse/Bind/Execute
+
+```erlang
+{ok, Statement} = epgsql:parse(C, [StatementName], Sql, [ParameterTypes]).
+```
+
+- `StatementName`   - optional, reusable, name for the prepared statement.
+- `ParameterTypes`  - optional list of PostgreSQL types for each parameter.
+
+For valid type names see `pgsql_types.erl`.
+
+`epgsqla:parse/2` sends `{C, Ref, {ok, Statement} | {error, Reason}}`.
+`epgsqli:parse/2` sends:
+ - `{C, Ref, {types, Types}}`
+ - `{C, Ref, {columns, Columns}}`
+ - `{C, Ref, no_data}` if statement will not return rows
+ - `{C, Ref, {error, Reason}}`
+
+```erlang
+ok = epgsql:bind(C, Statement, [PortalName], ParameterValues).
+```
+
+- `PortalName`      - optional name for the result portal.
+
+both `epgsqla:bind/3` and `epgsqli:bind/3` send `{C, Ref, ok | {error, Reason}}`
+
+```erlang
+{ok | partial, Rows} = epgsql:execute(C, Statement, [PortalName], [MaxRows]).
+{ok, Count}          = epgsql:execute(C, Statement, [PortalName]).
+{ok, Count, Rows}    = epgsql:execute(C, Statement, [PortalName]).
+```
+
+- `PortalName`      - optional portal name used in `epgsql:bind/4`.
+- `MaxRows`         - maximum number of rows to return (0 for all rows).
+
+`epgsql:execute/3` returns `{partial, Rows}` when more rows are available.
+
+`epgsqla:execute/3` sends `{C, Ref, Result}` where `Result` has same format as
+return value of `epgsql:execute/3`.
+
+`epgsqli:execute/3` sends
+- `{C, Ref, {data, Row}}`
+- `{C, Ref, {error, Reason}}`
+- `{C, Ref, suspended}` partial result was sent, more rows are available
+- `{C, Ref, {complete, {_Type, Count}}}`
+- `{C, Ref, {complete, _Type}}`
+
+```erlang
+ok = epgsql:close(C, Statement).
+ok = epgsql:close(C, statement | portal, Name).
+ok = epgsql:sync(C).
+```
+
+All epgsql functions return `{error, Error}` when an error occurs.
+
+`epgsqla`/`epgsqli` modules' `close` and `sync` functions send `{C, Ref, ok}`.
+
+
+## Batch execution
 
-* Connect
-
-         {ok, C} = epgsql:connect(Host, [Username], [Password], Opts).
-
-  Host      - host to connect to.
-  Username  - username to connect as, defaults to $USER.
-  Password  - optional password to authenticate with.
-  Opts      - property list of extra options. Supported properties:
-
-    + `{database, String}`
-    + `{port,     Integer}`
-    + `{ssl,      Atom}`       true | false | required
-    + `{ssl_opts, List}`       see ssl application docs in OTP
-    + `{timeout,  Integer}`    milliseconds, defaults to 5000
-    + `{async,    Pid}`        see Server Notifications section
-  
-  
-  Example:
-  
-      {ok, C} = epgsql:connect("localhost", "username", [{database, "test_db"}]).
-      ok = epgsql:close(C).
-
-  The timeout parameter will trigger an `{error, timeout}` result when the
-  socket fails to connect within Timeout milliseconds.
-
-  Asynchronous connect example (applies to epgsqli too):
-
-        {ok, C} = epgsqla:start_link(),
-        Ref = epgsqla:connect(C, "localhost", "username", [{database, "test_db"}]),
-        receive
-          {C, Ref, connected} ->
-              {ok, C};
-          {C, Ref, Error = {error, _}} ->
-              Error;
-          {'EXIT', C, _Reason} ->
-              {error, closed}
-        end.
-
-
-* Simple Query
-
-        {ok, Columns, Rows}        = epgsql:squery(C, "select ...").
-        {ok, Count}                = epgsql:squery(C, "update ...").
-        {ok, Count, Columns, Rows} = epgsql:squery(C, "insert ... returning ...").
-
-        {error, Error}             = epgsql:squery(C, "invalid SQL").
-
-  + `Columns`       - list of column records, see epgsql.hrl for definition.
-  + `Rows`          - list of tuples, one for each row.
-  + `Count`         - integer count of rows inserted/updated/etc
-
-  The simple query protocol returns all columns as text (Erlang binaries)
-  and does not support binding parameters.
-
-  Several queries separated by semicolon can be executed by squery.
-
-        [{ok, _, [{<<"1">>}]}, {ok, _, [{<<"2">>}]}] =
-          epgsql:squery(C, "select 1; select 2").
-
-  `epgsqla:squery` returns result as a single message:
-
-        Ref = epgsqla:squery(C, Sql),
-        receive
-          {C, Ref, Result} -> Result
-        end.
-
-  `Result` has same format as return value of epgsql:squery.
-
-  `epgsqli:squery` returns results incrementally for each query inside Sql and
-  for each row:
-
-        Ref = epgsqli:squery(C, Sql),
-        receive
-          {C, Ref, {columns, Columns}} ->
-              %% columns description
-              Columns;
-          {C, Ref, {data, Row}} ->
-              %% single data row
-              Row;
-          {C, Ref, {error, _E} = Error} ->
-              Error;
-          {C, Ref, {complete, {_Type, Count}}} ->
-              %% execution of one insert/update/delete has finished
-              {ok, Count}; % affected rows count
-          {C, Ref, {complete, _Type}} ->
-              %% execution of one select has finished
-              ok;
-          {C, Ref, done} ->
-              %% execution of all queries from Sql has finished
-              done;
-        end.
-
-
-* Extended Query
-
-        {ok, Columns, Rows}        = epgsql:equery(C, "select ...", [Parameters]).
-        {ok, Count}                = epgsql:equery(C, "update ...", [Parameters]).
-        {ok, Count, Columns, Rows} = epgsql:equery(C, "insert ... returning ...", [Parameters]).
-
-        {error, Error}             = epgsql:equery(C, "invalid SQL", [Parameters]).
-
-  + `Parameters`    - optional list of values to be bound to $1, $2, $3, etc.
-
-  The extended query protocol combines parse, bind, and execute using
-  the unnamed prepared statement and portal. A "select" statement returns
-  `{ok, Columns, Rows}`, "insert/update/delete" returns `{ok, Count}` or
-  `{ok, Count, Columns, Rows}` when a "returning" clause is present. When
-  an error occurs, all statements result in `{error, #error{}}`.
-
-  PostgreSQL's binary format is used to return integers as Erlang
-  integers, floats as floats, bytea/text/varchar columns as binaries,
-  bools as true/false, etc. For details see `epgsql_binary.erl` and the
-  Data Representation section below.
-
-  Asynchronous api equery requires you to parse statement beforehand
-
-        Ref = epgsqla:equery(C, Statement, [Parameters]),
-        receive
-          {C, Ref, Res} -> Res
-        end.
-
-  + `Statement` - parsed statement (see parse below)
-  + `Res` has same format as return value of `epgsql:equery`.
+Batch execution is `bind` + `execute` for several prepared statements.
+It uses unnamed portals and `MaxRows = 0`.
+
+```erlang
+Results = epgsql:execute_batch(C, Batch).
+```
 
-  `epgsqli:equery(C, Statement, [Parameters])` sends same set of messages as
-  squery including the final `{C, Ref, done}`.
+- `Batch`   - list of {Statement, ParameterValues}
+- `Results` - list of {ok, Count} or {ok, Count, Rows}
 
+example:
 
-* Parse/Bind/Execute
+```erlang
+{ok, S1} = epgsql:parse(C, "one", "select $1", [int4]),
+{ok, S2} = epgsql:parse(C, "two", "select $1 + $2", [int4, int4]),
+[{ok, [{1}]}, {ok, [{3}]}] = epgsql:execute_batch(C, [{S1, [1]}, {S2, [1, 2]}]).
+```
 
-         {ok, Statement} = epgsql:parse(C, [StatementName], Sql, [ParameterTypes]).
+`epgsqla:execute_batch/3` sends `{C, Ref, Results}`
+`epgsqli:execute_batch/3` sends
+- `{C, Ref, {data, Row}}`
+- `{C, Ref, {error, Reason}}`
+- `{C, Ref, {complete, {_Type, Count}}}`
+- `{C, Ref, {complete, _Type}}`
+- `{C, Ref, done}` - execution of all queries from Batch has finished
 
-  + `StatementName`   - optional, reusable, name for the prepared statement.
-  + `ParameterTypes`  - optional list of PostgreSQL types for each parameter.
 
-  For valid type names see `epgsql_types.erl`.
+## Data Representation
+PG type       | Representation
+--------------|-------------------------------------
+  null        | `null`
+  bool        | `true` | `false`
+  char        | `$A` | `binary`
+  intX        | `1`
+  floatX      | `1.0`
+  date        | `{Year, Month, Day}`
+  time        | `{Hour, Minute, Second.Microsecond}`
+  timetz      | `{time, Timezone}`
+  timestamp   | `{date, time}`
+  timestamptz | `{date, time}`
+  interval    | `{time, Days, Months}`
+  text        | `<<"a">>`
+  varchar     | `<<"a">>`
+  bytea       | `<<1, 2>>`
+  array       | `[1, 2, 3]`
+  record      | `{int2, time, text, ...}` (decode only)
+  point       |  `{10.2, 100.12}`
 
-  `epgsqla:parse` sends `{C, Ref, {ok, Statement} | {error, Reason}}`.
-  `epgsqli:parse` sends:
+  `timestamp` and `timestamptz` parameters can take `erlang:now()` format: `{MegaSeconds, Seconds, MicroSeconds}`
 
-        {C, Ref, {types, Types}}
-        {C, Ref, {columns, Columns}}
-        {C, Ref, no_data} if statement will not return rows
-        {C, Ref, {error, Reason}}
+## Errors
 
-        ok = epgsql:bind(C, Statement, [PortalName], ParameterValues).
+Errors originating from the PostgreSQL backend are returned as `{error, #error{}}`,
+see `epgsql.hrl` for the record definition. `epgsql` functions may also return
+`{error, What}` where `What` is one of the following:
 
-  + `PortalName`      - optional name for the result portal.
+- `{unsupported_auth_method, Method}`     - required auth method is unsupported
+- `timeout`                               - request timed out
+- `closed`                                - connection was closed
+- `sync_required`                         - error occured and epgsql:sync must be called
 
-  both `epgsqla:bind` and `epgsqli:bind` send `{C, Ref, ok | {error, Reason}}`
+## Server Notifications
 
-        {ok | partial, Rows} = epgsql:execute(C, Statement, [PortalName], [MaxRows]).
-        {ok, Count}          = epgsql:execute(C, Statement, [PortalName]).
-        {ok, Count, Rows}    = epgsql:execute(C, Statement, [PortalName]).
+PostgreSQL may deliver two types of asynchronous message: "notices" in response
+to notice and warning messages generated by the server, and "notifications" which
+are generated by the `LISTEN/NOTIFY` mechanism.
+
+Passing the `{async, Pid}` option to `epgsql:connect/3` will result in these async
+messages being sent to the specified process, otherwise they will be dropped.
 
-  + `PortalName`      - optional portal name used in `bind/4`.
-  + `MaxRows`         - maximum number of rows to return (0 for all rows).
+Message formats:
 
-  execute returns `{partial, Rows}` when more rows are available.
+```erlang
+{epgsql, Connection, {notification, Channel, Pid, Payload}}
+```
+- `Connection`  - connection the notification occurred on
+- `Channel`  - channel the notification occurred on
+- `Pid`  - database session pid that sent notification
+- `Payload`  - optional payload, only available from PostgreSQL >= 9.0
 
-  `epgsqla:execute` sends `{C, Ref, Result}` where `Result` has the same
-  format as the return value of `epgsql:execute`.
+```erlang
+{epgsql, Connection, {notice, Error}}
+```
+- `Connection`  - connection the notice occurred on
+- `Error`       - an `#error{}` record, see `epgsql.hrl`
 
-  `epgsqli:execute` sends
 
-        {C, Ref, {data, Row}}
-        {C, Ref, {error, Reason}}
-        {C, Ref, suspended} partial result was sent, more rows are available
-        {C, Ref, {complete, {_Type, Count}}}
-        {C, Ref, {complete, _Type}}
-
-        ok = epgsql:close(C, Statement).
-        ok = epgsql:close(C, statement | portal, Name).
-        ok = epgsql:sync(C).
-
-  All epgsql functions return `{error, Error}` when an error occurs.
-
-  epgsqla and epgsqli close and sync functions send `{C, Ref, ok}`.
-
-
-* Batch execution
-
-  Batch execution is bind + execute for several prepared statements.
-  It uses unnamed portals and MaxRows = 0.
-
-        Results = epgsql:execute_batch(C, Batch).
-
-  + `Batch`   - list of `{Statement, ParameterValues}`
-  + `Results` - list of `{ok, Count}` or `{ok, Count, Rows}`
-
-  Example
-
-        {ok, S1} = epgsql:parse(C, "one", "select $1", [int4]),
-        {ok, S2} = epgsql:parse(C, "two", "select $1 + $2", [int4, int4]),
-        [{ok, [{1}]}, {ok, [{3}]}] =
-          epgsql:execute_batch(C, [{S1, [1]}, {S2, [1, 2]}]).
-
-  `epgsqla:execute_batch` sends `{C, Ref, Results}`
-  `epgsqli:execute_batch` sends
-
-        {C, Ref, {data, Row}}
-        {C, Ref, {error, Reason}}
-        {C, Ref, {complete, {_Type, Count}}}
-        {C, Ref, {complete, _Type}}
-        {C, Ref, done} - execution of all queries from Batch has finished
-
-
-* Data Representation
-
-        null        = null
-        bool        = true | false
-        char        = $A | binary
-        intX        = 1
-        floatX      = 1.0
-        date        = {Year, Month, Day}
-        time        = {Hour, Minute, Second.Microsecond}
-        timetz      = {time, Timezone}
-        timestamp   = {date, time}
-        timestamptz = {date, time}
-        interval    = {time, Days, Months}
-        text        = <<"a">>
-        varchar     = <<"a">>
-        bytea       = <<1, 2>>
-        array       = [1, 2, 3]
-        point       = {10.2, 100.12}
-
-        record      = {int2, time, text, ...} (decode only)
-
-        timestamp and timestamptz parameters can take erlang:now() format {MegaSeconds, Seconds, MicroSeconds}
-
-* Errors
-
-  Errors originating from the PostgreSQL backend are returned as `{error, #error{}}`,
-  see `epgsql.hrl` for the record definition. epgsql functions may also return
-  `{error, What}` where What is one of the following:
-
-  + `{unsupported_auth_method, Method}`     - required auth method is unsupported
-  + `timeout`                               - request timed out
-  + `closed`                                - connection was closed
-  + `sync_required`                         - error occured and epgsql:sync must be called
-
-* Server Notifications
-
-  PostgreSQL may deliver two types of asynchronous message: "notices" in response
-  to notice and warning messages generated by the server, and "notifications" which
-  are generated by the LISTEN/NOTIFY mechanism.
-
-  Passing the `{async, Pid}` option to `epgsql:connect` will result in these async
-  messages being sent to the specified process, otherwise they will be dropped.
-
-  Message formats:
-
-  `{epgsql, Connection, {notification, Channel, Pid, Payload}}`
-
-  + `Connection`  - connection the notification occurred on
-  + `Channel`     - channel the notification occurred on
-  + `Pid`         - database session pid that sent notification
-  +` Payload`     - optional payload, only available from PostgreSQL >= 9.0
-
-          {epgsql, Connection, {notice, Error}}
-
-  + `Connection`  - connection the notice occurred on
-  + `Error`       - an `#error{}` record, see `epgsql.hrl`
-
-
-* Mailing list / forum
-
-https://groups.google.com/forum/#!forum/epgsql
+## Mailing list
 
+  [Google groups](https://groups.google.com/forum/#!forum/epgsql)
+  
 ## Test Setup
 
-In order to run the epgsql tests, you will need to make some
+In order to run the `epgsql` tests, you will need to make some
 modifications to your local Postgres setup:
 
-1. Add the lines at the top of `test_data/test_schema.sql` to your `/etc/postgresql/pg_hba.conf` file.  Change $USER to your username.
+1. Add the lines at the top of `test_data/test_schema.sql` to your `/etc/postgresql/pg_hba.conf` file.  Change `$USER` to your username.
 
-2. Run the test_data/test_schema.sql script like so: `psql template1 < test_data/test_schema.sql`, as the user you intend to run the tests as.
+2. Run the `test_data/test_schema.sql` script like so: `psql template1 < test_data/test_schema.sql`, as the user you intend to run the tests as.
 
 3. `make test` .  Currently, 6 of the tests fail.

+ 18 - 14
include/epgsql.hrl

@@ -1,18 +1,22 @@
 -type epgsql_type() :: atom() | {array, atom()} | {unknown_oid, integer()}.
 
--record(column,    {name :: binary(),
-                    type :: epgsql_type(),
-                    size :: -1 | pos_integer(),
-                    modifier :: -1 | pos_integer(),
-                    format :: integer()}).
+-record(column, {
+    name :: binary(),
+    type :: epgsql_type(),
+    size :: -1 | pos_integer(),
+    modifier :: -1 | pos_integer(),
+    format :: integer()
+}).
 
--record(statement, {name :: string(),
-                    columns :: [#column{}],
-                    types :: [epgsql_type()]}).
+-record(statement, {
+    name :: string(),
+    columns :: [#column{}],
+    types :: [epgsql_type()]
+}).
 
--record(error,  {severity :: fatal | error | atom(), %TODO: concretize
-                 code :: binary(),
-                 message :: binary(),
-                 extra :: [{detail, binary()}
-                           | {hint, binary()}
-                           | {position, binary()}]}).
+-record(error, {
+    severity :: fatal | error | atom(), %TODO: concretize
+    code :: binary(),
+    message :: binary(),
+    extra :: [{detail, binary()} | {hint, binary()} | {position, binary()}]
+}).

+ 34 - 23
src/epgsql.erl

@@ -23,17 +23,20 @@
 -export_type([connection/0, connect_option/0,
               connect_error/0, query_error/0,
               bind_param/0,
-              squery_row/0, equery_row/0, ok_reply/1]).
+              squery_row/0, equery_row/0, reply/1]).
 
 -include("epgsql.hrl").
 
+-type query() :: string() | iodata().
+-type host() :: inet:ip_address() | inet:hostname().
 -type connection() :: pid().
--type connect_option() :: {database, string()}
-                          | {port, inet:port_number()}
-                          | {ssl, boolean() | required}
-                          | {ssl_opts, list()} % ssl:option(), see OTP ssl_api.hrl
-                          | {timeout, timeout()}
-                          | {async, pid()}.
+-type connect_option() ::
+    {database, DBName     :: string()}             |
+    {port,     PortNum    :: inet:port_number()}   |
+    {ssl,      IsEnabled  :: boolean() | required} |
+    {ssl_opts, SslOptions :: [ssl:ssl_option()]}   | % @see OTP ssl app, ssl_api.hrl
+    {timeout,  TimeoutMs  :: timeout()}            | % default: 5000 ms
+    {async,    Receiver   :: pid()}. % process to receive LISTEN/NOTIFY msgs
 -type connect_error() :: #error{}.
 -type query_error() :: #error{}.
 
@@ -52,9 +55,12 @@
 
 -type squery_row() :: {binary()}.
 -type equery_row() :: {bind_param()}.
--type ok_reply(RowType) :: {ok, [#column{}], [RowType]}  % SELECT
-                         | {ok, non_neg_integer()}   % UPDATE / INSERT
-                         | {ok, non_neg_integer(), [#column{}], [RowType]}. % UPDATE / INSERT + RETURNING
+-type ok_reply(RowType) ::
+    {ok, Count :: non_neg_integer()} |                                                            % select
+    {ok, ColumnsDescription :: [#column{}], RowsValues :: [RowType]} |                            % update/insert
+    {ok, Count :: non_neg_integer(), ColumnsDescription :: [#column{}], RowsValues :: [RowType]}. % update/insert + returning
+-type error_reply() :: {error, query_error()}.
+-type reply(RowType) :: ok_reply(RowType) | error_reply().
 
 %% -- client interface --
 connect(Settings) ->
@@ -69,13 +75,21 @@ connect(Host, Opts) ->
 connect(Host, Username, Opts) ->
     connect(Host, Username, "", Opts).
 
+-spec connect(host(), string(), string(), [connect_option()])
+        -> {ok, Connection :: connection()} | {error, Reason :: connect_error()}.
+%% @doc connects to Postgres
+%% where
+%% `Host'     - host to connect to
+%% `Username' - username to connect as, defaults to `$USER'
+%% `Password' - optional password to authenticate with
+%% `Opts'     - proplist of extra options
+%% returns `{ok, Connection}' otherwise `{error, Reason}'
 connect(Host, Username, Password, Opts) ->
     {ok, C} = epgsql_sock:start_link(),
     connect(C, Host, Username, Password, Opts).
 
--spec connect(connection(), inet:ip_address() | inet:hostname(),
-              string(), string(), [connect_option()]) ->
-                     {ok, pid()} | {error, connect_error()}.
+-spec connect(connection(), host(), string(), string(), [connect_option()])
+        -> {ok, Connection :: connection()} | {error, Reason :: connect_error()}.
 connect(C, Host, Username, Password, Opts) ->
     %% TODO connect timeout
     case gen_server:call(C,
@@ -105,11 +119,10 @@ close(C) ->
 get_parameter(C, Name) ->
     epgsql_sock:get_parameter(C, Name).
 
--spec squery(connection(), string() | iodata()) ->
-                    ok_reply(squery_row()) | {error, query_error()} |
-                    [ok_reply(squery_row()) | {error, query_error()}].
-squery(C, Sql) ->
-    gen_server:call(C, {squery, Sql}, infinity).
+-spec squery(connection(), query()) -> reply(squery_row()) | [reply(squery_row())].
+%% @doc runs simple `SqlQuery' via given `Connection'
+squery(Connection, SqlQuery) ->
+    gen_server:call(Connection, {squery, SqlQuery}, infinity).
 
 equery(C, Sql) ->
     equery(C, Sql, []).
@@ -124,8 +137,7 @@ equery(C, Sql, Parameters) ->
             Error
     end.
 
--spec equery(connection(), string(), string() | iodata(), [bind_param()]) ->
-                    ok_reply(equery_row()) | {error, query_error()}.
+-spec equery(connection(), string(), query(), [bind_param()]) -> reply(equery_row()).
 equery(C, Name, Sql, Parameters) ->
     case parse(C, Name, Sql, []) of
         {ok, #statement{types = Types} = S} ->
@@ -143,7 +155,7 @@ parse(C, Sql) ->
 parse(C, Sql, Types) ->
     parse(C, "", Sql, Types).
 
--spec parse(connection(), iolist(), string() | iodata(), [epgsql_type()]) ->
+-spec parse(connection(), iolist(), query(), [epgsql_type()]) ->
                    {ok, #statement{}} | {error, query_error()}.
 parse(C, Name, Sql, Types) ->
     sync_on_error(C, gen_server:call(C, {parse, Name, Sql, Types}, infinity)).
@@ -177,8 +189,7 @@ execute(C, S, N) ->
 execute(C, S, PortalName, N) ->
     gen_server:call(C, {execute, S, PortalName, N}, infinity).
 
--spec execute_batch(connection(), [{#statement{}, [bind_param()]}]) ->
-                           [ok_reply(equery_row()) | {error, query_error()}].
+-spec execute_batch(connection(), [{#statement{}, [bind_param()]}]) -> [reply(equery_row())].
 execute_batch(C, Batch) ->
     gen_server:call(C, {execute_batch, Batch}, infinity).