use std::net::Ipv4Addr;
use crate::egress::column_kind::ColumnKind;
use crate::egress::wire::varint;
use crate::error::{Result, fmt};
pub const DECIMAL64_MAX_SCALE: i8 = 18;
pub const DECIMAL128_MAX_SCALE: i8 = 38;
pub const DECIMAL256_MAX_SCALE: i8 = 76;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum SimpleNullKind {
Boolean,
Byte,
Short,
Int,
Long,
Float,
Double,
Timestamp,
TimestampNanos,
Date,
Uuid,
Long256,
Char,
Ipv4,
}
impl SimpleNullKind {
pub fn as_column_kind(self) -> ColumnKind {
match self {
SimpleNullKind::Boolean => ColumnKind::Boolean,
SimpleNullKind::Byte => ColumnKind::Byte,
SimpleNullKind::Short => ColumnKind::Short,
SimpleNullKind::Int => ColumnKind::Int,
SimpleNullKind::Long => ColumnKind::Long,
SimpleNullKind::Float => ColumnKind::Float,
SimpleNullKind::Double => ColumnKind::Double,
SimpleNullKind::Timestamp => ColumnKind::Timestamp,
SimpleNullKind::TimestampNanos => ColumnKind::TimestampNanos,
SimpleNullKind::Date => ColumnKind::Date,
SimpleNullKind::Uuid => ColumnKind::Uuid,
SimpleNullKind::Long256 => ColumnKind::Long256,
SimpleNullKind::Char => ColumnKind::Char,
SimpleNullKind::Ipv4 => ColumnKind::Ipv4,
}
}
}
impl TryFrom<ColumnKind> for SimpleNullKind {
type Error = ColumnKind;
fn try_from(k: ColumnKind) -> std::result::Result<Self, Self::Error> {
Ok(match k {
ColumnKind::Boolean => SimpleNullKind::Boolean,
ColumnKind::Byte => SimpleNullKind::Byte,
ColumnKind::Short => SimpleNullKind::Short,
ColumnKind::Int => SimpleNullKind::Int,
ColumnKind::Long => SimpleNullKind::Long,
ColumnKind::Float => SimpleNullKind::Float,
ColumnKind::Double => SimpleNullKind::Double,
ColumnKind::Timestamp => SimpleNullKind::Timestamp,
ColumnKind::TimestampNanos => SimpleNullKind::TimestampNanos,
ColumnKind::Date => SimpleNullKind::Date,
ColumnKind::Uuid => SimpleNullKind::Uuid,
ColumnKind::Long256 => SimpleNullKind::Long256,
ColumnKind::Char => SimpleNullKind::Char,
ColumnKind::Ipv4 => SimpleNullKind::Ipv4,
other => return Err(other),
})
}
}
#[derive(Debug, Clone, PartialEq)]
#[non_exhaustive]
pub enum Bind {
Null(SimpleNullKind),
NullVarchar,
NullBinary,
NullDecimal64 {
scale: i8,
},
NullDecimal128 {
scale: i8,
},
NullDecimal256 {
scale: i8,
},
NullGeohash {
precision_bits: u8,
},
Bool(bool),
I8(i8),
I16(i16),
I32(i32),
I64(i64),
F32(f32),
F64(f64),
Varchar(String),
Binary(Vec<u8>),
TimestampMicros(i64),
TimestampNanos(i64),
DateMillis(i64),
Uuid([u8; 16]),
Long256([u8; 32]),
Char(u16),
Ipv4(Ipv4Addr),
Decimal64 {
value: i64,
scale: i8,
},
Decimal128 {
value: i128,
scale: i8,
},
Decimal256 {
bytes: [u8; 32],
scale: i8,
},
Geohash {
value: u64,
precision_bits: u8,
},
}
impl Bind {
pub fn kind(&self) -> ColumnKind {
match self {
Bind::Null(s) => s.as_column_kind(),
Bind::NullVarchar => ColumnKind::Varchar,
Bind::NullBinary => ColumnKind::Binary,
Bind::NullDecimal64 { .. } => ColumnKind::Decimal64,
Bind::NullDecimal128 { .. } => ColumnKind::Decimal128,
Bind::NullDecimal256 { .. } => ColumnKind::Decimal256,
Bind::NullGeohash { .. } => ColumnKind::Geohash,
Bind::Bool(_) => ColumnKind::Boolean,
Bind::I8(_) => ColumnKind::Byte,
Bind::I16(_) => ColumnKind::Short,
Bind::I32(_) => ColumnKind::Int,
Bind::I64(_) => ColumnKind::Long,
Bind::F32(_) => ColumnKind::Float,
Bind::F64(_) => ColumnKind::Double,
Bind::Varchar(_) => ColumnKind::Varchar,
Bind::Binary(_) => ColumnKind::Binary,
Bind::TimestampMicros(_) => ColumnKind::Timestamp,
Bind::TimestampNanos(_) => ColumnKind::TimestampNanos,
Bind::DateMillis(_) => ColumnKind::Date,
Bind::Uuid(_) => ColumnKind::Uuid,
Bind::Long256(_) => ColumnKind::Long256,
Bind::Char(_) => ColumnKind::Char,
Bind::Ipv4(_) => ColumnKind::Ipv4,
Bind::Decimal64 { .. } => ColumnKind::Decimal64,
Bind::Decimal128 { .. } => ColumnKind::Decimal128,
Bind::Decimal256 { .. } => ColumnKind::Decimal256,
Bind::Geohash { .. } => ColumnKind::Geohash,
}
}
fn is_null(&self) -> bool {
matches!(
self,
Bind::Null(_)
| Bind::NullVarchar
| Bind::NullBinary
| Bind::NullDecimal64 { .. }
| Bind::NullDecimal128 { .. }
| Bind::NullDecimal256 { .. }
| Bind::NullGeohash { .. }
)
}
}
pub fn encode_bind(bind: &Bind, out: &mut Vec<u8>) -> Result<()> {
out.push(bind.kind().as_u8());
let null = bind.is_null();
if null {
out.push(0x01); out.push(0x01); } else {
out.push(0x00);
}
match bind {
Bind::Decimal64 { scale, .. }
| Bind::Decimal128 { scale, .. }
| Bind::Decimal256 { scale, .. }
| Bind::NullDecimal64 { scale }
| Bind::NullDecimal128 { scale }
| Bind::NullDecimal256 { scale } => {
let max_scale = match bind {
Bind::Decimal64 { .. } | Bind::NullDecimal64 { .. } => DECIMAL64_MAX_SCALE,
Bind::Decimal128 { .. } | Bind::NullDecimal128 { .. } => DECIMAL128_MAX_SCALE,
_ => DECIMAL256_MAX_SCALE,
};
if *scale < 0 || *scale > max_scale {
return Err(fmt!(
InvalidBind,
"decimal scale {} outside 0..={}",
scale,
max_scale
));
}
out.push(*scale as u8);
}
Bind::Geohash { precision_bits, .. } | Bind::NullGeohash { precision_bits } => {
if *precision_bits == 0 || *precision_bits > 60 {
return Err(fmt!(
InvalidBind,
"geohash precision_bits {} outside 1..=60",
precision_bits
));
}
if let Bind::Geohash {
value,
precision_bits,
} = bind
{
if value >> precision_bits != 0 {
return Err(fmt!(
InvalidBind,
"geohash value 0x{:X} has bits set above precision_bits {}",
value,
precision_bits
));
}
}
varint::encode_u64(*precision_bits as u64, out);
}
Bind::Varchar(s) => write_varlen_offsets(&[s.len()], out)?,
Bind::Binary(b) => write_varlen_offsets(&[b.len()], out)?,
_ => {}
}
if null {
return Ok(());
}
match bind {
Bind::Null(_)
| Bind::NullVarchar
| Bind::NullBinary
| Bind::NullDecimal64 { .. }
| Bind::NullDecimal128 { .. }
| Bind::NullDecimal256 { .. }
| Bind::NullGeohash { .. } => unreachable!("handled above"),
Bind::Bool(v) => out.push(if *v { 0x01 } else { 0x00 }),
Bind::I8(v) => out.push(*v as u8),
Bind::I16(v) => out.extend_from_slice(&v.to_le_bytes()),
Bind::I32(v) => out.extend_from_slice(&v.to_le_bytes()),
Bind::I64(v) => out.extend_from_slice(&v.to_le_bytes()),
Bind::F32(v) => out.extend_from_slice(&v.to_le_bytes()),
Bind::F64(v) => out.extend_from_slice(&v.to_le_bytes()),
Bind::Char(v) => out.extend_from_slice(&v.to_le_bytes()),
Bind::TimestampMicros(v) | Bind::TimestampNanos(v) | Bind::DateMillis(v) => {
out.extend_from_slice(&v.to_le_bytes());
}
Bind::Uuid(b) => out.extend_from_slice(b),
Bind::Long256(b) => out.extend_from_slice(b),
Bind::Ipv4(addr) => out.extend_from_slice(&u32::from(*addr).to_le_bytes()),
Bind::Decimal64 { value, .. } => out.extend_from_slice(&value.to_le_bytes()),
Bind::Decimal128 { value, .. } => out.extend_from_slice(&value.to_le_bytes()),
Bind::Decimal256 { bytes, .. } => out.extend_from_slice(bytes),
Bind::Geohash {
value,
precision_bits,
} => {
let bw = (*precision_bits as usize).div_ceil(8);
let bytes = value.to_le_bytes();
out.extend_from_slice(&bytes[..bw]);
}
Bind::Varchar(s) => out.extend_from_slice(s.as_bytes()),
Bind::Binary(b) => out.extend_from_slice(b),
}
Ok(())
}
fn write_varlen_offsets(byte_lens: &[usize], out: &mut Vec<u8>) -> Result<()> {
let mut total: u32 = 0;
out.extend_from_slice(&total.to_le_bytes());
for &len in byte_lens {
let len32 = u32::try_from(len)
.map_err(|_| fmt!(InvalidBind, "varlen bind value too large: {} bytes", len))?;
total = total
.checked_add(len32)
.ok_or_else(|| fmt!(InvalidBind, "varlen bind offsets overflow u32"))?;
out.extend_from_slice(&total.to_le_bytes());
}
Ok(())
}
pub fn check_bindable(kind: ColumnKind) -> Result<()> {
match kind {
ColumnKind::Symbol
| ColumnKind::Binary
| ColumnKind::Ipv4
| ColumnKind::DoubleArray
| ColumnKind::LongArray => Err(fmt!(
InvalidBind,
"bind not supported for type {} (0x{:02X})",
kind.name(),
kind.as_u8()
)),
_ => Ok(()),
}
}
#[cfg(test)]
mod tests {
use super::*;
fn enc(b: Bind) -> Vec<u8> {
let mut out = Vec::new();
encode_bind(&b, &mut out).unwrap();
out
}
#[test]
fn simple_null_layout() {
assert_eq!(
enc(Bind::Null(SimpleNullKind::Long)),
vec![0x05, 0x01, 0x01]
);
}
#[test]
fn bool_layout() {
assert_eq!(enc(Bind::Bool(true)), vec![0x01, 0x00, 0x01]);
assert_eq!(enc(Bind::Bool(false)), vec![0x01, 0x00, 0x00]);
}
#[test]
fn i32_le() {
assert_eq!(
enc(Bind::I32(0x01020304)),
vec![0x04, 0x00, 0x04, 0x03, 0x02, 0x01]
);
}
#[test]
fn i64_le() {
assert_eq!(
enc(Bind::I64(0x0102_0304_0506_0708)),
vec![0x05, 0x00, 0x08, 0x07, 0x06, 0x05, 0x04, 0x03, 0x02, 0x01]
);
}
#[test]
fn f64_le() {
let mut expected = vec![0x07, 0x00];
expected.extend_from_slice(&1.0f64.to_le_bytes());
assert_eq!(enc(Bind::F64(1.0)), expected);
}
#[test]
fn ipv4_le() {
let bytes = enc(Bind::Ipv4(Ipv4Addr::new(192, 168, 1, 1)));
assert_eq!(bytes, vec![0x18, 0x00, 0x01, 0x01, 0xA8, 0xC0]);
}
#[test]
fn uuid_passthrough() {
let raw = [
0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E,
0x0F, 0x10,
];
let bytes = enc(Bind::Uuid(raw));
assert_eq!(bytes[0], 0x0C);
assert_eq!(bytes[1], 0x00);
assert_eq!(&bytes[2..], &raw);
}
#[test]
fn long256_passthrough() {
let raw: [u8; 32] = std::array::from_fn(|i| i as u8);
let bytes = enc(Bind::Long256(raw));
assert_eq!(bytes[0], 0x0D);
assert_eq!(bytes[1], 0x00);
assert_eq!(&bytes[2..], &raw);
}
#[test]
fn char_layout() {
assert_eq!(enc(Bind::Char(b'A' as u16)), vec![0x16, 0x00, 0x41, 0x00]);
}
#[test]
fn decimal64_value_layout() {
let bytes = enc(Bind::Decimal64 {
value: 12345,
scale: 2,
});
assert_eq!(bytes[0], 0x13);
assert_eq!(bytes[1], 0x00);
assert_eq!(bytes[2], 0x02);
assert_eq!(&bytes[3..], &12345i64.to_le_bytes());
}
#[test]
fn decimal64_null_carries_scale() {
assert_eq!(
enc(Bind::NullDecimal64 { scale: 4 }),
vec![0x13, 0x01, 0x01, 0x04]
);
}
#[test]
fn decimal_scale_negative_rejected() {
for bind in [
Bind::Decimal64 {
value: 0,
scale: -1,
},
Bind::Decimal128 {
value: 0,
scale: -1,
},
Bind::Decimal256 {
bytes: [0; 32],
scale: -1,
},
Bind::NullDecimal64 { scale: -1 },
Bind::NullDecimal128 { scale: -1 },
Bind::NullDecimal256 { scale: -1 },
] {
let mut out = Vec::new();
let err = encode_bind(&bind, &mut out).unwrap_err();
assert_eq!(err.code(), crate::ErrorCode::InvalidBind);
assert!(
err.msg().contains("decimal scale"),
"expected scale error msg, got: {}",
err.msg()
);
}
}
#[test]
fn decimal_scale_above_max_rejected() {
for bind in [
Bind::Decimal64 {
value: 0,
scale: DECIMAL64_MAX_SCALE + 1,
},
Bind::NullDecimal128 {
scale: DECIMAL128_MAX_SCALE + 1,
},
Bind::NullDecimal256 {
scale: DECIMAL256_MAX_SCALE + 1,
},
] {
let mut out = Vec::new();
let err = encode_bind(&bind, &mut out).unwrap_err();
assert_eq!(err.code(), crate::ErrorCode::InvalidBind);
}
}
#[test]
fn decimal_scale_at_boundaries_accepted() {
let cases = [
Bind::NullDecimal64 { scale: 0 },
Bind::NullDecimal64 {
scale: DECIMAL64_MAX_SCALE,
},
Bind::NullDecimal128 {
scale: DECIMAL128_MAX_SCALE,
},
Bind::NullDecimal256 {
scale: DECIMAL256_MAX_SCALE,
},
];
for bind in cases {
let scale = match bind {
Bind::NullDecimal64 { scale }
| Bind::NullDecimal128 { scale }
| Bind::NullDecimal256 { scale } => scale,
_ => unreachable!(),
};
let mut out = Vec::new();
encode_bind(&bind, &mut out).unwrap();
assert_eq!(out.last().copied(), Some(scale as u8));
}
}
#[test]
fn decimal128_value_layout() {
let bytes = enc(Bind::Decimal128 {
value: -42,
scale: 6,
});
assert_eq!(bytes[0], 0x14);
assert_eq!(bytes[1], 0x00);
assert_eq!(bytes[2], 0x06);
assert_eq!(&bytes[3..], &(-42i128).to_le_bytes());
}
#[test]
fn decimal128_null_carries_scale() {
assert_eq!(
enc(Bind::NullDecimal128 { scale: 8 }),
vec![0x14, 0x01, 0x01, 0x08]
);
}
#[test]
fn decimal256_value_layout() {
let raw: [u8; 32] = std::array::from_fn(|i| (i + 1) as u8);
let bytes = enc(Bind::Decimal256 {
bytes: raw,
scale: 12,
});
assert_eq!(bytes[0], 0x15);
assert_eq!(bytes[1], 0x00);
assert_eq!(bytes[2], 0x0C);
assert_eq!(&bytes[3..], &raw);
}
#[test]
fn decimal256_null_carries_scale() {
assert_eq!(
enc(Bind::NullDecimal256 { scale: 18 }),
vec![0x15, 0x01, 0x01, 0x12]
);
}
#[test]
fn geohash_value_layout() {
let bytes = enc(Bind::Geohash {
value: 0xAB,
precision_bits: 8,
});
assert_eq!(bytes, vec![0x0E, 0x00, 0x08, 0xAB]);
}
#[test]
fn geohash_60_bits_writes_8_bytes() {
let bytes = enc(Bind::Geohash {
value: 0x0102_0304_0506_0708,
precision_bits: 60,
});
let mut expected = vec![0x0E, 0x00, 0x3C];
expected.extend_from_slice(&0x0102_0304_0506_0708u64.to_le_bytes());
assert_eq!(bytes, expected);
}
#[test]
fn geohash_null_carries_precision() {
assert_eq!(
enc(Bind::NullGeohash { precision_bits: 20 }),
vec![0x0E, 0x01, 0x01, 0x14]
);
}
#[test]
fn geohash_invalid_precision_rejected() {
let mut out = Vec::new();
let err = encode_bind(
&Bind::Geohash {
value: 0,
precision_bits: 0,
},
&mut out,
)
.unwrap_err();
assert_eq!(err.code(), crate::ErrorCode::InvalidBind);
}
#[test]
fn geohash_value_above_precision_rejected() {
let mut out = Vec::new();
let err = encode_bind(
&Bind::Geohash {
value: u64::MAX,
precision_bits: 8,
},
&mut out,
)
.unwrap_err();
assert_eq!(err.code(), crate::ErrorCode::InvalidBind);
}
#[test]
fn varchar_value_layout() {
let bytes = enc(Bind::Varchar("hi".into()));
let expected = vec![0x0F, 0x00, 0, 0, 0, 0, 2, 0, 0, 0, b'h', b'i'];
assert_eq!(bytes, expected);
}
#[test]
fn varchar_null_emits_no_offsets_array() {
assert_eq!(enc(Bind::NullVarchar), vec![0x0F, 0x01, 0x01]);
}
#[test]
fn binary_value_layout() {
let bytes = enc(Bind::Binary(vec![0xDE, 0xAD]));
let expected = vec![0x17, 0x00, 0, 0, 0, 0, 2, 0, 0, 0, 0xDE, 0xAD];
assert_eq!(bytes, expected);
}
#[test]
fn binary_null_emits_no_offsets_array() {
assert_eq!(enc(Bind::NullBinary), vec![0x17, 0x01, 0x01]);
}
#[test]
fn null_varchar_then_i32_concatenates_cleanly() {
let mut out = Vec::new();
encode_bind(&Bind::NullVarchar, &mut out).unwrap();
encode_bind(&Bind::I32(7), &mut out).unwrap();
assert_eq!(
out,
vec![0x0F, 0x01, 0x01, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00]
);
}
#[test]
fn check_bindable_rejects_server_unsupported() {
assert!(check_bindable(ColumnKind::Symbol).is_err());
assert!(check_bindable(ColumnKind::Binary).is_err());
assert!(check_bindable(ColumnKind::Ipv4).is_err());
assert!(check_bindable(ColumnKind::DoubleArray).is_err());
assert!(check_bindable(ColumnKind::LongArray).is_err());
}
#[test]
fn check_bindable_accepts_remaining_types() {
for k in [
ColumnKind::Boolean,
ColumnKind::Byte,
ColumnKind::Short,
ColumnKind::Int,
ColumnKind::Long,
ColumnKind::Float,
ColumnKind::Double,
ColumnKind::Timestamp,
ColumnKind::TimestampNanos,
ColumnKind::Date,
ColumnKind::Uuid,
ColumnKind::Long256,
ColumnKind::Char,
ColumnKind::Varchar,
ColumnKind::Decimal64,
ColumnKind::Decimal128,
ColumnKind::Decimal256,
ColumnKind::Geohash,
] {
check_bindable(k).unwrap_or_else(|_| panic!("{}", k.name()));
}
}
#[test]
fn simple_null_kind_try_from_rejects_kinds_with_column_args() {
for kind in [
ColumnKind::Varchar,
ColumnKind::Binary,
ColumnKind::Decimal64,
ColumnKind::Decimal128,
ColumnKind::Decimal256,
ColumnKind::Geohash,
ColumnKind::Symbol,
ColumnKind::DoubleArray,
ColumnKind::LongArray,
] {
let r = SimpleNullKind::try_from(kind);
assert!(
r.is_err(),
"{} must not convert to SimpleNullKind",
kind.name()
);
}
}
#[test]
fn null_bind_accepts_simple_kinds() {
for kind in [
SimpleNullKind::Boolean,
SimpleNullKind::Byte,
SimpleNullKind::Short,
SimpleNullKind::Int,
SimpleNullKind::Long,
SimpleNullKind::Float,
SimpleNullKind::Double,
SimpleNullKind::Timestamp,
SimpleNullKind::TimestampNanos,
SimpleNullKind::Date,
SimpleNullKind::Uuid,
SimpleNullKind::Long256,
SimpleNullKind::Char,
SimpleNullKind::Ipv4,
] {
let mut out = Vec::new();
encode_bind(&Bind::Null(kind), &mut out).unwrap_or_else(|_| {
panic!("Bind::Null({}) should encode", kind.as_column_kind().name())
});
assert_eq!(out, vec![kind.as_column_kind().as_u8(), 0x01, 0x01]);
}
}
#[test]
fn null_bind_kind_preserved() {
assert_eq!(
Bind::NullDecimal64 { scale: 0 }.kind(),
ColumnKind::Decimal64
);
assert_eq!(Bind::NullVarchar.kind(), ColumnKind::Varchar);
assert_eq!(
Bind::NullGeohash { precision_bits: 8 }.kind(),
ColumnKind::Geohash
);
}
mod fuzz {
use super::*;
use proptest::prelude::*;
fn body_of_non_null(expected_kind: ColumnKind, encoded: &[u8]) -> &[u8] {
assert!(encoded.len() >= 2, "encoded bind too short");
assert_eq!(
encoded[0],
expected_kind.as_u8(),
"type code mismatch: encoded={:02x} expected={:02x} ({})",
encoded[0],
expected_kind.as_u8(),
expected_kind.name()
);
assert_eq!(encoded[1], 0x00, "null_flag must be 0x00 for non-null bind");
&encoded[2..]
}
proptest! {
#![proptest_config(ProptestConfig {
cases: 200,
.. ProptestConfig::default()
})]
#[test]
fn fuzz_bool(v: bool) {
let bytes = enc(Bind::Bool(v));
let body = body_of_non_null(ColumnKind::Boolean, &bytes);
prop_assert_eq!(body, &[v as u8][..]);
}
#[test]
fn fuzz_i8(v: i8) {
let bytes = enc(Bind::I8(v));
let body = body_of_non_null(ColumnKind::Byte, &bytes);
prop_assert_eq!(body, &[v as u8][..]);
}
#[test]
fn fuzz_i16(v: i16) {
let bytes = enc(Bind::I16(v));
let body = body_of_non_null(ColumnKind::Short, &bytes);
prop_assert_eq!(body.len(), 2);
let got = i16::from_le_bytes(body.try_into().unwrap());
prop_assert_eq!(got, v);
}
#[test]
fn fuzz_i32(v: i32) {
let bytes = enc(Bind::I32(v));
let body = body_of_non_null(ColumnKind::Int, &bytes);
prop_assert_eq!(body.len(), 4);
let got = i32::from_le_bytes(body.try_into().unwrap());
prop_assert_eq!(got, v);
}
#[test]
fn fuzz_i64(v: i64) {
let bytes = enc(Bind::I64(v));
let body = body_of_non_null(ColumnKind::Long, &bytes);
prop_assert_eq!(body.len(), 8);
let got = i64::from_le_bytes(body.try_into().unwrap());
prop_assert_eq!(got, v);
}
#[test]
fn fuzz_f32_bits(bits: u32) {
let v = f32::from_bits(bits);
let bytes = enc(Bind::F32(v));
let body = body_of_non_null(ColumnKind::Float, &bytes);
prop_assert_eq!(body.len(), 4);
let got = f32::from_le_bytes(body.try_into().unwrap());
prop_assert_eq!(got.to_bits(), v.to_bits());
}
#[test]
fn fuzz_f64_bits(bits: u64) {
let v = f64::from_bits(bits);
let bytes = enc(Bind::F64(v));
let body = body_of_non_null(ColumnKind::Double, &bytes);
prop_assert_eq!(body.len(), 8);
let got = f64::from_le_bytes(body.try_into().unwrap());
prop_assert_eq!(got.to_bits(), v.to_bits());
}
#[test]
fn fuzz_timestamp_micros(v: i64) {
let bytes = enc(Bind::TimestampMicros(v));
let body = body_of_non_null(ColumnKind::Timestamp, &bytes);
prop_assert_eq!(i64::from_le_bytes(body.try_into().unwrap()), v);
}
#[test]
fn fuzz_timestamp_nanos(v: i64) {
let bytes = enc(Bind::TimestampNanos(v));
let body = body_of_non_null(ColumnKind::TimestampNanos, &bytes);
prop_assert_eq!(i64::from_le_bytes(body.try_into().unwrap()), v);
}
#[test]
fn fuzz_date_millis(v: i64) {
let bytes = enc(Bind::DateMillis(v));
let body = body_of_non_null(ColumnKind::Date, &bytes);
prop_assert_eq!(i64::from_le_bytes(body.try_into().unwrap()), v);
}
#[test]
fn fuzz_char(v: u16) {
let bytes = enc(Bind::Char(v));
let body = body_of_non_null(ColumnKind::Char, &bytes);
prop_assert_eq!(body.len(), 2);
let got = u16::from_le_bytes(body.try_into().unwrap());
prop_assert_eq!(got, v);
}
#[test]
fn fuzz_ipv4(octets: [u8; 4]) {
let addr = Ipv4Addr::from(u32::from_be_bytes(octets));
let bytes = enc(Bind::Ipv4(addr));
let body = body_of_non_null(ColumnKind::Ipv4, &bytes);
prop_assert_eq!(body.len(), 4);
let got = Ipv4Addr::from(u32::from_le_bytes(body.try_into().unwrap()));
prop_assert_eq!(got, addr);
}
#[test]
fn fuzz_uuid(raw in proptest::array::uniform16(any::<u8>())) {
let bytes = enc(Bind::Uuid(raw));
let body = body_of_non_null(ColumnKind::Uuid, &bytes);
prop_assert_eq!(body, &raw[..]);
}
#[test]
fn fuzz_long256(raw in proptest::array::uniform32(any::<u8>())) {
let bytes = enc(Bind::Long256(raw));
let body = body_of_non_null(ColumnKind::Long256, &bytes);
prop_assert_eq!(body, &raw[..]);
}
#[test]
fn fuzz_decimal64(value: i64, scale in 0i8..=DECIMAL64_MAX_SCALE) {
let bytes = enc(Bind::Decimal64 { value, scale });
let body = body_of_non_null(ColumnKind::Decimal64, &bytes);
prop_assert_eq!(body.len(), 1 + 8);
prop_assert_eq!(body[0] as i8, scale);
prop_assert_eq!(i64::from_le_bytes(body[1..].try_into().unwrap()), value);
}
#[test]
fn fuzz_decimal128(value: i128, scale in 0i8..=DECIMAL128_MAX_SCALE) {
let bytes = enc(Bind::Decimal128 { value, scale });
let body = body_of_non_null(ColumnKind::Decimal128, &bytes);
prop_assert_eq!(body.len(), 1 + 16);
prop_assert_eq!(body[0] as i8, scale);
prop_assert_eq!(i128::from_le_bytes(body[1..].try_into().unwrap()), value);
}
#[test]
fn fuzz_decimal256(
raw in proptest::array::uniform32(any::<u8>()),
scale in 0i8..=DECIMAL256_MAX_SCALE,
) {
let bytes = enc(Bind::Decimal256 { bytes: raw, scale });
let body = body_of_non_null(ColumnKind::Decimal256, &bytes);
prop_assert_eq!(body.len(), 1 + 32);
prop_assert_eq!(body[0] as i8, scale);
prop_assert_eq!(&body[1..], &raw[..]);
}
#[test]
fn fuzz_geohash(raw_value: u64, precision_bits in 1u8..=60) {
let mask = if precision_bits == 64 {
!0u64
} else {
(1u64 << precision_bits) - 1
};
let value = raw_value & mask;
let bytes = enc(Bind::Geohash { value, precision_bits });
let body = body_of_non_null(ColumnKind::Geohash, &bytes);
prop_assert_eq!(body[0], precision_bits);
let byte_width = (precision_bits as usize).div_ceil(8);
prop_assert_eq!(body.len(), 1 + byte_width);
let mut buf = [0u8; 8];
buf[..byte_width].copy_from_slice(&body[1..]);
let got = u64::from_le_bytes(buf);
prop_assert_eq!(got, value);
}
#[test]
fn fuzz_varchar(s in ".{0,32}") {
let bytes = enc(Bind::Varchar(s.clone()));
let body = body_of_non_null(ColumnKind::Varchar, &bytes);
let utf8_bytes = s.as_bytes();
prop_assert_eq!(body.len(), 8 + utf8_bytes.len());
let offset0 = u32::from_le_bytes(body[0..4].try_into().unwrap());
let offset1 = u32::from_le_bytes(body[4..8].try_into().unwrap());
prop_assert_eq!(offset0, 0);
prop_assert_eq!(offset1 as usize, utf8_bytes.len());
prop_assert_eq!(&body[8..], utf8_bytes);
}
#[test]
fn fuzz_binary(buf in proptest::collection::vec(any::<u8>(), 0..32)) {
let bytes = enc(Bind::Binary(buf.clone()));
let body = body_of_non_null(ColumnKind::Binary, &bytes);
prop_assert_eq!(body.len(), 8 + buf.len());
let offset0 = u32::from_le_bytes(body[0..4].try_into().unwrap());
let offset1 = u32::from_le_bytes(body[4..8].try_into().unwrap());
prop_assert_eq!(offset0, 0);
prop_assert_eq!(offset1 as usize, buf.len());
prop_assert_eq!(&body[8..], &buf[..]);
}
}
}
}