iridium_core 0.1.0

SQL Server-compatible Rust engine core for Iridium SQL
Documentation
use super::super::value_helpers::{rescale_raw, value_to_f64};
use crate::types::Value;
use chrono::{NaiveDate, NaiveDateTime};
use std::cmp::Ordering;

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ValueCategory {
    Integer,
    Float,
    Decimal,
    Money,
    String,
    Binary,
    DateTime,
    Uuid,
    Null,
}

pub fn categorize(v: &Value) -> ValueCategory {
    match v {
        Value::Null => ValueCategory::Null,
        Value::Bit(_)
        | Value::TinyInt(_)
        | Value::SmallInt(_)
        | Value::Int(_)
        | Value::BigInt(_) => ValueCategory::Integer,
        Value::Float(_) => ValueCategory::Float,
        Value::Decimal(_, _) => ValueCategory::Decimal,
        Value::Money(_) | Value::SmallMoney(_) => ValueCategory::Money,
        Value::Char(_) | Value::VarChar(_) | Value::NChar(_) | Value::NVarChar(_) => {
            ValueCategory::String
        }
        Value::Binary(_) | Value::VarBinary(_) => ValueCategory::Binary,
        Value::Date(_) | Value::Time(_) | Value::DateTime(_) | Value::DateTime2(_) => {
            ValueCategory::DateTime
        }
        Value::UniqueIdentifier(_) => ValueCategory::Uuid,
        Value::SqlVariant(inner) => categorize(inner),
    }
}

pub fn compare_values(a: &Value, b: &Value) -> Ordering {
    let a = unwrap_sql_variant(a);
    let b = unwrap_sql_variant(b);

    let cat_a = categorize(&a);
    let cat_b = categorize(&b);

    match (cat_a, cat_b) {
        (ValueCategory::Null, ValueCategory::Null) => Ordering::Equal,
        (ValueCategory::Null, _) => Ordering::Less,
        (_, ValueCategory::Null) => Ordering::Greater,

        (ValueCategory::Integer, ValueCategory::Integer) => {
            let ai = a.to_integer_i64().unwrap_or(0);
            let bi = b.to_integer_i64().unwrap_or(0);
            ai.cmp(&bi)
        }

        (ValueCategory::Float, ValueCategory::Float)
        | (ValueCategory::Float, ValueCategory::Integer)
        | (ValueCategory::Integer, ValueCategory::Float)
        | (ValueCategory::Float, ValueCategory::Decimal)
        | (ValueCategory::Decimal, ValueCategory::Float) => {
            let af = value_to_f64(&a).unwrap_or(0.0);
            let bf = value_to_f64(&b).unwrap_or(0.0);
            af.partial_cmp(&bf).unwrap_or(Ordering::Equal)
        }

        (ValueCategory::Decimal, ValueCategory::Decimal)
        | (ValueCategory::Decimal, ValueCategory::Integer)
        | (ValueCategory::Integer, ValueCategory::Decimal) => {
            let (a_dec, b_dec) = to_comparable_decimals(&a, &b);
            a_dec.cmp(&b_dec)
        }

        (ValueCategory::Money, ValueCategory::Money)
        | (ValueCategory::Money, ValueCategory::Integer)
        | (ValueCategory::Integer, ValueCategory::Money)
        | (ValueCategory::Money, ValueCategory::Decimal)
        | (ValueCategory::Decimal, ValueCategory::Money) => {
            let am = extract_money_raw(&a);
            let bm = extract_money_raw(&b);
            am.cmp(&bm)
        }

        (ValueCategory::Money, ValueCategory::Float)
        | (ValueCategory::Float, ValueCategory::Money) => {
            let af = value_to_f64(&a).unwrap_or(0.0);
            let bf = value_to_f64(&b).unwrap_or(0.0);
            af.partial_cmp(&bf).unwrap_or(Ordering::Equal)
        }

        (ValueCategory::String, ValueCategory::String) => {
            extract_string(&a).cmp(&extract_string(&b))
        }

        (ValueCategory::Integer, ValueCategory::String)
        | (ValueCategory::Decimal, ValueCategory::String)
        | (ValueCategory::Float, ValueCategory::String)
        | (ValueCategory::Money, ValueCategory::String) => compare_numeric_with_string(&a, &b),

        (ValueCategory::String, ValueCategory::Integer)
        | (ValueCategory::String, ValueCategory::Decimal)
        | (ValueCategory::String, ValueCategory::Float)
        | (ValueCategory::String, ValueCategory::Money) => compare_numeric_with_string(&a, &b),

        (ValueCategory::DateTime, ValueCategory::DateTime) => {
            if let (Some(da), Some(db)) = (normalize_datetime(&a), normalize_datetime(&b)) {
                da.cmp(&db)
            } else {
                extract_string(&a).cmp(&extract_string(&b))
            }
        }

        (ValueCategory::DateTime, ValueCategory::String)
        | (ValueCategory::String, ValueCategory::DateTime) => {
            a.to_string_value().cmp(&b.to_string_value())
        }

        (ValueCategory::Uuid, ValueCategory::Uuid) => extract_string(&a).cmp(&extract_string(&b)),

        (ValueCategory::Binary, ValueCategory::Binary) => extract_bytes(&a).cmp(extract_bytes(&b)),

        _ => value_key(&a).cmp(&value_key(&b)),
    }
}

