quack_protocol 0.1.0

Rust client SDK for DuckDB's experimental Quack remote protocol
Documentation
use chrono::NaiveDate;

use crate::errors::{QuackError, Result};
use crate::vector::{
    DateValue, DecimalValue, IntervalValue, TimeTzValue, TimeUnit, TimeValue, TimestampUnit,
    TimestampValue, Value,
};

pub fn decimal_value(value: impl ToString, width: u64, scale: u64) -> Result<Value> {
    Ok(Value::Decimal(DecimalValue {
        value: parse_decimal_value(&value.to_string(), scale)?,
        width,
        scale,
    }))
}

pub fn date_value(days: i32) -> Value {
    Value::Date(DateValue { days })
}

pub fn date_from_iso_date(value: &str) -> Result<Value> {
    let date = NaiveDate::parse_from_str(value, "%Y-%m-%d")
        .map_err(|_| QuackError::protocol(format!("invalid ISO date {value}")))?;
    let epoch = NaiveDate::from_ymd_opt(1970, 1, 1).expect("valid epoch");
    Ok(date_value((date - epoch).num_days() as i32))
}

pub fn time_value(value: i64, unit: TimeUnit) -> Value {
    Value::Time(TimeValue { unit, value })
}

pub fn time_tz_value(bits: i64) -> Value {
    Value::TimeTz(TimeTzValue { bits })
}

pub fn timestamp_value(value: i64, unit: TimestampUnit, timezone_utc: bool) -> Value {
    Value::Timestamp(TimestampValue {
        unit,
        value,
        timezone_utc,
    })
}

pub fn interval_value(months: i32, days: i32, micros: i64) -> Value {
    Value::Interval(IntervalValue {
        months,
        days,
        micros,
    })
}

fn parse_decimal_value(value: &str, scale: u64) -> Result<i128> {
    let text = value.trim();
    let negative = text.starts_with('-');
    let unsigned = text.strip_prefix(['-', '+']).unwrap_or(text);
    let (integer, fraction) = unsigned.split_once('.').unwrap_or((unsigned, ""));
    if !integer.bytes().all(|byte| byte.is_ascii_digit())
        || !fraction.bytes().all(|byte| byte.is_ascii_digit())
    {
        return Err(QuackError::protocol(format!(
            "invalid decimal value {value}"
        )));
    }
    let mut padded_fraction = fraction.to_string();
    while padded_fraction.len() < scale as usize {
        padded_fraction.push('0');
    }
    padded_fraction.truncate(scale as usize);
    let unscaled = format!(
        "{}{}",
        if integer.is_empty() { "0" } else { integer },
        padded_fraction
    )
    .parse::<i128>()
    .map_err(|_| QuackError::protocol(format!("invalid decimal value {value}")))?;
    Ok(if negative { -unscaled } else { unscaled })
}