diff --git a/CHANGELOG.md b/CHANGELOG.md index 245fd0d..7a0f980 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,9 @@ ## v0.15.0 - Unreleased - Ensure `ssl` and `pgo` are running before using `gleam_pgo`. +- Fix a bug where timestamps are returned as `#(#(Int, Int, Int), #(Int, Int, Float))` + leading `decode_timestamp` to fail. `decode_timestamp` now properly returns + `#(#(Int, Int, Int), #(Int, Int, Float))`. ## v0.14.0 - 2024-08-15 diff --git a/README.md b/README.md index 2848cb8..4e88fbb 100644 --- a/README.md +++ b/README.md @@ -98,6 +98,16 @@ By default, `pgo` will return every selected value from your query as a tuple. In case you want a different output, you can activate `rows_as_maps` in `Config`. Once activated, every returned rows will take the form of a `Dict`. +## About timestamps + +If you're used to manage timestamps in Postgres, you know timestamp are stored +as bytes, and clients transforms them in their desired formats. `pgo` uses the +Erlang datetime convention, i.e. managing a timestamp as a tuple +`#(#(year, month, day), #(hour, minutes, seconds))`. When querying a timestamp, +unless you're casting it (in ISO-8601 for example) voluntarily in your SQL query, +`pgo` will give you such a tuple. You're expected to decode it using +`pgo.decode_timestamp` that will do the work of decoding it for you. + ## Atom generation Creating a connection pool with the `pgo.connect` function dynamically generates diff --git a/src/gleam/pgo.gleam b/src/gleam/pgo.gleam index f05d242..f3755bc 100644 --- a/src/gleam/pgo.gleam +++ b/src/gleam/pgo.gleam @@ -3,6 +3,7 @@ //// Gleam wrapper around pgo library import gleam/dynamic.{type DecodeErrors, type Decoder, type Dynamic} +import gleam/int import gleam/list import gleam/option.{type Option, None, Some} import gleam/result @@ -170,7 +171,7 @@ pub fn array(a: List(a)) -> Value /// Coerce a timestamp represented as `#(#(year, month, day), #(hour, minute, second))` into a `Value`. @external(erlang, "gleam_pgo_ffi", "coerce") -pub fn timestamp(a: #(#(Int, Int, Int), #(Int, Int, Int))) -> Value +pub fn timestamp(a: #(#(Int, Int, Int), #(Int, Int, Float))) -> Value /// Coerce a date represented as `#(year, month, day)` into a `Value`. @external(erlang, "gleam_pgo_ffi", "coerce") @@ -525,16 +526,24 @@ pub fn error_code_name(error_code: String) -> Result(String, Nil) { } } -/// Checks to see if the value is formatted as `#(#(Int, Int, Int), #(Int, Int, Int))` -/// to represent `#(#(year, month, day), #(hour, minute, second))`, and returns the +/// Checks to see if the value is formatted as `#(#(Int, Int, Int), #(Int, Int, Float))` +/// to represent `#(#(year, month, day), #(hour, minute, second.milliseconds))`, and returns the /// value if it is. pub fn decode_timestamp(value: dynamic.Dynamic) { dynamic.tuple2( dynamic.tuple3(dynamic.int, dynamic.int, dynamic.int), - dynamic.tuple3(dynamic.int, dynamic.int, dynamic.int), + dynamic.tuple3(dynamic.int, dynamic.int, decode_seconds), )(value) } +fn decode_int_to_float(value: dynamic.Dynamic) { + dynamic.int(value) |> result.map(int.to_float) +} + +fn decode_seconds(value: dynamic.Dynamic) { + dynamic.any([dynamic.float, decode_int_to_float])(value) +} + /// Checks to see if the value is formatted as `#(Int, Int, Int)` to represent a date /// as `#(year, month, day)`, and returns the value if it is. pub fn decode_date(value: dynamic.Dynamic) { diff --git a/test/gleam/pgo_test.gleam b/test/gleam/pgo_test.gleam index e9835d8..5af23dd 100644 --- a/test/gleam/pgo_test.gleam +++ b/test/gleam/pgo_test.gleam @@ -150,7 +150,7 @@ pub fn selecting_rows_test() { |> should.equal(1) returned.rows |> should.equal([ - #(id, "neo", True, ["black"], #(#(2022, 10, 10), #(11, 30, 30)), #( + #(id, "neo", True, ["black"], #(#(2022, 10, 10), #(11, 30, 30.0)), #( 2020, 3, 4, @@ -362,7 +362,7 @@ pub fn array_test() { pub fn datetime_test() { start_default() |> assert_roundtrip( - #(#(2022, 10, 12), #(11, 30, 33)), + #(#(2022, 10, 12), #(11, 30, 33.0)), "timestamp", pgo.timestamp, pgo.decode_timestamp, @@ -464,7 +464,7 @@ pub fn expected_maps_test() { |> should.equal(1) returned.rows |> should.equal([ - #(id, "neo", True, ["black"], #(#(2022, 10, 10), #(11, 30, 30)), #( + #(id, "neo", True, ["black"], #(#(2022, 10, 10), #(11, 30, 30.0)), #( 2020, 3, 4,