pub fn truthy(value: &Value) -> bool {
    match value {
        Value::Null => false,
        Value::Bit(v) => *v,
        Value::TinyInt(v) => *v != 0,
        Value::SmallInt(v) => *v != 0,
        Value::Int(v) => *v != 0,
        Value::BigInt(v) => *v != 0,
        Value::Float(v) => f64::from_bits(*v) != 0.0,
        Value::Decimal(raw, _) => *raw != 0,
        Value::Money(v) => *v != 0,
        Value::SmallMoney(v) => *v != 0,
        Value::Char(v) | Value::VarChar(v) | Value::NChar(v) | Value::NVarChar(v) => !v.is_empty(),
        Value::Binary(v) | Value::VarBinary(v) => !v.is_empty(),
        Value::Date(_)
        | Value::Time(_)
        | Value::DateTime(_)
        | Value::DateTime2(_)
        | Value::UniqueIdentifier(_) => true,
        Value::SqlVariant(inner) => truthy(inner),
    }
}

pub fn value_key(v: &Value) -> String {
    match v {
        Value::Null => "NULL".to_string(),
        Value::Bit(v) => format!("BIT:{}", v),
        Value::TinyInt(v) => format!("TINYINT:{}", v),
        Value::SmallInt(v) => format!("SMALLINT:{}", v),
        Value::Int(v) => format!("INT:{}", v),
        Value::BigInt(v) => format!("BIGINT:{}", v),
        Value::Float(v) => format!("FLOAT:{:?}", f64::from_bits(*v)),
        Value::Decimal(raw, scale) => format!("DECIMAL:{}:{}", raw, scale),
        Value::Money(v) => format!("MONEY:{}", v),
        Value::SmallMoney(v) => format!("SMALLMONEY:{}", v),
        Value::Char(v) => format!("CHAR:{}", v),
        Value::VarChar(v) => format!("VARCHAR:{}", v),
        Value::NChar(v) => format!("NCHAR:{}", v),
        Value::NVarChar(v) => format!("NVARCHAR:{}", v),
        Value::Binary(v) => format!("BINARY:{}", crate::types::format_binary(v)),
        Value::VarBinary(v) => format!("VARBINARY:{}", crate::types::format_binary(v)),
        Value::Date(v) => format!("DATE:{}", v),
        Value::Time(v) => format!("TIME:{}", v),
        Value::DateTime(v) => format!("DATETIME:{}", v),
        Value::DateTime2(v) => format!("DATETIME2:{}", v),
        Value::UniqueIdentifier(v) => format!("UNIQUEIDENTIFIER:{}", v),
        Value::SqlVariant(inner) => format!("SQL_VARIANT:{}", value_key(inner)),
    }
}

fn unwrap_sql_variant(v: &Value) -> Value {
    match v {
        Value::SqlVariant(inner) => unwrap_sql_variant(inner),
        other => other.clone(),
    }
}

