Files
core/test/rust/native/application/database/scalar.rs
T
2026-11-03 03:32:07 +00:00

259 lines
8.6 KiB
Rust

use super::*;
#[test]
fn database_scalar_source_preserves_exact_values_and_nullable_updates() {
use dever_runtime::value::decimal::Decimal;
let directory = Directory::new();
scalar_fixture(&directory.0);
let project = Project::load(&directory.0).unwrap();
let program = project.check().unwrap();
let binary = build::build(
&program,
&project.sources,
"main",
&directory.0.join("scalars"),
)
.unwrap();
std::fs::create_dir(directory.0.join("config")).unwrap();
std::fs::write(directory.0.join("config/setting.json"), r#"{"database":{"default":{"type":"sqlite","path":"data/scalars.db","max_connections":1,"max_page_size":20}}}"#).unwrap();
let run = |name: &str, input: serde_json::Value| {
let output = execute(&binary, &format!("demo.account.{name}"), &input.to_string());
assert!(
output.status.success(),
"{name}: {}",
String::from_utf8_lossy(&output.stderr)
);
serde_json::from_slice::<serde_json::Value>(&output.stdout).unwrap()["data"].clone()
};
let base = serde_json::json!({
"amount":"1.0000", "price":"999.99", "uid":"00112233-4455-6677-8899-aabbccddeeff",
"day":"0000-01-01", "time":"23:59:59.999", "elapsed":i64::MIN,
"document":{"items":[true,null,{"name":"雪"}]},
"optional_uid":"ffffffff-ffff-ffff-ffff-ffffffffffff", "optional_day":"9999-12-31",
"optional_time":"00:00:00", "optional_elapsed":i64::MAX, "optional_document":{"value":42}
});
let created = run("create", base.clone());
assert_eq!(created["id"], 1);
assert_eq!(created["amount"], "1");
for field in [
"price",
"uid",
"day",
"time",
"elapsed",
"document",
"optional_uid",
"optional_day",
"optional_time",
"optional_elapsed",
"optional_document",
] {
assert_eq!(created[field], base[field], "{field}");
}
assert_eq!(run("get", serde_json::json!({"id":1})), created);
for field in [
"price",
"optional_uid",
"optional_day",
"optional_time",
"optional_elapsed",
"optional_document",
] {
let mut changed = base.clone();
changed["id"] = 1.into();
changed[field] = serde_json::Value::Null;
assert!(run("update", changed)[field].is_null(), "{field}");
}
for (field, value) in [
("amount", "0.00001"),
("amount", "1000000000000000000000000000000"),
("price", "1000"),
("price", "0.001"),
("day", "2025-02-29"),
("time", "24:00:00"),
("uid", "00112233445566778899aabbccddeeff"),
] {
let mut invalid = base.clone();
invalid[field] = value.into();
invalid["uid"] = if field == "uid" {
value
} else {
"11112233-4455-6677-8899-aabbccddeeff"
}
.into();
let failed = execute(&binary, "demo.account.create", &invalid.to_string());
assert!(!failed.status.success(), "accepted {field}: {value}");
assert_eq!(run("count", serde_json::json!({})), 1);
}
let mut changed = base.clone();
changed["id"] = 1.into();
changed["amount"] = "-999999999999999999999999999999.9999".into();
let minimum = run("update", changed.clone());
assert_eq!(minimum["amount"], changed["amount"]);
for (index, amount) in [
"-10",
"-0.0001",
"0",
"0.0001",
"10",
"999999999999999999999999999999.9999",
]
.iter()
.enumerate()
{
let mut value = base.clone();
value["amount"] = (*amount).into();
value["uid"] = format!("00000000-0000-0000-0000-{:012x}", index).into();
value["price"] = serde_json::Value::Null;
let row = run("create", value);
assert_eq!(
Decimal::parse(row["amount"].as_str().unwrap()).unwrap(),
Decimal::parse(amount).unwrap()
);
assert!(row["price"].is_null());
}
let page = run("search", serde_json::json!({"low":"-10"}));
let actual: Vec<_> = page["items"]
.as_array()
.unwrap()
.iter()
.map(|row| Decimal::parse(row["amount"].as_str().unwrap()).unwrap())
.collect();
assert_eq!(
actual,
[
"-10",
"-0.0001",
"0",
"0.0001",
"10",
"999999999999999999999999999999.9999"
]
.map(|text| Decimal::parse(text).unwrap())
);
assert_eq!(
run("price", serde_json::json!({"price":"999.990"}))["total"],
1
);
assert_eq!(run("price", serde_json::json!({}))["total"], 6);
let reused = run(
"document",
serde_json::json!({"id":1,"document":base["document"]}),
);
assert_eq!(reused["document"], base["document"]);
assert_eq!(reused["optional_document"], base["document"]);
assert_eq!(run("get", serde_json::json!({"id":1})), reused);
let raw =
r#"{"id":1,"document":{"precise":123456789012345678901234567890.0010,"tiny":1e-1000}}"#;
let output = execute(&binary, "demo.account.document", raw);
assert!(
output.status.success(),
"{}",
String::from_utf8_lossy(&output.stderr)
);
let output = String::from_utf8(output.stdout).unwrap();
assert!(output.contains("123456789012345678901234567890.0010"));
assert!(output.contains("1e-1000"));
let output = execute(&binary, "demo.account.get", r#"{"id":1}"#);
assert!(
String::from_utf8(output.stdout)
.unwrap()
.contains("123456789012345678901234567890.0010")
);
}
fn scalar_fixture(root: &Path) {
let fields = [
("amount", "Decimal(34, 4)", "Decimal"),
("price", "Decimal(5, 2)?", "Decimal?"),
("uid", "Uuid", "Uuid"),
("day", "Date", "Date"),
("time", "Time", "Time"),
("elapsed", "Duration", "Duration"),
("document", "Json", "Json"),
("optional_uid", "Uuid?", "Uuid?"),
("optional_day", "Date?", "Date?"),
("optional_time", "Time?", "Time?"),
("optional_elapsed", "Duration?", "Duration?"),
("optional_document", "Json?", "Json?"),
];
let root = root.join("module/demo/account");
std::fs::create_dir_all(&root).unwrap();
let columns = fields
.iter()
.map(|(name, kind, _)| {
format!(
"{name}: {kind}{}\n",
if *name == "uid" { " unique" } else { "" }
)
})
.collect::<String>();
std::fs::write(
root.join("model.dever"),
format!("type Account {{\n{columns}}}\n"),
)
.unwrap();
let input = fields
.iter()
.map(|(name, _, kind)| format!("{name}: {kind}"))
.collect::<Vec<_>>()
.join(", ");
let values = fields
.iter()
.map(|(name, _, _)| format!("{name} = {name}"))
.collect::<Vec<_>>()
.join(", ");
let methods = [
(
"create",
input.clone(),
"record: model.Account",
format!("record = model.create({{ {values} }})"),
),
(
"update",
format!("id: model.id, {input}"),
"record: model.Account",
format!("record = model.update(id, {{ {values} }})"),
),
(
"get",
"id: model.id".into(),
"record: model.Account",
"record = model.get(id)".into(),
),
(
"count",
"".into(),
"total: Int",
"total = model.count()".into(),
),
(
"search",
"low: Decimal".into(),
"page: model.Page",
"page = model.list({ where = amount >= low, order = [amount.asc] })".into(),
),
(
"price",
"price: Decimal?".into(),
"page: model.Page",
"page = model.list({ where = price == price })".into(),
),
(
"document",
"id: model.id, document: Json".into(),
"record: model.Account",
"record = model.update(id, { document = document, optional_document = document })\nrecord.document = document".into(),
),
];
let mut app = String::new();
let mut api = String::new();
for (name, inputs, output, body) in methods {
app.push_str(&format!("{name}({inputs}) ({output}) {{\n{body}\n}}\n"));
api.push_str(&format!("cmd {name} = app.{name}\n"));
}
std::fs::write(root.join("app.dever"), app).unwrap();
std::fs::write(root.join("api.dever"), api).unwrap();
}