Browse Source

implement support for integer_datetimes

--HG--
rename : src/pgsql_datetime.erl => src/pgsql_fdatetime.erl
Will 16 years ago
parent
commit
7f8eeb68c8
7 changed files with 156 additions and 35 deletions
  1. 1 1
      Makefile
  2. 7 6
      src/pgsql_binary.erl
  3. 6 1
      src/pgsql_connection.erl
  4. 1 2
      src/pgsql_fdatetime.erl
  5. 96 0
      src/pgsql_idatetime.erl
  6. 35 19
      test_src/pgsql_tests.erl
  7. 10 6
      test_src/test_schema.sql

+ 1 - 1
Makefile

@@ -30,7 +30,7 @@ clean:
 	@rm -rf $(NAME)-$(VERSION) $(NAME)-*.tar.gz
 
 test: $(TESTS:test_src/%.erl=test_ebin/%.beam) $(BEAMS)
-	dialyzer --src -c src
+	@dialyzer --src -c src
 	$(ERL) -pa ebin/ -pa test_ebin/ -noshell -s pgsql_tests run_tests -s init stop
 
 # ------------------------------------------------------------------------

+ 7 - 6
src/pgsql_binary.erl

@@ -5,6 +5,7 @@
 -export([encode/2, decode/2, supports/1]).
 
 -define(int32, 1/big-signed-unit:32).
+-define(datetime, (get(datetime_mod))).
 
 encode(_Any, null)  -> <<-1:?int32>>;
 encode(bool, true)  -> <<1:?int32, 1:1/big-signed-unit:8>>;
@@ -15,9 +16,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(Type, B) when Type == time; Type == timetz          -> ?datetime:encode(Type, B);
+encode(Type, B) when Type == date; Type == timestamp       -> ?datetime:encode(Type, B);
+encode(Type, B) when Type == timestamptz; Type == interval -> ?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>>;
@@ -33,9 +34,9 @@ decode(int8, <<N:1/big-signed-unit:64>>)    -> N;
 decode(float4, <<N:1/big-float-unit:32>>)   -> N;
 decode(float8, <<N:1/big-float-unit:64>>)   -> N;
 decode(record, <<_:?int32, Rest/binary>>)   -> list_to_tuple(decode_record(Rest, []));
-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(Type, B) when Type == time; Type == timetz          -> ?datetime:decode(Type, B);
+decode(Type, B) when Type == date; Type == timestamp       -> ?datetime:decode(Type, B);
+decode(Type, B) when Type == timestamptz; Type == interval -> ?datetime:decode(Type, B);
 decode(_Other, Bin) -> Bin.
 
 decode_record(<<>>, Acc) ->

+ 6 - 1
src/pgsql_connection.erl

@@ -193,9 +193,14 @@ initializing({$E, Bin}, State) ->
 
 %% ReadyForQuery
 initializing({$Z, <<Status:8>>}, State) ->
+    #state{parameters = Parameters, reply_to = Reply_To} = State,
     erase(username),
     erase(password),
