Skip to main content

faucet_core/
fidelity.rs

1//! One value-fidelity model (RFC 0013, #885).
2//!
3//! A record is a `serde_json::Value`, and a `Value` holds every number it does
4//! not keep as an `i64` / `u64` as an `f64`. That is the root of the most common
5//! silent corruption in a data pipeline: a `DECIMAL(38,9)` rounded through a
6//! float, a timestamp that lost its zone or its microseconds, a byte column
7//! re-encoded differently by each connector. None of these fail a run.
8//!
9//! This module is the single definition of how a typed value from a backend
10//! becomes JSON. Every connector renders through it, so a value means the same
11//! thing whichever source produced it.
12//!
13//! # Canonical rendering rules
14//!
15//! | Backend value | JSON rendering | Converter |
16//! |---|---|---|
17//! | exact decimal (`DECIMAL`, `NUMERIC`, `MONEY`, `NUMBER(p,s)`, Arrow `Decimal*`, BSON `Decimal128`) | a **string** holding the exact digits at the column's own scale (`"12.50"`); never an `f64` | [`decimal_text`], [`decimal_from_i128`] |
18//! | integer within `i64` / `u64` | a JSON number (a `Value` holds it exactly) | [`integer_i128`], [`integer_u128`], [`integer_text`] |
19//! | integer beyond `i64` / `u64` (128-bit, `NUMERIC(38,0)`, `UInt128`) | a **string** of its digits | the same |
20//! | number with no declared scale (a JSON document, a `NUMBER` with no scale, a document-store number) | a JSON number when the `f64` it parses to renders back to the same value, otherwise a string of the exact digits | [`number_text`], [`parse_json`] |
21//! | `FLOAT` / `DOUBLE` | a JSON number; an `f32` keeps its shortest decimal form (`0.1`, not `0.10000000149011612`); `-0.0` keeps its sign | [`float64`], [`float32`] |
22//! | `NaN`, `±Infinity` (float or decimal) | **`null`**, counted: [`record_non_finite`] increments the run's `non_finite` row account and `faucet_fidelity_non_finite_total{pipeline,row}`, and the first one per process logs a warning | [`float64`], [`float32`], [`decimal_text`] |
23//! | instant (`TIMESTAMPTZ`, a timestamp with a zone, epoch + zone) | RFC 3339 with an explicit offset (`Z` for UTC) and the type's **native precision** — a microsecond column always renders six fractional digits | [`timestamp`], [`timestamp_from_epoch`] |
24//! | local date-time (a timestamp with no zone) | ISO 8601 date-time **without** an offset at native precision — an offset the backend never stored is not invented | [`timestamp_naive`] |
25//! | date / time of day | `YYYY-MM-DD` / `HH:MM:SS[.fff…]` at native precision | [`date`], [`time`] |
26//! | binary (`BYTEA`, `BLOB`, `VARBINARY`, Arrow `Binary`) | standard **base64** with padding | [`binary`] |
27//!
28//! Sinks read the same renderings back: a string in a decimal column is parsed
29//! as an exact decimal, never through `f64`.
30//!
31//! # The fidelity corpus
32//!
33//! `faucet_conformance::fidelity` holds the values that have broken a pipeline
34//! somewhere. Every connector runs it — a source/sink pair round trip, a
35//! one-sided read or write against the backend, or the connector's converter —
36//! or names the reason it cannot in `crates/conformance/fidelity-exempt.txt`.
37//! `scripts/fidelity-coverage.py` enforces that in CI, and
38//! `scripts/decimal-f64.py` refuses a decimal converted through a float.
39
40use std::future::Future;
41use std::sync::Arc;
42use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
43
44use base64::Engine as _;
45use chrono::{DateTime, FixedOffset, NaiveDate, NaiveDateTime, NaiveTime, SecondsFormat, TimeZone};
46use serde_json::{Number, Value};
47
48/// The largest integer an IEEE-754 double holds exactly (`2^53 - 1`). A
49/// consumer that reads JSON numbers as doubles corrupts integers past it; a
50/// `Value` itself holds every `i64` / `u64` exactly.
51pub const MAX_SAFE_INTEGER: u64 = (1 << 53) - 1;
52
53/// The precision a temporal type carries natively.
54#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
55#[non_exhaustive]
56pub enum TimePrecision {
57    /// Whole seconds.
58    Seconds,
59    /// Milliseconds: three fractional digits.
60    Millis,
61    /// Microseconds: six fractional digits.
62    Micros,
63    /// Nanoseconds: nine fractional digits.
64    Nanos,
65}
66
67impl TimePrecision {
68    /// The precision of a type with `digits` fractional-second digits
69    /// (`TIMESTAMP(3)`, `DATETIME2(7)`), rounded up to the next of 0/3/6/9.
70    pub fn from_digits(digits: u32) -> Self {
71        match digits {
72            0 => Self::Seconds,
73            1..=3 => Self::Millis,
74            4..=6 => Self::Micros,
75            _ => Self::Nanos,
76        }
77    }
78
79    fn seconds_format(self) -> SecondsFormat {
80        match self {
81            Self::Seconds => SecondsFormat::Secs,
82            Self::Millis => SecondsFormat::Millis,
83            Self::Micros => SecondsFormat::Micros,
84            Self::Nanos => SecondsFormat::Nanos,
85        }
86    }
87
88    fn fraction(self, nanos: u32) -> String {
89        match self {
90            Self::Seconds => String::new(),
91            Self::Millis => format!(".{:03}", nanos / 1_000_000),
92            Self::Micros => format!(".{:06}", nanos / 1_000),
93            Self::Nanos => format!(".{nanos:09}"),
94        }
95    }
96
97    fn per_second(self) -> i64 {
98        match self {
99            Self::Seconds => 1,
100            Self::Millis => 1_000,
101            Self::Micros => 1_000_000,
102            Self::Nanos => 1_000_000_000,
103        }
104    }
105}
106
107/// A value a converter could not read.
108#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
109#[non_exhaustive]
110pub enum FidelityError {
111    /// The text is not a decimal number.
112    #[error("{0:?} is not a decimal number")]
113    NotANumber(String),
114    /// The text is not an integer.
115    #[error("{0:?} is not an integer")]
116    NotAnInteger(String),
117    /// An epoch offset outside the range a date-time can represent.
118    #[error("epoch value {0} is outside the representable date-time range")]
119    OutOfRange(i64),
120}
121
122/// `(negative, digits without leading zeros, scale)` of a decimal literal:
123/// `value = digits × 10^-scale`. `None` when `text` is not one.
124fn parse_decimal(text: &str) -> Option<(bool, String, i64)> {
125    let t = text.trim();
126    let (neg, rest) = match t.as_bytes().first()? {
127        b'-' => (true, &t[1..]),
128        b'+' => (false, &t[1..]),
129        _ => (false, t),
130    };
131    let (mantissa, exp) = match rest.find(['e', 'E']) {
132        Some(i) => (&rest[..i], rest[i + 1..].parse::<i64>().ok()?),
133        None => (rest, 0),
134    };
135    let (int, frac) = mantissa.split_once('.').unwrap_or((mantissa, ""));
136    if (int.is_empty() && frac.is_empty())
137        || !int.bytes().chain(frac.bytes()).all(|b| b.is_ascii_digit())
138    {
139        return None;
140    }
141    let digits: String = format!("{int}{frac}");
142    let digits = digits.trim_start_matches('0').to_string();
143    let scale = (frac.len() as i64).checked_sub(exp)?;
144    Some((neg, digits, scale))
145}
146
147/// Positional text of `digits × 10^-scale`, keeping the scale's trailing zeros.
148fn render_decimal(negative: bool, digits: &str, scale: i64) -> String {
149    let mut out = String::new();
150    if scale <= 0 {
151        if digits.is_empty() {
152            return "0".to_string();
153        }
154        if negative {
155            out.push('-');
156        }
157        out.push_str(digits);
158        out.extend(std::iter::repeat_n('0', (-scale) as usize));
159        return out;
160    }
161    let scale = scale as usize;
162    let padded = if digits.len() <= scale {
163        format!("{}{digits}", "0".repeat(scale + 1 - digits.len()))
164    } else {
165        digits.to_string()
166    };
167    let (int, frac) = padded.split_at(padded.len() - scale);
168    if negative && !digits.is_empty() {
169        out.push('-');
170    }
171    out.push_str(int);
172    out.push('.');
173    out.push_str(frac);
174    out
175}
176
177fn is_non_finite_word(text: &str) -> bool {
178    let t = text.trim().trim_start_matches(['+', '-']);
179    t.eq_ignore_ascii_case("nan")
180        || t.eq_ignore_ascii_case("inf")
181        || t.eq_ignore_ascii_case("infinity")
182}
183
184/// An exact decimal from its text, as a JSON string at its own scale
185/// (`"1.50"` stays `"1.50"`, `"1.2E+3"` becomes `"1200"`). `NaN` / `Infinity`
186/// (a `NUMERIC` can hold them) become a counted `null`.
187pub fn decimal_text(text: &str) -> Result<Value, FidelityError> {
188    if is_non_finite_word(text) {
189        record_non_finite();
190        return Ok(Value::Null);
191    }
192    let (neg, digits, scale) =
193        parse_decimal(text).ok_or_else(|| FidelityError::NotANumber(text.to_string()))?;
194    Ok(Value::String(render_decimal(neg, &digits, scale)))
195}
196
197/// An exact decimal `unscaled × 10^-scale` (the Arrow / driver representation)
198/// as a JSON string.
199pub fn decimal_from_i128(unscaled: i128, scale: i32) -> Value {
200    let digits = unscaled.unsigned_abs().to_string();
201    let digits = digits.trim_start_matches('0');
202    Value::String(render_decimal(unscaled < 0, digits, i64::from(scale)))
203}
204
205/// An integer as a JSON number when a `Value` holds it exactly (`i64` / `u64`
206/// range), otherwise as a string of its digits.
207pub fn integer_i128(v: i128) -> Value {
208    if let Ok(i) = i64::try_from(v) {
209        Value::Number(i.into())
210    } else if let Ok(u) = u64::try_from(v) {
211        Value::Number(u.into())
212    } else {
213        Value::String(v.to_string())
214    }
215}
216
217/// An unsigned integer: a JSON number up to `u64::MAX`, a string beyond.
218pub fn integer_u128(v: u128) -> Value {
219    match u64::try_from(v) {
220        Ok(u) => Value::Number(u.into()),
221        Err(_) => Value::String(v.to_string()),
222    }
223}
224
225/// An integer from its text, of any size, per [`integer_i128`].
226pub fn integer_text(text: &str) -> Result<Value, FidelityError> {
227    let t = text.trim();
228    let body = t.strip_prefix(['-', '+']).unwrap_or(t);
229    if body.is_empty() || !body.bytes().all(|b| b.is_ascii_digit()) {
230        return Err(FidelityError::NotAnInteger(text.to_string()));
231    }
232    if let Ok(i) = t.parse::<i64>() {
233        return Ok(Value::Number(i.into()));
234    }
235    if let Ok(u) = t.trim_start_matches('+').parse::<u64>() {
236        return Ok(Value::Number(u.into()));
237    }
238    let digits = body.trim_start_matches('0');
239    let neg = t.starts_with('-') && !digits.is_empty();
240    Ok(Value::String(format!(
241        "{}{}",
242        if neg { "-" } else { "" },
243        if digits.is_empty() { "0" } else { digits }
244    )))
245}
246
247/// A number with no declared scale (a JSON document, an unscaled `NUMBER`, a
248/// document-store number): a JSON number when that is exact, the exact digits
249/// as a string otherwise.
250pub fn number_text(text: &str) -> Result<Value, FidelityError> {
251    let t = text.trim();
252    if is_non_finite_word(t) {
253        record_non_finite();
254        return Ok(Value::Null);
255    }
256    if !t.contains(['.', 'e', 'E'])
257        && let Ok(v) = integer_text(t)
258    {
259        return Ok(v);
260    }
261    let (neg, digits, scale) =
262        parse_decimal(t).ok_or_else(|| FidelityError::NotANumber(text.to_string()))?;
263    if digits.is_empty() {
264        return Ok(Value::Number(0.into()));
265    }
266    let positional = render_decimal(neg, &digits, scale);
267    for candidate in [t.trim_start_matches('+'), positional.as_str()] {
268        if crate::json_numbers::is_exact(candidate)
269            && let Ok(Value::Number(n)) = serde_json::from_str::<Value>(candidate)
270        {
271            return Ok(Value::Number(n));
272        }
273    }
274    if scale <= 0 && digits.len() as i64 - scale <= 39 {
275        return integer_text(&positional);
276    }
277    Ok(Value::String(positional))
278}
279
280/// A double as a JSON number; `NaN` / `±Infinity` become a counted `null`.
281pub fn float64(v: f64) -> Value {
282    match Number::from_f64(v) {
283        Some(n) => Value::Number(n),
284        None => {
285            record_non_finite();
286            Value::Null
287        }
288    }
289}
290
291/// A single-precision float through its shortest decimal form, so `0.1f32`
292/// renders `0.1`; `NaN` / `±Infinity` become a counted `null`.
293pub fn float32(v: f32) -> Value {
294    if !v.is_finite() {
295        record_non_finite();
296        return Value::Null;
297    }
298    v.to_string()
299        .parse::<f64>()
300        .map_or_else(|_| float64(f64::from(v)), float64)
301}
302
303/// The text a sink binds for a JSON number that is not an integer: its
304/// shortest round-trip decimal form, so the server parses the value the record
305/// holds. An `f64` bound into a `DECIMAL` column is converted from binary and
306/// lands as `0.1000000000000000055…`. `None` for an integer (bind it natively).
307pub fn non_integer_text(n: &Number) -> Option<String> {
308    n.is_f64().then(|| n.to_string())
309}
310
311/// A PostgreSQL-wire binary `NUMERIC` (`ndigits`, `weight`, `sign`,
312/// `dscale`, then base-10000 digits) as an exact decimal string at its
313/// display scale — the column's declared scale. `NaN` / `±Infinity` are a
314/// counted `null`. `None` when the bytes are not a binary numeric.
315pub fn decimal_from_pg_numeric(bytes: &[u8]) -> Option<Value> {
316    let word = |i: usize| -> Option<u16> {
317        Some(u16::from_be_bytes([*bytes.get(i)?, *bytes.get(i + 1)?]))
318    };
319    let ndigits = usize::from(word(0)?);
320    let weight = i64::from(word(2)? as i16);
321    let sign = word(4)?;
322    let dscale = usize::from(word(6)?);
323    if matches!(sign, 0xC000 | 0xD000 | 0xF000) {
324        record_non_finite();
325        return Some(Value::Null);
326    }
327    if !matches!(sign, 0x0000 | 0x4000) || bytes.len() != 8 + 2 * ndigits {
328        return None;
329    }
330    let digits: Vec<u16> = (0..ndigits)
331        .map(|i| word(8 + 2 * i))
332        .collect::<Option<_>>()?;
333    if digits.iter().any(|d| *d > 9999) {
334        return None;
335    }
336    let group = |g: i64| -> u16 {
337        usize::try_from(g)
338            .ok()
339            .and_then(|i| digits.get(i).copied())
340            .unwrap_or(0)
341    };
342    let mut text = String::new();
343    if sign == 0x4000 {
344        text.push('-');
345    }
346    if weight < 0 {
347        text.push('0');
348    } else {
349        text.push_str(&group(0).to_string());
350        for g in 1..=weight {
351            text.push_str(&format!("{:04}", group(g)));
352        }
353    }
354    if dscale > 0 {
355        let mut frac = String::new();
356        let mut g = weight + 1;
357        while frac.len() < dscale {
358            frac.push_str(&format!("{:04}", group(g)));
359            g += 1;
360        }
361        frac.truncate(dscale);
362        text.push('.');
363        text.push_str(&frac);
364    }
365    decimal_text(&text).ok()
366}
367
368/// Whether a SQL column type holds an exact decimal (`NUMERIC(p,s)`,
369/// `DECIMAL`, `MONEY`, `NUMBER`, `BIGNUMERIC`): a sink binds such a column's
370/// value as text, so the server parses exactly the digits the record holds.
371pub fn is_exact_decimal_type(sql_type: &str) -> bool {
372    let t = sql_type.trim().to_ascii_lowercase();
373    [
374        "numeric",
375        "decimal",
376        "dec",
377        "money",
378        "smallmoney",
379        "number",
380        "bignumeric",
381        "bigdecimal",
382    ]
383    .iter()
384    .any(|p| t == *p || t.starts_with(&format!("{p}(")) || t.starts_with(&format!("{p} ")))
385}
386
387/// Bytes as standard base64 with padding.
388pub fn binary(bytes: &[u8]) -> Value {
389    Value::String(base64::engine::general_purpose::STANDARD.encode(bytes))
390}
391
392/// An instant as RFC 3339 with an explicit offset (`Z` for UTC) at `precision`.
393pub fn timestamp<Tz: TimeZone>(dt: &DateTime<Tz>, precision: TimePrecision) -> Value {
394    Value::String(
395        dt.fixed_offset()
396            .to_rfc3339_opts(precision.seconds_format(), true),
397    )
398}
399
400/// A local date-time (no zone in the source type) without an offset, at
401/// `precision`.
402pub fn timestamp_naive(dt: &NaiveDateTime, precision: TimePrecision) -> Value {
403    Value::String(format!(
404        "{}{}",
405        dt.format("%Y-%m-%dT%H:%M:%S"),
406        precision.fraction(dt.and_utc().timestamp_subsec_nanos() % 1_000_000_000)
407    ))
408}
409
410/// A date as `YYYY-MM-DD`.
411pub fn date(d: &NaiveDate) -> Value {
412    Value::String(d.format("%Y-%m-%d").to_string())
413}
414
415/// A time of day as `HH:MM:SS` plus the fraction `precision` carries.
416pub fn time(t: &NaiveTime, precision: TimePrecision) -> Value {
417    use chrono::Timelike;
418    Value::String(format!(
419        "{}{}",
420        t.format("%H:%M:%S"),
421        precision.fraction(t.nanosecond() % 1_000_000_000)
422    ))
423}
424
425/// An epoch offset in `unit`s: with a zone, an instant ([`timestamp`]);
426/// without one, a local date-time ([`timestamp_naive`]).
427pub fn timestamp_from_epoch(
428    value: i64,
429    unit: TimePrecision,
430    zone: Option<FixedOffset>,
431) -> Result<Value, FidelityError> {
432    let per = unit.per_second();
433    let secs = value.div_euclid(per);
434    let nanos = (value.rem_euclid(per) * (1_000_000_000 / per)) as u32;
435    let utc = DateTime::from_timestamp(secs, nanos).ok_or(FidelityError::OutOfRange(value))?;
436    Ok(match zone {
437        Some(z) => timestamp(&utc.with_timezone(&z), unit),
438        None => timestamp_naive(&utc.naive_utc(), unit),
439    })
440}
441
442/// Parse JSON text a serializer produced (an HTTP body, a message payload, a
443/// JSON Lines record) the way [`serde_json::from_str`] does, except that a
444/// number literal the parsed value would not spell again — an integer beyond
445/// `u64`, a decimal with more digits than a double carries — becomes a string
446/// of its exact digits instead of the nearest `f64`. A 17-digit rendering of a
447/// double (`0.10000000000000001`) names that double and stays a number.
448pub fn from_str(text: &str) -> Result<Value, serde_json::Error> {
449    let value: Value = serde_json::from_str(text)?;
450    let lossy = crate::file_format::json::unfaithful_literals(text.as_bytes());
451    if lossy.is_empty() {
452        return Ok(value);
453    }
454    let mut rewritten = String::with_capacity(text.len() + 2 * lossy.len());
455    let mut last = 0;
456    for r in &lossy {
457        rewritten.push_str(&text[last..r.start]);
458        rewritten.push('"');
459        rewritten.push_str(&text[r.clone()]);
460        rewritten.push('"');
461        last = r.end;
462    }
463    rewritten.push_str(&text[last..]);
464    serde_json::from_str(&rewritten)
465}
466
467/// [`from_str`] over bytes, for a body that should be UTF-8.
468pub fn from_slice(bytes: &[u8]) -> Result<Value, serde_json::Error> {
469    match std::str::from_utf8(bytes) {
470        Ok(text) => from_str(text),
471        Err(_) => serde_json::from_slice(bytes),
472    }
473}
474
475/// Parse JSON text a database stored (a `JSON` / `JSONB` / `VARIANT` column)
476/// keeping every number exact: a number a `Value` cannot hold exactly — by
477/// decimal value, so `0.10000000000000001` is not `0.1` — becomes a string of
478/// its digits (see [`number_text`]).
479pub fn parse_json(text: &[u8]) -> Result<Value, FidelityError> {
480    let text = std::str::from_utf8(text)
481        .map_err(|e| FidelityError::NotANumber(format!("invalid UTF-8 in JSON: {e}")))?;
482    crate::json_numbers::parse_json_exact(text, crate::json_numbers::JsonBigNumbers::String)
483        .map(|(v, _)| v)
484        .map_err(|e| FidelityError::NotANumber(e.to_string()))
485}
486
487/// A per-run count of non-finite values rendered as `null`.
488///
489/// [`Pipeline::run`](crate::Pipeline::run) installs one around the run;
490/// [`record_non_finite`] counts into the innermost installed counter. A
491/// connector that converts values on a task or thread of its own carries the
492/// counter there with [`NonFiniteCounter::current`] and
493/// [`NonFiniteCounter::scope`] / [`NonFiniteCounter::scope_sync`].
494#[derive(Debug, Clone, Default)]
495pub struct NonFiniteCounter(Arc<AtomicU64>);
496
497tokio::task_local! {
498    static NON_FINITE: NonFiniteCounter;
499}
500
501static UNSCOPED: AtomicU64 = AtomicU64::new(0);
502static WARNED: AtomicBool = AtomicBool::new(false);
503
504impl NonFiniteCounter {
505    /// A counter at zero.
506    pub fn new() -> Self {
507        Self::default()
508    }
509
510    /// Values counted so far.
511    pub fn count(&self) -> u64 {
512        self.0.load(Ordering::Relaxed)
513    }
514
515    /// The counter installed around the current task, if any.
516    pub fn current() -> Option<Self> {
517        NON_FINITE.try_with(Clone::clone).ok()
518    }
519
520    /// Run `fut` with this counter installed.
521    pub fn scope<F: Future>(&self, fut: F) -> impl Future<Output = F::Output> {
522        NON_FINITE.scope(self.clone(), fut)
523    }
524
525    /// Run `f` (on a blocking thread, say) with this counter installed.
526    pub fn scope_sync<R>(&self, f: impl FnOnce() -> R) -> R {
527        NON_FINITE.sync_scope(self.clone(), f)
528    }
529}
530
531/// Run `f` under the current task's counter, if any — for a connector that
532/// hands conversion to `spawn_blocking`.
533pub fn propagate<R>(counter: &Option<NonFiniteCounter>, f: impl FnOnce() -> R) -> R {
534    match counter {
535        Some(c) => c.scope_sync(f),
536        None => f(),
537    }
538}
539
540/// Count one non-finite value rendered as `null`. Outside any run (a library
541/// call with no [`Pipeline`](crate::Pipeline)) it counts into
542/// `faucet_fidelity_non_finite_total{pipeline="",row=""}`.
543pub fn record_non_finite() {
544    if NON_FINITE
545        .try_with(|c| c.0.fetch_add(1, Ordering::Relaxed))
546        .is_err()
547    {
548        UNSCOPED.fetch_add(1, Ordering::Relaxed);
549        metrics::counter!("faucet_fidelity_non_finite_total", "pipeline" => "", "row" => "")
550            .increment(1);
551    }
552    if !WARNED.swap(true, Ordering::Relaxed) {
553        tracing::warn!(
554            "a NaN or infinite value has no JSON representation and was written as null; \
555             the run summary counts them as `non_finite`"
556        );
557    }
558}
559
560/// Non-finite values counted outside any installed counter, process-wide.
561pub fn unscoped_non_finite() -> u64 {
562    UNSCOPED.load(Ordering::Relaxed)
563}
564
565#[cfg(test)]
566mod tests {
567    use super::*;
568    use serde_json::json;
569
570    #[test]
571    fn decimals_render_as_exact_strings_at_their_scale() {
572        for (input, out) in [
573            ("1.50", "1.50"),
574            ("-1.50", "-1.50"),
575            ("+7", "7"),
576            ("0007.10", "7.10"),
577            ("1.2E+3", "1200"),
578            ("1.25e-3", "0.00125"),
579            (".5", "0.5"),
580            ("5.", "5"),
581            ("-0.00", "0.00"),
582            ("0", "0"),
583            ("-0", "0"),
584            (
585                "12345678901234567890.123456789",
586                "12345678901234567890.123456789",
587            ),
588            ("1e2", "100"),
589        ] {
590            assert_eq!(decimal_text(input).unwrap(), json!(out), "{input}");
591        }
592        for bad in ["", "-", "1.2.3", "abc", "1e", "1e9999999999999999999", "."] {
593            assert!(decimal_text(bad).is_err(), "{bad}");
594        }
595        assert_eq!(
596            decimal_text("x").unwrap_err().to_string(),
597            "\"x\" is not a decimal number"
598        );
599    }
600
601    #[test]
602    fn decimals_from_unscaled_parts() {
603        assert_eq!(decimal_from_i128(12345, 2), json!("123.45"));
604        assert_eq!(decimal_from_i128(-5, 3), json!("-0.005"));
605        assert_eq!(decimal_from_i128(0, 2), json!("0.00"));
606        assert_eq!(decimal_from_i128(42, 0), json!("42"));
607        assert_eq!(decimal_from_i128(42, -2), json!("4200"));
608        assert_eq!(decimal_from_i128(0, -2), json!("0"));
609        assert_eq!(
610            decimal_from_i128(i128::MIN, 10),
611            json!("-17014118346046923173168730371.5884105728")
612        );
613    }
614
615    #[test]
616    fn integers_are_numbers_until_a_value_cannot_hold_them() {
617        assert_eq!(integer_i128(i128::from(i64::MIN)), json!(i64::MIN));
618        assert_eq!(integer_i128(i128::from(u64::MAX)), json!(u64::MAX));
619        assert_eq!(
620            integer_i128(i128::from(u64::MAX) + 1),
621            json!("18446744073709551616")
622        );
623        assert_eq!(
624            integer_i128(i128::from(i64::MIN) - 1),
625            json!("-9223372036854775809")
626        );
627        assert_eq!(integer_u128(7), json!(7));
628        assert_eq!(integer_u128(u128::MAX), json!(u128::MAX.to_string()));
629        assert_eq!(integer_text(" 42 ").unwrap(), json!(42));
630        assert_eq!(integer_text("+42").unwrap(), json!(42));
631        assert_eq!(
632            integer_text("-000123456789012345678901").unwrap(),
633            json!("-123456789012345678901")
634        );
635        assert_eq!(integer_text("-0000000000000000000000").unwrap(), json!(0));
636        assert_eq!(
637            integer_text("18446744073709551615").unwrap(),
638            json!(u64::MAX)
639        );
640        assert!(integer_text("1.5").is_err());
641        assert!(integer_text("-").is_err());
642        assert_eq!(
643            integer_text("x").unwrap_err().to_string(),
644            "\"x\" is not an integer"
645        );
646        const { assert!(MAX_SAFE_INTEGER == 9_007_199_254_740_991) };
647    }
648
649    #[test]
650    fn unscaled_numbers_stay_numbers_only_when_exact() {
651        assert_eq!(number_text("1.5").unwrap(), json!(1.5));
652        assert_eq!(number_text(".25").unwrap(), json!(0.25));
653        assert_eq!(number_text("-.5").unwrap(), json!(-0.5));
654        assert_eq!(number_text("+2.5").unwrap(), json!(2.5));
655        assert_eq!(number_text("1E+3").unwrap(), json!(1000.0));
656        assert_eq!(number_text("0.1").unwrap(), json!(0.1));
657        assert_eq!(number_text("42").unwrap(), json!(42));
658        assert_eq!(number_text("1e2").unwrap(), json!(100.0));
659        assert_eq!(number_text("0.000").unwrap(), json!(0));
660        assert_eq!(
661            number_text("12345678901234567890.123").unwrap(),
662            json!("12345678901234567890.123")
663        );
664        assert_eq!(
665            number_text("123456789012345678901234567890").unwrap(),
666            json!("123456789012345678901234567890")
667        );
668        assert_eq!(number_text("1e30").unwrap(), json!(1e30));
669        assert_eq!(
670            number_text("1.000000000000000000001e400").unwrap(),
671            json!(format!("1000000000000000000001{}", "0".repeat(379)))
672        );
673        assert_eq!(
674            number_text("123456789012345678901234567890e3").unwrap(),
675            json!("123456789012345678901234567890000")
676        );
677        assert!(number_text("abc").is_err());
678    }
679
680    #[test]
681    fn floats_keep_their_value_and_sign() {
682        assert_eq!(float64(1.5), json!(1.5));
683        assert_eq!(float64(-0.0).to_string(), "-0.0");
684        assert_eq!(float32(0.1), json!(0.1));
685        assert_eq!(float32(-0.0).to_string(), "-0.0");
686        assert_eq!(float32(3.0e38), json!(3.0e38));
687        assert_eq!(float32(16_777_217.0), json!(16_777_216.0));
688    }
689
690    #[tokio::test]
691    async fn non_finite_values_become_counted_nulls() {
692        let counter = NonFiniteCounter::new();
693        let seen = counter
694            .scope(async {
695                assert!(NonFiniteCounter::current().is_some());
696                assert_eq!(float64(f64::NAN), Value::Null);
697                assert_eq!(float64(f64::INFINITY), Value::Null);
698                assert_eq!(float32(f32::NEG_INFINITY), Value::Null);
699                assert_eq!(decimal_text("NaN").unwrap(), Value::Null);
700                assert_eq!(number_text("-Infinity").unwrap(), Value::Null);
701                let inner = NonFiniteCounter::current();
702                std::thread::spawn(move || propagate(&inner, || float64(f64::NAN)))
703                    .join()
704                    .unwrap();
705                propagate(&None, || ());
706                NonFiniteCounter::current().unwrap().count()
707            })
708            .await;
709        assert_eq!(seen, 6);
710        assert_eq!(counter.count(), 6);
711        assert!(NonFiniteCounter::current().is_none());
712        let before = unscoped_non_finite();
713        assert_eq!(float64(f64::NAN), Value::Null);
714        assert!(unscoped_non_finite() > before);
715        assert_eq!(counter.scope_sync(|| 1), 1);
716    }
717
718    #[test]
719    fn non_integers_bind_as_their_shortest_text() {
720        let n = |v: Value| match v {
721            Value::Number(n) => n,
722            _ => unreachable!(),
723        };
724        assert_eq!(non_integer_text(&n(json!(0.1))), Some("0.1".into()));
725        assert_eq!(
726            non_integer_text(&n(json!(-1.5e300))),
727            Some("-1.5e+300".into())
728        );
729        assert_eq!(non_integer_text(&n(json!(7))), None);
730        assert_eq!(non_integer_text(&n(json!(u64::MAX))), None);
731    }
732
733    /// `NUMERIC` in the PostgreSQL binary wire format.
734    fn pg_numeric(weight: i16, sign: u16, dscale: u16, digits: &[u16]) -> Vec<u8> {
735        let mut b = Vec::new();
736        for w in [digits.len() as u16, weight as u16, sign, dscale] {
737            b.extend_from_slice(&w.to_be_bytes());
738        }
739        for d in digits {
740            b.extend_from_slice(&d.to_be_bytes());
741        }
742        b
743    }
744
745    #[test]
746    fn binary_pg_numerics_keep_their_scale() {
747        // 1.50 at scale 2.
748        assert_eq!(
749            decimal_from_pg_numeric(&pg_numeric(0, 0, 2, &[1, 5000])),
750            Some(json!("1.50"))
751        );
752        // 12345678901234567890.123456789 at scale 9.
753        assert_eq!(
754            decimal_from_pg_numeric(&pg_numeric(
755                4,
756                0,
757                9,
758                &[1234, 5678, 9012, 3456, 7890, 1234, 5678, 9000]
759            )),
760            Some(json!("12345678901234567890.123456789"))
761        );
762        // -0.0005 at scale 4: weight -1 skips the units group.
763        assert_eq!(
764            decimal_from_pg_numeric(&pg_numeric(-1, 0x4000, 4, &[5])),
765            Some(json!("-0.0005"))
766        );
767        // 0.00000001: weight -2.
768        assert_eq!(
769            decimal_from_pg_numeric(&pg_numeric(-2, 0, 8, &[1])),
770            Some(json!("0.00000001"))
771        );
772        // 20000 (trailing zero group dropped by the server), scale 0.
773        assert_eq!(
774            decimal_from_pg_numeric(&pg_numeric(1, 0, 0, &[2])),
775            Some(json!("20000"))
776        );
777        assert_eq!(
778            decimal_from_pg_numeric(&pg_numeric(0, 0, 0, &[])),
779            Some(json!("0"))
780        );
781        assert_eq!(
782            decimal_from_pg_numeric(&pg_numeric(0, 0xC000, 0, &[])),
783            Some(Value::Null)
784        );
785        assert_eq!(
786            decimal_from_pg_numeric(&pg_numeric(0, 0x1234, 0, &[])),
787            None
788        );
789        assert_eq!(
790            decimal_from_pg_numeric(&pg_numeric(0, 0, 0, &[10000])),
791            None
792        );
793        assert_eq!(decimal_from_pg_numeric(&[0, 1]), None);
794        let mut short = pg_numeric(0, 0, 0, &[1, 2]);
795        short.pop();
796        assert_eq!(decimal_from_pg_numeric(&short), None);
797    }
798
799    #[test]
800    fn exact_decimal_types_are_recognised() {
801        for t in [
802            "NUMERIC(38,9)",
803            "numeric",
804            "DECIMAL(10, 2)",
805            "money",
806            "NUMBER",
807            "dec(5)",
808            "BIGNUMERIC",
809        ] {
810            assert!(is_exact_decimal_type(t), "{t}");
811        }
812        for t in [
813            "double precision",
814            "real",
815            "bigint",
816            "text",
817            "numerical_thing",
818            "decimals",
819        ] {
820            assert!(!is_exact_decimal_type(t), "{t}");
821        }
822    }
823
824    #[test]
825    fn binary_is_standard_base64() {
826        assert_eq!(binary(&[0, 1, 2, 253, 254, 255]), json!("AAEC/f7/"));
827        assert_eq!(binary(b""), json!(""));
828    }
829
830    #[test]
831    fn instants_carry_an_offset_and_native_precision() {
832        let utc = DateTime::from_timestamp(1_758_198_896, 123_000_000).unwrap();
833        assert_eq!(
834            timestamp(&utc, TimePrecision::Micros),
835            json!("2025-09-18T12:34:56.123000Z")
836        );
837        assert_eq!(
838            timestamp(&utc, TimePrecision::Seconds),
839            json!("2025-09-18T12:34:56Z")
840        );
841        let ist = FixedOffset::east_opt(5 * 3600 + 1800).unwrap();
842        assert_eq!(
843            timestamp(&utc.with_timezone(&ist), TimePrecision::Millis),
844            json!("2025-09-18T18:04:56.123+05:30")
845        );
846        assert_eq!(
847            timestamp(&utc, TimePrecision::Nanos),
848            json!("2025-09-18T12:34:56.123000000Z")
849        );
850        assert_eq!(
851            timestamp_naive(&utc.naive_utc(), TimePrecision::Micros),
852            json!("2025-09-18T12:34:56.123000")
853        );
854        assert_eq!(
855            timestamp_naive(&utc.naive_utc(), TimePrecision::Seconds),
856            json!("2025-09-18T12:34:56")
857        );
858        assert_eq!(date(&utc.date_naive()), json!("2025-09-18"));
859        assert_eq!(
860            time(&utc.time(), TimePrecision::Nanos),
861            json!("12:34:56.123000000")
862        );
863    }
864
865    #[test]
866    fn epoch_values_honour_unit_and_zone() {
867        assert_eq!(
868            timestamp_from_epoch(-1, TimePrecision::Micros, None).unwrap(),
869            json!("1969-12-31T23:59:59.999999")
870        );
871        assert_eq!(
872            timestamp_from_epoch(
873                1_500,
874                TimePrecision::Millis,
875                Some(FixedOffset::east_opt(0).unwrap())
876            )
877            .unwrap(),
878            json!("1970-01-01T00:00:01.500Z")
879        );
880        assert_eq!(
881            timestamp_from_epoch(5, TimePrecision::Seconds, None).unwrap(),
882            json!("1970-01-01T00:00:05")
883        );
884        assert_eq!(
885            timestamp_from_epoch(1, TimePrecision::Nanos, None).unwrap(),
886            json!("1970-01-01T00:00:00.000000001")
887        );
888        let err = timestamp_from_epoch(i64::MAX, TimePrecision::Seconds, None).unwrap_err();
889        assert!(err.to_string().contains("outside"), "{err}");
890    }
891
892    #[test]
893    fn precision_from_declared_digits() {
894        assert_eq!(TimePrecision::from_digits(0), TimePrecision::Seconds);
895        assert_eq!(TimePrecision::from_digits(2), TimePrecision::Millis);
896        assert_eq!(TimePrecision::from_digits(6), TimePrecision::Micros);
897        assert_eq!(TimePrecision::from_digits(7), TimePrecision::Nanos);
898    }
899
900    #[test]
901    fn serialized_json_keeps_unfaithful_literals_as_strings() {
902        let v = from_str(
903            r#"{"id": 18446744073709551616, "amt": 0.12345678901234567890, "d": 0.10000000000000001, "s": "1234567890123456789", "n": [1, 2.5]}"#,
904        )
905        .unwrap();
906        assert_eq!(
907            v,
908            json!({"id": "18446744073709551616", "amt": "0.12345678901234567890", "d": 0.1, "s": "1234567890123456789", "n": [1, 2.5]})
909        );
910        assert_eq!(from_slice(b"[1]").unwrap(), json!([1]));
911        assert!(from_str("{").is_err());
912        assert!(from_slice(&[b'"', 0xff, b'"']).is_err());
913        assert_eq!(
914            from_str("1e99999").is_err(),
915            serde_json::from_str::<Value>("1e99999").is_err()
916        );
917    }
918
919    #[test]
920    fn json_text_parses_with_exact_numbers() {
921        let v = parse_json(br#"{"a": 12345678901234567890.5, "b": 1.5, "c": [7]}"#).unwrap();
922        assert_eq!(
923            v,
924            json!({"a": "12345678901234567890.5", "b": 1.5, "c": [7]})
925        );
926        assert!(parse_json(b"{").is_err());
927        assert!(parse_json(&[0xff]).is_err());
928    }
929}