Skip to content

Commit f2c8164

Browse files
lovasoacursoragent
andauthored
Test postgresql range type decoding (#1097)
* Add support for PostgreSQL range types in SQL to JSON conversion Co-authored-by: contact <[email protected]> * add support for postgres range types --------- Co-authored-by: Cursor Agent <[email protected]>
1 parent 8498b76 commit f2c8164

File tree

4 files changed

+85
-13
lines changed

4 files changed

+85
-13
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
# CHANGELOG.md
22

3+
## unrelease
4+
- add support for postgres range types
5+
36
## v0.39.1 (2025-11-08)
47
- More precise server timing tracking to debug performance issues
58
- Fix missing server timing header in some cases

Cargo.lock

Lines changed: 8 additions & 8 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ panic = "abort"
1818
codegen-units = 2
1919

2020
[dependencies]
21-
sqlx = { package = "sqlx-oldapi", version = "0.6.50", default-features = false, features = [
21+
sqlx = { package = "sqlx-oldapi", version = "0.6.51", default-features = false, features = [
2222
"any",
2323
"runtime-tokio-rustls",
2424
"migrate",

src/webserver/database/sql_to_json.rs

Lines changed: 73 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
use crate::utils::add_value_to_map;
22
use crate::webserver::database::blob_to_data_url;
33
use bigdecimal::BigDecimal;
4-
use chrono::{DateTime, FixedOffset, NaiveDateTime};
4+
use chrono::{DateTime, FixedOffset, NaiveDate, NaiveDateTime};
55
use serde_json::{self, Map, Value};
66
use sqlx::any::{AnyColumn, AnyRow, AnyTypeInfo, AnyTypeInfoKind};
7-
use sqlx::Decode;
7+
use sqlx::postgres::types::PgRange;
8+
use sqlx::postgres::PgValueRef;
89
use sqlx::{Column, Row, TypeInfo, ValueRef};
10+
use sqlx::{Decode, Type};
911

1012
pub fn row_to_json(row: &AnyRow) -> Value {
1113
use Value::Object;
@@ -68,6 +70,25 @@ fn decode_raw<'a, T: Decode<'a, sqlx::any::Any> + Default>(
6870
}
6971
}
7072

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+
7192
fn decimal_to_json(decimal: &BigDecimal) -> Value {
7293
// to_plain_string always returns a valid JSON string
7394
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
124145
"BLOB" | "BYTEA" | "FILESTREAM" | "VARBINARY" | "BIGVARBINARY" | "BINARY" | "IMAGE" => {
125146
blob_to_data_url::vec_to_data_uri_value(&decode_raw::<Vec<u8>>(raw_value))
126147
}
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),
127154
// Deserialize as a string by default
128155
_ => decode_raw::<String>(raw_value).into(),
129156
}
@@ -220,7 +247,13 @@ mod tests {
220247
justify_interval(interval '1 year 2 months 3 days') as justified_interval,
221248
1234.56::MONEY as money_val,
222249
'\\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
224257
",
225258
)
226259
.fetch_one(&mut c)
@@ -249,7 +282,13 @@ mod tests {
249282
"justified_interval": "1 year 2 mons 3 days",
250283
"money_val": "$1,234.56",
251284
"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)"
253292
}),
254293
);
255294
Ok(())
@@ -295,6 +334,36 @@ mod tests {
295334
Ok(())
296335
}
297336

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+
298367
#[actix_web::test]
299368
async fn test_mysql_types() -> anyhow::Result<()> {
300369
let db_url = db_specific_test("mysql").or_else(|| db_specific_test("mariadb"));

0 commit comments

Comments
 (0)