edgedb-protocol 0.6.1

Low-level protocol implemenentation for EdgeDB database client. Use edgedb-tokio for applications instead.
Documentation
use super::Decimal;
use crate::model::OutOfRangeError;

impl std::convert::TryFrom<bigdecimal::BigDecimal> for Decimal {
    type Error = OutOfRangeError;
    fn try_from(dec: bigdecimal::BigDecimal) -> Result<Decimal, Self::Error> {
        use num_traits::{ToPrimitive, Zero};
        use std::cmp::max;
        use std::convert::TryInto;

        let mut digits = Vec::new();
        let (v, scale) = dec.into_bigint_and_exponent();
        let (negative, mut val) = match v.sign() {
            num_bigint::Sign::Minus => (true, -v),
            num_bigint::Sign::NoSign => (false, v),
            num_bigint::Sign::Plus => (false, v),
        };
        let scale_4digits = if scale < 0 { scale / 4 } else { scale / 4 + 1 };
        let pad = scale_4digits * 4 - scale;

        if pad > 0 {
            val *= 10u16.pow(pad as u32);
        }
        while !val.is_zero() {
            digits.push((&val % 10000u16).to_u16().unwrap());
            val /= 10000;
        }
        digits.reverse();

        // These return "out of range integral type conversion attempted"
        // which should be good enough for this error
        let decimal_digits = max(0, scale).try_into()?;
        let weight = i16::try_from(digits.len() as i64 - scale_4digits - 1)?;

        // TODO(tailhook) normalization can be optimized here
        Ok(Decimal {
            negative,
            weight,
            decimal_digits,
            digits,
        }
        .normalize())
    }
}

impl From<Decimal> for bigdecimal::BigDecimal {
    fn from(v: Decimal) -> bigdecimal::BigDecimal {
        (&v).into()
    }
}

impl From<&Decimal> for bigdecimal::BigDecimal {
    fn from(val: &Decimal) -> bigdecimal::BigDecimal {
        use bigdecimal::BigDecimal;
        use num_bigint::BigInt;
        use num_traits::pow;
        use std::cmp::max;
        if val.digits.is_empty() {
            return BigDecimal::from(0);
        }

        let mut r = BigInt::from(0);
        // TODO(tailhook) this is quite slow, use preallocated vector
        for &digit in &val.digits {
            r *= 10000;
            r += digit;
        }
        let decimal_stored = 4 * max(0, val.digits.len() as i64 - val.weight as i64 - 1) as usize;
        let pad = if decimal_stored > 0 {
            let pad = decimal_stored as i64 - val.decimal_digits as i64;
            match pad {
                1.. => {
                    r /= pow(10, pad as usize);
                }
                0 => {}
                ..=-1 => {
                    r *= pow(10, (-pad) as usize);
                }
            }

            pad
        } else {
            0
        };

        let scale = if val.decimal_digits == 0 {
            -(val.weight as i64 + 1 - val.digits.len() as i64) * 4 - pad as i64
        } else {
            if decimal_stored == 0 {
                let power =
                    (val.weight as usize + 1 - val.digits.len()) * 4 + val.decimal_digits as usize;
                if power > 0 {
                    r *= pow(BigInt::from(10), power);
                }
            }
            val.decimal_digits as i64
        };
        if val.negative {
            r = -r;
        }
        BigDecimal::new(r, scale)
    }
}

#[cfg(test)]
mod test {
    use super::super::test_helpers::{gen_i64, gen_u64};
    use super::Decimal;
    use bigdecimal::BigDecimal;
    use rand::{rngs::StdRng, Rng, SeedableRng};
    use std::convert::TryFrom;
    use std::str::FromStr;

