add support for postgres range types

This commit is contained in:
lovasoa
2025-11-12 18:01:40 +01:00
parent 02615c6c62
commit 6cd14b4a90
4 changed files with 50 additions and 35 deletions
+3
View File
@@ -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
Generated
+8 -8
View File
@@ -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",
+1 -1
View File
@@ -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",
+38 -26
View File
@@ -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;
@@ -20,20 +22,6 @@ pub fn row_to_json(row: &AnyRow) -> Value {
Object(map)
}
fn is_postgres_range_type(type_name: &str) -> bool {
const RANGE_TYPES: [&str; 6] = [
"int4range",
"int8range",
"numrange",
"tsrange",
"tstzrange",
"daterange",
];
RANGE_TYPES
.iter()
.any(|candidate| type_name.eq_ignore_ascii_case(candidate))
}
fn canonical_col_name(col: &AnyColumn) -> String {
// Some databases fold all unquoted identifiers to uppercase but SQLPage uses lowercase property names
if matches!(col.type_info().0, AnyTypeInfoKind::Odbc(_))
@@ -82,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<sqlx::postgres::Postgres>
+ for<'a> sqlx::Decode<'a, sqlx::postgres::Postgres>,
{
let Ok(pg_val): Result<PgValueRef<'r>, _> = raw_value.try_into() else {
log::error!("Only postgres range values are supported");
return Value::Null;
};
match <PgRange<T> 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(
@@ -138,7 +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::<Vec<u8>>(raw_value))
}
_ if is_postgres_range_type(type_name) => decode_raw::<String>(raw_value).into(),
"INT4RANGE" => decode_pg_range::<i32>(raw_value),
"INT8RANGE" => decode_pg_range::<i64>(raw_value),
"NUMRANGE" => decode_pg_range::<BigDecimal>(raw_value),
"DATERANGE" => decode_pg_range::<NaiveDate>(raw_value),
"TSRANGE" => decode_pg_range::<NaiveDateTime>(raw_value),
"TSTZRANGE" => decode_pg_range::<DateTime<FixedOffset>>(raw_value),
// Deserialize as a string by default
_ => decode_raw::<String>(raw_value).into(),
}
@@ -239,8 +251,8 @@ mod tests {
'[1,5)'::INT4RANGE as int4range,
'[1,5]'::INT8RANGE as int8range,
'[1.5,4.5)'::NUMRANGE as numrange,
'[\"2024-11-12 00:00:00\",\"2024-11-12 23:00:00\")'::TSRANGE as tsrange,
'[\"2024-11-12 00:00:00+00\",\"2024-11-12 23:00:00+00\")'::TSTZRANGE as tstzrange,
-- '[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
",
)
@@ -272,10 +284,10 @@ mod tests {
"blob_data": "data:application/octet-stream;base64,aGVsbG8gd29ybGQ=",
"uuid": "550e8400-e29b-41d4-a716-446655440000",
"int4range": "[1,5)",
"int8range": "[1,5]",
"int8range": "[1,6)",
"numrange": "[1.5,4.5)",
"tsrange": "[\"2024-11-12 00:00:00\",\"2024-11-12 23:00:00\")",
"tstzrange": "[\"2024-11-12 00:00:00+00\",\"2024-11-12 23:00:00+00\")",
//"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)"
}),
);
@@ -331,8 +343,8 @@ mod tests {
let row = sqlx::query(
"SELECT
'[1,5)'::INT4RANGE as int4range,
'[\"2024-11-12 00:00:00\",\"2024-11-12 23:00:00\")'::TSRANGE as tsrange,
'[\"2024-11-12 00:00:00+00\",\"2024-11-12 23:00:00+00\")'::TSTZRANGE as tstzrange,
'[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",
)
@@ -344,8 +356,8 @@ mod tests {
&row_to_json(&row),
&serde_json::json!({
"int4range": "[1,5)",
"tsrange": "[\"2024-11-12 00:00:00\",\"2024-11-12 23:00:00\")",
"tstzrange": "[\"2024-11-12 00:00:00+00\",\"2024-11-12 23:00:00+00\")",
"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)"
}),
);