mirror of
https://github.com/launchbadge/sqlx.git
synced 2026-03-19 08:39:44 +00:00
doc(pg): document behavior of bigdecimal and rust_decimal with out-of-range values
also add a regression test
This commit is contained in:
20
sqlx-postgres/src/types/bigdecimal-range.md
Normal file
20
sqlx-postgres/src/types/bigdecimal-range.md
Normal file
@@ -0,0 +1,20 @@
|
||||
#### Note: `BigDecimal` Has a Larger Range than `NUMERIC`
|
||||
`BigDecimal` can represent values with a far, far greater range than the `NUMERIC` type in Postgres can.
|
||||
|
||||
`NUMERIC` is limited to 131,072 digits before the decimal point, and 16,384 digits after it.
|
||||
See [Section 8.1, Numeric Types] of the Postgres manual for details.
|
||||
|
||||
Meanwhile, `BigDecimal` can theoretically represent a value with an arbitrary number of decimal digits, albeit
|
||||
with a maximum of 2<sup>63</sup> significant figures.
|
||||
|
||||
Because encoding in the current API design _must_ be infallible,
|
||||
when attempting to encode a `BigDecimal` that cannot fit in the wire representation of `NUMERIC`,
|
||||
SQLx may instead encode a sentinel value that falls outside the allowed range but is still representable.
|
||||
|
||||
This will cause the query to return a `DatabaseError` with code `22P03` (`invalid_binary_representation`)
|
||||
and the error message `invalid scale in external "numeric" value` (though this may be subject to change).
|
||||
|
||||
However, `BigDecimal` should be able to decode any `NUMERIC` value except `NaN`,
|
||||
for which it has no representation.
|
||||
|
||||
[Section 8.1, Numeric Types]: https://www.postgresql.org/docs/current/datatype-numeric.html
|
||||
@@ -127,10 +127,7 @@ impl TryFrom<&'_ BigDecimal> for PgNumeric {
|
||||
}
|
||||
|
||||
Ok(PgNumeric::Number {
|
||||
sign: match sign {
|
||||
Sign::Plus | Sign::NoSign => PgNumericSign::Positive,
|
||||
Sign::Minus => PgNumericSign::Negative,
|
||||
},
|
||||
sign: sign_to_pg(sign),
|
||||
scale,
|
||||
weight,
|
||||
digits,
|
||||
@@ -138,17 +135,22 @@ impl TryFrom<&'_ BigDecimal> for PgNumeric {
|
||||
}
|
||||
}
|
||||
|
||||
#[doc=include_str!("bigdecimal-range.md")]
|
||||
impl Encode<'_, Postgres> for BigDecimal {
|
||||
fn encode_by_ref(&self, buf: &mut PgArgumentBuffer) -> IsNull {
|
||||
use std::str::FromStr;
|
||||
// If the argument is too big, then we replace it with a less big argument.
|
||||
// This less big argument is already outside the range of allowed PostgreSQL DECIMAL, which
|
||||
// means that PostgreSQL will return the 22P03 error kind upon receiving it. This is the
|
||||
// expected error, and the user should be ready to handle it anyway.
|
||||
PgNumeric::try_from(self)
|
||||
.unwrap_or_else(|_| {
|
||||
PgNumeric::try_from(&BigDecimal::from_str(&format!("{:030000}", 0)).unwrap())
|
||||
.unwrap()
|
||||
PgNumeric::Number {
|
||||
digits: vec![1],
|
||||
// This is larger than the maximum allowed value, so Postgres should return an error.
|
||||
scale: 0x4000,
|
||||
weight: 0,
|
||||
sign: sign_to_pg(self.sign()),
|
||||
}
|
||||
})
|
||||
.encode(buf);
|
||||
|
||||
@@ -162,6 +164,9 @@ impl Encode<'_, Postgres> for BigDecimal {
|
||||
}
|
||||
}
|
||||
|
||||
/// ### Note: `NaN`
|
||||
/// `BigDecimal` has a greater range than `NUMERIC` (see the corresponding `Encode` impl for details)
|
||||
/// but cannot represent `NaN`, so decoding may return an error.
|
||||
impl Decode<'_, Postgres> for BigDecimal {
|
||||
fn decode(value: PgValueRef<'_>) -> Result<Self, BoxDynError> {
|
||||
match value.format() {
|
||||
@@ -171,6 +176,13 @@ impl Decode<'_, Postgres> for BigDecimal {
|
||||
}
|
||||
}
|
||||
|
||||
fn sign_to_pg(sign: Sign) -> PgNumericSign {
|
||||
match sign {
|
||||
Sign::Plus | Sign::NoSign => PgNumericSign::Positive,
|
||||
Sign::Minus => PgNumericSign::Negative,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod bigdecimal_to_pgnumeric {
|
||||
use super::{BigDecimal, PgNumeric, PgNumericSign};
|
||||
|
||||
@@ -32,6 +32,8 @@
|
||||
//! |---------------------------------------|------------------------------------------------------|
|
||||
//! | `bigdecimal::BigDecimal` | NUMERIC |
|
||||
//!
|
||||
#![doc=include_str!("bigdecimal-range.md")]
|
||||
//!
|
||||
//! ### [`rust_decimal`](https://crates.io/crates/rust_decimal)
|
||||
//! Requires the `rust_decimal` Cargo feature flag.
|
||||
//!
|
||||
@@ -39,6 +41,8 @@
|
||||
//! |---------------------------------------|------------------------------------------------------|
|
||||
//! | `rust_decimal::Decimal` | NUMERIC |
|
||||
//!
|
||||
#![doc=include_str!("rust_decimal-range.md")]
|
||||
//!
|
||||
//! ### [`chrono`](https://crates.io/crates/chrono)
|
||||
//!
|
||||
//! Requires the `chrono` Cargo feature flag.
|
||||
|
||||
10
sqlx-postgres/src/types/rust_decimal-range.md
Normal file
10
sqlx-postgres/src/types/rust_decimal-range.md
Normal file
@@ -0,0 +1,10 @@
|
||||
#### Note: `rust_decimal::Decimal` Has a Smaller Range than `NUMERIC`
|
||||
`NUMERIC` is can have up to 131,072 digits before the decimal point, and 16,384 digits after it.
|
||||
See [Section 8.1, Numeric Types] of the Postgres manual for details.
|
||||
|
||||
However, `rust_decimal::Decimal` is limited to a maximum absolute magnitude of 2<sup>96</sup> - 1,
|
||||
a number with 67 decimal digits, and a minimum absolute magnitude of 10<sup>-28</sup>, a number with, unsurprisingly,
|
||||
28 decimal digits.
|
||||
|
||||
Thus, in contrast with `BigDecimal`, `NUMERIC` can actually represent every possible value of `rust_decimal::Decimal`,
|
||||
but not the other way around. This means that encoding should never fail, but decoding can.
|
||||
@@ -71,6 +71,7 @@ impl TryFrom<PgNumeric> for Decimal {
|
||||
}
|
||||
}
|
||||
|
||||
// This impl is effectively infallible because `NUMERIC` has a greater range than `Decimal`.
|
||||
impl TryFrom<&'_ Decimal> for PgNumeric {
|
||||
type Error = BoxDynError;
|
||||
|
||||
@@ -142,18 +143,17 @@ impl TryFrom<&'_ Decimal> for PgNumeric {
|
||||
}
|
||||
}
|
||||
|
||||
/// ### Panics
|
||||
/// If this `Decimal` cannot be represented by `PgNumeric`.
|
||||
impl Encode<'_, Postgres> for Decimal {
|
||||
fn encode_by_ref(&self, buf: &mut PgArgumentBuffer) -> IsNull {
|
||||
PgNumeric::try_from(self)
|
||||
.expect("Decimal magnitude too great for Postgres NUMERIC type")
|
||||
.expect("BUG: `Decimal` to `PgNumeric` conversion should be infallible")
|
||||
.encode(buf);
|
||||
|
||||
IsNull::No
|
||||
}
|
||||
}
|
||||
|
||||
#[doc=include_str!("rust_decimal-range.md")]
|
||||
impl Decode<'_, Postgres> for Decimal {
|
||||
fn decode(value: PgValueRef<'_>) -> Result<Self, BoxDynError> {
|
||||
match value.format() {
|
||||
|
||||
Reference in New Issue
Block a user