    #[test]
    fn decimal_conversion() -> Result<(), Box<dyn std::error::Error>> {
        let x = Decimal::try_from(BigDecimal::from_str("42.00")?)?;
        assert_eq!(x.weight, 0);
        assert_eq!(x.decimal_digits, 2);
        assert_eq!(x.digits, &[42]);
        let x = Decimal::try_from(BigDecimal::from_str("42.07")?)?;
        assert_eq!(x.weight, 0);
        assert_eq!(x.decimal_digits, 2);
        assert_eq!(x.digits, &[42, 700]);
        let x = Decimal::try_from(BigDecimal::from_str("0.07")?)?;
        assert_eq!(x.weight, -1);
        assert_eq!(x.decimal_digits, 2);
        assert_eq!(x.digits, &[700]);
        let x = Decimal::try_from(BigDecimal::from_str("420000.00")?)?;
        assert_eq!(x.weight, 1);
        assert_eq!(x.decimal_digits, 2);
        assert_eq!(x.digits, &[42]);

        let x = Decimal::try_from(BigDecimal::from_str("-42.00")?)?;
        assert_eq!(x.weight, 0);
        assert_eq!(x.decimal_digits, 2);
        assert_eq!(x.digits, &[42]);
        let x = Decimal::try_from(BigDecimal::from_str("-42.07")?)?;
        assert_eq!(x.weight, 0);
        assert_eq!(x.decimal_digits, 2);
        assert_eq!(x.digits, &[42, 700]);
        let x = Decimal::try_from(BigDecimal::from_str("-0.07")?)?;
        assert_eq!(x.weight, -1);
        assert_eq!(x.decimal_digits, 2);
        assert_eq!(x.digits, &[700]);
        let x = Decimal::try_from(BigDecimal::from_str(
            "10000000000000000000000000000000000000.00000",
        )?)?;
        assert_eq!(x.digits, &[10]);
        assert_eq!(x.weight, 9);
        assert_eq!(x.decimal_digits, 5);
        let x = Decimal::try_from(BigDecimal::from_str("1e100")?)?;
        assert_eq!(x.weight, 25);
        assert_eq!(x.decimal_digits, 0);
        assert_eq!(x.digits, &[1]);
        let x = Decimal::try_from(BigDecimal::from_str(
            "-703367234220692490200000000000000000000000000",
        )?)?;
        assert_eq!(x.weight, 11);
        assert_eq!(x.decimal_digits, 0);
        assert_eq!(x.digits, &[7, 336, 7234, 2206, 9249, 200]);
        let x = Decimal::try_from(BigDecimal::from_str("-7033672342206924902e26")?)?;
        assert_eq!(x.weight, 11);
        assert_eq!(x.decimal_digits, 0);
        assert_eq!(x.digits, &[7, 336, 7234, 2206, 9249, 200]);

        let x = Decimal::try_from(BigDecimal::from_str(
            "6545218855030988517.14400196897187081925e47",
        )?)?;
        assert_eq!(x.weight, 16);
        assert_eq!(x.decimal_digits, 0);
        assert_eq!(
            x.digits,
            &[65, 4521, 8855, 309, 8851, 7144, 19, 6897, 1870, 8192, 5000]
        );
        let x = Decimal::try_from(BigDecimal::from_str(
            "-260399300000000000000000000000000000000000000.\
                000000000007745502260",
        )?)?;
        assert_eq!(x.weight, 11);
        assert_eq!(x.decimal_digits, 21);
        assert_eq!(
            x.digits,
            &[
                2, 6039, 9300, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // decimal digits start here
                0, 7, 7455, 226,
            ]
        );

        Ok(())
    }

    #[test]
    fn convert_special() {
        let orig = Decimal {
            negative: false,
            weight: 0,
            decimal_digits: 1,
            digits: Vec::new(),
        };
        let big: BigDecimal = orig.into();
        assert_eq!(big.to_string(), "0");
        let orig = Decimal {
            negative: false,
            weight: 0,
            decimal_digits: 0,
            digits: Vec::new(),
        };
        let big: BigDecimal = orig.into();
        assert_eq!(big.to_string(), "0");
    }

    fn dec_roundtrip(s: &str) -> BigDecimal {
        let rust = BigDecimal::from_str(s).expect("can parse big decimal");
        let edgedb = Decimal::try_from(rust).expect("can convert for edgedb");
        BigDecimal::from(edgedb)
    }

    #[test]
    fn decimal_roundtrip() -> Result<(), Box<dyn std::error::Error>> {
        use bigdecimal::BigDecimal as B;

        assert_eq!(dec_roundtrip("1"), B::from_str("1")?);
        assert_eq!(dec_roundtrip("1000"), B::from_str("1000")?);
        assert_eq!(dec_roundtrip("1e100"), B::from_str("1e100")?);
        assert_eq!(dec_roundtrip("0"), B::from_str("0")?);
        assert_eq!(dec_roundtrip("-1000"), B::from_str("-1000")?);
        assert_eq!(dec_roundtrip("1.01"), B::from_str("1.01")?);
        assert_eq!(dec_roundtrip("1000.0070"), B::from_str("1000.0070")?);
        assert_eq!(dec_roundtrip("0.00008"), B::from_str("0.00008")?);
        assert_eq!(dec_roundtrip("-1000.1"), B::from_str("-1000.1")?);
        assert_eq!(
            dec_roundtrip("10000000000000000000000000000000000000.00001"),
            B::from_str("10000000000000000000000000000000000000.00001")?
        );
        assert_eq!(
            dec_roundtrip("12345678901234567890012345678901234567890123"),
            B::from_str("12345678901234567890012345678901234567890123")?
        );
        assert_eq!(
            dec_roundtrip("1234567890123456789.012345678901234567890123"),
            B::from_str("1234567890123456789.012345678901234567890123")?
        );
        assert_eq!(
            dec_roundtrip("0.000000000000000000000000000000000000017238"),
            B::from_str("0.000000000000000000000000000000000000017238")?
        );
        assert_eq!(dec_roundtrip("1234.00000"), B::from_str("1234.00000")?);
        assert_eq!(
            dec_roundtrip("10000000000000000000000000000000000000.00000"),
            B::from_str("10000000000000000000000000000000000000.00000")?
        );
        assert_eq!(
            dec_roundtrip("100010001000000000000000000000000000"),
            B::from_str("100010001000000000000000000000000000")?
        );

        Ok(())
    }

