use super::*; #[test] fn conditional_writes_compare_and_modify_in_one_statement() { let directory = Directory::new(); let project = Project::load(&fixture().with_file_name("database")).unwrap(); let program = project.check().unwrap(); let binary = build::build( &program, &project.sources, "main", &directory.0.join("conditional"), ) .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":"conditional.db"}}}"#, ) .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 first = run("create", serde_json::json!({"name":"original"})); run("create", serde_json::json!({"name":"other"})); let input = serde_json::json!({"id":first["id"],"expected":"original","note":null,"next":"updated"}); assert_eq!(run("compare_name", input.clone()), 1); assert_eq!(run("compare_name", input), 0); let updated = run("get", serde_json::json!({"id":first["id"]})); assert_eq!(updated["name"], "updated"); assert_eq!(updated["note"], "changed"); let invalid = serde_json::json!({"id":first["id"],"expected":"updated","note":"changed","next":"x".repeat(65)}); assert!( !execute(&binary, "demo.account.compare_name", &invalid.to_string()) .status .success() ); assert_eq!(run("get", serde_json::json!({"id":first["id"]})), updated); assert_eq!( run("remove_matching", serde_json::json!({"rank":7,"note":null})), 1 ); assert_eq!( run("remove_matching", serde_json::json!({"rank":7,"note":null})), 0 ); assert_eq!(run("get", serde_json::json!({"id":first["id"]})), updated); assert_eq!( run( "remove_matching", serde_json::json!({"rank":7,"note":"changed"}) ), 1 ); assert_eq!(run("count", serde_json::json!({})), 0); } #[test] fn static_query_predicates_match_literal_text_and_inclusive_ranges() { let directory = Directory::new(); let project = Project::load(&fixture().with_file_name("database")).unwrap(); let program = project.check().unwrap(); let binary = build::build( &program, &project.sources, "main", &directory.0.join("queries"), ) .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":"query.db"}}}"#, ) .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 names = [ "ab", "Ab", "a\0b", "ab\0", "前雪尾", "雪_%' OR 1=1 --", "excluded", ]; let created: Vec<_> = names .into_iter() .map(|name| run("create", serde_json::json!({"name":name}))) .collect(); let recent = run("recent", serde_json::json!({})); assert_eq!( recent["items"], serde_json::json!(created.iter().rev().collect::>()) ); assert_eq!(recent["has_more"], false); assert!(recent["next"].is_null()); let mut ordered: Vec<_> = created .iter() .filter(|row| row["name"] != "excluded") .cloned() .collect(); ordered.sort_by(|left, right| { left["created_at"] .as_str() .cmp(&right["created_at"].as_str()) .then_with(|| right["name"].as_str().cmp(&left["name"].as_str())) .then_with(|| right["id"].as_i64().cmp(&left["id"].as_i64())) }); for size in [1, 2, 4, 20] { let mut after = serde_json::Value::Null; let mut seen = Vec::new(); loop { let page = run("scan", serde_json::json!({"after":after,"size":size})); let items = page["items"].as_array().unwrap(); assert!(items.len() <= size); seen.extend(items.iter().cloned()); assert!(seen.len() <= ordered.len(), "cursor must advance"); if page["has_more"] == false { assert!(page["next"].is_null()); break; } assert_eq!(page["next"], *items.last().unwrap()); after = page["next"].clone(); } assert_eq!(seen, ordered); } for size in [0, -1, 101] { assert!( !execute( &binary, "demo.account.scan", &serde_json::json!({"after":null,"size":size}).to_string() ) .status .success() ); } for needle in [ "", "a", "A", "ab", "\0", "a\0", "b\0", "雪", "%", "_", "' OR 1=1 --", "missing", ] { let actual = run("probe", serde_json::json!({"needle":needle})); assert_eq!( actual, serde_json::json!({ "contained":names.iter().filter(|name|name.contains(needle)).count(), "prefixed":names.iter().filter(|name|name.starts_with(needle)).count(), "suffixed":names.iter().filter(|name|name.ends_with(needle)).count(), "notes":0 }), "needle {needle:?}" ); } assert_eq!( run("select", serde_json::json!({"first":"ab","second":"Ab"})), 2 ); assert_eq!( run("select", serde_json::json!({"first":"ab","second":"ab"})), 1 ); for (lower, upper) in [("Ab", "ab"), ("ab", "ab"), ("ab", "Ab"), ("", "雪")] { let expected = names .iter() .filter(|name| **name >= lower && **name <= upper && **name != "excluded") .count(); assert_eq!( run("range", serde_json::json!({"lower":lower,"upper":upper})), expected ); } assert_eq!( run( "bulk", serde_json::json!({"first":"bulk one","second":"bulk two"}) ), 2 ); let bulk = run("recent", serde_json::json!({})); assert_eq!(bulk["items"][0]["rank"], 9); assert_eq!(bulk["items"][1]["rank"], 7); let mut unique = std::collections::HashSet::new(); for row in bulk["items"].as_array().unwrap() { let uid = row["uid"].as_str().unwrap(); assert_eq!(&uid[14..15], "7"); assert!(unique.insert(uid)); } let before = run("count", serde_json::json!({})); for (command, input) in [ ( "bulk", serde_json::json!({"first":"must rollback","second":"ab"}), ), ( "failed_bulk", serde_json::json!({"first":"rollback one","second":"rollback two","value":i64::MAX}), ), ] { let output = execute( &binary, &format!("demo.account.{command}"), &input.to_string(), ); assert!(!output.status.success(), "{command}"); assert_eq!(run("count", serde_json::json!({})), before); } let created = run( "save", serde_json::json!({"name":"upsert","initial":"first","replacement":"unused"}), ); assert_eq!(created["note"], "first"); let updated = run( "save", serde_json::json!({"name":"upsert","initial":"ignored","replacement":"changed"}), ); assert_eq!(updated["id"], created["id"]); assert_eq!(updated["created_at"], created["created_at"]); assert_eq!(updated["uid"], created["uid"]); assert_eq!(updated["note"], "changed"); assert_eq!(run("retain", serde_json::json!({"name":"upsert"})), updated); }