fn compare_numeric_with_string(num: &Value, str_val: &Value) -> Ordering {
    let num_str = extract_string(num);
    if let Some((ar, as_)) = parse_string_as_numeric(&num_str) {
        let str_parsed = parse_string_as_numeric(&extract_string(str_val));
        if let Some((br, bs)) = str_parsed {
            let (an, bn) = normalize_decimals(ar, as_, br, bs);
            return an.cmp(&bn);
        }
    }
    num.to_string_value().cmp(&str_val.to_string_value())
}

fn extract_string(v: &Value) -> String {
    match v {
        Value::Char(s) | Value::VarChar(s) | Value::NChar(s) | Value::NVarChar(s) => s.clone(),
        Value::Date(d) => d.format("%Y-%m-%d").to_string(),
        Value::Time(t) => t.format("%H:%M:%S%.f").to_string(),
        Value::DateTime(dt) | Value::DateTime2(dt) => dt.format("%Y-%m-%d %H:%M:%S%.f").to_string(),
        _ => String::new(),
    }
}

fn extract_bytes(v: &Value) -> &[u8] {
    match v {
        Value::Binary(b) | Value::VarBinary(b) => b,
        _ => &[],
    }
}

fn normalize_datetime(v: &Value) -> Option<NaiveDateTime> {
    match v {
        Value::Date(d) => d.and_hms_opt(0, 0, 0),
        Value::Time(t) => NaiveDate::from_ymd_opt(1900, 1, 1).map(|d| d.and_time(*t)),
        Value::DateTime(dt) | Value::DateTime2(dt) => Some(*dt),
        Value::SqlVariant(inner) => normalize_datetime(inner),
        _ => None,
    }
}

fn extract_money_raw(v: &Value) -> i128 {
    match v {
        Value::Money(r) => *r,
        Value::SmallMoney(r) => *r as i128,
        Value::Decimal(raw, scale) => rescale_raw(*raw, *scale, 4),
        Value::Int(v) => *v as i128 * 10000,
        Value::BigInt(v) => *v as i128 * 10000,
        Value::TinyInt(v) => *v as i128 * 10000,
        Value::SmallInt(v) => *v as i128 * 10000,
        _ => 0,
    }
}

fn normalize_decimals(ar: i128, as_: u8, br: i128, bs: u8) -> (i128, i128) {
    let max_scale = as_.max(bs);
    let an = rescale_raw(ar, as_, max_scale);
    let bn = rescale_raw(br, bs, max_scale);
    (an, bn)
}

fn to_comparable_decimals(a: &Value, b: &Value) -> (i128, i128) {
    let (ar, as_) = match a {
        Value::Decimal(r, s) => (*r, *s),
        _ => (a.to_integer_i64().unwrap_or(0) as i128, 0),
    };
    let (br, bs) = match b {
        Value::Decimal(r, s) => (*r, *s),
        _ => (b.to_integer_i64().unwrap_or(0) as i128, 0),
    };
    normalize_decimals(ar, as_, br, bs)
}

fn parse_string_as_numeric(input: &str) -> Option<(i128, u8)> {
    let s = input.trim();
    if s.is_empty() {
        return None;
    }
    if let Ok(i) = s.parse::<i128>() {
        return Some((i, 0));
    }
    let negative = s.starts_with('-');
    let core = if negative || s.starts_with('+') {
        &s[1..]
    } else {
        s
    };
    let parts: Vec<&str> = core.splitn(2, '.').collect();
    if parts.len() != 2 {
        return None;
    }
    let whole = parts[0].parse::<i128>().ok()?;
    let frac_raw = parts[1];
    if frac_raw.is_empty() || !frac_raw.chars().all(|c| c.is_ascii_digit()) {
        return None;
    }
    let scale = frac_raw.len() as u8;
    let frac = frac_raw.parse::<i128>().ok()?;
    let mut raw = whole * 10i128.pow(scale as u32) + frac;
    if negative {
        raw = -raw;
    }
    Some((raw, scale))
}