use std::path::{Path, PathBuf};
fn fixture(tag: &str) -> PathBuf {
let dir = std::env::temp_dir().join(format!("quarb-reflabel-{}-{tag}", std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
let db = rusqlite::Connection::open(dir.join("org.db")).unwrap();
db.execute_batch(
"CREATE TABLE employees (id INTEGER PRIMARY KEY, name TEXT NOT NULL,
manager_id INTEGER REFERENCES employees(id));
INSERT INTO employees VALUES
(1, 'Ada', NULL), (2, 'Bo', 1), (3, 'Cy', 1), (4, 'Dee', 2), (5, 'Eve', 2), (6, 'Fay', 4);
CREATE TABLE books (id INTEGER PRIMARY KEY, title TEXT NOT NULL,
authorId INTEGER REFERENCES employees(id),
editor INTEGER REFERENCES employees(id),
editor_id INTEGER REFERENCES employees(id));
INSERT INTO books VALUES (1, 'Emma', 2, 3, 4);",
)
.unwrap();
std::fs::write(
dir.join("org.model"),
"ref /employees/*::manager_id --> /employees/*[::id = $] acyclic;\n",
)
.unwrap();
std::fs::write(
dir.join("boss.model"),
"ref /employees/*::manager_id --> /employees/*[::id = $] as boss acyclic;\n",
)
.unwrap();
std::fs::write(
dir.join("figures.json"),
r#"{"figures": [{"id": 1, "name": "Gaia", "mother_id": null},
{"id": 2, "name": "Uranus", "mother_id": 1},
{"id": 3, "name": "Cronus", "mother_id": 1}]}"#,
)
.unwrap();
std::fs::write(
dir.join("figures.model"),
"ref /figures/*::mother_id --> /figures/*[::id = $] as mother;\n",
)
.unwrap();
dir
}
fn run(dir: &Path, args: &[&str]) -> (bool, String, String) {
let out = std::process::Command::new(env!("CARGO_BIN_EXE_qua"))
.env("QUARB_LOCALE", "C")
.current_dir(dir)
.args(args)
.stdin(std::process::Stdio::null())
.output()
.unwrap();
(
out.status.success(),
String::from_utf8_lossy(&out.stdout).to_string(),
String::from_utf8_lossy(&out.stderr).to_string(),
)
}
fn lines(dir: &Path, args: &[&str]) -> Vec<String> {
let (ok, out, err) = run(dir, args);
assert!(ok, "{args:?}: {err}");
out.lines().map(str::to_string).collect()
}
fn qua(dir: &Path, query: &str) -> Vec<String> {
lines(dir, &[query, "org.db"])
}
fn qua_model(dir: &Path, model: &str, query: &str) -> Vec<String> {
lines(dir, &["--model", model, query, "org.db"])
}
#[test]
fn a_foreign_key_labels_its_edge_without_the_suffix() {
let dir = fixture("fk");
assert_eq!(qua(&dir, "/employees/*->manager @| count"), ["5"]);
assert_eq!(
qua(&dir, "/employees/6(->manager)+::name"),
["Dee", "Bo", "Ada"]
);
assert_eq!(qua(&dir, "/employees/1<-manager::name"), ["Bo", "Cy"]);
assert_eq!(qua(&dir, "/employees/6::manager_id"), ["4"]);
assert_eq!(
qua(&dir, "/employees/*->*[$- = \"manager\"] @| count"),
["5"]
);
assert_eq!(
qua(&dir, "/employees/*->*[$- = \"manager_id\"] @| count"),
["0"]
);
assert_eq!(
qua(
&dir,
"/employees/6::manager_id-->[$- = \"manager\"] @| count"
),
["1"]
);
assert_eq!(qua(&dir, "/employees/*->manager_id @| count"), ["0"]);
assert_eq!(
qua(&dir, "/employees/1<--(/employees/*::manager_id)::name"),
["Bo", "Cy"]
);
let table = qua(&dir, "/employees/* @| edges(->*)").join("\n");
assert!(table.contains("\"manager\""), "{table}");
assert!(!table.contains("manager_id"), "{table}");
}
#[test]
fn camel_case_and_collisions() {
let dir = fixture("camel");
assert_eq!(qua(&dir, "/books/1->author::name"), ["Bo"]);
assert_eq!(qua(&dir, "/books/1->editor::name"), ["Cy"]);
assert_eq!(qua(&dir, "/books/1->editor_id::name"), ["Dee"]);
assert_eq!(qua(&dir, "/books/1->*[$- = \"author\"]::name"), ["Bo"]);
assert_eq!(qua(&dir, "/books/1->*[$- = \"editor\"]::name"), ["Cy"]);
assert_eq!(qua(&dir, "/books/1->*[$- = \"editor_id\"]::name"), ["Dee"]);
assert_eq!(qua(&dir, "/books/1->authorId @| count"), ["0"]);
}
#[test]
fn a_model_ref_on_a_declared_column_refines_the_one_edge() {
let dir = fixture("model");
assert_eq!(
qua_model(&dir, "org.model", "/employees/* @| edges(->*)"),
qua(&dir, "/employees/* @| edges(->*)")
);
assert_eq!(
qua_model(&dir, "org.model", "/employees/*(->manager)+ @| count"),
qua(&dir, "/employees/*(->manager)+ @| count")
);
assert_eq!(
qua_model(&dir, "org.model", "/employees/6(->manager)+::name"),
["Dee", "Bo", "Ada"]
);
assert_eq!(
qua_model(&dir, "org.model", "/employees/*->employees @| count"),
["0"]
);
let db = rusqlite::Connection::open(dir.join("org.db")).unwrap();
db.execute_batch("UPDATE employees SET manager_id = 6 WHERE id = 1;")
.unwrap();
let (ok, _, err) = run(
&dir,
&[
"--model",
"org.model",
"/employees/6(->manager)+::name",
"org.db",
],
);
assert!(!ok, "a cycle under the promise should refuse");
assert!(err.contains("a cycle under the promise"), "{err}");
}
#[test]
fn a_pushed_down_prefilter_keeps_every_edge() {
let dir = fixture("prefilter");
assert_eq!(
qua_model(
&dir,
"org.model",
"/employees/*[::name = \"Dee\"]->manager::name"
),
["Bo"]
);
assert_eq!(
qua_model(
&dir,
"org.model",
"/employees/*[::name = \"Bo\"]<-manager @| count"
),
["2"]
);
assert_eq!(
qua_model(&dir, "org.model", "/employees/*[::id = 1]<-manager::name"),
["Bo", "Cy"]
);
assert_eq!(
qua(&dir, "/employees/*[::name = \"Dee\"]->manager::name"),
["Bo"]
);
assert_eq!(
qua(&dir, "/employees/*[::name = \"Bo\"]<-manager @| count"),
["2"]
);
assert_eq!(
qua_model(
&dir,
"org.model",
"/employees/*[(->manager)+[::name = \"Ada\"]] @| count"
),
["5"]
);
}
#[test]
fn a_ref_as_name_renames_the_edge() {
let dir = fixture("as");
let boss = |q: &str| qua_model(&dir, "boss.model", q);
let table = boss("/employees/* @| edges(->*)").join("\n");
assert!(table.contains("\"boss\""), "{table}");
assert!(!table.contains("\"manager\""), "{table}");
assert_eq!(boss("/employees/*->* @| count"), ["5"]);
assert_eq!(boss("/employees/6(->boss)+::name"), ["Dee", "Bo", "Ada"]);
assert_eq!(boss("/employees/1<-boss::name"), ["Bo", "Cy"]);
assert_eq!(boss("/employees/*->manager @| count"), ["0"]);
assert_eq!(boss("/employees/6->*[$- = \"boss\"] @| count"), ["1"]);
assert_eq!(boss("/employees/6::manager_id-->::name"), ["Dee"]);
let db = rusqlite::Connection::open(dir.join("org.db")).unwrap();
db.execute_batch("UPDATE employees SET manager_id = 6 WHERE id = 1;")
.unwrap();
let (ok, _, err) = run(
&dir,
&[
"--model",
"boss.model",
"/employees/6(->boss)+::name",
"org.db",
],
);
assert!(!ok, "a cycle under the promise should refuse");
assert!(err.contains("a cycle under the promise"), "{err}");
let fig = |q: &str| lines(&dir, &["--model", "figures.model", q, "figures.json"]);
assert_eq!(fig("/figures/1->mother::name"), ["Gaia"]);
assert_eq!(fig("/figures/0<-mother::name"), ["Uranus", "Cronus"]);
assert_eq!(fig("/figures/1->*[$- = \"mother\"] @| count"), ["1"]);
assert_eq!(fig("/figures/1->figures @| count"), ["0"]);
assert_eq!(fig("/figures/*->* @| count"), ["2"]);
}