vantage-sql 0.10.2

Vantage extension for SQL databases (Postgres, MySQL, SQLite)
Documentation
//! Numeric type matrix tests via Table<MysqlDB, Entity>.
//!
//! Tests Decimal, i64, and f64 across column types (VARCHAR, DECIMAL, DOUBLE,
//! FLOAT, BIGINT). Tables pre-created by v5.sql (same shape: id, name, value).

use rust_decimal::Decimal;
use std::str::FromStr;

#[allow(unused_imports)]
use vantage_sql::mysql::MysqlType;
use vantage_sql::mysql::{AnyMysqlType, MysqlDB};
use vantage_table::table::Table;
use vantage_types::entity;

use vantage_dataset::{ReadableDataSet, WritableDataSet};

const MYSQL_URL: &str = "mysql://vantage:vantage@localhost:3306/vantage_v5";

async fn db() -> MysqlDB {
    MysqlDB::connect(MYSQL_URL).await.unwrap()
}

#[entity(MysqlType)]
#[derive(Debug, Clone, PartialEq, Default)]
struct ValDecimal {
    name: String,
    value: Decimal,
}

macro_rules! table_for {
    ($table:expr, $db:expr) => {
        Table::<MysqlDB, ValDecimal>::new($table, $db)
            .with_id_column("id")
            .with_column_of::<String>("name")
            .with_column_of::<Decimal>("value")
    };
}

/// Try replace + read-back.
async fn try_round_trip(
    table: &Table<MysqlDB, ValDecimal>,
    id: &str,
    entity: &ValDecimal,
) -> Result<ValDecimal, String> {
    table.replace(id, entity).await.map_err(|e| e.to_string())?;
    table
        .get(id)
        .await
        .map_err(|e| e.to_string())?
        .ok_or_else(|| "row missing after replace".to_string())
}

fn dec(s: &str) -> Decimal {
    Decimal::from_str(s).unwrap()
}

// ═════════════════════════════════════════════════════════════════════════
// 1. VARCHAR — exact string round-trip
// ═════════════════════════════════════════════════════════════════════════

#[tokio::test]
async fn test_varchar_high_precision() {
    let t = table_for!("decimal_varchar", db().await);
    let orig = ValDecimal {
        name: "hp".into(),
        value: dec("99999999999999999.123456789012345"),
    };
    let fetched = try_round_trip(&t, "vc_hp", &orig).await.unwrap();
    assert_eq!(fetched, orig);
}

// ═════════════════════════════════════════════════════════════════════════
// 2. DECIMAL(p,s) — the column's scale decides what survives
// ═════════════════════════════════════════════════════════════════════════

#[tokio::test]
async fn test_decimal_scale() {
    let db = db().await;
    let narrow = table_for!("decimal_decimal", db.clone());
    let wide = table_for!("decimal_decimal_wide", db);
    let rounded = ValDecimal {
        name: "trunc".into(),
        value: dec("123.123456789"),
    };
    let exact = ValDecimal {
        name: "hp".into(),
        value: dec("99999999999999999.123456789012345"),
    };
    // DECIMAL(20,6) rounds to 6 places; DECIMAL(38,15) keeps all 33 digits
    let fetched = try_round_trip(&narrow, "dd_trunc", &rounded).await.unwrap();
    assert_eq!(fetched.value, dec("123.123457"));
    assert_eq!(try_round_trip(&wide, "dw_hp", &exact).await.unwrap(), exact);
}

// ═════════════════════════════════════════════════════════════════════════
// 3. DOUBLE — f64 precision (~15 significant digits)
// ═════════════════════════════════════════════════════════════════════════

#[tokio::test]
async fn test_double_simple() {
    let t = table_for!("decimal_double", db().await);
    let orig = ValDecimal {
        name: "d".into(),
        value: dec("123.456"),
    };
    let fetched = try_round_trip(&t, "dbl_s", &orig).await.unwrap();
    // DOUBLE may lose trailing precision
    let diff = (fetched.value - orig.value).abs();
    assert!(diff < dec("0.001"), "diff too large: {}", diff);
}

// ═════════════════════════════════════════════════════════════════════════
// 5. BIGINT — integer only, fractional part lost
// ═════════════════════════════════════════════════════════════════════════

#[tokio::test]
async fn test_bigint_rounds_fraction() {
    let t = table_for!("decimal_bigint", db().await);
    let orig = ValDecimal {
        name: "frac".into(),
        value: dec("123.999"),
    };
    let fetched = try_round_trip(&t, "bi_frac", &orig).await.unwrap();
    // BIGINT rounds the fractional part
    assert_eq!(fetched.value, dec("124"));
}

