Browse Source

Merge pull request #39 from hairyhum/devel

Geometry type support for postgis
David N. Welton 10 years ago
parent
commit
ce5cfed312
5 changed files with 826 additions and 92 deletions
  1. 309 0
      README
  2. 8 0
      src/epgsql_binary.erl
  3. 94 92
      src/epgsql_types.erl
  4. 403 0
      src/ewkb.erl
  5. 12 0
      test/epgsql_tests.erl

+ 309 - 0
README

@@ -0,0 +1,309 @@
+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
+provide a common fork for community development.
+
+
+* 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.
+
+
+* Difference highlights
+
+  + 3 API sets: pgsql, apgsql and ipgsql:
+    pgsql maintains backwards compatibility with the original driver API,
+    apgsql delivers complete results as regular erlang messages,
+    ipgsql 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.
+
+* Differences between devel branch and mabrek's original async fork:
+
+  + 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.
+
+* 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
+  (thousands of messages).
+  Usage of unnamed prepared statement and portals leads to unpredicted results
+  in case of concurrent access to same connection.
+
+
+* Connect
+
+  {ok, C} = pgsql: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
+
+  {ok, C} = pgsql:connect("localhost", "username", [{database, "test_db"}]).
+  ok = pgsql: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 ipgsql too):
+
+  {ok, C} = apgsql:start_link(),
+  Ref = apgsql: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}        = pgsql:squery(C, "select ...").
+  {ok, Count}                = pgsql:squery(C, "update ...").
+  {ok, Count, Columns, Rows} = pgsql:squery(C, "insert ... returning ...").
+
+  {error, Error}             = pgsql:squery(C, "invalid SQL").
+
+  Columns       - list of column records, see pgsql.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">>}]}] =
+    pgsql:squery(C, "select 1; select 2").
+
+  apgsql:squery returns result as a single message:
+
+  Ref = apgsql:squery(C, Sql),
+  receive
+    {C, Ref, Result} -> Result
+  end.
+  Result has same format as return value of pgsql:squery.
+
+  ipgsql:squery returns results incrementally for each query inside Sql and
+  for each row:
+
+  Ref = ipgsql: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}        = pgsql:equery(C, "select ...", [Parameters]).
+  {ok, Count}                = pgsql:equery(C, "update ...", [Parameters]).
+  {ok, Count, Columns, Rows} = pgsql:equery(C, "insert ... returning ...", [Parameters]).
+
+  {error, Error}             = pgsql: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 pgsql_binary.erl and the
+  Data Representation section below.
+
+  Asynchronous api equery requires you to parse statement beforehand
+
+  Ref = apgsql:equery(C, Statement, [Parameters]),
+  receive
+    {C, Ref, Res} -> Res
+  end.
+  Statement - parsed statement (see parse below)
+  Res has same format as return value of pgsql:equery.
+
+  ipgsql:equery(C, Statement, [Parameters]) sends same set of messages as
+  squery including final {C, Ref, done}.
+
+
+* Parse/Bind/Execute
+
+  {ok, Statement} = pgsql: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.
+
+  apgsql:parse sends {C, Ref, {ok, Statement} | {error, Reason}}.
+  ipgsql:parse sends:
+    {C, Ref, {types, Types}}
+    {C, Ref, {columns, Columns}}
+    {C, Ref, no_data} if statement will not return rows
+    {C, Ref, {error, Reason}}
+
+  ok = pgsql:bind(C, Statement, [PortalName], ParameterValues).
+
+  PortalName      - optional name for the result portal.
+
+  both apgsql:bind and ipgsql:bind send {C, Ref, ok | {error, Reason}}
+
+  {ok | partial, Rows} = pgsql:execute(C, Statement, [PortalName], [MaxRows]).
+  {ok, Count}          = pgsql:execute(C, Statement, [PortalName]).
+  {ok, Count, Rows}    = pgsql:execute(C, Statement, [PortalName]).
+
+  PortalName      - optional portal name used in bind/4.
+  MaxRows         - maximum number of rows to return (0 for all rows).
+
+  execute returns {partial, Rows} when more rows are available.
+
+  apgsql:execute sends {C, Ref, Result} where Result has same format as
+  return value of pgsql:execute.
+
+  ipgsql: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 = pgsql:close(C, Statement).
+  ok = pgsql:close(C, statement | portal, Name).
+  ok = pgsql:sync(C).
+
+  All pgsql functions return {error, Error} when an error occurs.
+
+  apgsql and ipgsql 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 = pgsql:execute_batch(C, Batch).
+
+  Batch   - list of {Statement, ParameterValues}
+  Results - list of {ok, Count} or {ok, Count, Rows}
+
+  Example
+
+  {ok, S1} = pgsql:parse(C, "one", "select $1", [int4]),
+  {ok, S2} = pgsql:parse(C, "two", "select $1 + $2", [int4, int4]),
+  [{ok, [{1}]}, {ok, [{3}]}] =
+    pgsql:execute_batch(C, [{S1, [1]}, {S2, [1, 2]}]).
+
+  apgsql:execute_batch sends {C, Ref, Results}
+  ipgsql: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.1, 100.0}
+
+  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 pgsql.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 pgsql: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 pgsql:connect will result in these async
+  messages being sent to the specified process, otherwise they will be dropped.
+
+  Message formats:
+
+    {pgsql, 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
+
+    {pgsql, Connection, {notice, Error}}
+
+      Connection  - connection the notice occurred on
+      Error       - an #error{} record, see pgsql.hrl
+
+
+* Mailing list
+
+  https://groups.google.com/forum/#!forum/epgsql
+

+ 8 - 0
src/epgsql_binary.erl

@@ -66,6 +66,7 @@ encode({array, char}, L, Codec) when is_list(L) -> encode_array(bpchar, type2oid
 encode({array, Type}, L, Codec) when is_list(L) -> encode_array(Type, type2oid(Type, Codec), L, Codec);
 encode(hstore, {L}, _) when is_list(L)      -> encode_hstore(L);
 encode(point, {X,Y}, _)                     -> encode_point({X,Y});
+encode(geometry, Data, _)                   -> encode_geometry(Data);
 encode(Type, L, Codec) when is_list(L)      -> encode(Type, list_to_binary(L), Codec);
 encode(_Type, _Value, _)                    -> {error, unsupported}.
 
@@ -88,6 +89,7 @@ decode(uuid, B, _)                             -> decode_uuid(B);
 decode(hstore, Hstore, _)                      -> decode_hstore(Hstore);
 decode({array, _Type}, B, Codec)               -> decode_array(B, Codec);
 decode(point, B, _)                            -> decode_point(B);
+decode(geometry, B, _)                         -> ewkb:decode_geometry(B);
 decode(_Other, Bin, _)                         -> Bin.
 
 encode_array(Type, Oid, A, Codec) ->
@@ -190,6 +192,12 @@ encode_point({X, Y}) when is_number(X), is_number(Y) ->
 decode_point(<<X:1/big-float-unit:64, Y:1/big-float-unit:64>>) ->
     {X, Y}.
 
+encode_geometry(Data) ->
+    Bin = ewkb:encode_geometry(Data),
+    Size = byte_size(Bin),
+    <<Size:?int32, Bin/binary>>.
+
+supports(geometry) -> true;
 supports(point)   -> true;
 supports(bool)    -> true;
 supports(bpchar)  -> true;

+ 94 - 92
src/epgsql_types.erl

@@ -2,98 +2,99 @@
 
 -export([oid2type/1, type2oid/1]).
 
-oid2type(16)   -> bool;
-oid2type(17)   -> bytea;
-oid2type(18)   -> char;
-oid2type(19)   -> name;
-oid2type(20)   -> int8;
-oid2type(21)   -> int2;
-oid2type(22)   -> int2vector;
-oid2type(23)   -> int4;
-oid2type(24)   -> regproc;
-oid2type(25)   -> text;
-oid2type(26)   -> oid;
-oid2type(27)   -> tid;
-oid2type(28)   -> xid;
-oid2type(29)   -> cid;
-oid2type(30)   -> oidvector;
-oid2type(71)   -> pg_type_reltype;
-oid2type(75)   -> pg_attribute_reltype;
-oid2type(81)   -> pg_proc_reltype;
-oid2type(83)   -> pg_class_reltype;
-oid2type(142)  -> xml;
-oid2type(600)  -> point;
-oid2type(601)  -> lseg;
-oid2type(602)  -> path;
-oid2type(603)  -> box;
-oid2type(604)  -> polygon;
-oid2type(628)  -> line;
-oid2type(700)  -> float4;
-oid2type(701)  -> float8;
-oid2type(702)  -> abstime;
-oid2type(703)  -> reltime;
-oid2type(704)  -> tinterval;
-oid2type(705)  -> unknown;
-oid2type(718)  -> circle;
-oid2type(790)  -> cash;
-oid2type(829)  -> macaddr;
-oid2type(869)  -> inet;
-oid2type(650)  -> cidr;
-oid2type(1000) -> {array, bool};
-oid2type(1005) -> {array, int2};
-oid2type(1007) -> {array, int4};
-oid2type(1009) -> {array, text};
-oid2type(1014) -> {array, char};
-oid2type(1015) -> {array, varchar};
-oid2type(1016) -> {array, int8};
-oid2type(1021) -> {array, float4};
-oid2type(1022) -> {array, float8};
-oid2type(1033) -> aclitem;
-oid2type(1263) -> {array, cstring};
-oid2type(1042) -> bpchar;
-oid2type(1043) -> varchar;
-oid2type(1082) -> date;
-oid2type(1083) -> time;
-oid2type(1114) -> timestamp;
-oid2type(1115) -> {array, timestamp};
-oid2type(1182) -> {array, date};
-oid2type(1183) -> {array, time};
-oid2type(1184) -> timestamptz;
-oid2type(1185) -> {array, timestamptz};
-oid2type(1186) -> interval;
-oid2type(1187) -> {array, interval};
-oid2type(1266) -> timetz;
-oid2type(1270) -> {array, timetz};
-oid2type(1560) -> bit;
-oid2type(1562) -> varbit;
-oid2type(1700) -> numeric;
-oid2type(1790) -> refcursor;
-oid2type(2202) -> regprocedure;
-oid2type(2203) -> regoper;
-oid2type(2204) -> regoperator;
-oid2type(2205) -> regclass;
-oid2type(2206) -> regtype;
-oid2type(2211) -> {array, regtype};
-oid2type(3614) -> tsvector;
-oid2type(3642) -> gtsvector;
-oid2type(3615) -> tsquery;
-oid2type(3734) -> regconfig;
-oid2type(3769) -> regdictionary;
-oid2type(2249) -> record;
-oid2type(2275) -> cstring;
-oid2type(2276) -> any;
-oid2type(2277) -> {array, any};
-oid2type(2278) -> void;
-oid2type(2279) -> trigger;
-oid2type(2280) -> language_handler;
-oid2type(2281) -> internal;
-oid2type(2282) -> opaque;
-oid2type(2283) -> anyelement;
-oid2type(2776) -> anynonarray;
-oid2type(2950) -> uuid;
-oid2type(2951) -> {array, uuid};
-oid2type(3500) -> anyenum;
-oid2type(Oid)  -> {unknown_oid, Oid}.
+oid2type(16)    -> bool;
+oid2type(17)    -> bytea;
+oid2type(18)    -> char;
+oid2type(19)    -> name;
+oid2type(20)    -> int8;
+oid2type(21)    -> int2;
+oid2type(22)    -> int2vector;
+oid2type(23)    -> int4;
+oid2type(24)    -> regproc;
+oid2type(25)    -> text;
+oid2type(26)    -> oid;
+oid2type(27)    -> tid;
+oid2type(28)    -> xid;
+oid2type(29)    -> cid;
+oid2type(30)    -> oidvector;
+oid2type(71)    -> pg_type_reltype;
+oid2type(75)    -> pg_attribute_reltype;
+oid2type(81)    -> pg_proc_reltype;
+oid2type(83)    -> pg_class_reltype;
+oid2type(142)   -> xml;
+oid2type(600)   -> point;
+oid2type(601)   -> lseg;
+oid2type(602)   -> path;
+oid2type(603)   -> box;
+oid2type(604)   -> polygon;
+oid2type(628)   -> line;
+oid2type(700)   -> float4;
+oid2type(701)   -> float8;
+oid2type(702)   -> abstime;
+oid2type(703)   -> reltime;
+oid2type(704)   -> tinterval;
+oid2type(705)   -> unknown;
+oid2type(718)   -> circle;
+oid2type(790)   -> cash;
+oid2type(829)   -> macaddr;
+oid2type(869)   -> inet;
+oid2type(650)   -> cidr;
+oid2type(1000)  -> {array, bool};
+oid2type(1005)  -> {array, int2};
+oid2type(1007)  -> {array, int4};
+oid2type(1009)  -> {array, text};
+oid2type(1014)  -> {array, char};
+oid2type(1015)  -> {array, varchar};
+oid2type(1016)  -> {array, int8};
+oid2type(1021)  -> {array, float4};
+oid2type(1022)  -> {array, float8};
+oid2type(1033)  -> aclitem;
+oid2type(1263)  -> {array, cstring};
+oid2type(1042)  -> bpchar;
+oid2type(1043)  -> varchar;
+oid2type(1082)  -> date;
+oid2type(1083)  -> time;
+oid2type(1114)  -> timestamp;
+oid2type(1115)  -> {array, timestamp};
+oid2type(1182)  -> {array, date};
+oid2type(1183)  -> {array, time};
+oid2type(1184)  -> timestamptz;
+oid2type(1185)  -> {array, timestamptz};
+oid2type(1186)  -> interval;
+oid2type(1187)  -> {array, interval};
+oid2type(1266)  -> timetz;
+oid2type(1270)  -> {array, timetz};
+oid2type(1560)  -> bit;
+oid2type(1562)  -> varbit;
+oid2type(1700)  -> numeric;
+oid2type(1790)  -> refcursor;
+oid2type(2202)  -> regprocedure;
+oid2type(2203)  -> regoper;
+oid2type(2204)  -> regoperator;
+oid2type(2205)  -> regclass;
+oid2type(2206)  -> regtype;
+oid2type(2211)  -> {array, regtype};
+oid2type(3614)  -> tsvector;
+oid2type(3642)  -> gtsvector;
+oid2type(3615)  -> tsquery;
+oid2type(3734)  -> regconfig;
+oid2type(3769)  -> regdictionary;
+oid2type(2249)  -> record;
+oid2type(2275)  -> cstring;
+oid2type(2276)  -> any;
+oid2type(2277)  -> {array, any};
+oid2type(2278)  -> void;
+oid2type(2279)  -> trigger;
+oid2type(2280)  -> language_handler;
+oid2type(2281)  -> internal;
+oid2type(2282)  -> opaque;
+oid2type(2283)  -> anyelement;
+oid2type(2776)  -> anynonarray;
+oid2type(2950)  -> uuid;
+oid2type(2951)  -> {array, uuid};
+oid2type(3500)  -> anyenum;
+oid2type(26256) -> geometry;
+oid2type(Oid)   -> {unknown_oid, Oid}.
 
 type2oid(bool)                  -> 16;
 type2oid(bytea)                 -> 17;
@@ -186,4 +187,5 @@ type2oid(anynonarray)           -> 2776;
 type2oid(uuid)                  -> 2950;
 type2oid({array, uuid})         -> 2951;
 type2oid(anyenum)               -> 3500;
+type2oid(geometry)              -> 26256;
 type2oid(Type)                  -> {unknown_type, Type}.

+ 403 - 0
src/ewkb.erl

@@ -0,0 +1,403 @@
+-module(ewkb).
+
+-export([decode_geometry/1, encode_geometry/1]).
+
+-type point_type() :: '2d' | '3d' | '2dm' | '3dm'.
+
+-record(point,{
+  point_type :: point_type(),
+  x :: float(),
+  y :: float(),
+  z :: float(),
+  m :: float()
+  }).
+
+-type point(PointType) :: #point{ point_type :: PointType }.
+
+-record(multi_point,{
+  point_type :: PointType,
+  points :: [point(PointType)]
+  }).
+
+-type multi_point(PointType) :: #multi_point{ point_type :: PointType }.
+
+-record(line_string,{
+  point_type :: PointType,
+  points :: [point(PointType)]
+  }).
+
+-type line_string(PointType) :: #line_string{ point_type :: PointType }.
+
+-record(multi_line_string,{
+  point_type :: PointType,
+  line_strings :: [line_string(PointType)]
+  }).
+
+-type multi_line_string(PointType) :: #multi_line_string{ point_type :: PointType }.
+
+-record(circular_string,{
+  point_type :: PointType,
+  points :: [point(PointType)]
+  }).
+
+-type basic_string(PointType) :: #circular_string{ point_type :: PointType } | #line_string{ point_type :: PointType }.
+
+-record(compound_curve,{
+  point_type :: PointType,
+  lines :: [basic_string(PointType)]
+  }).
+
+-type curve(PointType) :: #circular_string{ point_type :: PointType } | #line_string{ point_type :: PointType } | #compound_curve{ point_type :: PointType }.
+
+-record(multi_curve,{
+  point_type :: PointType,
+  curves :: [curve(PointType)]
+  }).
+
+-type multi_curve(PointType) :: #multi_curve{ point_type :: PointType }.
+
+-record(polygon,{
+  point_type :: PointType,
+  rings :: [line_string(PointType)]
+  }).
+
+-type polygon(PointType) :: #polygon{ point_type :: PointType }.
+
+-record(multi_polygon,{
+  point_type :: PointType,
+  polygons :: [polygon(PointType)]
+  }).
+
+-type multi_polygon(PointType) :: #multi_polygon{ point_type :: PointType }.
+
+-record(triangle,{
+  point_type :: PointType,
+  rings :: [line_string(PointType)]
+  }).
+
+-type triangle(PointType) :: #triangle{ point_type :: PointType }.
+
+-record(curve_polygon,{
+  point_type :: PointType,
+  rings :: [curve(PointType)] 
+  }).
+
+-type curve_polygon(PointType) :: #curve_polygon{ point_type :: PointType }.
+
+-record(polyhedral_surface,{
+  point_type :: PointType,
+  polygons :: [polygon(PointType)]
+  }).
+
+-type polyhedral_surface(PointType) :: #polyhedral_surface{ point_type :: PointType }.
+
+-type surface(PointType) :: polygon(PointType) | curve_polygon(PointType) | polyhedral_surface(PointType).
+
+-record(multi_surface,{
+  point_type :: PointType,
+  surfaces :: [surface(PointType)]
+  }).
+
+-type multi_surface(PointType) :: #multi_surface{ point_type :: PointType }.
+
+-record(tin,{
+  point_type :: PointType,
+  triangles :: [triangle(PointType)]
+  }).
+
+-type tin(PointType) :: #tin{ point_type :: PointType }.
+
+-type geometry(PointType) :: point(PointType) | 
+                             line_string(PointType) | 
+                             triangle(PointType) | 
+                             tin(PointType) | 
+                             curve(PointType) | 
+                             surface(PointType) |
+                             multi_point(PointType) | 
+                             multi_line_string(PointType) | 
+                             multi_polygon(PointType) | 
+                             multi_curve(PointType) |
+                             multi_surface(PointType) |
+                             geometry_collection(PointType).
+
+-type geometry() :: geometry(point_type()).
+
+-type geometry_collection(PointType) :: [geometry(PointType)].
+
+-type geom_type() :: geometry
+                   | point       %
+                   | line_string%
+                   | polygon%
+                   | multi_point%
+                   | multi_line_string%
+                   | multi_polygon%
+                   | geometry_collection%
+                   | circular_string%
+                   | compound_curve%
+                   | curve_polygon%
+                   | multi_curve%
+                   | multi_surface%
+                   | curve%
+                   | surface%
+                   | polyhedral_surface%
+                   | tin%
+                   | triangle.%
+
+
+decode_geometry(Binary) ->
+  {Geometry, <<>>} = decode_geometry_data(Binary),
+  Geometry.
+
+encode_geometry(Geometry) ->
+  Type = encode_type(Geometry),
+  PointType = encode_point_type(Geometry),
+  Data = encode_geometry_data(Geometry),
+  <<1, Type/binary, PointType/binary, Data/binary>>.
+
+encode_geometry_data(#point{ point_type = '2d', x = X, y = Y }) ->
+  Xbin = encode_float64(X),
+  Ybin = encode_float64(Y),
+  <<Xbin/binary, Ybin/binary>>;
+encode_geometry_data(#point{ point_type = '2dm', x = X, y = Y, m = M }) ->
+  Xbin = encode_float64(X),
+  Ybin = encode_float64(Y),
+  Mbin = encode_float64(M),
+  <<Xbin/binary, Ybin/binary, Mbin/binary>>;
+encode_geometry_data(#point{ point_type = '3d', x = X, y = Y, z = Z }) ->
+  Xbin = encode_float64(X),
+  Ybin = encode_float64(Y),
+  Zbin = encode_float64(Z),
+  <<Xbin/binary, Ybin/binary, Zbin/binary>>;
+encode_geometry_data(#point{ point_type = '3dm', x = X, y = Y, z = Z, m = M }) ->
+  Xbin = encode_float64(X),
+  Ybin = encode_float64(Y),
+  Zbin = encode_float64(Z),
+  Mbin = encode_float64(M),
+  <<Xbin/binary, Ybin/binary, Zbin/binary, Mbin/binary>>;
+encode_geometry_data({SimpleCollection, _, Data})
+    when SimpleCollection == line_string;
+         SimpleCollection == circular_string;
+         SimpleCollection == polygon;
+         SimpleCollection == triangle ->
+  encode_collection(Data);
+encode_geometry_data({TypedCollection, _, Data})
+    when 
+        TypedCollection == multi_point;
+        TypedCollection == multi_line_string;
+        TypedCollection == multi_curve;
+        TypedCollection == multi_polygon;
+        TypedCollection == multi_surface;
+        TypedCollection == compound_curve;
+        TypedCollection == curve_polygon;
+        TypedCollection == geometry_collection;
+        TypedCollection == polyhedral_surface;
+        TypedCollection == tin ->
+  encode_typed_collection(Data).
+
+encode_collection(Collection) when is_list(Collection) ->
+  Length = length(Collection),
+  LengthBin = encode_int32(Length),
+  CollectionBin = lists:foldl(
+    fun(Element, Acc) ->
+      ElementBin = encode_geometry_data(Element),
+      <<Acc/binary, ElementBin/binary>>
+    end,
+    <<>>,
+    Collection),
+  <<LengthBin/binary, CollectionBin/binary>>.
+
+encode_typed_collection(Collection) when is_list(Collection) ->
+  Length = length(Collection),
+  LengthBin = encode_int32(Length),
+  CollectionBin = lists:foldl(
+    fun(Element, Acc) ->
+      ElementBin = encode_geometry(Element),
+      <<Acc/binary, ElementBin/binary>>
+    end,
+    <<>>,
+    Collection),
+  <<LengthBin/binary, CollectionBin/binary>>.  
+  
+
+encode_int32(Int) when is_integer(Int) ->
+  <<Int:1/little-integer-unit:32>>.
+
+encode_float64(Int) when is_number(Int) ->
+  <<Int:1/little-float-unit:64>>.
+
+-spec decode_geometry_data(binary()) -> {geometry(), binary()}.
+decode_geometry_data(Binary) ->
+  <<1, TypeCode:2/binary, SubtypeCode:2/binary, Data/binary>> = Binary,
+  Type = decode_type(TypeCode),
+  Subtype = decode_point_type(SubtypeCode),
+  decode_geometry_data(Type, Subtype, Data).
+
+-spec decode_geometry_data(geom_type(), point_type(), binary()) -> {geometry(), binary()}.
+decode_geometry_data(curve, _, _) -> error({curve, not_supported});
+decode_geometry_data(surface, _, _) -> error({surface, not_supported});
+decode_geometry_data(geometry, _, _) -> error({geometry, not_supported});
+decode_geometry_data(point, PointType, Data) ->
+  decode_point(PointType, Data);
+decode_geometry_data(LineType, PointType, Data) 
+    when LineType == line_string; 
+         LineType == circular_string ->
+  {Points, Rest} = decode_collection(point, PointType, Data),
+  {{LineType, PointType, Points}, Rest};
+decode_geometry_data(polygon, PointType, Data) ->
+  {Lines, Rest} = decode_collection(line_string, PointType, Data),
+  {#polygon{ point_type = PointType, rings = Lines }, Rest};
+decode_geometry_data(triangle, PointType, Data) ->
+  {#polygon{ rings = Rings }, Rest} = decode_geometry_data(polygon, PointType, Data),
+  {#triangle{ point_type = PointType, rings = Rings }, Rest};
+decode_geometry_data(Collection, PointType, Data)
+    when 
+        Collection == multi_point;
+        Collection == multi_line_string;
+        Collection == multi_curve;
+        Collection == multi_polygon;
+        Collection == multi_surface;
+        Collection == compound_curve;
+        Collection == curve_polygon;
+        Collection == geometry_collection;
+        Collection == polyhedral_surface;
+        Collection == tin  ->
+    {Lines, Rest} = decode_typed_collection(Data),
+    {{Collection, PointType, Lines}, Rest}.
+
+-spec decode_collection(geom_type(), point_type(), binary()) -> {[geometry()], binary()}.
+decode_collection(Type, PointType, Data) ->
+  {Length, CountRest} = decode_int32(Data),
+  lists:foldl(
+    fun(_, {Geoms, Rest}) ->
+      {Geom, R} = decode_geometry_data(Type, PointType, Rest),
+      {Geoms ++ [Geom], R}
+    end,
+    {[], CountRest},
+    lists:seq(1, Length)).
+
+-spec decode_typed_collection(binary()) -> {[geometry()], binary()}.
+decode_typed_collection(Data) ->
+  {Length, CountRest} = decode_int32(Data),
+  lists:foldl(
+    fun(_, {Geoms, Rest}) ->
+      {Geom, R} = decode_geometry_data(Rest),
+      {Geoms ++ [Geom], R}
+    end,
+    {[], CountRest},
+    lists:seq(1, Length)).
+
+
+-spec decode_int32(binary()) -> {integer(), binary()}.
+decode_int32(<<Hex:4/binary, Rest/binary>>) ->
+  <<Int:1/little-integer-unit:32>> = Hex,
+  {Int, Rest}.
+
+-spec decode_float64(binary()) -> {float(), binary()}.
+decode_float64(<<Hex:8/binary, Rest/binary>>) ->
+  <<Float:1/little-float-unit:64>> = Hex,
+  {Float, Rest}.
+
+decode_point(PointType, Data) ->
+  {Values, Rest} = lists:foldl(
+    fun(_, {Values, Rest}) ->
+      {Value, R} = decode_float64(Rest),
+      {Values ++ [Value], R}
+    end,
+    {[], Data},
+    lists:seq(1, point_size(PointType))),
+  Point = case {PointType, Values} of
+    {'2d', [X,Y]} ->
+      #point{ point_type = PointType, x = X, y = Y };
+    {'2dm', [X,Y,M]} ->
+      #point{ point_type = PointType, x = X, y = Y, m = M };
+    {'3d', [X,Y,Z]} ->
+      #point{ point_type = PointType, x = X, y = Y, z = Z };
+    {'3dm', [X,Y,Z,M]} ->
+      #point{ point_type = PointType, x = X, y = Y, z = Z, m = M }
+  end,
+  {Point, Rest}.
+
+-spec point_size(point_type()) -> 2..4.
+point_size('2d')  -> 2;
+point_size('2dm') -> 3;
+point_size('3d')  -> 3;
+point_size('3dm') -> 4.
+
+-spec decode_type(binary()) -> geom_type().
+decode_type(<<0,0>>) -> geometry;
+decode_type(<<1,0>>) -> point;
+decode_type(<<2,0>>) -> line_string;
+decode_type(<<3,0>>) -> polygon;
+decode_type(<<4,0>>) -> multi_point;
+decode_type(<<5,0>>) -> multi_line_string;
+decode_type(<<6,0>>) -> multi_polygon;
+decode_type(<<7,0>>) -> geometry_collection;
+decode_type(<<8,0>>) -> circular_string;
+decode_type(<<9,0>>) -> compound_curve;
+decode_type(<<10,0>>) -> curve_polygon;
+decode_type(<<11,0>>) -> multi_curve;
+decode_type(<<12,0>>) -> multi_surface;
+decode_type(<<13,0>>) -> curve;
+decode_type(<<14,0>>) -> surface;
+decode_type(<<15,0>>) -> polyhedral_surface;
+decode_type(<<16,0>>) -> tin;
+decode_type(<<17,0>>) -> triangle.
+
+-spec encode_type(geometry() | geom_type()) -> binary().
+encode_type(Geometry) when is_tuple(Geometry) ->
+  encode_type(element(1, Geometry));
+encode_type(geometry)            -> <<00, 0>>;
+encode_type(point)               -> <<01, 0>>;
+encode_type(line_string)         -> <<02, 0>>;
+encode_type(polygon)             -> <<03, 0>>;
+encode_type(multi_point)         -> <<04, 0>>;
+encode_type(multi_line_string)   -> <<05, 0>>;
+encode_type(multi_polygon)       -> <<06, 0>>;
+encode_type(geometry_collection) -> <<07, 0>>;
+encode_type(circular_string)     -> <<08, 0>>;
+encode_type(compound_curve)      -> <<09, 0>>;
+encode_type(curve_polygon)       -> <<10, 0>>;
+encode_type(multi_curve)         -> <<11, 0>>;
+encode_type(multi_surface)       -> <<12, 0>>;
+encode_type(curve)               -> <<13, 0>>;
+encode_type(surface)             -> <<14, 0>>;
+encode_type(polyhedral_surface)  -> <<15, 0>>;
+encode_type(tin)                 -> <<16, 0>>;
+encode_type(triangle)            -> <<17, 0>>.
+
+
+-spec decode_point_type(binary()) -> point_type().
+decode_point_type(<<0,0>>) -> '2d';
+decode_point_type(<<0, 64>>) -> '2dm';
+decode_point_type(<<0, 128>>) -> '3d';
+decode_point_type(<<0, 192>>) -> '3dm'.
+
+-spec encode_point_type(geometry() | point_type()) -> binary().
+encode_point_type(Geometry) when is_tuple(Geometry) ->
+  encode_point_type(element(2, Geometry));
+encode_point_type('2d') -> <<0,0>>;
+encode_point_type('2dm') -> <<0,64>>;
+encode_point_type('3d') -> <<0,128>>;
+encode_point_type('3dm') -> <<0,192>>.
+
+hex_to_bin(<<C:2/binary>>) ->
+  Int = binary_to_integer(C, 16),
+  << Int >>;
+hex_to_bin(<<C:2/binary, Rest/binary>>) ->
+  Int = binary_to_integer(C, 16),
+  RestBin = hex_to_bin(Rest),
+  << Int, RestBin/binary >>.
+
+bin_to_hex(<<C>>) ->
+  Int = int_to_hex(C),
+  << Int/binary >>;
+bin_to_hex(<<C, Rest/binary>>) ->
+  Int = int_to_hex(C),
+  RestBin = bin_to_hex(Rest),
+  << Int/binary, RestBin/binary >>.
+
+int_to_hex(C) ->
+  case integer_to_binary(C, 16) of
+    <<_,_>> = Val -> Val;
+    <<Byte>> -> <<"0", Byte>>
+  end.

+ 12 - 0
test/epgsql_tests.erl

@@ -497,6 +497,18 @@ uuid_type_test(Module) ->
 point_type_test(Module) ->
     check_type(Module, point, "'(23.15, 100)'", {23.15, 100.0}, []).
 
+geometry_type_test(Module) ->
+    check_type(Module, geometry, "'COMPOUNDCURVE(CIRCULARSTRING(0 0,1 1,1 0),(1 0,0 1))'", 
+      {compound_curve,'2d',
+          [{circular_string,'2d',
+              [{point,'2d',0.0,0.0,undefined,undefined},
+               {point,'2d',1.0,1.0,undefined,undefined},
+               {point,'2d',1.0,0.0,undefined,undefined}]},
+         {line_string,'2d',
+              [{point,'2d',1.0,0.0,undefined,undefined},
+                 {point,'2d',0.0,1.0,undefined,undefined}]}]},
+      []).
+
 uuid_select_test(Module) ->
     with_rollback(
       Module,