fluidattacks-database-core-domain 0.1.0

Database core functional core: database-agnostic table identifiers and types (no I/O)
Documentation
use alloc::string::String;
use alloc::vec::Vec;
use core::fmt;

use chrono::{DateTime, NaiveDate, Utc};

/// Most digits a decimal holds.
pub const MAX_PRECISION: u8 = 38;

/// Most digits after the point a decimal holds.
pub const MAX_SCALE: u8 = 37;

#[must_use]
pub const fn valid_decimal(precision: u8, scale: u8) -> bool {
    precision >= 1 && precision <= MAX_PRECISION && scale <= MAX_SCALE && scale <= precision
}

const MAX_UNSCALED: u128 = 99_999_999_999_999_999_999_999_999_999_999_999_999;

#[derive(Debug, Clone, PartialEq, Eq)]
pub enum DecimalError {
    TooManyDigits { precision: u8 },
    ScaleTooLarge,
}

impl fmt::Display for DecimalError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::TooManyDigits { precision } => {
                write!(f, "decimal has more than {precision} digits")
            }
            Self::ScaleTooLarge => write!(f, "decimal scale is over {MAX_SCALE}"),
        }
    }
}

impl core::error::Error for DecimalError {}

/// An exact decimal: `unscaled * 10^-scale`, up to 38 digits. The `i128`
/// reaches the 38 digits common decimal crates do not.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Decimal {
    unscaled: i128,
    scale: u8,
}

impl Decimal {
    pub const fn new(unscaled: i128, scale: u8) -> Result<Self, DecimalError> {
        if unscaled.unsigned_abs() > MAX_UNSCALED {
            Err(DecimalError::TooManyDigits {
                precision: MAX_PRECISION,
            })
        } else if scale > MAX_SCALE {
            Err(DecimalError::ScaleTooLarge)
        } else {
            Ok(Self { unscaled, scale })
        }
    }

    #[must_use]
    pub const fn unscaled(&self) -> i128 {
        self.unscaled
    }

    #[must_use]
    pub const fn scale(&self) -> u8 {
        self.scale
    }
}

/// One cell as a row hands it over: not validated against a schema.
#[derive(Debug, Clone, PartialEq)]
pub enum RawValue {
    Null,
    Text(String),
    Integer(i64),
    Decimal(Decimal),
    Float(f64),
    Boolean(bool),
    Date(NaiveDate),
    Timestamp(DateTime<Utc>),
    Array(Vec<Self>),
    Json(serde_json::Value),
}

#[cfg(test)]
mod tests {
    use super::{Decimal, DecimalError};

    const MAX: i128 = 99_999_999_999_999_999_999_999_999_999_999_999_999;

    #[test]
    fn decimal_keeps_38_digits_either_sign() {
        assert_eq!(Decimal::new(MAX, 10).map(|d| d.unscaled()), Ok(MAX));
        assert_eq!(Decimal::new(-MAX, 0).map(|d| d.unscaled()), Ok(-MAX));
    }

    #[test]
    fn decimal_rejects_39_digits() {
        assert_eq!(
            Decimal::new(100_000_000_000_000_000_000_000_000_000_000_000_000, 0),
            Err(DecimalError::TooManyDigits { precision: 38 })
        );
        assert_eq!(
            Decimal::new(-100_000_000_000_000_000_000_000_000_000_000_000_000, 0),
            Err(DecimalError::TooManyDigits { precision: 38 })
        );
        assert_eq!(
            Decimal::new(i128::MIN, 0),
            Err(DecimalError::TooManyDigits { precision: 38 })
        );
    }

    #[test]
    fn decimal_rejects_scale_over_37() {
        assert_eq!(Decimal::new(1, 37).map(|d| d.scale()), Ok(37));
        assert_eq!(Decimal::new(1, 38), Err(DecimalError::ScaleTooLarge));
    }
}