Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
16 changes: 8 additions & 8 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
77 changes: 73 additions & 4 deletions src/webserver/database/sql_to_json.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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<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(
Expand Down Expand Up @@ -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::<Vec<u8>>(raw_value))
}
"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(),
}
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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(())
Expand Down Expand Up @@ -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"));
Expand Down