-    gen_fsm:reply(State#state.reply_to, {ok, self()}),
+    case lists:keysearch(<<"integer_datetimes">>, 1, Parameters) of
+        {value, {_, <<"on">>}}  -> put(datetime_mod, pgsql_idatetime);
+        {value, {_, <<"off">>}} -> put(datetime_mod, pgsql_fdatetime)
+    end,
+    gen_fsm:reply(Reply_To, {ok, self()}),
     {next_state, ready, State#state{txstatus = Status}}.
 
 ready(_Msg, State) ->

+ 1 - 2
src/pgsql_datetime.erl → src/pgsql_fdatetime.erl

@@ -1,10 +1,9 @@
 %%% Copyright (C) 2008 - Will Glozer.  All rights reserved.
 
--module(pgsql_datetime).
+-module(pgsql_fdatetime).
 
 -export([decode/2, encode/2]).
 
--define(int16, 1/big-signed-unit:16).
 -define(int32, 1/big-signed-unit:32).
 
 -define(postgres_epoc_jdate, 2451545).

+ 96 - 0
src/pgsql_idatetime.erl

@@ -0,0 +1,96 @@
+%%% Copyright (C) 2008 - Will Glozer.  All rights reserved.
+
+-module(pgsql_idatetime).
+
+-export([decode/2, encode/2]).
+
+-define(int32, 1/big-signed-unit:32).
+-define(int64, 1/big-signed-unit:64).
+
+-define(postgres_epoc_jdate, 2451545).
+
+-define(mins_per_hour, 60).
+-define(secs_per_minute, 60).
+
+-define(usecs_per_day, 86400000000).
+-define(usecs_per_hour, 3600000000).
+-define(usecs_per_minute, 60000000).
+-define(usecs_per_sec, 1000000).
+
+decode(date, <<J:?int32>>)                         -> j2date(?postgres_epoc_jdate + J);
+decode(time, <<N:?int64>>)                         -> i2time(N);
+decode(timetz, <<N:?int64, TZ:?int32>>)            -> {i2time(N), TZ};
+decode(timestamp, <<N:?int64>>)                    -> i2timestamp(N);
+decode(timestamptz, <<N:?int64>>)                  -> i2timestamp(N);
+decode(interval, <<N:?int64, D:?int32, M:?int32>>) -> {i2time(N), D, M}.
+
+encode(date, D)         -> <<4:?int32, (date2j(D) - ?postgres_epoc_jdate):?int32>>;
+encode(time, T)         -> <<8:?int32, (time2i(T)):?int64>>;
+encode(timetz, {T, TZ}) -> <<12:?int32, (time2i(T)):?int64, TZ:?int32>>;
+encode(timestamp, TS)   -> <<8:?int32, (timestamp2i(TS)):?int64>>;
+encode(timestamptz, TS) -> <<8:?int32, (timestamp2i(TS)):?int64>>;
+encode(interval, {T, D, M}) -> <<16:?int32, (time2i(T)):?int64, 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.
+
+i2time(N) ->
+    Hour = N div ?usecs_per_hour,
+    R1 = N - Hour * ?usecs_per_hour,
+    Min = R1 div ?usecs_per_minute,
+    R2 = R1 - Min * ?usecs_per_minute,
+    Sec = R2 div ?usecs_per_sec,
+    US = R2 - Sec * ?usecs_per_sec,
+    {Hour, Min, Sec + US / ?usecs_per_sec}.
+
+time2i({H, M, S}) ->
+    US = trunc(round(S * ?usecs_per_sec)),
+    ((H * ?mins_per_hour + M) * ?secs_per_minute) * ?usecs_per_sec + US.
+
+i2timestamp(N) ->
+    case tmodulo(N, ?usecs_per_day) of
+        {T, D} when T < 0 -> i2timestamp2(D - 1 + ?postgres_epoc_jdate, T + ?usecs_per_day);
+        {T, D}            -> i2timestamp2(D + ?postgres_epoc_jdate, T)
+    end.
+
+i2timestamp2(D, T) ->
+    {j2date(D), i2time(T)}.
+
+timestamp2i({Date, Time}) ->
+    D = date2j(Date) - ?postgres_epoc_jdate,
+    D * ?usecs_per_day + time2i(Time).
+
+tmodulo(T, U) ->
+    case T div U of
+        0 -> {T, 0};
+        Q -> {T - (Q * U), Q}
+    end.

+ 35 - 19
test_src/pgsql_tests.erl

@@ -6,6 +6,7 @@
 -include("pgsql.hrl").
 
 -define(host, "localhost").
+-define(port, 5432).
 
 connect_test() ->
     connect_only([[]]).
@@ -14,7 +15,7 @@ connect_to_db_test() ->
     connect_only([[{database, "epgsql_test_db1"}]]).
 
 connect_as_test() ->
-    connect_only(["epgsql_test1", [{database, "epgsql_test_db1"}]]).
+    connect_only(["epgsql_test", [{database, "epgsql_test_db1"}]]).
 
 connect_with_cleartext_test() ->
     connect_only(["epgsql_test_cleartext",
@@ -31,7 +32,7 @@ connect_with_invalid_password_test() ->
         pgsql:connect(?host,
                       "epgsql_test_md5",
                       "epgsql_test_sha1",
-                      [{database, "epgsql_test_db1"}]).
+                      [{port, ?port}, {database, "epgsql_test_db1"}]).
 
 select_test() ->
     with_connection(
@@ -317,7 +318,7 @@ execute_function_test() ->
 parameter_get_test() ->
     with_connection(
       fun(C) ->
-              {ok, <<"off">>} = pgsql:get_parameter(C, "integer_datetimes")
+              {ok, <<"off">>} = pgsql:get_parameter(C, "is_superuser")
       end).
 
 parameter_set_test() ->
@@ -331,24 +332,39 @@ parameter_set_test() ->
               {ok, _Cols, [{<<"02.01.2000">>}]} = pgsql:squery(C, "select '2000-01-02'::date")
       end).
 
-type_test() ->
-    check_type(bool, "true", true, [true, false]),
-    check_type(bpchar, "'A'", $A, [1, $1, 255], "c_char"),
+numeric_type_test() ->
     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(float8, "1.0", 1.0, [0.0, 1.23456789012345, -1.23456789012345]).
+
+character_type_test() ->
+    check_type(bpchar, "'A'", $A, [1, $1, 255], "c_char"),
     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}]).
