#![allow(dead_code, private_interfaces)]
use std::str::FromStr;
use umbral::orm::{DecimalSpec, Model, SqlType};
#[derive(Debug, Clone, sqlx::FromRow, serde::Serialize, serde::Deserialize, umbral::orm::Model)]
#[umbral(table = "umbral_decimal_precision_price")]
struct Price {
id: i64,
#[umbral(precision = 10, scale = 2)]
amount: rust_decimal::Decimal,
fallback: rust_decimal::Decimal,
}
#[test]
fn precision_scale_attribute_classifies_as_decimaln() {
let by_name: std::collections::HashMap<&str, &umbral::orm::FieldSpec> =
<Price as Model>::FIELDS
.iter()
.map(|f| (f.name, f))
.collect();
match by_name.get("amount").unwrap().ty {
SqlType::DecimalN(DecimalSpec { precision, scale }) => {
assert_eq!(precision, 10);
assert_eq!(scale, 2);
}
other => panic!("expected DecimalN(10, 2), got {other:?}"),
}
assert_eq!(by_name.get("fallback").unwrap().ty, SqlType::Decimal);
}
#[test]
fn postgres_ddl_renders_the_chosen_dimensions() {
use umbral::migrate::{Column, Operation, render_operation_for};
let cols: Vec<Column> = <Price as Model>::FIELDS.iter().map(Column::from).collect();
let op = Operation::CreateTable {
table: "umbral_decimal_precision_price".to_string(),
columns: cols,
indexes: Vec::new(),
unique_together: Vec::new(),
};
let sql = render_operation_for(&op, "postgres")
.join("\n")
.to_lowercase();
assert!(
sql.contains("numeric(10, 2)") || sql.contains("decimal(10, 2)"),
"the precision/scale column must render numeric(10, 2); got:\n{sql}"
);
assert!(
sql.contains("(19, 4)"),
"the attribute-free column keeps the (19, 4) default; got:\n{sql}"
);
}
#[tokio::test]
#[ignore = "needs a live Postgres (UMBRAL_TEST_POSTGRES_URL); Decimal is Postgres-only"]
async fn decimaln_round_trips_and_enforces_scale() {
let Ok(url) = std::env::var("UMBRAL_TEST_POSTGRES_URL") else {
return;
};
let pool = umbral_core::db::connect_postgres(&url)
.await
.expect("pg pool");
let mut settings = umbral::Settings::from_env().expect("settings");
settings.database_url = url.clone();
umbral::App::builder()
.settings(settings)
.database("default", pool.clone())
.model::<Price>()
.build()
.expect("App::build (DecimalN is valid on Postgres)");
sqlx::query("DROP TABLE IF EXISTS umbral_decimal_precision_price")
.execute(&pool)
.await
.unwrap();
umbral_core::migrate::create_tables_for_tests()
.await
.expect("create the numeric(10,2) table");
let (prec, scale): (i32, i32) = sqlx::query_as(
"SELECT numeric_precision, numeric_scale FROM information_schema.columns \
WHERE table_name = 'umbral_decimal_precision_price' AND column_name = 'amount'",
)
.fetch_one(&pool)
.await
.unwrap();
assert_eq!((prec, scale), (10, 2), "column must be numeric(10, 2)");
let meta = umbral::migrate::ModelMeta::for_::<Price>();
let mut body = serde_json::Map::new();
body.insert("amount".into(), serde_json::json!("123.45"));
body.insert("fallback".into(), serde_json::json!("1.0"));
umbral::orm::DynQuerySet::for_meta(&meta)
.insert_json(&body)
.await
.expect("insert a numeric(10,2) row");
let rows = Price::objects().fetch_pg(&pool).await.expect("fetch_pg");
assert_eq!(rows.len(), 1);
assert_eq!(
rows[0].amount,
rust_decimal::Decimal::from_str("123.45").unwrap(),
"the numeric(10,2) value must round-trip exactly"
);
}