// ═════════════════════════════════════════════════════════════════════════
// i64 across column types
// ═════════════════════════════════════════════════════════════════════════

#[entity(MysqlType)]
#[derive(Debug, Clone, PartialEq, Default)]
struct ValI64 {
    name: String,
    value: i64,
}

macro_rules! table_i64 {
    ($table:expr, $db:expr) => {
        Table::<MysqlDB, ValI64>::new($table, $db)
            .with_id_column("id")
            .with_column_of::<String>("name")
            .with_column_of::<i64>("value")
    };
}

async fn try_i64(
    table: &Table<MysqlDB, ValI64>,
    id: &str,
    entity: &ValI64,
) -> Result<ValI64, String> {
    table.replace(id, entity).await.map_err(|e| e.to_string())?;
    table
        .get(id)
        .await
        .map_err(|e| e.to_string())?
        .ok_or_else(|| "row missing after replace".to_string())
}

#[tokio::test]
async fn test_i64_varchar() {
    let t = table_i64!("decimal_varchar", db().await);
    let orig = ValI64 {
        name: "i".into(),
        value: 42,
    };
    let fetched = try_i64(&t, "i64_vc", &orig).await.unwrap();
    assert_eq!(fetched, orig);
}

#[tokio::test]
async fn test_i64_bigint() {
    let t = table_i64!("decimal_bigint", db().await);
    let orig = ValI64 {
        name: "i".into(),
        value: i64::MAX,
    };
    let fetched = try_i64(&t, "i64_bi", &orig).await.unwrap();
    assert_eq!(fetched, orig);
}

#[tokio::test]
async fn test_i64_decimal() {
    // DECIMAL returns Tag(10, Text) — no Decimal→Integer cross-conversion
    let t = table_i64!("decimal_decimal", db().await);
    let orig = ValI64 {
        name: "i".into(),
        value: 42,
    };
    assert!(try_i64(&t, "i64_dec", &orig).await.is_err());
}

// ═════════════════════════════════════════════════════════════════════════
// f64 across column types
// ═════════════════════════════════════════════════════════════════════════

#[entity(MysqlType)]
#[derive(Debug, Clone, PartialEq, Default)]
struct ValF64 {
    name: String,
    value: f64,
}

macro_rules! table_f64 {
    ($table:expr, $db:expr) => {
        Table::<MysqlDB, ValF64>::new($table, $db)
            .with_id_column("id")
            .with_column_of::<String>("name")
            .with_column_of::<f64>("value")
    };
}

async fn try_f64(
    table: &Table<MysqlDB, ValF64>,
    id: &str,
    entity: &ValF64,
) -> Result<ValF64, String> {
    table.replace(id, entity).await.map_err(|e| e.to_string())?;
    table
        .get(id)
        .await
        .map_err(|e| e.to_string())?
        .ok_or_else(|| "row missing after replace".to_string())
}

#[tokio::test]
async fn test_f64_varchar() {
    let t = table_f64!("decimal_varchar", db().await);
    let orig = ValF64 {
        name: "f".into(),
        value: 123.456,
    };
    let fetched = try_f64(&t, "f64_vc", &orig).await.unwrap();
    assert_eq!(fetched, orig);
}

#[tokio::test]
async fn test_f64_double() {
    let t = table_f64!("decimal_double", db().await);
    let orig = ValF64 {
        name: "f".into(),
        value: 123.456,
    };
    let fetched = try_f64(&t, "f64_dbl", &orig).await.unwrap();
    assert_eq!(fetched, orig);
}

#[tokio::test]
async fn test_f64_float() {
    // FLOAT is f32 — precision loss
    let t = table_f64!("decimal_float", db().await);
    let orig = ValF64 {
        name: "f".into(),
        value: 123.456,
    };
    let fetched = try_f64(&t, "f64_flt", &orig).await.unwrap();
    assert!((fetched.value - orig.value).abs() < 0.01, "diff too large");
}

#[tokio::test]
async fn test_f64_decimal() {
    // DECIMAL returns Tag(10, Text) — no Decimal→Float cross-conversion
    let t = table_f64!("decimal_decimal", db().await);
    let orig = ValF64 {
        name: "f".into(),
        value: 123.456,
    };
    assert!(try_f64(&t, "f64_dec", &orig).await.is_err());
}