|
1 | 1 | use crate::utils::add_value_to_map; |
2 | 2 | use crate::webserver::database::blob_to_data_url; |
3 | 3 | use bigdecimal::BigDecimal; |
4 | | -use chrono::{DateTime, FixedOffset, NaiveDateTime}; |
| 4 | +use chrono::{DateTime, FixedOffset, NaiveDate, NaiveDateTime}; |
5 | 5 | use serde_json::{self, Map, Value}; |
6 | 6 | use sqlx::any::{AnyColumn, AnyRow, AnyTypeInfo, AnyTypeInfoKind}; |
7 | | -use sqlx::Decode; |
| 7 | +use sqlx::postgres::types::PgRange; |
| 8 | +use sqlx::postgres::PgValueRef; |
8 | 9 | use sqlx::{Column, Row, TypeInfo, ValueRef}; |
| 10 | +use sqlx::{Decode, Type}; |
9 | 11 |
|
10 | 12 | pub fn row_to_json(row: &AnyRow) -> Value { |
11 | 13 | use Value::Object; |
@@ -68,6 +70,25 @@ fn decode_raw<'a, T: Decode<'a, sqlx::any::Any> + Default>( |
68 | 70 | } |
69 | 71 | } |
70 | 72 |
|
| 73 | +fn decode_pg_range<'r, T>(raw_value: sqlx::any::AnyValueRef<'r>) -> Value |
| 74 | +where |
| 75 | + T: std::fmt::Display |
| 76 | + + Type<sqlx::postgres::Postgres> |
| 77 | + + for<'a> sqlx::Decode<'a, sqlx::postgres::Postgres>, |
| 78 | +{ |
| 79 | + let Ok(pg_val): Result<PgValueRef<'r>, _> = raw_value.try_into() else { |
| 80 | + log::error!("Only postgres range values are supported"); |
| 81 | + return Value::Null; |
| 82 | + }; |
| 83 | + match <PgRange<T> as sqlx::Decode<'r, sqlx::postgres::Postgres>>::decode(pg_val) { |
| 84 | + Ok(pg_range) => pg_range.to_string().into(), |
| 85 | + Err(e) => { |
| 86 | + log::error!("Failed to decode postgres range value: {e}"); |
| 87 | + Value::Null |
| 88 | + } |
| 89 | + } |
| 90 | +} |
| 91 | + |
71 | 92 | fn decimal_to_json(decimal: &BigDecimal) -> Value { |
72 | 93 | // to_plain_string always returns a valid JSON string |
73 | 94 | 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 |
124 | 145 | "BLOB" | "BYTEA" | "FILESTREAM" | "VARBINARY" | "BIGVARBINARY" | "BINARY" | "IMAGE" => { |
125 | 146 | blob_to_data_url::vec_to_data_uri_value(&decode_raw::<Vec<u8>>(raw_value)) |
126 | 147 | } |
| 148 | + "INT4RANGE" => decode_pg_range::<i32>(raw_value), |
| 149 | + "INT8RANGE" => decode_pg_range::<i64>(raw_value), |
| 150 | + "NUMRANGE" => decode_pg_range::<BigDecimal>(raw_value), |
| 151 | + "DATERANGE" => decode_pg_range::<NaiveDate>(raw_value), |
| 152 | + "TSRANGE" => decode_pg_range::<NaiveDateTime>(raw_value), |
| 153 | + "TSTZRANGE" => decode_pg_range::<DateTime<FixedOffset>>(raw_value), |
127 | 154 | // Deserialize as a string by default |
128 | 155 | _ => decode_raw::<String>(raw_value).into(), |
129 | 156 | } |
@@ -220,7 +247,13 @@ mod tests { |
220 | 247 | justify_interval(interval '1 year 2 months 3 days') as justified_interval, |
221 | 248 | 1234.56::MONEY as money_val, |
222 | 249 | '\\x68656c6c6f20776f726c64'::BYTEA as blob_data, |
223 | | - '550e8400-e29b-41d4-a716-446655440000'::UUID as uuid |
| 250 | + '550e8400-e29b-41d4-a716-446655440000'::UUID as uuid, |
| 251 | + '[1,5)'::INT4RANGE as int4range, |
| 252 | + '[1,5]'::INT8RANGE as int8range, |
| 253 | + '[1.5,4.5)'::NUMRANGE as numrange, |
| 254 | + -- '[2024-11-12 01:02:03,2024-11-12 23:00:00)'::TSRANGE as tsrange, |
| 255 | + -- '[2024-11-12 01:02:03+01:00,2024-11-12 23:00:00+00:00)'::TSTZRANGE as tstzrange, |
| 256 | + '[2024-11-12,2024-11-13)'::DATERANGE as daterange |
224 | 257 | ", |
225 | 258 | ) |
226 | 259 | .fetch_one(&mut c) |
@@ -249,7 +282,13 @@ mod tests { |
249 | 282 | "justified_interval": "1 year 2 mons 3 days", |
250 | 283 | "money_val": "$1,234.56", |
251 | 284 | "blob_data": "data:application/octet-stream;base64,aGVsbG8gd29ybGQ=", |
252 | | - "uuid": "550e8400-e29b-41d4-a716-446655440000" |
| 285 | + "uuid": "550e8400-e29b-41d4-a716-446655440000", |
| 286 | + "int4range": "[1,5)", |
| 287 | + "int8range": "[1,6)", |
| 288 | + "numrange": "[1.5,4.5)", |
| 289 | + //"tsrange": "[2024-11-12 01:02:03,2024-11-12 23:00:00)", // todo: bug in sqlx datetime range parsing |
| 290 | + //"tstzrange": "[\"2024-11-12 02:00:00 +01:00\",\"2024-11-12 23:00:00 +00:00\")", // todo: tz info is lost in sqlx |
| 291 | + "daterange": "[2024-11-12,2024-11-13)" |
253 | 292 | }), |
254 | 293 | ); |
255 | 294 | Ok(()) |
@@ -295,6 +334,36 @@ mod tests { |
295 | 334 | Ok(()) |
296 | 335 | } |
297 | 336 |
|
| 337 | + #[actix_web::test] |
| 338 | + async fn test_postgres_prepared_range_types() -> anyhow::Result<()> { |
| 339 | + let Some(db_url) = db_specific_test("postgres") else { |
| 340 | + return Ok(()); |
| 341 | + }; |
| 342 | + let mut c = sqlx::AnyConnection::connect(&db_url).await?; |
| 343 | + let row = sqlx::query( |
| 344 | + "SELECT |
| 345 | + '[1,5)'::INT4RANGE as int4range, |
| 346 | + '[2024-11-12 01:02:03,2024-11-12 23:00:00)'::TSRANGE as tsrange, |
| 347 | + '[2024-11-12 01:02:03+01:00,2024-11-12 23:00:00+00:00)'::TSTZRANGE as tstzrange, |
| 348 | + '[2024-11-12,2024-11-13)'::DATERANGE as daterange |
| 349 | + where $1", |
| 350 | + ) |
| 351 | + .bind(true) |
| 352 | + .fetch_one(&mut c) |
| 353 | + .await?; |
| 354 | + |
| 355 | + expect_json_object_equal( |
| 356 | + &row_to_json(&row), |
| 357 | + &serde_json::json!({ |
| 358 | + "int4range": "[1,5)", |
| 359 | + "tsrange": "[2024-11-12 01:02:03,2024-11-12 23:00:00)", |
| 360 | + "tstzrange": "[2024-11-12 00:02:03 +00:00,2024-11-12 23:00:00 +00:00)", // todo: tz info is lost in sqlx |
| 361 | + "daterange": "[2024-11-12,2024-11-13)" |
| 362 | + }), |
| 363 | + ); |
| 364 | + Ok(()) |
| 365 | + } |
| 366 | + |
298 | 367 | #[actix_web::test] |
299 | 368 | async fn test_mysql_types() -> anyhow::Result<()> { |
300 | 369 | let db_url = db_specific_test("mysql").or_else(|| db_specific_test("mariadb")); |
|
0 commit comments