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::(&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::(); 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::>() .join(", "); let values = fields .iter() .map(|(name, _, _)| format!("{name} = {name}")) .collect::>() .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(); }