+    check_type(varchar, "'hi'", <<"hi">>, [<<"">>, <<"hi">>]).
+
+date_time_type_test() ->
+    with_connection(
+      fun(C) ->
+              case pgsql:get_parameter(C, "integer_datetimes") of
+                  {ok, <<"on">>}  -> MaxTsDate = 294276;
+                  {ok, <<"off">>} -> MaxTsDate = 5874897
+              end,
+
+              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}}, {{MaxTsDate,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}])
+      end).
+
+misc_type_test() ->
+    check_type(bool, "true", true, [true, false]),
+    check_type(bytea, "E'\001\002'", <<1,2>>, [<<>>, <<0,128,255>>]).
 
 text_format_test() ->
     with_connection(
@@ -373,12 +389,13 @@ run_tests() ->
 %% -- internal functions --
 
 connect_only(Args) ->
-    {ok, C} = apply(pgsql, connect, [?host | Args]),
+    {ok, C} = apply(pgsql, connect, [?host, [{port, ?port} | Args]]),
     pgsql:close(C),
     flush().
 
 with_connection(F) ->
-    {ok, C} = pgsql:connect(?host, "epgsql_test1", [{database, "epgsql_test_db1"}]),
+    Args = [{port, ?port}, {database, "epgsql_test_db1"}],
+    {ok, C} = pgsql:connect(?host, "epgsql_test", Args),
     try
         F(C)
     after
@@ -386,7 +403,6 @@ with_connection(F) ->
     end,
     flush().
 
-
 with_rollback(F) ->
     with_connection(
       fun(C) ->

+ 10 - 6
test_src/test_schema.sql

@@ -3,11 +3,15 @@
 -- this script should be run as the same user the tests will be run as,
 -- so that the test for connecting as the 'current user' succeeds
 --
--- the following lines must be added to pg_hba.conf for the relevant
--- auth tests to succeed:
+-- the following lines must be added to pg_hba.conf for all tests to
+-- succeed:
 --
+-- host    epgsql_test_db1 epgsql_test             127.0.0.1/32    trust
 -- host    epgsql_test_db1 epgsql_test_md5         127.0.0.1/32    md5
 -- host    epgsql_test_db1 epgsql_test_cleartext   127.0.0.1/32    password
+--
+-- any 'trust all' must be commented out for the invalid password test
+-- to succeed.
 
 
 CREATE USER epgsql_test;
@@ -65,7 +69,7 @@ begin
 end
 $$ language plpgsql;
 
-GRANT ALL ON TABLE test_table1 TO epgsql_test1;
-GRANT ALL ON TABLE test_table2 TO epgsql_test1;
-GRANT ALL ON FUNCTION insert_test1(integer, text) TO epgsql_test1;
-GRANT ALL ON FUNCTION do_nothing() TO epgsql_test1;
+GRANT ALL ON TABLE test_table1 TO epgsql_test;
+GRANT ALL ON TABLE test_table2 TO epgsql_test;
+GRANT ALL ON FUNCTION insert_test1(integer, text) TO epgsql_test;
+GRANT ALL ON FUNCTION do_nothing() TO epgsql_test;