    #[test]
    fn decimal_rand_i64() -> Result<(), Box<dyn std::error::Error>> {
        use bigdecimal::BigDecimal as B;

        let mut rng = StdRng::seed_from_u64(1);
        for _ in 0..10000 {
            let head = gen_u64(&mut rng);
            let txt = format!("{}", head);
            assert_eq!(dec_roundtrip(&txt), B::from_str(&txt)?, "parsing: {}", txt);
        }
        Ok(())
    }

    #[test]
    fn decimal_rand_nulls() -> Result<(), Box<dyn std::error::Error>> {
        use bigdecimal::BigDecimal as B;

        let mut rng = StdRng::seed_from_u64(2);
        for iter in 0..10000 {
            let head = gen_u64(&mut rng);
            let nulls = rng.gen_range(0..100);
            let txt = format!("{0}{1:0<2$}", head, "", nulls);
            assert_eq!(
                dec_roundtrip(&txt),
                B::from_str(&txt)?,
                "parsing {}: {}",
                iter,
                txt
            );
        }
        Ok(())
    }

    #[test]
    fn decimal_rand_eplus() -> Result<(), Box<dyn std::error::Error>> {
        use bigdecimal::BigDecimal as B;

        let mut rng = StdRng::seed_from_u64(3);
        for iter in 0..10000 {
            let head = gen_u64(&mut rng);
            let nulls = rng.gen_range(-100..100);
            let txt = format!("{}e{}", head, nulls);
            assert_eq!(
                dec_roundtrip(&txt),
                B::from_str(&txt)?,
                "parsing {}: {}",
                iter,
                txt
            );
        }
        Ok(())
    }

    #[test]
    fn decimal_rand_fract_eplus() -> Result<(), Box<dyn std::error::Error>> {
        use bigdecimal::BigDecimal as B;

        let mut rng = StdRng::seed_from_u64(4);
        for iter in 0..10000 {
            let head = gen_i64(&mut rng);
            let fract = gen_u64(&mut rng);
            let nulls = rng.gen_range(-100..100);
            let txt = format!("{}.{}e{}", head, fract, nulls);
            let rt = dec_roundtrip(&txt);
            let dec = if head == 0 && fract == 0 {
                // Zeros are normalized
                B::from(0)
            } else {
                B::from_str(&txt)?
            };
            assert_eq!(rt, dec, "parsing {}: {}", iter, txt);
            if dec.as_bigint_and_exponent().1 > 0 {
                // check precision
                // (if scale is negative it's integer, we don't have precision)
                assert_eq!(
                    rt.as_bigint_and_exponent().1,
                    dec.as_bigint_and_exponent().1,
                    "precision: {}",
                    txt
                );
            }
        }
        Ok(())
    }

    #[test]
    fn decimal_rand_nulls_eplus() -> Result<(), Box<dyn std::error::Error>> {
        use bigdecimal::BigDecimal as B;

        let mut rng = StdRng::seed_from_u64(5);
        for iter in 0..10000 {
            let head = gen_i64(&mut rng);
            let nulls1 = rng.gen_range(0..100);
            let nulls2 = rng.gen_range(0..100);
            let txt = format!("{0}{1:0<2$}e{3}", head, "", nulls1, nulls2);
            let rt = dec_roundtrip(&txt);
            let dec = B::from_str(&txt)?;
            assert_eq!(rt, dec, "parsing {}: {}", iter, txt);
            if dec.as_bigint_and_exponent().1 > 0 {
                // check precision
                // (if scale is negative it's integer, we don't have precision)
                assert_eq!(
                    rt.as_bigint_and_exponent().1,
                    dec.as_bigint_and_exponent().1,
                    "precision: {}",
                    txt
                );
            }
        }
        Ok(())
    }

    #[test]
    fn decimal_rand_decim() -> Result<(), Box<dyn std::error::Error>> {
        use bigdecimal::BigDecimal as B;

        let mut rng = StdRng::seed_from_u64(6);
        for iter in 0..10000 {
            let head = gen_i64(&mut rng);
            let nulls1 = rng.gen_range(0..100);
            let nulls2 = rng.gen_range(0..100);
            let decimals = gen_u64(&mut rng);
            let txt = format!(
                "{0}{1:0<2$}.{1:0<3$}{4}",
                head, "", nulls1, nulls2, decimals
            );
            let dec = if head == 0 && decimals == 0 {
                // Zeros are normalized
                B::from(0)
            } else {
                B::from_str(&txt)?
            };
            assert_eq!(dec_roundtrip(&txt), dec, "parsing {}: {}", iter, txt);
            assert_eq!(
                dec_roundtrip(&txt).as_bigint_and_exponent().1,
                dec.as_bigint_and_exponent().1,
                "precision: {}",
                txt
            );
        }
        Ok(())
    }
}