diff --git a/CHANGELOG.md b/CHANGELOG.md index 265106b..dc88491 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## v3.0.0 - 2024-12-22 + +- Updated for `gleam_stdlib` v0.50.0. +- The `decode_*` functions have been replaced with `*_decoder` functions. +- `gleam/dynamic/decode.Decoder` is now used for decoding. + ## v2.0.0 - 2024-12-21 - Add support for `sslmode` in connection strings. diff --git a/gleam.toml b/gleam.toml index 7ade26e..3b34593 100644 --- a/gleam.toml +++ b/gleam.toml @@ -1,5 +1,5 @@ name = "pog" -version = "2.0.0" +version = "3.0.0" gleam = ">= 1.4.0" licences = ["Apache-2.0"] description = "A PostgreSQL database client for Gleam, based on PGO" @@ -16,11 +16,11 @@ pages = [ ] [dependencies] -gleam_stdlib = ">= 0.20.0 and < 2.0.0" +gleam_stdlib = ">= 0.50.0 and < 2.0.0" pgo = ">= 0.12.0 and < 2.0.0" [dev-dependencies] -gleeunit = "~> 1.0" +gleeunit = ">= 1.0.0 and < 2.0.0" exception = ">= 2.0.0 and < 3.0.0" gleam_erlang = ">= 0.30.0 and < 1.0.0" diff --git a/manifest.toml b/manifest.toml index 1dbb0b1..857c42c 100644 --- a/manifest.toml +++ b/manifest.toml @@ -4,11 +4,10 @@ packages = [ { name = "backoff", version = "1.1.6", build_tools = ["rebar3"], requirements = [], otp_app = "backoff", source = "hex", outer_checksum = "CF0CFFF8995FB20562F822E5CC47D8CCF664C5ECDC26A684CBE85C225F9D7C39" }, { name = "exception", version = "2.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "exception", source = "hex", outer_checksum = "F5580D584F16A20B7FCDCABF9E9BE9A2C1F6AC4F9176FA6DD0B63E3B20D450AA" }, - { name = "gleam_erlang", version = "0.30.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_erlang", source = "hex", outer_checksum = "760618870AE4A497B10C73548E6E44F43B76292A54F0207B3771CBB599C675B4" }, - { name = "gleam_stdlib", version = "0.34.0", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "1FB8454D2991E9B4C0C804544D8A9AD0F6184725E20D63C3155F0AEB4230B016" }, - { name = "gleeunit", version = "1.0.2", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "D364C87AFEB26BDB4FB8A5ABDE67D635DC9FA52D6AB68416044C35B096C6882D" }, - { name = "opentelemetry_api", version = "1.3.0", build_tools = ["rebar3", "mix"], requirements = ["opentelemetry_semantic_conventions"], otp_app = "opentelemetry_api", source = "hex", outer_checksum = "B9E5FF775FD064FA098DBA3C398490B77649A352B40B0B730A6B7DC0BDD68858" }, - { name = "opentelemetry_semantic_conventions", version = "0.2.0", build_tools = ["rebar3", "mix"], requirements = [], otp_app = "opentelemetry_semantic_conventions", source = "hex", outer_checksum = "D61FA1F5639EE8668D74B527E6806E0503EFC55A42DB7B5F39939D84C07D6895" }, + { name = "gleam_erlang", version = "0.33.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_erlang", source = "hex", outer_checksum = "A1D26B80F01901B59AABEE3475DD4C18D27D58FA5C897D922FCB9B099749C064" }, + { name = "gleam_stdlib", version = "0.50.0", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "489AFCEE829FADE57F20F4C69AD67C59D80610D8A3C7DB5A7E12BE75946AAF55" }, + { name = "gleeunit", version = "1.2.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "F7A7228925D3EE7D0813C922E062BFD6D7E9310F0BEE585D3A42F3307E3CFD13" }, + { name = "opentelemetry_api", version = "1.4.0", build_tools = ["rebar3", "mix"], requirements = [], otp_app = "opentelemetry_api", source = "hex", outer_checksum = "3DFBBFAA2C2ED3121C5C483162836C4F9027DEF469C41578AF5EF32589FCFC58" }, { name = "pg_types", version = "0.4.0", build_tools = ["rebar3"], requirements = [], otp_app = "pg_types", source = "hex", outer_checksum = "B02EFA785CAECECF9702C681C80A9CA12A39F9161A846CE17B01FB20AEEED7EB" }, { name = "pgo", version = "0.14.0", build_tools = ["rebar3"], requirements = ["backoff", "opentelemetry_api", "pg_types"], otp_app = "pgo", source = "hex", outer_checksum = "71016C22599936E042DC0012EE4589D24C71427D266292F775EBF201D97DF9C9" }, ] @@ -16,6 +15,6 @@ packages = [ [requirements] exception = { version = ">= 2.0.0 and < 3.0.0" } gleam_erlang = { version = ">= 0.30.0 and < 1.0.0" } -gleam_stdlib = { version = ">= 0.20.0 and < 2.0.0" } -gleeunit = { version = "~> 1.0" } +gleam_stdlib = { version = ">= 0.50.0 and < 2.0.0" } +gleeunit = { version = ">= 1.0.0 and < 2.0.0" } pgo = { version = ">= 0.12.0 and < 2.0.0" } diff --git a/src/pog.gleam b/src/pog.gleam index eb2360c..5c859a7 100644 --- a/src/pog.gleam +++ b/src/pog.gleam @@ -4,7 +4,8 @@ // TODO: add time and timestamp with zone once pgo supports them -import gleam/dynamic.{type DecodeErrors, type Decoder, type Dynamic} +import gleam/dynamic.{type DecodeErrors, type Dynamic} +import gleam/dynamic/decode.{type Decoder} import gleam/float import gleam/int import gleam/list @@ -430,7 +431,7 @@ pub fn query(sql: String) -> Query(Nil) { Query( sql:, parameters: [], - row_decoder: fn(_) { Ok(Nil) }, + row_decoder: decode.success(Nil), timeout: option.None, ) } @@ -461,10 +462,6 @@ pub fn timeout(query: Query(t1), timeout: Int) -> Query(t1) { /// Run a query against a PostgreSQL database. /// -/// The provided dynamic decoder is used to decode the rows returned by -/// PostgreSQL. If you are not interested in any returned rows you may want to -/// use the `dynamic.dynamic` decoder. -/// pub fn execute( query query: Query(t), on pool: Connection, @@ -477,7 +474,7 @@ pub fn execute( query.timeout, )) use rows <- result.then( - list.try_map(over: rows, with: query.row_decoder) + list.try_map(over: rows, with: decode.run(_, query.row_decoder)) |> result.map_error(UnexpectedResultType), ) Ok(Returned(count, rows)) @@ -756,47 +753,41 @@ pub fn error_code_name(error_code: String) -> Result(String, Nil) { } } -pub fn decode_timestamp( - value: dynamic.Dynamic, -) -> Result(Timestamp, DecodeErrors) { - dynamic.decode2( - Timestamp, - dynamic.element(0, decode_date), - dynamic.element(1, decode_time), - )(value) +pub fn timestamp_decoder() -> decode.Decoder(Timestamp) { + use date <- decode.field(0, date_decoder()) + use time <- decode.field(1, time_decoder()) + decode.success(Timestamp(date, time)) } -pub fn decode_date(value: dynamic.Dynamic) -> Result(Date, DecodeErrors) { - dynamic.decode3( - Date, - dynamic.element(0, dynamic.int), - dynamic.element(1, dynamic.int), - dynamic.element(2, dynamic.int), - )(value) +pub fn date_decoder() -> decode.Decoder(Date) { + use year <- decode.field(0, decode.int) + use month <- decode.field(1, decode.int) + use day <- decode.field(2, decode.int) + decode.success(Date(year:, month:, day:)) } -pub fn decode_time(value: dynamic.Dynamic) -> Result(Time, DecodeErrors) { - case dynamic.tuple3(dynamic.int, dynamic.int, decode_seconds)(value) { - Error(e) -> Error(e) - Ok(#(hours, minutes, #(seconds, microseconds))) -> - Ok(Time(hours:, minutes:, seconds:, microseconds:)) - } +pub fn time_decoder() -> decode.Decoder(Time) { + use hours <- decode.field(0, decode.int) + use minutes <- decode.field(1, decode.int) + use #(seconds, microseconds) <- decode.field(2, seconds_decoder()) + decode.success(Time(hours:, minutes:, seconds:, microseconds:)) } -fn decode_seconds(value: dynamic.Dynamic) -> Result(#(Int, Int), DecodeErrors) { - case dynamic.int(value) { - Ok(i) -> Ok(#(i, 0)) - Error(_) -> - case dynamic.float(value) { - Error(e) -> Error(e) - Ok(i) -> { - let floored = float.floor(i) - let seconds = float.round(floored) - let microseconds = float.round({ i -. floored } *. 1_000_000.0) - Ok(#(seconds, microseconds)) - } - } +fn seconds_decoder() -> decode.Decoder(#(Int, Int)) { + let int = { + decode.int + |> decode.map(fn(i) { #(i, 0) }) + } + let float = { + decode.float + |> decode.map(fn(f) { + let floored = float.floor(f) + let seconds = float.round(floored) + let microseconds = float.round({ f -. floored } *. 1_000_000.0) + #(seconds, microseconds) + }) } + decode.one_of(int, [float]) } pub type Date { diff --git a/test/pog_test.gleam b/test/pog_test.gleam index 2d57771..6c754ab 100644 --- a/test/pog_test.gleam +++ b/test/pog_test.gleam @@ -1,5 +1,6 @@ import exception -import gleam/dynamic.{type Decoder} +import gleam/dynamic +import gleam/dynamic/decode.{type Decoder} import gleam/erlang/atom import gleam/option.{None, Some} import gleeunit @@ -113,7 +114,7 @@ pub fn inserting_new_rows_and_returning_test() { name" let assert Ok(returned) = pog.query(sql) - |> pog.returning(dynamic.element(0, dynamic.string)) + |> pog.returning(decode.at([0], decode.string)) |> pog.execute(db) returned.count @@ -137,20 +138,21 @@ pub fn selecting_rows_test() { let assert Ok(pog.Returned(rows: [id], ..)) = pog.query(sql) - |> pog.returning(dynamic.element(0, dynamic.int)) + |> pog.returning(decode.at([0], decode.int)) |> pog.execute(db) let assert Ok(returned) = pog.query("SELECT * FROM cats WHERE id = $1") |> pog.parameter(pog.int(id)) - |> pog.returning(dynamic.tuple6( - dynamic.int, - dynamic.string, - dynamic.bool, - dynamic.list(dynamic.string), - pog.decode_timestamp, - pog.decode_date, - )) + |> pog.returning({ + use x0 <- decode.field(0, decode.int) + use x1 <- decode.field(1, decode.string) + use x2 <- decode.field(2, decode.bool) + use x3 <- decode.field(3, decode.list(decode.string)) + use x4 <- decode.field(4, pog.timestamp_decoder()) + use x5 <- decode.field(5, pog.date_decoder()) + decode.success(#(x0, x1, x2, x3, x4, x5)) + }) |> pog.execute(db) returned.count @@ -259,7 +261,6 @@ pub fn execute_with_wrong_number_of_arguments_test() { let sql = "SELECT * FROM cats WHERE id = $1" pog.query(sql) - |> pog.returning(dynamic.dynamic) |> pog.execute(db) |> should.equal(Error(pog.UnexpectedArgumentCount(expected: 1, got: 0))) @@ -275,7 +276,7 @@ fn assert_roundtrip( ) -> pog.Connection { pog.query("select $1::" <> type_name) |> pog.parameter(encoder(value)) - |> pog.returning(dynamic.element(0, decoder)) + |> pog.returning(decode.at([0], decoder)) |> pog.execute(db) |> should.equal(Ok(pog.Returned(count: 1, rows: [value]))) db @@ -285,7 +286,7 @@ pub fn null_test() { let db = start_default() pog.query("select $1") |> pog.parameter(pog.null()) - |> pog.returning(dynamic.element(0, dynamic.optional(dynamic.int))) + |> pog.returning(decode.at([0], decode.optional(decode.int))) |> pog.execute(db) |> should.equal(Ok(pog.Returned(count: 1, rows: [None]))) @@ -294,72 +295,72 @@ pub fn null_test() { pub fn bool_test() { start_default() - |> assert_roundtrip(True, "bool", pog.bool, dynamic.bool) - |> assert_roundtrip(False, "bool", pog.bool, dynamic.bool) + |> assert_roundtrip(True, "bool", pog.bool, decode.bool) + |> assert_roundtrip(False, "bool", pog.bool, decode.bool) |> pog.disconnect } pub fn int_test() { start_default() - |> assert_roundtrip(0, "int", pog.int, dynamic.int) - |> assert_roundtrip(1, "int", pog.int, dynamic.int) - |> assert_roundtrip(2, "int", pog.int, dynamic.int) - |> assert_roundtrip(3, "int", pog.int, dynamic.int) - |> assert_roundtrip(4, "int", pog.int, dynamic.int) - |> assert_roundtrip(5, "int", pog.int, dynamic.int) - |> assert_roundtrip(-0, "int", pog.int, dynamic.int) - |> assert_roundtrip(-1, "int", pog.int, dynamic.int) - |> assert_roundtrip(-2, "int", pog.int, dynamic.int) - |> assert_roundtrip(-3, "int", pog.int, dynamic.int) - |> assert_roundtrip(-4, "int", pog.int, dynamic.int) - |> assert_roundtrip(-5, "int", pog.int, dynamic.int) - |> assert_roundtrip(10_000_000, "int", pog.int, dynamic.int) + |> assert_roundtrip(0, "int", pog.int, decode.int) + |> assert_roundtrip(1, "int", pog.int, decode.int) + |> assert_roundtrip(2, "int", pog.int, decode.int) + |> assert_roundtrip(3, "int", pog.int, decode.int) + |> assert_roundtrip(4, "int", pog.int, decode.int) + |> assert_roundtrip(5, "int", pog.int, decode.int) + |> assert_roundtrip(-0, "int", pog.int, decode.int) + |> assert_roundtrip(-1, "int", pog.int, decode.int) + |> assert_roundtrip(-2, "int", pog.int, decode.int) + |> assert_roundtrip(-3, "int", pog.int, decode.int) + |> assert_roundtrip(-4, "int", pog.int, decode.int) + |> assert_roundtrip(-5, "int", pog.int, decode.int) + |> assert_roundtrip(10_000_000, "int", pog.int, decode.int) |> pog.disconnect } pub fn float_test() { start_default() - |> assert_roundtrip(0.123, "float", pog.float, dynamic.float) - |> assert_roundtrip(1.123, "float", pog.float, dynamic.float) - |> assert_roundtrip(2.123, "float", pog.float, dynamic.float) - |> assert_roundtrip(3.123, "float", pog.float, dynamic.float) - |> assert_roundtrip(4.123, "float", pog.float, dynamic.float) - |> assert_roundtrip(5.123, "float", pog.float, dynamic.float) - |> assert_roundtrip(-0.654, "float", pog.float, dynamic.float) - |> assert_roundtrip(-1.654, "float", pog.float, dynamic.float) - |> assert_roundtrip(-2.654, "float", pog.float, dynamic.float) - |> assert_roundtrip(-3.654, "float", pog.float, dynamic.float) - |> assert_roundtrip(-4.654, "float", pog.float, dynamic.float) - |> assert_roundtrip(-5.654, "float", pog.float, dynamic.float) - |> assert_roundtrip(10_000_000.0, "float", pog.float, dynamic.float) + |> assert_roundtrip(0.123, "float", pog.float, decode.float) + |> assert_roundtrip(1.123, "float", pog.float, decode.float) + |> assert_roundtrip(2.123, "float", pog.float, decode.float) + |> assert_roundtrip(3.123, "float", pog.float, decode.float) + |> assert_roundtrip(4.123, "float", pog.float, decode.float) + |> assert_roundtrip(5.123, "float", pog.float, decode.float) + |> assert_roundtrip(-0.654, "float", pog.float, decode.float) + |> assert_roundtrip(-1.654, "float", pog.float, decode.float) + |> assert_roundtrip(-2.654, "float", pog.float, decode.float) + |> assert_roundtrip(-3.654, "float", pog.float, decode.float) + |> assert_roundtrip(-4.654, "float", pog.float, decode.float) + |> assert_roundtrip(-5.654, "float", pog.float, decode.float) + |> assert_roundtrip(10_000_000.0, "float", pog.float, decode.float) |> pog.disconnect } pub fn text_test() { start_default() - |> assert_roundtrip("", "text", pog.text, dynamic.string) - |> assert_roundtrip("✨", "text", pog.text, dynamic.string) - |> assert_roundtrip("Hello, Joe!", "text", pog.text, dynamic.string) + |> assert_roundtrip("", "text", pog.text, decode.string) + |> assert_roundtrip("✨", "text", pog.text, decode.string) + |> assert_roundtrip("Hello, Joe!", "text", pog.text, decode.string) |> pog.disconnect } pub fn bytea_test() { start_default() - |> assert_roundtrip(<<"":utf8>>, "bytea", pog.bytea, dynamic.bit_array) - |> assert_roundtrip(<<"✨":utf8>>, "bytea", pog.bytea, dynamic.bit_array) + |> assert_roundtrip(<<"":utf8>>, "bytea", pog.bytea, decode.bit_array) + |> assert_roundtrip(<<"✨":utf8>>, "bytea", pog.bytea, decode.bit_array) |> assert_roundtrip( <<"Hello, Joe!":utf8>>, "bytea", pog.bytea, - dynamic.bit_array, + decode.bit_array, ) - |> assert_roundtrip(<<1>>, "bytea", pog.bytea, dynamic.bit_array) - |> assert_roundtrip(<<1, 2, 3>>, "bytea", pog.bytea, dynamic.bit_array) + |> assert_roundtrip(<<1>>, "bytea", pog.bytea, decode.bit_array) + |> assert_roundtrip(<<1, 2, 3>>, "bytea", pog.bytea, decode.bit_array) |> pog.disconnect } pub fn array_test() { - let decoder = dynamic.list(dynamic.string) + let decoder = decode.list(decode.string) start_default() |> assert_roundtrip(["black"], "text[]", pog.array(pog.text, _), decoder) |> assert_roundtrip(["gray"], "text[]", pog.array(pog.text, _), decoder) @@ -368,7 +369,7 @@ pub fn array_test() { [1, 2, 3], "integer[]", pog.array(pog.int, _), - dynamic.list(dynamic.int), + decode.list(decode.int), ) |> pog.disconnect } @@ -379,14 +380,19 @@ pub fn datetime_test() { pog.Timestamp(pog.Date(2022, 10, 12), pog.Time(11, 30, 33, 101)), "timestamp", pog.timestamp, - pog.decode_timestamp, + pog.timestamp_decoder(), ) |> pog.disconnect } pub fn date_test() { start_default() - |> assert_roundtrip(pog.Date(2022, 10, 11), "date", pog.date, pog.decode_date) + |> assert_roundtrip( + pog.Date(2022, 10, 11), + "date", + pog.date, + pog.date_decoder(), + ) |> pog.disconnect } @@ -396,25 +402,25 @@ pub fn nullable_test() { Some("Hello, Joe"), "text", pog.nullable(pog.text, _), - dynamic.optional(dynamic.string), + decode.optional(decode.string), ) |> assert_roundtrip( None, "text", pog.nullable(pog.text, _), - dynamic.optional(dynamic.string), + decode.optional(decode.string), ) |> assert_roundtrip( Some(123), "int", pog.nullable(pog.int, _), - dynamic.optional(dynamic.int), + decode.optional(decode.int), ) |> assert_roundtrip( None, "int", pog.nullable(pog.int, _), - dynamic.optional(dynamic.int), + decode.optional(decode.int), ) |> pog.disconnect } @@ -423,7 +429,7 @@ pub fn expected_argument_type_test() { let db = start_default() pog.query("select $1::int") - |> pog.returning(dynamic.element(0, dynamic.string)) + |> pog.returning(decode.at([0], decode.string)) |> pog.parameter(pog.float(1.2)) |> pog.execute(db) |> should.equal(Error(pog.UnexpectedArgumentType("int4", "1.2"))) @@ -434,7 +440,7 @@ pub fn expected_argument_type_test() { pub fn expected_return_type_test() { let db = start_default() pog.query("select 1") - |> pog.returning(dynamic.element(0, dynamic.string)) + |> pog.returning(decode.at([0], decode.string)) |> pog.execute(db) |> should.equal( Error( @@ -453,7 +459,7 @@ pub fn expected_five_millis_timeout_test() { pog.query("select sub.ret from (select pg_sleep(0.05), 'OK' as ret) as sub") |> pog.timeout(5) - |> pog.returning(dynamic.element(0, dynamic.string)) + |> pog.returning(decode.at([0], decode.string)) |> pog.execute(db) |> should.equal(Error(pog.QueryTimeout)) @@ -466,7 +472,7 @@ pub fn expected_ten_millis_no_timeout_test() { pog.query("select sub.ret from (select pg_sleep(0.01), 'OK' as ret) as sub") |> pog.timeout(30) - |> pog.returning(dynamic.element(0, dynamic.string)) + |> pog.returning(decode.at([0], decode.string)) |> pog.execute(db) |> should.equal(Ok(pog.Returned(1, ["Ok"]))) @@ -481,7 +487,7 @@ pub fn expected_ten_millis_no_default_timeout_test() { |> pog.connect pog.query("select sub.ret from (select pg_sleep(0.01), 'OK' as ret) as sub") - |> pog.returning(dynamic.element(0, dynamic.string)) + |> pog.returning(decode.at([0], decode.string)) |> pog.execute(db) |> should.equal(Ok(pog.Returned(1, ["Ok"]))) @@ -502,23 +508,24 @@ pub fn expected_maps_test() { let assert Ok(pog.Returned(rows: [id], ..)) = pog.query(sql) - |> pog.returning(dynamic.field("id", dynamic.int)) + |> pog.returning(decode.at(["id"], decode.int)) |> pog.execute(db) let assert Ok(returned) = pog.query("SELECT * FROM cats WHERE id = $1") |> pog.parameter(pog.int(id)) - |> pog.returning(dynamic.decode6( - fn(id, name, is_cute, colors, last_petted_at, birthday) { - #(id, name, is_cute, colors, last_petted_at, birthday) - }, - dynamic.field("id", dynamic.int), - dynamic.field("name", dynamic.string), - dynamic.field("is_cute", dynamic.bool), - dynamic.field("colors", dynamic.list(dynamic.string)), - dynamic.field("last_petted_at", pog.decode_timestamp), - dynamic.field("birthday", pog.decode_date), - )) + |> pog.returning({ + use id <- decode.field("id", decode.int) + use name <- decode.field("name", decode.string) + use is_cute <- decode.field("is_cute", decode.bool) + use colors <- decode.field("colors", decode.list(decode.string)) + use last_petted_at <- decode.field( + "last_petted_at", + pog.timestamp_decoder(), + ) + use birthday <- decode.field("birthday", pog.date_decoder()) + decode.success(#(id, name, is_cute, colors, last_petted_at, birthday)) + }) |> pog.execute(db) returned.count @@ -540,7 +547,7 @@ pub fn expected_maps_test() { pub fn transaction_commit_test() { let db = start_default() - let id_decoder = dynamic.element(0, dynamic.int) + let id_decoder = decode.at([0], decode.int) let assert Ok(_) = pog.query("truncate table cats") |> pog.execute(db) let insert = fn(db, name) {