fix(postgres): decode point query values

Closes #5711
This commit is contained in:
t8y2 committed 2026-08-09 20:00:16 +08:00
1 parent cdfa993642
commit 09edbfbef3
2 files changed
+106

No files matched your search

+63
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.