diff --git a/CHANGELOG.md b/CHANGELOG.md index 59ee2bd8..e3963fa6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ # CHANGELOG.md +## unrelease + - add support for postgres range types + ## v0.39.1 (2025-11-08) - More precise server timing tracking to debug performance issues - Fix missing server timing header in some cases diff --git a/Cargo.lock b/Cargo.lock index 38f29956..39fec691 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4292,9 +4292,9 @@ dependencies = [ [[package]] name = "sqlx-core-oldapi" -version = "0.6.50" +version = "0.6.51" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "31f61c87a5ec3419eca470d22b5586f6f7a482873ff78b6c04a95744ab5d7b92" +checksum = "8b9869b844b6ab5f575c33e29ad579a3c880bc514bb47c4c9991d0dd6979949b" dependencies = [ "ahash", "atoi", @@ -4357,9 +4357,9 @@ dependencies = [ [[package]] name = "sqlx-macros-oldapi" -version = "0.6.50" +version = "0.6.51" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec16ba1323ade8dfa8ffba57bc31725fb1636201a05dd8358c510bdd7afebbdc" +checksum = "78820a192cc29b877b735c32e1c1a8e51459019b699fff6f5ba86a128fa9ef9d" dependencies = [ "dotenvy", "either", @@ -4377,9 +4377,9 @@ dependencies = [ [[package]] name = "sqlx-oldapi" -version = "0.6.50" +version = "0.6.51" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7279c6a34a8f074d685b61fe041516f1d0e95c4145b66b71216392e21c8615d" +checksum = "1a74816da5fc417f929012d46ca806381dabca75de303b248519aad466844044" dependencies = [ "sqlx-core-oldapi", "sqlx-macros-oldapi", @@ -4387,9 +4387,9 @@ dependencies = [ [[package]] name = "sqlx-rt-oldapi" -version = "0.6.50" +version = "0.6.51" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d15c09ee3698ec42841ac81316f8c5ddeeb0c3fd5e0320604d85d9d428eb507f" +checksum = "b9b54748f0bfadc0b3407b4ee576132b4b5ad0730ebec82e0dbec9d0d1a233bc" dependencies = [ "once_cell", "tokio", diff --git a/Cargo.toml b/Cargo.toml index b63101e5..37fd1380 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,7 +18,7 @@ panic = "abort" codegen-units = 2 [dependencies] -sqlx = { package = "sqlx-oldapi", version = "0.6.50", default-features = false, features = [ +sqlx = { package = "sqlx-oldapi", version = "0.6.51", default-features = false, features = [ "any", "runtime-tokio-rustls", "migrate", diff --git a/src/webserver/database/sql_to_json.rs b/src/webserver/database/sql_to_json.rs index 98a3bc08..48971307 100644 --- a/src/webserver/database/sql_to_json.rs +++ b/src/webserver/database/sql_to_json.rs @@ -1,11 +1,13 @@ use crate::utils::add_value_to_map; use crate::webserver::database::blob_to_data_url; use bigdecimal::BigDecimal; -use chrono::{DateTime, FixedOffset, NaiveDateTime}; +use chrono::{DateTime, FixedOffset, NaiveDate, NaiveDateTime}; use serde_json::{self, Map, Value}; use sqlx::any::{AnyColumn, AnyRow, AnyTypeInfo, AnyTypeInfoKind}; -use sqlx::Decode; +use sqlx::postgres::types::PgRange; +use sqlx::postgres::PgValueRef; use sqlx::{Column, Row, TypeInfo, ValueRef}; +use sqlx::{Decode, Type}; pub fn row_to_json(row: &AnyRow) -> Value { use Value::Object; @@ -68,6 +70,25 @@ fn decode_raw<'a, T: Decode<'a, sqlx::any::Any> + Default>( } } +fn decode_pg_range<'r, T>(raw_value: sqlx::any::AnyValueRef<'r>) -> Value +where + T: std::fmt::Display + + Type + + for<'a> sqlx::Decode<'a, sqlx::postgres::Postgres>, +{ + let Ok(pg_val): Result, _> = raw_value.try_into() else { + log::error!("Only postgres range values are supported"); + return Value::Null; + }; + match as sqlx::Decode<'r, sqlx::postgres::Postgres>>::decode(pg_val) { + Ok(pg_range) => pg_range.to_string().into(), + Err(e) => { + log::error!("Failed to decode postgres range value: {e}"); + Value::Null + } + } +} + fn decimal_to_json(decimal: &BigDecimal) -> Value { // to_plain_string always returns a valid JSON string Value::Number(serde_json::Number::from_string_unchecked( @@ -124,6 +145,12 @@ pub fn sql_nonnull_to_json<'r>(mut get_ref: impl FnMut() -> sqlx::any::AnyValueR "BLOB" | "BYTEA" | "FILESTREAM" | "VARBINARY" | "BIGVARBINARY" | "BINARY" | "IMAGE" => { blob_to_data_url::vec_to_data_uri_value(&decode_raw::>(raw_value)) } + "INT4RANGE" => decode_pg_range::(raw_value), + "INT8RANGE" => decode_pg_range::(raw_value), + "NUMRANGE" => decode_pg_range::(raw_value), + "DATERANGE" => decode_pg_range::(raw_value), + "TSRANGE" => decode_pg_range::(raw_value), + "TSTZRANGE" => decode_pg_range::>(raw_value), // Deserialize as a string by default _ => decode_raw::(raw_value).into(), } @@ -220,7 +247,13 @@ mod tests { justify_interval(interval '1 year 2 months 3 days') as justified_interval, 1234.56::MONEY as money_val, '\\x68656c6c6f20776f726c64'::BYTEA as blob_data, - '550e8400-e29b-41d4-a716-446655440000'::UUID as uuid + '550e8400-e29b-41d4-a716-446655440000'::UUID as uuid, + '[1,5)'::INT4RANGE as int4range, + '[1,5]'::INT8RANGE as int8range, + '[1.5,4.5)'::NUMRANGE as numrange, + -- '[2024-11-12 01:02:03,2024-11-12 23:00:00)'::TSRANGE as tsrange, + -- '[2024-11-12 01:02:03+01:00,2024-11-12 23:00:00+00:00)'::TSTZRANGE as tstzrange, + '[2024-11-12,2024-11-13)'::DATERANGE as daterange ", ) .fetch_one(&mut c) @@ -249,7 +282,13 @@ mod tests { "justified_interval": "1 year 2 mons 3 days", "money_val": "$1,234.56", "blob_data": "data:application/octet-stream;base64,aGVsbG8gd29ybGQ=", - "uuid": "550e8400-e29b-41d4-a716-446655440000" + "uuid": "550e8400-e29b-41d4-a716-446655440000", + "int4range": "[1,5)", + "int8range": "[1,6)", + "numrange": "[1.5,4.5)", + //"tsrange": "[2024-11-12 01:02:03,2024-11-12 23:00:00)", // todo: bug in sqlx datetime range parsing + //"tstzrange": "[\"2024-11-12 02:00:00 +01:00\",\"2024-11-12 23:00:00 +00:00\")", // todo: tz info is lost in sqlx + "daterange": "[2024-11-12,2024-11-13)" }), ); Ok(()) @@ -295,6 +334,36 @@ mod tests { Ok(()) } + #[actix_web::test] + async fn test_postgres_prepared_range_types() -> anyhow::Result<()> { + let Some(db_url) = db_specific_test("postgres") else { + return Ok(()); + }; + let mut c = sqlx::AnyConnection::connect(&db_url).await?; + let row = sqlx::query( + "SELECT + '[1,5)'::INT4RANGE as int4range, + '[2024-11-12 01:02:03,2024-11-12 23:00:00)'::TSRANGE as tsrange, + '[2024-11-12 01:02:03+01:00,2024-11-12 23:00:00+00:00)'::TSTZRANGE as tstzrange, + '[2024-11-12,2024-11-13)'::DATERANGE as daterange + where $1", + ) + .bind(true) + .fetch_one(&mut c) + .await?; + + expect_json_object_equal( + &row_to_json(&row), + &serde_json::json!({ + "int4range": "[1,5)", + "tsrange": "[2024-11-12 01:02:03,2024-11-12 23:00:00)", + "tstzrange": "[2024-11-12 00:02:03 +00:00,2024-11-12 23:00:00 +00:00)", // todo: tz info is lost in sqlx + "daterange": "[2024-11-12,2024-11-13)" + }), + ); + Ok(()) + } + #[actix_web::test] async fn test_mysql_types() -> anyhow::Result<()> { let db_url = db_specific_test("mysql").or_else(|| db_specific_test("mariadb"));