use crate::ErrorCode;
use crate::ingress::{Buffer, DecimalView, ProtocolVersion};
use crate::tests::{TestResult, assert_err_contains};
use rstest::rstest;
fn serialize_decimal(decimal: DecimalView) -> Vec<u8> {
let mut out = Vec::new();
decimal.serialize(&mut out);
out
}
#[test]
fn test_str_positive_decimal() -> TestResult {
let decimal = DecimalView::try_new_string("123.45")?;
let result = serialize_decimal(decimal);
assert_eq!(result, b"123.45d");
Ok(())
}
#[test]
fn test_str_negative_decimal() -> TestResult {
let decimal = DecimalView::try_new_string("-123.45")?;
let result = serialize_decimal(decimal);
assert_eq!(result, b"-123.45d");
Ok(())
}
#[test]
fn test_str_zero() -> TestResult {
let decimal = DecimalView::try_new_string("0")?;
let result = serialize_decimal(decimal);
assert_eq!(result, b"0d");
Ok(())
}
#[test]
fn test_str_nan() -> TestResult {
let decimal = DecimalView::try_new_string("NaN")?;
let result = serialize_decimal(decimal);
assert_eq!(result, b"NaNd");
Ok(())
}
#[test]
fn test_str_inf() -> TestResult {
let decimal = DecimalView::try_new_string("Infinity")?;
let result = serialize_decimal(decimal);
assert_eq!(result, b"Infinityd");
Ok(())
}
#[test]
fn test_str_negative_infinity() -> TestResult {
let decimal = DecimalView::try_new_string("-Infinity")?;
let result = serialize_decimal(decimal);
assert_eq!(result, b"-Infinityd");
Ok(())
}
#[test]
fn test_str_scientific_notation() -> TestResult {
let decimal = DecimalView::try_new_string("1.5e-3")?;
let result = serialize_decimal(decimal);
assert_eq!(result, b"1.5e-3d");
Ok(())
}
#[test]
fn test_str_large_decimal() -> TestResult {
let decimal = DecimalView::try_new_string("999999999999999999.123456789")?;
let result = serialize_decimal(decimal);
assert_eq!(result, b"999999999999999999.123456789d");
Ok(())
}
#[test]
fn test_str_with_leading_zero() -> TestResult {
let decimal = DecimalView::try_new_string("0.001")?;
let result = serialize_decimal(decimal);
assert_eq!(result, b"0.001d");
Ok(())
}
#[test]
fn test_str_rejects_space() -> TestResult {
let result = DecimalView::try_new_string("12 3.45");
assert_err_contains(result, ErrorCode::InvalidDecimal, "reserved character");
Ok(())
}
#[test]
fn test_str_rejects_comma() -> TestResult {
let result = DecimalView::try_new_string("1,234.56");
assert_err_contains(result, ErrorCode::InvalidDecimal, "reserved character");
Ok(())
}
#[test]
fn test_str_rejects_equals() -> TestResult {
let result = DecimalView::try_new_string("123=45");
assert_err_contains(result, ErrorCode::InvalidDecimal, "reserved character");
Ok(())
}
#[test]
fn test_str_rejects_newline() -> TestResult {
let result = DecimalView::try_new_string("123\n45");
assert_err_contains(result, ErrorCode::InvalidDecimal, "reserved character");
Ok(())
}
#[test]
fn test_str_rejects_backslash() -> TestResult {
let result = DecimalView::try_new_string("123\\45");
assert_err_contains(result, ErrorCode::InvalidDecimal, "reserved character");
Ok(())
}
#[cfg(any(feature = "rust_decimal", feature = "bigdecimal"))]
fn parse_binary_decimal(bytes: &[u8]) -> (u8, i128) {
use crate::ingress::DECIMAL_BINARY_FORMAT_TYPE;
assert_eq!(bytes[0], b'=', "Missing binary format marker");
assert_eq!(
bytes[1], DECIMAL_BINARY_FORMAT_TYPE,
"Invalid decimal type byte"
);
let scale = bytes[2];
let length = bytes[3] as usize;
assert!(scale <= 76, "Scale {} exceeds maximum of 76", scale);
assert_eq!(
bytes.len(),
4 + length,
"Binary data length mismatch: expected {} bytes, got {}",
4 + length,
bytes.len()
);
let mantissa_bytes = &bytes[4..];
let mut i128_bytes = [0u8; 16];
let offset = 16 - length;
i128_bytes[offset..].copy_from_slice(mantissa_bytes);
if mantissa_bytes[0] & 0x80 != 0 {
i128_bytes[..offset].fill(0xFF);
}
let unscaled = i128::from_be_bytes(i128_bytes);
(scale, unscaled)
}
#[cfg(feature = "rust_decimal")]
mod rust_decimal_tests {
use super::*;
use crate::ingress::DecimalView;
use rust_decimal::Decimal;
use std::convert::TryInto;
use std::str::FromStr;
#[test]
fn test_decimal_binary_format_zero() -> TestResult {
let dec = Decimal::ZERO;
let ilp_dec: DecimalView = (&dec).try_into()?;
let result = serialize_decimal(ilp_dec);
let (scale, unscaled) = parse_binary_decimal(&result);
assert_eq!(scale, 0, "Zero should have scale 0");
assert_eq!(unscaled, 0, "Zero should have unscaled value 0");
Ok(())
}
#[test]
fn test_decimal_binary_format_positive() -> TestResult {
let dec = Decimal::from_str("123.45")?;
let ilp_dec: DecimalView = (&dec).try_into()?;
let result = serialize_decimal(ilp_dec);
let (scale, unscaled) = parse_binary_decimal(&result);
assert_eq!(scale, 2, "123.45 should have scale 2");
assert_eq!(unscaled, 12345, "123.45 should have unscaled value 12345");
Ok(())
}
#[test]
fn test_decimal_binary_format_negative() -> TestResult {
let dec = Decimal::from_str("-123.45")?;
let ilp_dec: DecimalView = (&dec).try_into()?;
let result = serialize_decimal(ilp_dec);
let (scale, unscaled) = parse_binary_decimal(&result);
assert_eq!(scale, 2, "-123.45 should have scale 2");
assert_eq!(
unscaled, -12345,
"-123.45 should have unscaled value -12345"
);
Ok(())
}
#[test]
fn test_decimal_binary_format_one() -> TestResult {
let dec = Decimal::ONE;
let ilp_dec: DecimalView = (&dec).try_into()?;
let result = serialize_decimal(ilp_dec);
let (scale, unscaled) = parse_binary_decimal(&result);
assert_eq!(scale, 0, "One should have scale 0");
assert_eq!(unscaled, 1, "One should have unscaled value 1");
Ok(())
}
#[test]
fn test_decimal_binary_format_max_scale() -> TestResult {
let dec = Decimal::from_str("0.0000000000000000000000000001")?;
let ilp_dec: DecimalView = (&dec).try_into()?;
let result = serialize_decimal(ilp_dec);
let (scale, unscaled) = parse_binary_decimal(&result);
assert_eq!(scale, 28, "Should have maximum scale of 28");
assert_eq!(unscaled, 1, "Should have unscaled value 1");
Ok(())
}
#[test]
fn test_decimal_binary_format_large_value() -> TestResult {
let dec = Decimal::MAX;
let ilp_dec: DecimalView = (&dec).try_into()?;
let result = serialize_decimal(ilp_dec);
let (scale, unscaled) = parse_binary_decimal(&result);
assert_eq!(scale, 0, "Large integer should have scale 0");
assert_eq!(
unscaled, 79228162514264337593543950335i128,
"Should have correct unscaled value"
);
Ok(())
}
#[test]
fn test_decimal_binary_format_large_value2() -> TestResult {
let dec = Decimal::MIN;
let ilp_dec: DecimalView = (&dec).try_into()?;
let result = serialize_decimal(ilp_dec);
let (scale, unscaled) = parse_binary_decimal(&result);
assert_eq!(scale, 0, "Large integer should have scale 0");
assert_eq!(
unscaled, -79228162514264337593543950335i128,
"Should have correct unscaled value"
);
Ok(())
}
#[test]
fn test_decimal_binary_format_small_negative() -> TestResult {
let dec = Decimal::from_str("-0.01")?;
let ilp_dec: DecimalView = (&dec).try_into()?;
let result = serialize_decimal(ilp_dec);
let (scale, unscaled) = parse_binary_decimal(&result);
assert_eq!(scale, 2, "-0.01 should have scale 2");
assert_eq!(unscaled, -1, "-0.01 should have unscaled value -1");
Ok(())
}
#[test]
fn test_decimal_binary_format_trailing_zeros() -> TestResult {
let dec = Decimal::from_str("1.00")?;
let ilp_dec: DecimalView = (&dec).try_into()?;
let result = serialize_decimal(ilp_dec);
let (scale, unscaled) = parse_binary_decimal(&result);
assert_eq!(scale, 2, "1.00 should have scale 2");
assert_eq!(unscaled, 100, "1.00 should have unscaled value 100");
Ok(())
}
}
#[cfg(feature = "bigdecimal")]
mod bigdecimal_tests {
use super::*;
use crate::ingress::DecimalView;
use bigdecimal::BigDecimal;
use std::convert::TryInto;
use std::str::FromStr;
#[test]
fn test_bigdecimal_binary_format_zero() -> TestResult {
let dec = BigDecimal::from_str("0")?;
let ilp_dec: DecimalView = (&dec).try_into()?;
let result = serialize_decimal(ilp_dec);
let (scale, unscaled) = parse_binary_decimal(&result);
assert_eq!(scale, 0, "Zero should have scale 0");
assert_eq!(unscaled, 0, "Zero should have unscaled value 0");
Ok(())
}
#[test]
fn test_bigdecimal_binary_format_positive() -> TestResult {
let dec = BigDecimal::from_str("123.45")?;
let ilp_dec: DecimalView = (&dec).try_into()?;
let result = serialize_decimal(ilp_dec);
let (scale, unscaled) = parse_binary_decimal(&result);
assert_eq!(scale, 2, "123.45 should have scale 2");
assert_eq!(unscaled, 12345, "123.45 should have unscaled value 12345");
Ok(())
}
#[test]
fn test_bigdecimal_binary_format_negative() -> TestResult {
let dec = BigDecimal::from_str("-123.45")?;
let ilp_dec: DecimalView = (&dec).try_into()?;
let result = serialize_decimal(ilp_dec);
let (scale, unscaled) = parse_binary_decimal(&result);
assert_eq!(scale, 2, "-123.45 should have scale 2");
assert_eq!(
unscaled, -12345,
"-123.45 should have unscaled value -12345"
);
Ok(())
}
#[test]
fn test_bigdecimal_binary_format_one() -> TestResult {
let dec = BigDecimal::from_str("1")?;
let ilp_dec: DecimalView = (&dec).try_into()?;
let result = serialize_decimal(ilp_dec);
let (scale, unscaled) = parse_binary_decimal(&result);
assert_eq!(scale, 0, "One should have scale 0");
assert_eq!(unscaled, 1, "One should have unscaled value 1");
Ok(())
}
#[test]
fn test_bigdecimal_binary_format_high_precision() -> TestResult {
let dec = BigDecimal::from_str("0.123456789012345678901234567890")?;
let ilp_dec: DecimalView = (&dec).try_into()?;
let result = serialize_decimal(ilp_dec);
let (scale, unscaled) = parse_binary_decimal(&result);
assert_eq!(scale, 30, "Should preserve high precision scale");
assert_eq!(
unscaled, 123456789012345678901234567890i128,
"Should have correct unscaled value"
);
Ok(())
}
#[test]
fn test_bigdecimal_binary_format_large_value() -> TestResult {
let dec = BigDecimal::from_str("79228162514264337593543950335")?;
let ilp_dec: DecimalView = (&dec).try_into()?;
let result = serialize_decimal(ilp_dec);
let (scale, unscaled) = parse_binary_decimal(&result);
assert_eq!(scale, 0, "Large integer should have scale 0");
assert_eq!(
unscaled, 79228162514264337593543950335i128,
"Should have correct unscaled value"
);
Ok(())
}
#[test]
fn test_bigdecimal_binary_format_large_negative() -> TestResult {
let dec = BigDecimal::from_str("-79228162514264337593543950335")?;
let ilp_dec: DecimalView = (&dec).try_into()?;
let result = serialize_decimal(ilp_dec);
let (scale, unscaled) = parse_binary_decimal(&result);
assert_eq!(scale, 0, "Large negative integer should have scale 0");
assert_eq!(
unscaled, -79228162514264337593543950335i128,
"Should have correct unscaled value"
);
Ok(())
}
#[test]
fn test_bigdecimal_binary_format_small_negative() -> TestResult {
let dec = BigDecimal::from_str("-0.01")?;
let ilp_dec: DecimalView = (&dec).try_into()?;
let result = serialize_decimal(ilp_dec);
let (scale, unscaled) = parse_binary_decimal(&result);
assert_eq!(scale, 2, "-0.01 should have scale 2");
assert_eq!(unscaled, -1, "-0.01 should have unscaled value -1");
Ok(())
}
#[test]
fn test_bigdecimal_binary_format_trailing_zeros() -> TestResult {
let dec = BigDecimal::from_str("1.00")?;
let ilp_dec: DecimalView = (&dec).try_into()?;
let result = serialize_decimal(ilp_dec);
let (scale, unscaled) = parse_binary_decimal(&result);
assert_eq!(scale, 2, "1.00 should have scale 2");
assert_eq!(unscaled, 100, "1.00 should have unscaled value 100");
Ok(())
}
#[test]
fn test_bigdecimal_binary_format_max_scale() -> TestResult {
let dec = BigDecimal::from_str(
"0.0000000000000000000000000000000000000000000000000000000000000000000000000001",
)?;
let ilp_dec: DecimalView = (&dec).try_into()?;
let result = serialize_decimal(ilp_dec);
let (scale, unscaled) = parse_binary_decimal(&result);
assert_eq!(scale, 76, "Should have maximum scale of 76");
assert_eq!(unscaled, 1, "Should have unscaled value 1");
Ok(())
}
#[test]
fn test_bigdecimal_binary_format_exceeds_max_scale() -> TestResult {
let dec = BigDecimal::from_str(
"0.00000000000000000000000000000000000000000000000000000000000000000000000000001",
)?;
let result: crate::Result<DecimalView> = (&dec).try_into();
assert_err_contains(result, ErrorCode::InvalidDecimal, "scale greater than 76");
Ok(())
}
#[test]
fn test_bigdecimal_binary_negative_scale() -> TestResult {
let dec = BigDecimal::from_str("1.23e12")?;
let ilp_dec: DecimalView = (&dec).try_into()?;
let result = serialize_decimal(ilp_dec);
let (scale, unscaled) = parse_binary_decimal(&result);
assert_eq!(scale, 0, "Should have scale of 0");
assert_eq!(
unscaled, 1230000000000,
"Should have unscaled value 1230000000000"
);
Ok(())
}
#[test]
fn test_bigdecimal_binary_value_too_large() -> TestResult {
let dec = BigDecimal::from_str("1e1000")?;
let result: crate::Result<DecimalView> = (&dec).try_into();
assert_err_contains(result, ErrorCode::InvalidDecimal, "decimal longer than");
Ok(())
}
}
#[rstest]
fn test_buffer_column_decimal_str(
#[values(ProtocolVersion::V3)] version: ProtocolVersion,
) -> TestResult {
let mut buffer = Buffer::new(version);
buffer
.table("test")?
.symbol("sym", "val")?
.column_dec("dec", "123.45")?
.at_now()?;
let output = std::str::from_utf8(buffer.as_bytes())?;
assert!(output.starts_with("test,sym=val dec=123.45d"));
Ok(())
}
#[rstest]
fn test_buffer_column_decimal_str_unsupported(
#[values(ProtocolVersion::V1, ProtocolVersion::V2)] version: ProtocolVersion,
) -> TestResult {
let mut buffer = Buffer::new(version);
let result = buffer
.table("test")?
.symbol("sym", "val")?
.column_dec("dec", "123.45");
assert_err_contains(
result,
ErrorCode::ProtocolVersionError,
"does not support the decimal datatype",
);
Ok(())
}
#[cfg(feature = "rust_decimal")]
#[test]
fn test_buffer_column_decimal_rust_decimal() -> TestResult {
use rust_decimal::Decimal;
use std::str::FromStr;
let mut buffer = Buffer::new(ProtocolVersion::V3);
let dec = Decimal::from_str("123.45")?;
buffer
.table("test")?
.symbol("sym", "val")?
.column_dec("dec", &dec)?
.at_now()?;
let bytes = buffer.as_bytes();
assert!(bytes.starts_with(b"test,sym=val dec="));
assert!(bytes.ends_with(b"\n"));
let dec_binary = &bytes[17..bytes.len() - 1];
let (scale, unscaled) = parse_binary_decimal(dec_binary);
assert_eq!(scale, 2, "123.45 should have scale 2");
assert_eq!(unscaled, 12345, "123.45 should have unscaled value 12345");
Ok(())
}
#[test]
fn test_buffer_multiple_decimals() -> TestResult {
let mut buffer = Buffer::new(ProtocolVersion::V3);
buffer
.table("test")?
.column_dec("dec1", "123.45")?
.column_dec("dec2", "-67.89")?
.column_dec("dec3", "0.001")?
.at_now()?;
let output = std::str::from_utf8(buffer.as_bytes())?;
assert!(output.contains("dec1=123.45d"));
assert!(output.contains("dec2=-67.89d"));
assert!(output.contains("dec3=0.001d"));
Ok(())
}
#[test]
fn test_decimal_column_name_too_long() -> TestResult {
let mut buffer = Buffer::with_max_name_len(ProtocolVersion::V3, 4);
let name = "a name too long";
let err = buffer.table("tbl")?.column_dec(name, "123.45").unwrap_err();
assert_eq!(err.code(), ErrorCode::InvalidName);
assert_eq!(
err.msg(),
r#"Bad name: "a name too long": Too long (max 4 characters)"#
);
Ok(())
}
#[cfg(feature = "bigdecimal")]
#[test]
fn test_buffer_column_decimal_bigdecimal() -> TestResult {
use bigdecimal::BigDecimal;
use std::str::FromStr;
let mut buffer = Buffer::new(ProtocolVersion::V3);
let dec = BigDecimal::from_str("123.45")?;
buffer
.table("test")?
.symbol("sym", "val")?
.column_dec("dec", &dec)?
.at_now()?;
let bytes = buffer.as_bytes();
assert!(bytes.starts_with(b"test,sym=val dec="));
assert!(bytes.ends_with(b"\n"));
let dec_binary = &bytes[17..bytes.len() - 1];
let (scale, unscaled) = parse_binary_decimal(dec_binary);
assert_eq!(scale, 2, "123.45 should have scale 2");
assert_eq!(unscaled, 12345, "123.45 should have unscaled value 12345");
Ok(())
}