use super::codecs::{
decode_binary_double, decode_binary_float, decode_datetime_value, decode_interval_ds,
decode_interval_ym, decode_number_value, decode_text_value, encode_binary_double,
encode_binary_float, encode_interval_ds, encode_interval_ym, encode_number_text,
encode_oracle_date, encode_oracle_timestamp, encode_oracle_timestamp_tz,
encode_oracle_timestamp_tz_with_offset,
};
use super::constants::CS_FORM_NCHAR;
use super::types::QueryValue;
use proptest::prelude::*;
const CASES: u32 = 2_048;
fn config() -> ProptestConfig {
ProptestConfig {
cases: CASES,
..ProptestConfig::default()
}
}
fn number_key(text: &str) -> (bool, String, String) {
let (neg, rest) = match text.strip_prefix('-') {
Some(r) => (true, r),
None => (false, text),
};
let (int_part, frac_part) = match rest.split_once('.') {
Some((i, f)) => (i, f),
None => (rest, ""),
};
let int_norm = int_part.trim_start_matches('0').to_string();
let frac_norm = frac_part.trim_end_matches('0').to_string();
let is_zero = int_norm.is_empty() && frac_norm.is_empty();
(if is_zero { false } else { neg }, int_norm, frac_norm)
}
fn number_text_strategy() -> impl Strategy<Value = String> {
(
any::<bool>(), "[0-9]{1,38}", 0usize..=37, -120i32..=120i32, )
.prop_map(|(neg, digits, dp, exp)| {
let dp = dp.min(digits.len());
let (int_part, frac_part) = digits.split_at(dp);
let int_part = if int_part.is_empty() { "0" } else { int_part };
let mut s = String::new();
if neg {
s.push('-');
}
s.push_str(int_part);
if !frac_part.is_empty() {
s.push('.');
s.push_str(frac_part);
}
s.push('e');
s.push_str(&exp.to_string());
s
})
}
proptest! {
#![proptest_config(config())]
#[test]
fn number_round_trip_value_preserving(text in number_text_strategy()) {
let Ok(wire) = encode_number_text(&text) else { return Ok(()); };
let decoded = decode_number_value(&wire).expect("decode our own encoding");
let QueryValue::Number(num) = decoded else {
panic!("decode_number_value returned a non-Number variant");
};
let out = num.to_canonical_string();
prop_assert_eq!(
number_key(&out),
number_key(&normalize_input(&text)),
"input {} -> wire {:02x?} -> {}",
text, wire, out
);
}
}
fn normalize_input(text: &str) -> String {
let (neg, rest) = match text.strip_prefix('-') {
Some(r) => (true, r),
None => (false, text),
};
let (mantissa, exp) = match rest.split_once(['e', 'E']) {
Some((m, e)) => (m, e.parse::<i32>().unwrap_or(0)),
None => (rest, 0),
};
let (int_part, frac_part) = match mantissa.split_once('.') {
Some((i, f)) => (i.to_string(), f.to_string()),
None => (mantissa.to_string(), String::new()),
};
let mut digits: Vec<u8> = int_part.bytes().chain(frac_part.bytes()).collect();
let mut point = int_part.len() as i64 + exp as i64;
while digits.first() == Some(&b'0') {
digits.remove(0);
point -= 1;
}
while digits.last() == Some(&b'0') {
digits.pop();
}
if digits.is_empty() {
return "0".to_string();
}
let mut out = String::new();
if neg {
out.push('-');
}
if point <= 0 {
out.push_str("0.");
for _ in 0..(-point) {
out.push('0');
}
out.extend(digits.iter().map(|b| *b as char));
} else if (point as usize) >= digits.len() {
out.extend(digits.iter().map(|b| *b as char));
for _ in 0..(point as usize - digits.len()) {
out.push('0');
}
} else {
let (i, f) = digits.split_at(point as usize);
out.extend(i.iter().map(|b| *b as char));
out.push('.');
out.extend(f.iter().map(|b| *b as char));
}
out
}
proptest! {
#![proptest_config(config())]
#[test]
fn number_round_trip_i64(value: i64) {
let wire = encode_number_text(&value.to_string()).expect("i64 always encodable");
let decoded = decode_number_value(&wire).expect("decode i64 number");
prop_assert_eq!(decoded.as_i64(), Some(value));
}
#[test]
fn number_round_trip_u64(value: u64) {
let wire = encode_number_text(&value.to_string()).expect("u64 always encodable");
let decoded = decode_number_value(&wire).expect("decode u64 number");
let QueryValue::Number(num) = decoded else { panic!("not a Number") };
prop_assert_eq!(num.to_canonical_string().parse::<u64>().ok(), Some(value));
}
#[test]
fn number_encoding_is_order_preserving(a: i32, b: i32) {
prop_assume!(a != b);
let (lo, hi) = if a < b { (a, b) } else { (b, a) };
let lo_w = encode_number_text(&lo.to_string()).expect("encode lo");
let hi_w = encode_number_text(&hi.to_string()).expect("encode hi");
prop_assert!(
lo_w.as_slice() < hi_w.as_slice(),
"order broken: {lo} -> {lo_w:02x?} should be < {hi} -> {hi_w:02x?}"
);
}
#[test]
fn number_order_preserving_decimals(
base in -1_000_000i64..=1_000_000i64,
gap in 1i64..=1_000_000i64,
scale in 0u32..=6,
) {
let divisor = 10i64.pow(scale);
let to_text = |n: i64| {
if scale == 0 {
n.to_string()
} else {
format!("{}e-{}", n, scale)
}
};
let lo = base;
let hi = base.saturating_add(gap);
prop_assume!(lo != hi);
let _ = divisor;
let lo_w = encode_number_text(&to_text(lo)).expect("encode lo");
let hi_w = encode_number_text(&to_text(hi)).expect("encode hi");
prop_assert!(
lo_w.as_slice() < hi_w.as_slice(),
"decimal order broken: {} -> {lo_w:02x?} !< {} -> {hi_w:02x?}",
to_text(lo), to_text(hi)
);
}
}
#[test]
fn number_boundary_cases_round_trip() {
let cases = [
"0",
"1",
"-1",
"100",
"-100",
"99",
"-99",
"0.01",
"-0.01",
"1e125",
"-1e125",
"1e-120",
"-1e-120",
"12345678901234567890123456789012345678",
"-12345678901234567890123456789012345678",
"0.12345678901234567890123456789012345678",
"9999999999999999999999999999999999999",
];
for text in cases {
let wire = encode_number_text(text).unwrap_or_else(|e| panic!("encode {text}: {e:?}"));
assert!(wire.len() <= 21, "{text} encoded to {} bytes", wire.len());
let decoded = decode_number_value(&wire).unwrap_or_else(|e| panic!("decode {text}: {e:?}"));
let QueryValue::Number(num) = decoded else {
panic!("{text}: not a Number");
};
let out = num.to_canonical_string();
assert_eq!(
number_key(&out),
number_key(&normalize_input(text)),
"{text} round-trip -> {out}"
);
}
}
#[test]
fn number_negative_zero_is_zero() {
let wire = encode_number_text("-0").expect("encode -0");
let decoded = decode_number_value(&wire).expect("decode -0");
assert_eq!(decoded.as_i64(), Some(0));
assert_eq!(decoded.as_number_text().as_deref(), Some("0"));
}
prop_compose! {
fn civil_datetime()(
year in 1i32..=9999,
month in 1u8..=12,
day in 1u8..=28, hour in 0u8..=23,
minute in 0u8..=59,
second in 0u8..=59,
) -> (i32, u8, u8, u8, u8, u8) {
(year, month, day, hour, minute, second)
}
}
proptest! {
#![proptest_config(config())]
#[test]
fn date_round_trip((y, mo, d, h, mi, s) in civil_datetime()) {
let wire = encode_oracle_date(y, mo, d, h, mi, s).expect("encode date");
prop_assert_eq!(wire.len(), 7, "DATE is 7 bytes");
let decoded = decode_datetime_value(&wire).expect("decode date");
prop_assert_eq!(decoded, QueryValue::DateTime {
year: y, month: mo, day: d, hour: h, minute: mi, second: s, nanosecond: 0,
});
}
#[test]
fn timestamp_round_trip(
(y, mo, d, h, mi, s) in civil_datetime(),
nanosecond in 0u32..=999_999_999,
) {
let wire = encode_oracle_timestamp(y, mo, d, h, mi, s, nanosecond).expect("encode ts");
let decoded = decode_datetime_value(&wire).expect("decode ts");
prop_assert_eq!(decoded, QueryValue::DateTime {
year: y, month: mo, day: d, hour: h, minute: mi, second: s, nanosecond,
});
}
#[test]
fn timestamp_tz_zero_offset_round_trip(
(y, mo, d, h, mi, s) in civil_datetime(),
nanosecond in 0u32..=999_999_999,
) {
let wire = encode_oracle_timestamp_tz(y, mo, d, h, mi, s, nanosecond).expect("encode tstz");
prop_assert_eq!(wire.len(), 13, "TSTZ is 13 bytes");
let decoded = decode_datetime_value(&wire).expect("decode tstz");
prop_assert_eq!(decoded, QueryValue::TimestampTz {
year: y, month: mo, day: d, hour: h, minute: mi, second: s, nanosecond,
offset_minutes: 0,
});
}
#[test]
fn datetime_offset_shift_is_invertible(
(y, mo, d, h, mi, s) in civil_datetime(),
offset in -1439i32..=1439i32,
) {
let forward = super::codecs::adjust_datetime_by_minutes(y, mo, d, h, mi, s, offset)
.expect("forward shift");
let (y2, mo2, d2, h2, mi2, s2) = forward;
let back = super::codecs::adjust_datetime_by_minutes(y2, mo2, d2, h2, mi2, s2, -offset)
.expect("back shift");
prop_assert_eq!(back, (y, mo, d, h, mi, s));
}
}
#[test]
fn timestamp_tz_preserves_negative_half_hour_offset_bytes() {
let wire = encode_oracle_timestamp_tz_with_offset(2026, 6, 29, 12, 34, 56, 123_456_789, -330)
.expect("encode negative half-hour offset");
assert_eq!(wire[11], 15);
assert_eq!(wire[12], 30);
let decoded = decode_datetime_value(&wire).expect("decode negative half-hour offset");
assert_eq!(
decoded,
QueryValue::TimestampTz {
year: 2026,
month: 6,
day: 29,
hour: 12,
minute: 34,
second: 56,
nanosecond: 123_456_789,
offset_minutes: -330,
}
);
}
#[test]
fn datetime_boundary_cases() {
let cases: &[(i32, u8, u8, u8, u8, u8, u32)] = &[
(1, 1, 1, 0, 0, 0, 0), (9999, 12, 31, 23, 59, 59, 999_999_999), (1970, 1, 1, 0, 0, 0, 0), (1969, 12, 31, 23, 59, 59, 0), (2000, 2, 29, 12, 0, 0, 500_000_000), ];
for &(y, mo, d, h, mi, s, ns) in cases {
let wire = encode_oracle_timestamp(y, mo, d, h, mi, s, ns)
.unwrap_or_else(|e| panic!("encode {y}-{mo}-{d}: {e:?}"));
let decoded = decode_datetime_value(&wire).expect("decode ts boundary");
assert_eq!(
decoded,
QueryValue::DateTime {
year: y,
month: mo,
day: d,
hour: h,
minute: mi,
second: s,
nanosecond: ns
},
"{y}-{mo}-{d} {h}:{mi}:{s}.{ns}"
);
}
}
proptest! {
#![proptest_config(config())]
#[test]
fn interval_ym_round_trip(years in -100_000i32..=100_000, months in -11i32..=11) {
let wire = encode_interval_ym(years, months).expect("encode interval ym");
prop_assert_eq!(wire.len(), 5, "INTERVAL YM is 5 bytes");
let decoded = decode_interval_ym(&wire).expect("decode interval ym");
prop_assert_eq!(decoded, QueryValue::IntervalYM { years, months });
}
#[test]
fn interval_ds_round_trip(
days in -100_000i32..=100_000,
hours in 0i32..=23,
minutes in 0i32..=59,
secs in 0i32..=59,
nanoseconds in 0i32..=999_999_999,
) {
let total_seconds = hours * 3600 + minutes * 60 + secs;
let wire = encode_interval_ds(days, total_seconds, nanoseconds).expect("encode ds");
prop_assert_eq!(wire.len(), 11, "INTERVAL DS is 11 bytes");
let decoded = decode_interval_ds(&wire).expect("decode ds");
prop_assert_eq!(decoded, QueryValue::IntervalDS {
days, hours, minutes, seconds: secs, fseconds: nanoseconds,
});
}
}
#[test]
fn interval_boundary_cases() {
for (y, m) in [(0, 0), (100_000, 11), (-100_000, -11), (5, -11), (-5, 11)] {
let wire = encode_interval_ym(y, m).expect("encode ym boundary");
assert_eq!(
decode_interval_ym(&wire).expect("decode ym boundary"),
QueryValue::IntervalYM {
years: y,
months: m
}
);
}
for (d, total, ns) in [(0, 0, 0), (100_000, 86_399, 999_999_999), (-100_000, 0, 0)] {
let wire = encode_interval_ds(d, total, ns).expect("encode ds boundary");
let decoded = decode_interval_ds(&wire).expect("decode ds boundary");
let QueryValue::IntervalDS { days, fseconds, .. } = decoded else {
panic!("not DS")
};
assert_eq!(days, d);
assert_eq!(fseconds, ns);
}
}
proptest! {
#![proptest_config(config())]
#[test]
fn binary_double_round_trip_bits(bits: u64) {
let value = f64::from_bits(bits);
prop_assume!(!value.is_nan());
let wire = encode_binary_double(value);
let decoded = decode_binary_double(&wire).expect("decode bdouble");
prop_assert_eq!(decoded.to_bits(), value.to_bits(), "f64 {} bits differ", value);
}
#[test]
fn binary_float_round_trip_bits(bits: u32) {
let value = f32::from_bits(bits);
prop_assume!(!value.is_nan());
let wire = encode_binary_float(value);
let decoded = decode_binary_float(&wire).expect("decode bfloat");
prop_assert_eq!(decoded.to_bits(), value.to_bits(), "f32 {} bits differ", value);
}
#[test]
fn binary_double_order_preserving_nonneg(a in 0.0f64..1e308, b in 0.0f64..1e308) {
prop_assume!(a != b && a.is_finite() && b.is_finite());
let (lo, hi) = if a < b { (a, b) } else { (b, a) };
let lo_w = encode_binary_double(lo);
let hi_w = encode_binary_double(hi);
prop_assert!(lo_w < hi_w, "order broken: {lo} -> {lo_w:02x?} !< {hi} -> {hi_w:02x?}");
}
}
#[test]
fn binary_float_double_boundary_values() {
let f64_cases = [
0.0f64,
-0.0,
f64::MIN_POSITIVE,
f64::MAX,
f64::MIN,
f64::from_bits(1), f64::INFINITY,
f64::NEG_INFINITY,
];
for v in f64_cases {
let decoded = decode_binary_double(&encode_binary_double(v)).expect("decode f64 boundary");
assert_eq!(decoded.to_bits(), v.to_bits(), "f64 boundary {v}");
}
let nan = decode_binary_double(&encode_binary_double(f64::NAN)).expect("decode f64 nan");
assert!(nan.is_nan(), "f64 NaN must round-trip to a NaN");
let f32_cases = [
0.0f32,
-0.0,
f32::MIN_POSITIVE,
f32::MAX,
f32::MIN,
f32::from_bits(1),
f32::INFINITY,
f32::NEG_INFINITY,
];
for v in f32_cases {
let decoded = decode_binary_float(&encode_binary_float(v)).expect("decode f32 boundary");
assert_eq!(decoded.to_bits(), v.to_bits(), "f32 boundary {v}");
}
let nan = decode_binary_float(&encode_binary_float(f32::NAN)).expect("decode f32 nan");
assert!(nan.is_nan(), "f32 NaN must round-trip to a NaN");
}
proptest! {
#![proptest_config(config())]
#[test]
fn text_utf8_round_trip(s in ".{0,512}") {
let bytes = s.as_bytes();
let decoded = decode_text_value(bytes, 0).expect("decode utf8 text");
prop_assert_eq!(decoded, s);
}
#[test]
fn text_nchar_utf16_round_trip(s in ".{0,256}") {
let mut bytes = Vec::new();
for unit in s.encode_utf16() {
bytes.extend_from_slice(&unit.to_be_bytes());
}
let decoded = decode_text_value(&bytes, CS_FORM_NCHAR).expect("decode nchar text");
prop_assert_eq!(decoded, s);
}
}
#[test]
fn text_boundary_cases() {
assert_eq!(decode_text_value(b"", 0).expect("empty"), "");
assert_eq!(decode_text_value(b"x", 0).expect("single"), "x");
let big = "a".repeat(32_767);
assert_eq!(
decode_text_value(big.as_bytes(), 0).expect("big").len(),
32_767
);
for s in ["Ć©", "ā¬", "š", "aĆ©ā¬šz"] {
assert_eq!(decode_text_value(s.as_bytes(), 0).expect("multibyte"), s);
}
let astral = "š".repeat(1000);
assert_eq!(
decode_text_value(astral.as_bytes(), 0).expect("astral"),
astral
);
}
#[test]
fn text_invalid_utf8_fails_closed() {
assert!(decode_text_value(&[0x80], 0).is_err());
assert!(decode_text_value(&[0x00], CS_FORM_NCHAR).is_err());
}