diff --git a/README b/README index 2bfdc10..e0c3d99 100644 --- a/README +++ b/README @@ -31,7 +31,9 @@ Erlang PostgreSQL Database Client the unnamed prepared statement and portal. 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. + + For details see pgsql_binary.erl and the Data Representation section + below. * Parse/Bind/Execute @@ -56,3 +58,22 @@ Erlang PostgreSQL Database Client ok = pgsql:close(C, Statement). ok = pgsql:close(C, statement | portal, Name). ok = pgsql:sync(C). + +* Data Representation + + null = null + bool = true | false + char = $A + 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>> + + record = {int2, time, text, ...} (decode only) diff --git a/src/pgsql_binary.erl b/src/pgsql_binary.erl index eacefc0..8d0dcd8 100644 --- a/src/pgsql_binary.erl +++ b/src/pgsql_binary.erl @@ -15,6 +15,9 @@ encode(int4, N) -> <<4:?int32, N:1/big-signed-unit:32>>; encode(int8, N) -> <<8:?int32, N:1/big-signed-unit:64>>; encode(float4, N) -> <<4:?int32, N:1/big-float-unit:32>>; encode(float8, N) -> <<8:?int32, N:1/big-float-unit:64>>; +encode(Type, B) when Type == time; Type == timetz -> pgsql_datetime:encode(Type, B); +encode(Type, B) when Type == date; Type == timestamp -> pgsql_datetime:encode(Type, B); +encode(Type, B) when Type == timestamptz; Type == interval -> pgsql_datetime:encode(Type, B); encode(bytea, B) when is_binary(B) -> <<(byte_size(B)):?int32, B/binary>>; encode(text, B) when is_binary(B) -> <<(byte_size(B)):?int32, B/binary>>; encode(varchar, B) when is_binary(B) -> <<(byte_size(B)):?int32, B/binary>>; @@ -30,7 +33,10 @@ decode(int8, <>) -> N; decode(float4, <>) -> N; decode(float8, <>) -> N; decode(record, <<_:?int32, Rest/binary>>) -> list_to_tuple(decode_record(Rest, [])); -decode(_Other, Bin) -> Bin. +decode(Type, B) when Type == time; Type == timetz -> pgsql_datetime:decode(Type, B); +decode(Type, B) when Type == date; Type == timestamp -> pgsql_datetime:decode(Type, B); +decode(Type, B) when Type == timestamptz; Type == interval -> pgsql_datetime:decode(Type, B); +decode(_Other, Bin) -> Bin. decode_record(<<>>, Acc) -> lists:reverse(Acc); @@ -51,4 +57,10 @@ supports(bytea) -> true; supports(text) -> true; supports(varchar) -> true; supports(record) -> true; -supports(_Type) -> false. +supports(date) -> true; +supports(time) -> true; +supports(timetz) -> true; +supports(timestamp) -> true; +supports(timestamptz) -> true; +supports(interval) -> true; +supports(_Type) -> false. diff --git a/src/pgsql_datetime.erl b/src/pgsql_datetime.erl new file mode 100644 index 0000000..2620b32 --- /dev/null +++ b/src/pgsql_datetime.erl @@ -0,0 +1,126 @@ +%%% Copyright (C) 2008 - Will Glozer. All rights reserved. + +-module(pgsql_datetime). + +-export([decode/2, encode/2]). + +-define(int16, 1/big-signed-unit:16). +-define(int32, 1/big-signed-unit:32). + +-define(postgres_epoc_jdate, 2451545). + +-define(mins_per_hour, 60). +-define(secs_per_day, 86400.0). +-define(secs_per_hour, 3600.0). +-define(secs_per_minute, 60.0). + +decode(date, <>) -> j2date(?postgres_epoc_jdate + J); +decode(time, <>) -> f2time(N); +decode(timetz, <>) -> {f2time(N), TZ}; +decode(timestamp, <>) -> f2timestamp(N); +decode(timestamptz, <>) -> f2timestamp(N); +decode(interval, <>) -> {f2time(N), D, M}. + +encode(date, D) -> <<4:?int32, (date2j(D) - ?postgres_epoc_jdate):1/big-signed-unit:32>>; +encode(time, T) -> <<8:?int32, (time2f(T)):1/big-float-unit:64>>; +encode(timetz, {T, TZ}) -> <<12:?int32, (time2f(T)):1/big-float-unit:64, TZ:?int32>>; +encode(timestamp, TS) -> <<8:?int32, (timestamp2f(TS)):1/big-float-unit:64>>; +encode(timestamptz, TS) -> <<8:?int32, (timestamp2f(TS)):1/big-float-unit:64>>; +encode(interval, {T, D, M}) -> <<16:?int32, (time2f(T)):1/big-float-unit:64, D:?int32, M:?int32>>. + +j2date(N) -> + J = N + 32044, + Q1 = J div 146097, + Extra = (J - Q1 * 146097) * 4 + 3, + J2 = J + 60 + Q1 * 3 + Extra div 146097, + Q2 = J2 div 1461, + J3 = J2 - Q2 * 1461, + Y = J3 * 4 div 1461, + case Y of + 0 -> J4 = ((J3 + 306) rem 366) + 123; + _ -> J4 = ((J3 + 305) rem 365) + 123 + end, + Year = (Y + Q2 * 4) - 4800, + Q3 = J4 * 2141 div 65536, + Day = J4 - 7834 * Q3 div 256, + Month = (Q3 + 10) rem 12 + 1, + {Year, Month, Day}. + +date2j({Y, M, D}) -> + case M > 2 of + true -> + M2 = M + 1, + Y2 = Y + 4800; + false -> + M2 = M + 13, + Y2 = Y + 4799 + end, + C = Y2 div 100, + J1 = Y2 * 365 - 32167, + J2 = J1 + (Y2 div 4 - C + C div 4), + J2 + 7834 * M2 div 256 + D. + +f2time(N) -> + {R1, Hour} = tmodulo(N, ?secs_per_hour), + {R2, Min} = tmodulo(R1, ?secs_per_minute), + {R3, Sec} = tmodulo(R2, 1.0), + case timeround(R3) of + US when US >= 1.0 -> f2time(ceiling(N)); + US -> {Hour, Min, Sec + US} + end. + +time2f({H, M, S}) -> + ((H * ?mins_per_hour + M) * ?secs_per_minute) + S. + +f2timestamp(N) -> + case tmodulo(N, ?secs_per_day) of + {T, D} when T < 0 -> f2timestamp2(D - 1 + ?postgres_epoc_jdate, T + ?secs_per_day); + {T, D} -> f2timestamp2(D + ?postgres_epoc_jdate, T) + end. + +f2timestamp2(D, T) -> + {_H, _M, S} = Time = f2time(T), + Date = j2date(D), + case tsround(S - trunc(S)) of + N when N >= 1.0 -> + case ceiling(T) of + T2 when T2 > ?secs_per_day -> f2timestamp2(D + 1, 0.0); + T2 -> f2timestamp2(T2, D) + end; + _ -> ok + end, + {Date, Time}. + +timestamp2f({Date, Time}) -> + D = date2j(Date) - ?postgres_epoc_jdate, + D * ?secs_per_day + time2f(Time). + +tmodulo(T, U) -> + case T < 0 of + true -> Q = ceiling(T / U); + false -> Q = floor(T / U) + end, + case Q of + 0 -> {T, Q}; + _ -> {T - rint(Q * U), Q} + end. + +rint(N) -> round(N) * 1.0. +timeround(J) -> rint(J * 10000000000.0) / 10000000000.0. +tsround(J) -> rint(J * 1000000.0) / 1000000.0. + +floor(X) -> + T = erlang:trunc(X), + case (X - T) of + N when N < 0 -> T - 1; + N when N > 0 -> T; + _ -> T + end. + +ceiling(X) -> + T = erlang:trunc(X), + case (X - T) of + N when N < 0 -> T; + N when N > 0 -> T + 1; + _ -> T + end. diff --git a/test_src/pgsql_tests.erl b/test_src/pgsql_tests.erl index 5aad47f..ee1d0df 100644 --- a/test_src/pgsql_tests.erl +++ b/test_src/pgsql_tests.erl @@ -331,53 +331,24 @@ parameter_set_test() -> {ok, _Cols, [{<<"02.01.2000">>}]} = pgsql:squery(C, "select '2000-01-02'::date") end). -decode_binary_format_test() -> - with_connection( - fun(C) -> - {ok, [#column{type = unknown}], [{null}]} = pgsql:equery(C, "select null"), - {ok, [#column{type = bool}], [{true}]} = pgsql:equery(C, "select true"), - {ok, [#column{type = bool}], [{false}]} = pgsql:equery(C, "select false"), - {ok, [#column{type = bpchar}], [{$A}]} = pgsql:equery(C, "select 'A'::char"), - {ok, [#column{type = int2}], [{1}]} = pgsql:equery(C, "select 1::int2"), - {ok, [#column{type = int2}], [{-1}]} = pgsql:equery(C, "select -1::int2"), - {ok, [#column{type = int4}], [{1}]} = pgsql:equery(C, "select 1::int4"), - {ok, [#column{type = int4}], [{-1}]} = pgsql:equery(C, "select -1::int4"), - {ok, [#column{type = int8}], [{1}]} = pgsql:equery(C, "select 1::int8"), - {ok, [#column{type = int8}], [{-1}]} = pgsql:equery(C, "select -1::int8"), - {ok, [#column{type = float4}], [{1.0}]} = pgsql:equery(C, "select 1.0::float4"), - {ok, [#column{type = float4}], [{-1.0}]} = pgsql:equery(C, "select -1.0::float4"), - {ok, [#column{type = float8}], [{1.0}]} = pgsql:equery(C, "select 1.0::float8"), - {ok, [#column{type = float8}], [{-1.0}]} = pgsql:equery(C, "select -1.0::float8"), - {ok, [#column{type = bytea}], [{<<1, 2>>}]} = pgsql:equery(C, "select E'\001\002'::bytea"), - {ok, [#column{type = text}], [{<<"hi">>}]} = pgsql:equery(C, "select 'hi'::text"), - {ok, [#column{type = varchar}], [{<<"hi">>}]} = pgsql:equery(C, "select 'hi'::varchar"), - {ok, [#column{type = record}], [{{1, null, <<"hi">>}}]} = pgsql:equery(C, "select (1, null, 'hi')") - end). - -encode_binary_format_test() -> - with_connection( - fun(C) -> - {ok, 1} = pgsql:equery(C, "insert into test_table2 (c_bool) values ($1)", [null]), - {ok, 1} = pgsql:equery(C, "insert into test_table2 (c_bool) values ($1)", [true]), - {ok, 1} = pgsql:equery(C, "insert into test_table2 (c_bool) values ($1)", [false]), - {ok, 1} = pgsql:equery(C, "insert into test_table2 (c_char) values ($1)", [$A]), - {ok, 1} = pgsql:equery(C, "insert into test_table2 (c_int2) values ($1)", [1]), - {ok, 1} = pgsql:equery(C, "insert into test_table2 (c_int2) values ($1)", [-1]), - {ok, 1} = pgsql:equery(C, "insert into test_table2 (c_int4) values ($1)", [1]), - {ok, 1} = pgsql:equery(C, "insert into test_table2 (c_int4) values ($1)", [-1]), - {ok, 1} = pgsql:equery(C, "insert into test_table2 (c_int8) values ($1)", [1]), - {ok, 1} = pgsql:equery(C, "insert into test_table2 (c_int8) values ($1)", [-1]), - {ok, 1} = pgsql:equery(C, "insert into test_table2 (c_float4) values ($1)", [1.0]), - {ok, 1} = pgsql:equery(C, "insert into test_table2 (c_float4) values ($1)", [-1.0]), - {ok, 1} = pgsql:equery(C, "insert into test_table2 (c_float8) values ($1)", [1.0]), - {ok, 1} = pgsql:equery(C, "insert into test_table2 (c_float8) values ($1)", [-1.0]), - {ok, 1} = pgsql:equery(C, "insert into test_table2 (c_bytea) values ($1)", [<<1, 2>>]), - {ok, 1} = pgsql:equery(C, "insert into test_table2 (c_bytea) values ($1)", [[1, 2]]), - {ok, 1} = pgsql:equery(C, "insert into test_table2 (c_text) values ($1)", [<<"hi">>]), - {ok, 1} = pgsql:equery(C, "insert into test_table2 (c_text) values ($1)", ["hi"]), - {ok, 1} = pgsql:equery(C, "insert into test_table2 (c_varchar) values ($1)", [<<"hi">>]), - {ok, 1} = pgsql:equery(C, "insert into test_table2 (c_varchar) values ($1)", ["hi"]) - end). +type_test() -> + check_type(bool, "true", true, [true, false]), + check_type(bpchar, "'A'", $A, [1, $1, 255], "c_char"), + check_type(int2, "1", 1, [0, 256, -32768, +32767]), + check_type(int4, "1", 1, [0, 512, -2147483648, +2147483647]), + check_type(int8, "1", 1, [0, 1024, -9223372036854775808, +9223372036854775807]), + check_type(float4, "1.0", 1.0, [0.0, 1.23456, -1.23456]), + check_type(float8, "1.0", 1.0, [0.0, 1.23456789012345, -1.23456789012345]), + check_type(bytea, "E'\001\002'", <<1,2>>, [<<>>, <<0,128,255>>]), + check_type(text, "'hi'", <<"hi">>, [<<"">>, <<"hi">>]), + check_type(varchar, "'hi'", <<"hi">>, [<<"">>, <<"hi">>]), + check_type(date, "'2008-01-02'", {2008,1,2}, [{-4712,1,1}, {5874897,1,1}]), + check_type(time, "'00:01:02'", {0,1,2.0}, [{0,0,0.0}, {24,0,0.0}]), + check_type(timetz, "'00:01:02-01'", {{0,1,2.0},1*60*60}, [{{0,0,0.0},0}, {{24,0,0.0},-13*60*60}]), + check_type(timestamp, "'2008-01-02 03:04:05'", {{2008,1,2},{3,4,5.0}}, + [{{-4712,1,1},{0,0,0.0}}, {{5874897,12,31}, {23,59,59.0}}]), + check_type(interval, "'1 hour 2 minutes 3.1 seconds'", {{1,2,3.1},0,0}, + [{{0,0,0.0},0,-178000000 * 12}, {{0,0,0.0},0,178000000 * 12}]). text_format_test() -> with_connection( @@ -388,9 +359,7 @@ text_format_test() -> {ok, _Cols, [{V2}]} = pgsql:equery(C, Query, [V]), {ok, _Cols, [{V2}]} = pgsql:equery(C, Query, [V2]) end, - Select("timestamp", "2000-01-02 03:04:05"), - Select("date", "2000-01-02"), - Select("time", "03:04:05"), + Select("inet", "127.0.0.1"), Select("numeric", "123456") end). @@ -429,6 +398,34 @@ with_rollback(F) -> end end). +check_type(Type, In, Out, Values) -> + Column = "c_" ++ atom_to_list(Type), + check_type(Type, In, Out, Values, Column). + +check_type(Type, In, Out, Values, Column) -> + with_connection( + fun(C) -> + Select = io_lib:format("select ~s::~w", [In, Type]), + {ok, [#column{type = Type}], [{Out}]} = pgsql:equery(C, Select), + Sql = io_lib:format("insert into test_table2 (~s) values ($1) returning ~s", [Column, Column]), + {ok, #statement{columns = [#column{type = Type}]} = S} = pgsql:parse(C, Sql), + Insert = fun(V) -> + pgsql:bind(C, S, [V]), + {ok, 1, [{V2}]} = pgsql:execute(C, S), + case compare(Type, V, V2) of + true -> ok; + false -> ?debugFmt("~p =/= ~p~n", [V, V2]), ?assert(false) + end, + ok = pgsql:sync(C) + end, + lists:foreach(Insert, [null | Values]) + end). + +compare(_Type, null, null) -> true; +compare(float4, V1, V2) -> abs(V2 - V1) < 0.000001; +compare(float8, V1, V2) -> abs(V2 - V1) < 0.000000000000001; +compare(_Type, V1, V2) -> V1 =:= V2. + %% flush mailbox flush() -> ?assertEqual([], flush([])). diff --git a/test_src/test_schema.sql b/test_src/test_schema.sql index b6db446..748306e 100644 --- a/test_src/test_schema.sql +++ b/test_src/test_schema.sql @@ -39,7 +39,13 @@ CREATE TABLE test_table2 ( c_float8 float8, c_bytea bytea, c_text text, - c_varchar varchar(64)); + c_varchar varchar(64), + c_date date, + c_time time, + c_timetz timetz, + c_timestamp timestamp, + c_timestamptz timestamptz, + c_interval interval); CREATE LANGUAGE plpgsql;