use core::fmt;
use vitaminc_protected::{Choice, OpaqueDebug, TimingSafeEq};
use zeroize::Zeroize;
#[derive(Clone, Copy, OpaqueDebug, TimingSafeEq)]
pub enum Physical<'a> {
Null,
Bool(bool),
Int(Int),
Float(Float),
Text(&'a str),
Bytes(&'a [u8]),
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Kind {
Null,
Bool,
Int,
Float,
Text,
Bytes,
}
impl Kind {
pub const fn to_wire(self) -> u8 {
match self {
Kind::Null => 0,
Kind::Bool => 1,
Kind::Int => 2,
Kind::Float => 3,
Kind::Text => 4,
Kind::Bytes => 5,
}
}
}
impl Physical<'_> {
pub fn kind(&self) -> Kind {
match self {
Physical::Null => Kind::Null,
Physical::Bool(_) => Kind::Bool,
Physical::Int(_) => Kind::Int,
Physical::Float(_) => Kind::Float,
Physical::Text(_) => Kind::Text,
Physical::Bytes(_) => Kind::Bytes,
}
}
}
#[derive(Clone, Copy, OpaqueDebug, Eq, Zeroize)]
pub enum Int {
I8(i8),
I16(i16),
I32(i32),
I64(i64),
I128(i128),
U8(u8),
U16(u16),
U32(u32),
U64(u64),
U128(u128),
}
impl Int {
pub fn to_i128(self) -> Option<i128> {
match self {
Int::I8(v) => Some(v.into()),
Int::I16(v) => Some(v.into()),
Int::I32(v) => Some(v.into()),
Int::I64(v) => Some(v.into()),
Int::I128(v) => Some(v),
Int::U8(v) => Some(v.into()),
Int::U16(v) => Some(v.into()),
Int::U32(v) => Some(v.into()),
Int::U64(v) => Some(v.into()),
Int::U128(v) => i128::try_from(v).ok(),
}
}
pub fn to_u128(self) -> Option<u128> {
match self {
Int::U128(v) => Some(v),
other => other.to_i128().and_then(|v| u128::try_from(v).ok()),
}
}
pub fn sign_and_word(self) -> (bool, u128) {
let (sign, word) = self.sign_bit_and_word();
(sign == 1, word)
}
fn sign_bit_and_word(self) -> (u8, u128) {
let (word, signed) = match self {
Int::I8(v) => (i128::from(v) as u128, 1),
Int::I16(v) => (i128::from(v) as u128, 1),
Int::I32(v) => (i128::from(v) as u128, 1),
Int::I64(v) => (i128::from(v) as u128, 1),
Int::I128(v) => (v as u128, 1),
Int::U8(v) => (u128::from(v), 0),
Int::U16(v) => (u128::from(v), 0),
Int::U32(v) => (u128::from(v), 0),
Int::U64(v) => (u128::from(v), 0),
Int::U128(v) => (v, 0),
};
(((word >> 127) as u8) & signed, word)
}
}
macro_rules! int_from {
($($ty:ty => $variant:ident),* $(,)?) => {
$(
impl From<$ty> for Int {
fn from(v: $ty) -> Self {
Int::$variant(v)
}
}
)*
};
}
int_from! {
i8 => I8, i16 => I16, i32 => I32, i64 => I64, i128 => I128,
u8 => U8, u16 => U16, u32 => U32, u64 => U64, u128 => U128,
}
#[derive(Clone, Copy, OpaqueDebug, TimingSafeEq, Zeroize)]
pub enum Float {
F32(f32),
F64(f64),
}
impl TimingSafeEq for Int {
fn ts_eq(&self, other: &Self) -> Choice {
let (sign_a, word_a) = self.sign_bit_and_word();
let (sign_b, word_b) = other.sign_bit_and_word();
let same_width = core::mem::discriminant(self) == core::mem::discriminant(other);
Choice::from(u8::from(same_width)) & sign_a.ts_eq(&sign_b) & word_a.ts_eq(&word_b)
}
}
impl PartialEq for Int {
fn eq(&self, other: &Self) -> bool {
self.ts_eq(other).into()
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub enum Logical {
Undefined,
Date,
Timestamp(TimeUnit),
Decimal { scale: u8 },
Uuid,
Unknown(UnknownLogical),
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum TimeUnit {
Seconds,
Millis,
Micros,
Nanos,
}
impl TimeUnit {
pub fn nanos(self) -> i128 {
match self {
TimeUnit::Seconds => 1_000_000_000,
TimeUnit::Millis => 1_000_000,
TimeUnit::Micros => 1_000,
TimeUnit::Nanos => 1,
}
}
fn from_wire(param: u8) -> Option<Self> {
[
TimeUnit::Seconds,
TimeUnit::Millis,
TimeUnit::Micros,
TimeUnit::Nanos,
]
.get(usize::from(param))
.copied()
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct UnknownLogical {
id: u8,
param: u8,
}
impl UnknownLogical {
pub fn id(self) -> u8 {
self.id
}
pub fn param(self) -> u8 {
self.param
}
}
pub const MAX_DECIMAL_SCALE: u8 = 28;
const DECIMAL_MANTISSA_LIMIT: i128 = 1 << 96;
pub const MIN_DATE_DAYS: i32 = -96_465_292;
pub const MAX_DATE_DAYS: i32 = 95_026_236;
pub const MIN_TIMESTAMP_NANOS: i128 = MIN_DATE_DAYS as i128 * NANOS_PER_DAY;
pub const MAX_TIMESTAMP_NANOS: i128 = (MAX_DATE_DAYS as i128 + 1) * NANOS_PER_DAY - 1;
const NANOS_PER_DAY: i128 = 86_400 * 1_000_000_000;
pub mod id {
pub const NONE: u8 = 0;
pub const UNDEFINED: u8 = 1;
pub const DATE: u8 = 2;
pub const TIMESTAMP: u8 = 3;
pub const DECIMAL: u8 = 4;
pub const UUID: u8 = 5;
pub const LAST_KNOWN: u8 = UUID;
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub enum KnownLogical {
Undefined,
Date,
Timestamp,
Decimal,
Uuid,
}
impl KnownLogical {
pub const fn id(self) -> u8 {
match self {
KnownLogical::Undefined => id::UNDEFINED,
KnownLogical::Date => id::DATE,
KnownLogical::Timestamp => id::TIMESTAMP,
KnownLogical::Decimal => id::DECIMAL,
KnownLogical::Uuid => id::UUID,
}
}
pub const fn kind(self) -> Kind {
match self {
KnownLogical::Undefined => Kind::Null,
KnownLogical::Date | KnownLogical::Timestamp | KnownLogical::Decimal => Kind::Int,
KnownLogical::Uuid => Kind::Bytes,
}
}
pub const fn from_id(id: u8) -> Option<Self> {
match id {
id::UNDEFINED => Some(KnownLogical::Undefined),
id::DATE => Some(KnownLogical::Date),
id::TIMESTAMP => Some(KnownLogical::Timestamp),
id::DECIMAL => Some(KnownLogical::Decimal),
id::UUID => Some(KnownLogical::Uuid),
_ => None,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct UnknownLogicalId(u8);
impl UnknownLogicalId {
pub const fn new(id: u8) -> Option<Self> {
if id > id::LAST_KNOWN {
Some(Self(id))
} else {
None
}
}
pub const fn get(self) -> u8 {
self.0
}
}
impl Logical {
pub fn to_wire(self) -> (u8, u8) {
match self {
Logical::Undefined => (id::UNDEFINED, 0),
Logical::Date => (id::DATE, 0),
Logical::Timestamp(unit) => (id::TIMESTAMP, unit as u8),
Logical::Decimal { scale } => (id::DECIMAL, scale),
Logical::Uuid => (id::UUID, 0),
Logical::Unknown(u) => (u.id, u.param),
}
}
pub fn from_wire(id: u8, param: u8) -> Option<Self> {
match (id, param) {
(id::NONE, _) => None,
(id::UNDEFINED, 0) => Some(Logical::Undefined),
(id::DATE, 0) => Some(Logical::Date),
(id::TIMESTAMP, p) => TimeUnit::from_wire(p).map(Logical::Timestamp),
(id::DECIMAL, scale) => {
(scale <= MAX_DECIMAL_SCALE).then_some(Logical::Decimal { scale })
}
(id::UUID, 0) => Some(Logical::Uuid),
(id::UNDEFINED..=id::LAST_KNOWN, _) => None,
(id, param) => Some(Logical::Unknown(UnknownLogical { id, param })),
}
}
pub fn admits(self, v: &Physical<'_>) -> bool {
match (self, *v) {
(Logical::Undefined, Physical::Null) => true,
(Logical::Date, Physical::Int(i)) => date_in_range(i),
(Logical::Timestamp(unit), Physical::Int(i)) => timestamp_nanos(unit, i).is_some(),
(Logical::Decimal { scale }, Physical::Int(i)) => decimal_in_range(scale, i),
(Logical::Uuid, Physical::Bytes(b)) => b.len() == 16,
(Logical::Unknown(_), _) => true,
_ => false,
}
}
}
fn date_in_range(days: Int) -> bool {
days.to_i128()
.and_then(|d| i32::try_from(d).ok())
.is_some_and(|d| (MIN_DATE_DAYS..=MAX_DATE_DAYS).contains(&d))
}
pub fn timestamp_nanos(unit: TimeUnit, count: Int) -> Option<i128> {
let nanos = count.to_i128()?.checked_mul(unit.nanos())?;
(MIN_TIMESTAMP_NANOS..=MAX_TIMESTAMP_NANOS)
.contains(&nanos)
.then_some(nanos)
}
fn decimal_in_range(scale: u8, mantissa: Int) -> bool {
scale <= MAX_DECIMAL_SCALE
&& mantissa
.to_i128()
.is_some_and(|m| m > -DECIMAL_MANTISSA_LIMIT && m < DECIMAL_MANTISSA_LIMIT)
}
#[cfg(feature = "chrono")]
pub mod chrono {
use super::{Int, TimeUnit};
use ::chrono::{DateTime, Datelike, NaiveDate, Utc};
const EPOCH_DAYS_FROM_CE: i32 = 719_163;
pub fn date(days: Int) -> Option<NaiveDate> {
let days = i32::try_from(days.to_i128()?).ok()?;
NaiveDate::from_num_days_from_ce_opt(days.checked_add(EPOCH_DAYS_FROM_CE)?)
}
pub fn date_days(date: NaiveDate) -> i32 {
date.num_days_from_ce() - EPOCH_DAYS_FROM_CE
}
pub fn timestamp(unit: TimeUnit, count: Int) -> Option<DateTime<Utc>> {
let nanos = super::timestamp_nanos(unit, count)?;
let seconds = i64::try_from(nanos.div_euclid(1_000_000_000)).ok()?;
DateTime::from_timestamp(seconds, nanos.rem_euclid(1_000_000_000) as u32)
}
pub fn timestamp_nanos(ts: DateTime<Utc>) -> Option<i128> {
let sub = ts.timestamp_subsec_nanos();
(sub < 1_000_000_000).then(|| i128::from(ts.timestamp()) * 1_000_000_000 + i128::from(sub))
}
pub fn timestamp_count(unit: TimeUnit, nanos: i128) -> Option<i128> {
(nanos % unit.nanos() == 0).then(|| nanos / unit.nanos())
}
#[cfg(test)]
mod tests {
use super::super::{
MAX_DATE_DAYS, MAX_TIMESTAMP_NANOS, MIN_DATE_DAYS, MIN_TIMESTAMP_NANOS,
};
use super::*;
#[test]
fn range_constants_match_chrono() {
assert_eq!(date_days(NaiveDate::MIN), MIN_DATE_DAYS);
assert_eq!(date_days(NaiveDate::MAX), MAX_DATE_DAYS);
assert_eq!(
timestamp_nanos(::chrono::NaiveDateTime::MIN.and_utc()),
Some(MIN_TIMESTAMP_NANOS)
);
assert_eq!(
timestamp_nanos(::chrono::NaiveDateTime::MAX.and_utc()),
Some(MAX_TIMESTAMP_NANOS)
);
assert_eq!(
date_days(NaiveDate::from_ymd_opt(1970, 1, 1).expect("date")),
0
);
assert_eq!(date(Int::I32(0)), NaiveDate::from_ymd_opt(1970, 1, 1));
}
#[test]
fn out_of_range_counts_have_no_chrono_value() {
assert!(date(Int::I32(MIN_DATE_DAYS - 1)).is_none());
assert!(date(Int::I32(MAX_DATE_DAYS + 1)).is_none());
assert!(date(Int::I64(i64::MAX)).is_none());
assert!(timestamp(TimeUnit::Nanos, Int::I128(MAX_TIMESTAMP_NANOS + 1)).is_none());
assert!(timestamp(TimeUnit::Seconds, Int::I64(i64::MAX)).is_none());
assert!(timestamp(TimeUnit::Nanos, Int::I128(MAX_TIMESTAMP_NANOS)).is_some());
}
#[test]
fn leap_second_has_no_count() {
let leap = NaiveDate::from_ymd_opt(2016, 12, 31)
.and_then(|d| d.and_hms_nano_opt(23, 59, 59, 1_500_000_000))
.expect("leap second")
.and_utc();
assert!(timestamp_nanos(leap).is_none());
let first_leap_nano = NaiveDate::from_ymd_opt(2016, 12, 31)
.and_then(|d| d.and_hms_nano_opt(23, 59, 59, 1_000_000_000))
.expect("leap second")
.and_utc();
assert!(timestamp_nanos(first_leap_nano).is_none());
let last_ordinary_nano = NaiveDate::from_ymd_opt(2016, 12, 31)
.and_then(|d| d.and_hms_nano_opt(23, 59, 59, 999_999_999))
.expect("instant")
.and_utc();
assert_eq!(
timestamp_nanos(last_ordinary_nano),
Some(1_483_228_799_999_999_999)
);
}
#[test]
fn a_count_before_the_epoch_is_the_right_instant() {
let one_ns_before = timestamp(TimeUnit::Nanos, Int::I128(-1)).expect("instant");
assert_eq!(
one_ns_before,
DateTime::from_timestamp(-1, 999_999_999).expect("instant")
);
assert_eq!(timestamp_nanos(one_ns_before), Some(-1));
let ms = timestamp(TimeUnit::Millis, Int::I64(-1_500)).expect("instant");
assert_eq!(
ms,
DateTime::from_timestamp(-2, 500_000_000).expect("instant")
);
assert_eq!(timestamp_nanos(ms), Some(-1_500_000_000));
}
#[test]
fn a_count_is_reduced_only_when_exact() {
assert_eq!(timestamp_count(TimeUnit::Nanos, 7), Some(7));
assert_eq!(timestamp_count(TimeUnit::Millis, 3_000_000), Some(3));
assert_eq!(timestamp_count(TimeUnit::Millis, -3_000_000), Some(-3));
assert_eq!(timestamp_count(TimeUnit::Seconds, 2_000_000_000), Some(2));
assert_eq!(timestamp_count(TimeUnit::Millis, 3_000_001), None);
assert_eq!(timestamp_count(TimeUnit::Seconds, 1_500_000_000), None);
}
}
}
#[cfg(feature = "rust_decimal")]
pub mod decimal {
use super::Int;
use rust_decimal::Decimal;
pub fn decimal(scale: u8, mantissa: Int) -> Option<Decimal> {
Decimal::try_from_i128_with_scale(mantissa.to_i128()?, scale.into()).ok()
}
pub fn parts(d: Decimal) -> (u8, i128) {
(d.scale() as u8, d.mantissa())
}
#[cfg(test)]
mod tests {
use super::*;
use core::str::FromStr;
#[test]
fn negative_zero_normalizes_to_zero_with_its_scale() {
let mut neg = Decimal::new(0, 2);
neg.set_sign_negative(true);
assert!(neg.is_sign_negative());
assert_eq!(parts(neg), (2, 0));
let back = decimal(2, Int::I128(0)).expect("decimal");
assert!(!back.is_sign_negative());
assert_eq!(back.to_string(), "0.00");
}
#[test]
fn scale_and_mantissa_round_trip() {
let d = Decimal::from_str("-1234.5678").expect("decimal");
let (scale, mantissa) = parts(d);
assert_eq!((scale, mantissa), (4, -12_345_678));
assert_eq!(decimal(scale, Int::I128(mantissa)), Some(d));
}
#[test]
fn what_rust_decimal_cannot_hold_has_no_decimal() {
assert_eq!(decimal(28, Int::I128(1)).map(|d| d.scale()), Some(28));
assert_eq!(decimal(29, Int::I128(1)), None);
assert_eq!(decimal(0, Int::I128(1 << 96)), None);
assert_eq!(decimal(0, Int::I128(-(1 << 96))), None);
assert_eq!(decimal(0, Int::U128(u128::MAX)), None);
}
}
}
impl fmt::Display for Logical {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Logical::Undefined => f.write_str("undefined"),
Logical::Date => f.write_str("date"),
Logical::Timestamp(unit) => {
let unit = match unit {
TimeUnit::Seconds => "s",
TimeUnit::Millis => "ms",
TimeUnit::Micros => "us",
TimeUnit::Nanos => "ns",
};
write!(f, "timestamp({unit})")
}
Logical::Decimal { scale } => write!(f, "decimal({scale})"),
Logical::Uuid => f.write_str("uuid"),
Logical::Unknown(u) => write!(f, "logical({}, {})", u.id, u.param),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn every_known_logical_type_round_trips_through_the_wire() {
let known = [
Logical::Undefined,
Logical::Date,
Logical::Timestamp(TimeUnit::Seconds),
Logical::Timestamp(TimeUnit::Millis),
Logical::Timestamp(TimeUnit::Micros),
Logical::Timestamp(TimeUnit::Nanos),
Logical::Decimal { scale: 0 },
Logical::Decimal { scale: 28 },
Logical::Uuid,
];
for ty in known {
let (id, param) = ty.to_wire();
assert_eq!(Logical::from_wire(id, param), Some(ty), "{ty}");
}
}
#[test]
fn id_zero_is_no_logical_type() {
assert_eq!(Logical::from_wire(0, 0), None);
assert_eq!(Logical::from_wire(0, 7), None);
}
#[test]
fn a_known_id_with_a_bad_parameter_is_refused() {
assert_eq!(Logical::from_wire(1, 1), None);
assert_eq!(Logical::from_wire(2, 1), None);
assert_eq!(Logical::from_wire(3, 4), None);
assert_eq!(Logical::from_wire(4, 29), None);
assert_eq!(Logical::from_wire(5, 1), None);
}
#[test]
fn an_unknown_id_is_carried_through() {
let ty = Logical::from_wire(200, 9).expect("unknown");
assert!(matches!(ty, Logical::Unknown(u) if u.id() == 200 && u.param() == 9));
assert_eq!(ty.to_wire(), (200, 9));
assert!(ty.admits(&Physical::Text("anything")));
}
fn known(logical: Logical) -> Option<KnownLogical> {
match logical {
Logical::Undefined => Some(KnownLogical::Undefined),
Logical::Date => Some(KnownLogical::Date),
Logical::Timestamp(_) => Some(KnownLogical::Timestamp),
Logical::Decimal { .. } => Some(KnownLogical::Decimal),
Logical::Uuid => Some(KnownLogical::Uuid),
Logical::Unknown(_) => None,
}
}
#[test]
fn known_logical_agrees_with_logical_on_every_id() {
for id in 0..=u8::MAX {
let known_id = KnownLogical::from_id(id);
assert_eq!(
known_id.is_some(),
(id::UNDEFINED..=id::LAST_KNOWN).contains(&id),
"id {id}"
);
assert_eq!(known_id.map(KnownLogical::id), known_id.map(|_| id));
for param in 0..=u8::MAX {
if let Some(logical) = Logical::from_wire(id, param) {
assert_eq!(known(logical), known_id, "{logical:?}");
}
}
let unknown = UnknownLogicalId::new(id);
assert_eq!(unknown.is_some(), id > id::LAST_KNOWN, "id {id}");
assert_eq!(unknown.map(UnknownLogicalId::get), unknown.map(|_| id));
}
assert_eq!(known(Logical::from_wire(200, 0).expect("unknown")), None);
}
#[test]
fn kind_is_the_kind_admits_accepts() {
let samples = [
Physical::Null,
Physical::Bool(true),
Physical::Int(Int::I64(0)),
Physical::Float(Float::F64(0.0)),
Physical::Text(""),
Physical::Bytes(&[0; 16]),
];
let logicals = [
Logical::Undefined,
Logical::Date,
Logical::Timestamp(TimeUnit::Millis),
Logical::Decimal { scale: 2 },
Logical::Uuid,
];
for logical in logicals {
for sample in &samples {
let kind = known(logical).expect("known").kind();
assert_eq!(
logical.admits(sample),
kind == sample.kind(),
"{logical:?} over {:?}",
sample.kind()
);
}
}
}
#[test]
fn admits_checks_the_physical_kind() {
assert!(Logical::Undefined.admits(&Physical::Null));
assert!(!Logical::Undefined.admits(&Physical::Bool(false)));
assert!(Logical::Date.admits(&Physical::Int(Int::I32(0))));
assert!(!Logical::Date.admits(&Physical::Text("2024-01-01")));
assert!(Logical::Uuid.admits(&Physical::Bytes(&[0; 16])));
assert!(!Logical::Uuid.admits(&Physical::Bytes(&[0; 15])));
assert!(!Logical::Uuid.admits(&Physical::Text("")));
}
#[test]
fn admits_checks_the_range() {
assert!(Logical::Date.admits(&Physical::Int(Int::I64(MIN_DATE_DAYS.into()))));
assert!(!Logical::Date.admits(&Physical::Int(Int::I64(i64::from(MIN_DATE_DAYS) - 1))));
assert!(!Logical::Date.admits(&Physical::Int(Int::U128(u128::MAX))));
let ns = Logical::Timestamp(TimeUnit::Nanos);
assert!(ns.admits(&Physical::Int(Int::I128(MAX_TIMESTAMP_NANOS))));
assert!(!ns.admits(&Physical::Int(Int::I128(MAX_TIMESTAMP_NANOS + 1))));
assert!(!Logical::Timestamp(TimeUnit::Seconds).admits(&Physical::Int(Int::I128(i128::MAX))));
assert!(Logical::Decimal { scale: 28 }.admits(&Physical::Int(Int::I128((1 << 96) - 1))));
assert!(!Logical::Decimal { scale: 28 }.admits(&Physical::Int(Int::I128(1 << 96))));
assert!(!Logical::Decimal { scale: 29 }.admits(&Physical::Int(Int::I8(1))));
assert!(Logical::Date.admits(&Physical::Int(Int::I32(MAX_DATE_DAYS))));
assert!(!Logical::Date.admits(&Physical::Int(Int::I64(i64::from(MAX_DATE_DAYS) + 1))));
assert!(ns.admits(&Physical::Int(Int::I128(MIN_TIMESTAMP_NANOS))));
assert!(!ns.admits(&Physical::Int(Int::I128(MIN_TIMESTAMP_NANOS - 1))));
}
#[test]
fn floats_compare_by_bit_pattern() {
assert_ne!(Float::F64(-0.0), Float::F64(0.0));
assert_eq!(Float::F64(f64::NAN), Float::F64(f64::NAN));
assert_eq!(Float::F32(1.5), Float::F32(1.5));
assert_ne!(Float::F32(-0.0), Float::F32(0.0));
assert_ne!(Float::F32(1.0), Float::F64(1.0));
assert_ne!(Physical::Int(Int::I8(34)), Physical::Int(Int::U64(34)));
}
#[test]
fn ints_compare_by_width_sign_and_value() {
assert_eq!(Int::I8(-1), Int::I8(-1));
assert_ne!(Int::I8(-1), Int::I8(1));
assert_ne!(Int::I8(-1), Int::I128(-1));
assert_ne!(Int::I8(-1), Int::U8(255));
assert_eq!(Int::U128(u128::MAX), Int::U128(u128::MAX));
assert_ne!(Int::U128(u128::MAX), Int::I128(-1));
}
#[test]
fn every_physical_kind_compares_to_itself_and_nothing_else() {
let kinds = [
Physical::Null,
Physical::Bool(true),
Physical::Int(Int::I32(1)),
Physical::Float(Float::F64(1.0)),
Physical::Text("a"),
Physical::Bytes(b"a"),
];
for (i, a) in kinds.iter().enumerate() {
for (j, b) in kinds.iter().enumerate() {
assert_eq!(a == b, i == j, "{a:?} {b:?}");
}
}
assert_ne!(Physical::Bool(true), Physical::Bool(false));
assert_ne!(Physical::Text("a"), Physical::Text("b"));
assert_ne!(Physical::Text("a"), Physical::Text("ab"));
assert_ne!(Physical::Bytes(b"a"), Physical::Bytes(b"b"));
assert_ne!(Physical::Bytes(b"a"), Physical::Bytes(b"ab"));
}
#[test]
fn debug_output_is_redacted() {
for v in [
format!("{:?}", Physical::Text("secret")),
format!("{:?}", Physical::Bytes(b"secret")),
format!("{:?}", Physical::Int(Int::I64(1_234_567))),
format!("{:?}", Physical::Float(Float::F64(1_234_567.0))),
format!("{:?}", Physical::Bool(true)),
format!("{:?}", Int::U8(123)),
] {
assert!(
!v.contains("secret")
&& !v.contains("1234567")
&& !v.contains("true")
&& !v.contains("123"),
"{v}"
);
assert!(v.contains("***"), "{v}");
}
assert!(format!("{:?}", crate::plaintext::Bytes(b"secret")).contains("***"));
assert!(!format!("{:?}", crate::plaintext::Bytes(b"secret")).contains("secret"));
let raw = format!(
"{:?}",
crate::plaintext::Raw(None, Physical::Text("secret"))
);
assert!(raw.contains("***") && !raw.contains("secret"), "{raw}");
let null = format!("{:?}", Physical::Null);
assert!(null.ends_with("::Null"), "{null}");
}
#[test]
fn ints_and_floats_zeroize_in_place() {
let mut i = Int::I64(7);
i.zeroize();
assert_eq!(i, Int::I64(0));
let mut f = Float::F32(1.5);
f.zeroize();
assert_eq!(f, Float::F32(0.0));
}
#[test]
fn a_javascript_date_beyond_chrono_is_refused() {
let millis = Logical::Timestamp(TimeUnit::Millis);
assert!(!millis.admits(&Physical::Int(Int::I64(8_640_000_000_000_000))));
assert!(millis.admits(&Physical::Int(Int::I64(8_000_000_000_000_000))));
}
#[test]
fn int_conversions() {
assert_eq!(Int::U128(u128::MAX).to_i128(), None);
assert_eq!(Int::I8(-1).to_u128(), None);
assert_eq!(Int::U128(u128::MAX).to_u128(), Some(u128::MAX));
assert_eq!(Int::I8(5).to_u128(), Some(5));
assert_eq!(Int::I8(-1).sign_and_word(), (true, u128::MAX));
assert_eq!(Int::U128(u128::MAX).sign_and_word(), (false, u128::MAX));
assert_eq!(Int::U64(34).sign_and_word(), (false, 34));
assert_eq!(Int::I8(0).sign_and_word(), (false, 0));
}
#[test]
fn every_width_has_one_sign_and_word() {
for v in [
Int::I8(-1),
Int::I16(-1),
Int::I32(-1),
Int::I64(-1),
Int::I128(-1),
] {
assert_eq!(v.sign_and_word(), (true, u128::MAX));
}
let unsigned = [
(Int::U8(u8::MAX), u128::from(u8::MAX)),
(Int::U16(u16::MAX), u128::from(u16::MAX)),
(Int::U32(u32::MAX), u128::from(u32::MAX)),
(Int::U64(u64::MAX), u128::from(u64::MAX)),
(Int::U128(u128::MAX), u128::MAX),
];
for (v, word) in unsigned {
assert_eq!(v.sign_and_word(), (false, word));
}
assert_eq!(Int::I128(i128::MIN).sign_and_word(), (true, 1 << 127));
assert_eq!(Int::U128(1 << 127).sign_and_word(), (false, 1 << 127));
assert_eq!(
Int::I64(i64::MAX).sign_and_word(),
(false, i64::MAX as u128)
);
assert_ne!(Int::I128(i128::MIN), Int::I128(i128::MAX));
}
#[test]
fn a_decimal_mantissa_is_bounded_on_both_sides() {
let ty = Logical::Decimal { scale: 0 };
assert!(ty.admits(&Physical::Int(Int::I128(1 - (1 << 96)))));
assert!(!ty.admits(&Physical::Int(Int::I128(-(1 << 96)))));
}
#[test]
fn logical_displays_its_type() {
let logical = [
(Logical::Undefined, "undefined"),
(Logical::Date, "date"),
(Logical::Timestamp(TimeUnit::Seconds), "timestamp(s)"),
(Logical::Timestamp(TimeUnit::Millis), "timestamp(ms)"),
(Logical::Timestamp(TimeUnit::Micros), "timestamp(us)"),
(Logical::Timestamp(TimeUnit::Nanos), "timestamp(ns)"),
(Logical::Decimal { scale: 4 }, "decimal(4)"),
(Logical::Uuid, "uuid"),
(
Logical::from_wire(200, 9).expect("unknown"),
"logical(200, 9)",
),
];
for (v, s) in logical {
assert_eq!(v.to_string(), s);
}
}
#[test]
fn every_physical_value_names_its_kind_and_kinds_have_fixed_wire_bytes() {
let pairs = [
(Physical::Null, Kind::Null, 0),
(Physical::Bool(true), Kind::Bool, 1),
(Physical::Int(Int::I8(1)), Kind::Int, 2),
(Physical::Float(Float::F32(1.0)), Kind::Float, 3),
(Physical::Text("a"), Kind::Text, 4),
(Physical::Bytes(&[1]), Kind::Bytes, 5),
];
for (value, kind, wire) in pairs {
assert_eq!(value.kind(), kind);
assert_eq!(kind.to_wire(), wire, "{kind:?}");
}
}
}