fix(postgres): display money query values

This commit is contained in:
onenewcode authored and GitHub committed 2026-08-24 13:27:57 +08:00
1 parent bd9f10cb5a
commit 2f47a23c4c
1 file changed
+48 -2
+48 -2
View File
@@ -167,6 +167,34 @@ impl<'a> FromSql<'a> for PgAnyString {
}
}
/// PostgreSQL `money` is sent in the binary protocol as a signed 64-bit
/// integer scaled by 100 (the smallest unit of the currency). It is not
/// decoded by `rust_decimal`, so attempting to read it as `Decimal` produces
/// NULL in the result grid.
struct PgMoney(i64);
impl<'a> FromSql<'a> for PgMoney {
fn from_sql(_: &Type, raw: &'a [u8]) -> Result<Self, Box<dyn std::error::Error + Sync + Send>> {
let bytes: [u8; 8] = raw.try_into().map_err(|_| "expected 8 bytes for PostgreSQL money")?;
Ok(Self(i64::from_be_bytes(bytes)))
}
fn accepts(ty: &Type) -> bool {
*ty == Type::MONEY
}
}
fn format_pg_money(value: i64) -> String {
let negative = value < 0;
let absolute = (value as i128).abs();
let formatted = format!("{}.{:02}", absolute / 100, absolute % 100);
if negative {
format!("-{formatted}")
} else {
formatted
}
}
/// A `FromSql` adapter that accepts any PostgreSQL type and returns the raw
/// bytes unchanged. Used to decode custom types like pgvector whose binary
/// format we handle ourselves.
@@ -598,6 +626,7 @@ pub(crate) enum PgColType {
DateRange,
Temporal { fallback: PgTemporalFallback },
Numeric,
Money,
Uuid,
Inet { cidr: bool },
MacAddr,
@@ -702,7 +731,10 @@ pub(crate) fn classify_pg_type(type_name: &str) -> PgColType {
};
return PgColType::Temporal { fallback };
}
if upper == "NUMERIC" || upper == "DECIMAL" || upper == "MONEY" {
if upper == "MONEY" {
return PgColType::Money;
}
if upper == "NUMERIC" || upper == "DECIMAL" {
return PgColType::Numeric;
}
if upper == "UUID" {
@@ -790,6 +822,10 @@ pub(crate) fn pg_value_to_json_classified(row: &Row, idx: usize, col_type: PgCol
.try_get::<_, Decimal>(idx)
.map(|v: Decimal| serde_json::Value::String(v.to_string()))
.unwrap_or(serde_json::Value::Null),
PgColType::Money => row
.try_get::<_, PgMoney>(idx)
.map(|v| serde_json::Value::String(format_pg_money(v.0)))
.unwrap_or(serde_json::Value::Null),
PgColType::Uuid => row
.try_get::<_, uuid::Uuid>(idx)
.map(|v| serde_json::Value::String(v.to_string()))
@@ -7335,6 +7371,16 @@ mod tests {
use tokio::net::TcpListener;
use tokio_postgres::types::FromSql;
#[test]
fn postgres_money_binary_values_are_decoded_with_two_decimal_places() {
assert_eq!(format_pg_money(12345), "123.45");
assert_eq!(format_pg_money(-5), "-0.05");
assert_eq!(format_pg_money(0), "0.00");
let decoded = PgMoney::from_sql(&Type::MONEY, &(-12345_i64).to_be_bytes()).unwrap();
assert_eq!(format_pg_money(decoded.0), "-123.45");
}
fn postgres_error_response(message: &str) -> Vec<u8> {
let mut payload = Vec::new();
payload.extend_from_slice(b"SERROR\0");
@@ -8220,7 +8266,7 @@ mod tests {
// 同时命中时间关键字与 VECTOR( 前缀的类型名,原实现时间解码失败后走 vector 分支
assert_eq!(classify_pg_type("vector(timestamp)"), PgColType::Temporal { fallback: PgTemporalFallback::Vector });
assert_eq!(classify_pg_type("numeric"), PgColType::Numeric);
assert_eq!(classify_pg_type("money"), PgColType::Numeric);
assert_eq!(classify_pg_type("money"), PgColType::Money);
assert_eq!(classify_pg_type("uuid"), PgColType::Uuid);
assert_eq!(classify_pg_type("inet"), PgColType::Inet { cidr: false });
assert_eq!(classify_pg_type("cidr"), PgColType::Inet { cidr: true });