fix(postgres): decode point query values

Closes #5711
This commit is contained in:
t8y2 2026-08-09 20:00:16 +08:00
parent cdfa993642
commit 09edbfbef3
No known key found for this signature in database
2 changed files with 106 additions and 0 deletions

View File

@ -151,6 +151,44 @@ impl<'a> FromSql<'a> for PgRawBytes {
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
struct PgPoint {
x: f64,
y: f64,
}
impl<'a> FromSql<'a> for PgPoint {
fn from_sql(_: &Type, raw: &'a [u8]) -> Result<Self, Box<dyn std::error::Error + Sync + Send>> {
decode_pg_point_bytes(raw).ok_or_else(|| "expected 16 bytes for PostgreSQL point".into())
}
fn accepts(ty: &Type) -> bool {
*ty == Type::POINT
}
}
fn decode_pg_point_bytes(raw: &[u8]) -> Option<PgPoint> {
let raw: [u8; 16] = raw.try_into().ok()?;
Some(PgPoint {
x: f64::from_be_bytes(raw[0..8].try_into().ok()?),
y: f64::from_be_bytes(raw[8..16].try_into().ok()?),
})
}
fn format_pg_float(value: f64) -> String {
if value == f64::INFINITY {
"Infinity".to_string()
} else if value == f64::NEG_INFINITY {
"-Infinity".to_string()
} else {
value.to_string()
}
}
fn format_pg_point(point: PgPoint) -> String {
format!("({},{})", format_pg_float(point.x), format_pg_float(point.y))
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
struct PgInterval {
microseconds: i64,
@ -524,6 +562,7 @@ pub(crate) enum PgColType {
Bytea,
Json,
Bool,
Point,
Interval,
DateRange,
Temporal { fallback: PgTemporalFallback },
@ -572,6 +611,9 @@ pub(crate) fn classify_pg_type(type_name: &str) -> PgColType {
if upper == "BOOL" {
return PgColType::Bool;
}
if upper == "POINT" {
return PgColType::Point;
}
if upper == "INTERVAL" {
return PgColType::Interval;
}
@ -655,6 +697,10 @@ pub(crate) fn pg_value_to_json_classified(row: &Row, idx: usize, col_type: PgCol
serde_json::Value::Null
}
PgColType::Bool => pg_bool_value_to_json(row, idx),
PgColType::Point => row
.try_get::<_, PgPoint>(idx)
.map(|point| serde_json::Value::String(format_pg_point(point)))
.unwrap_or(serde_json::Value::Null),
PgColType::Interval => row
.try_get::<_, PgInterval>(idx)
.map(|interval| serde_json::Value::String(format_pg_interval(interval)))
@ -5516,6 +5562,7 @@ mod tests {
assert_eq!(classify_pg_type("json"), PgColType::Json);
assert_eq!(classify_pg_type("JSONB"), PgColType::Json);
assert_eq!(classify_pg_type("bool"), PgColType::Bool);
assert_eq!(classify_pg_type("point"), PgColType::Point);
assert_eq!(classify_pg_type("timestamp"), PgColType::Temporal { fallback: PgTemporalFallback::Probe });
assert_eq!(classify_pg_type("timestamptz"), PgColType::Temporal { fallback: PgTemporalFallback::Probe });
assert_eq!(classify_pg_type("date"), PgColType::Temporal { fallback: PgTemporalFallback::Probe });
@ -5954,6 +6001,22 @@ mod tests {
assert!(!PgSystemU32::accepts(&Type::INT4));
}
#[test]
fn pg_point_decodes_binary_coordinates_as_text() {
let mut raw = Vec::new();
raw.extend_from_slice(&(-194.0_f64).to_be_bytes());
raw.extend_from_slice(&53.0_f64.to_be_bytes());
let point = PgPoint::from_sql(&Type::POINT, &raw).unwrap();
assert_eq!(point, PgPoint { x: -194.0, y: 53.0 });
assert_eq!(format_pg_point(point), "(-194,53)");
assert_eq!(format_pg_point(PgPoint { x: f64::NEG_INFINITY, y: f64::INFINITY }), "(-Infinity,Infinity)");
assert!(PgPoint::from_sql(&Type::POINT, &[0; 15]).is_err());
assert!(PgPoint::accepts(&Type::POINT));
assert!(!PgPoint::accepts(&Type::BYTEA));
}
#[test]
fn pg_any_string_accepts_all_types_and_decodes_utf8() {
// Accepts any type — built-in, custom enum OIDs, domains, etc.

View File

@ -0,0 +1,43 @@
use std::time::Duration;
use dbx_core::db::postgres;
#[tokio::test]
#[ignore = "requires DBX_TEST_POSTGRES_URL pointing at a writable PostgreSQL database"]
async fn postgres_point_columns_match_their_text_representation() {
let url = std::env::var("DBX_TEST_POSTGRES_URL").expect("DBX_TEST_POSTGRES_URL");
let pool = postgres::connect(&url, Duration::from_secs(5)).await.expect("connect postgres");
let schema = format!("dbx_point_{}", std::process::id());
let schema_ident = format!("\"{}\"", schema.replace('"', "\"\""));
let table = format!("{schema_ident}.cities");
let _ = postgres::execute_query(&pool, &format!("DROP SCHEMA IF EXISTS {schema_ident} CASCADE")).await;
postgres::execute_query(&pool, &format!("CREATE SCHEMA {schema_ident}")).await.expect("create schema");
postgres::execute_query(&pool, &format!("CREATE TABLE {table} (name text NOT NULL, location point)"))
.await
.expect("create table");
postgres::execute_query(
&pool,
&format!("INSERT INTO {table} VALUES ('San Francisco', '(-194.0, 53.0)'), ('Unknown', NULL)"),
)
.await
.expect("insert rows");
let result = postgres::execute_query(
&pool,
&format!("SELECT name, location, location::text AS location_text FROM {table} ORDER BY name"),
)
.await
.expect("select point rows");
postgres::execute_query(&pool, &format!("DROP SCHEMA IF EXISTS {schema_ident} CASCADE"))
.await
.expect("drop schema");
assert_eq!(result.column_types, vec!["text", "point", "text"]);
assert_eq!(result.rows.len(), 2);
assert_eq!(result.rows[0][1], serde_json::json!("(-194,53)"));
assert_eq!(result.rows[0][1], result.rows[0][2]);
assert_eq!(result.rows[1][1], serde_json::Value::Null);
assert_eq!(result.rows[1][2], serde_json::Value::Null);
}