use std::path::{Path, PathBuf};
fn fixture(tag: &str) -> PathBuf {
let dir = std::env::temp_dir().join(format!("quarb-edgerows-{}-{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("flights.db")).unwrap();
db.execute_batch(
"CREATE TABLE airports (code TEXT PRIMARY KEY, city TEXT NOT NULL);
INSERT INTO airports VALUES ('LHR', 'London'), ('JFK', 'New York'), ('CDG', 'Paris');
CREATE TABLE flights (id INTEGER PRIMARY KEY, no TEXT NOT NULL, carrier TEXT,
origin TEXT NOT NULL REFERENCES airports(code),
dest TEXT NOT NULL REFERENCES airports(code),
minutes INTEGER NOT NULL);
INSERT INTO flights VALUES
(1, 'QB100', 'panam', 'LHR', 'JFK', 460),
(2, 'QB102', 'swissair', 'LHR', 'JFK', 490),
(3, 'QB101', 'panam', 'JFK', 'LHR', 420),
(4, 'QB200', 'swissair', 'LHR', 'CDG', 75),
(5, 'QB300', NULL, 'CDG', 'LHR', 80);",
)
.unwrap();
std::fs::write(
dir.join("flights.model"),
"ref /flights/*::origin --> /airports/*[::code = $];\n\
ref /flights/*::dest --> /airports/*[::code = $];\n\
edge /flights/*: ::origin -- ::dest as flight;\n",
)
.unwrap();
std::fs::write(
dir.join("carriers.model"),
"ref /flights/*::origin --> /airports/*[::code = $];\n\
ref /flights/*::dest --> /airports/*[::code = $];\n\
edge /flights/*[::carrier = \"panam\"]: ::origin -- ::dest as panam;\n\
edge /flights/*[::carrier = \"swissair\"]: ::origin -- ::dest as swissair;\n",
)
.unwrap();
std::fs::write(
dir.join("by-carrier.model"),
"ref /flights/*::origin --> /airports/*[::code = $] as ::carrier;\n\
ref /flights/*::dest --> /airports/*[::code = $];\n\
edge /flights/*: ::origin -- ::dest as ::carrier;\n",
)
.unwrap();
std::fs::write(
dir.join("ref-only.model"),
"ref /flights/*::origin --> /airports/*[::code = $];\n",
)
.unwrap();
std::fs::write(
dir.join("bad-label.model"),
"ref /flights/*::origin --> /airports/*[::code = $];\n\
ref /flights/*::dest --> /airports/*[::code = $];\n\
edge /flights/*: ::origin -- ::dest as ::minutes;\n",
)
.unwrap();
dir
}
fn fails(dir: &Path, model: &str, query: &str) -> String {
let out = std::process::Command::new(env!("CARGO_BIN_EXE_qua"))
.env("QUARB_LOCALE", "C")
.current_dir(dir)
.args(["--model", model, query, "flights.db"])
.stdin(std::process::Stdio::null())
.output()
.unwrap();
assert!(!out.status.success(), "{query} succeeded");
String::from_utf8_lossy(&out.stderr).to_string()
}
fn lines(dir: &Path, model: &str, query: &str) -> Vec<String> {
let out = std::process::Command::new(env!("CARGO_BIN_EXE_qua"))
.env("QUARB_LOCALE", "C")
.current_dir(dir)
.args(["--model", model, query, "flights.db"])
.stdin(std::process::Stdio::null())
.output()
.unwrap();
assert!(
out.status.success(),
"{query}: {}",
String::from_utf8_lossy(&out.stderr)
);
String::from_utf8_lossy(&out.stdout)
.lines()
.map(str::to_string)
.collect()
}
#[test]
fn two_rows_on_one_leg_are_two_edges() {
let dir = fixture("two");
let q = |query: &str| lines(&dir, "flights.model", query);
assert_eq!(q("/airports/LHR->flight @| count"), ["3"]);
assert_eq!(q("/airports/LHR->flight::code"), ["JFK", "JFK", "CDG"]);
assert_eq!(
q("/airports/LHR->flight | $-::no"),
["QB100", "QB102", "QB200"]
);
assert_eq!(q("/airports/JFK<-flight | $-::no"), ["QB100", "QB102"]);
assert_eq!(
q("/airports/LHR->flight[$-::minutes > 480] | $-::no"),
["QB102"]
);
assert_eq!(
q("/airports/LHR(->flight){1} | $-::no"),
["QB100", "QB102", "QB200"]
);
assert_eq!(
q("/airports/LHR(->flight){1} | [::code = \"JFK\"] @| count"),
["2"]
);
assert_eq!(
q("/airports/LHR(->flight){1} | [::code = \"JFK\"] @| collapse @| count"),
["1"]
);
assert_eq!(q("/airports/JFK--flight @| count"), ["3"]);
assert_eq!(q("/airports/LHR--flight @| count"), ["5"]);
assert_eq!(
q("/airports/JFK--flight | $-::no"),
["QB101", "QB100", "QB102"]
);
let table = q("/airports/* @| edges(->flight)");
let rows: Vec<&String> = table
.iter()
.filter(|l| l.trim_start().starts_with("%("))
.collect();
assert_eq!(rows.len(), 5, "{table:?}");
for no in ["QB100", "QB102", "QB200", "QB101", "QB300"] {
assert!(
rows.iter().any(|l| l.contains(&format!("\"{no}\""))),
"{table:?}"
);
}
assert!(
table[0].contains("no") && table[0].contains("minutes"),
"{table:?}"
);
assert_eq!(q("/airports/* | (->flight @| count) @| sum"), ["5"]);
}
#[test]
fn a_label_by_property_names_each_edge_by_its_row() {
let dir = fixture("by-carrier");
let q = |query: &str| lines(&dir, "by-carrier.model", query);
assert_eq!(q("/airports/LHR->panam | $-::no"), ["QB100"]);
assert_eq!(q("/airports/LHR->swissair | $-::no"), ["QB102", "QB200"]);
assert_eq!(
q("/airports/LHR->* | $-"),
["panam", "swissair", "swissair"]
);
assert_eq!(q("/airports/LHR->(panam || swissair) @| count"), ["3"]);
let mut into = q("/airports/JFK<-* | $-");
into.sort();
assert_eq!(into, ["panam", "swissair"]);
assert_eq!(q("/airports/JFK<-dest @| count"), ["2"]);
assert_eq!(q("/airports/CDG->* @| count"), ["0"]);
assert_eq!(q("/airports/CDG--* | $-"), ["swissair"]);
assert_eq!(q("/airports/CDG<-origin::no"), ["QB300"]);
assert_eq!(q("/flights/5->* @| count"), ["0"]);
let mut five = q("/flights/5->(origin || dest) | $-");
five.sort();
assert_eq!(five, ["dest", "origin"]);
assert_eq!(q("/flights/1->panam::code"), ["LHR"]);
assert_eq!(q("/flights/2->swissair::code"), ["LHR"]);
assert_eq!(q("/flights/2->swissair | $-::no"), ["QB102"]);
assert_eq!(q("/airports/LHR<-swissair | $-::no"), ["QB102", "QB200"]);
let err = fails(&dir, "bad-label.model", "/airports/LHR->* @| count");
assert!(
err.contains("::minutes") && err.contains("no text"),
"{err}"
);
}
#[test]
fn an_edge_scope_may_carry_a_predicate() {
let dir = fixture("carriers");
let q = |query: &str| lines(&dir, "carriers.model", query);
assert_eq!(q("/airports/LHR->panam | $-::no"), ["QB100"]);
assert_eq!(q("/airports/LHR->swissair | $-::no"), ["QB102", "QB200"]);
assert_eq!(q("/airports/LHR->* @| count"), ["3"]);
assert_eq!(q("/airports/LHR->(panam || swissair) @| count"), ["3"]);
assert_eq!(
q("/airports/LHR->* | $-"),
["panam", "swissair", "swissair"]
);
}
#[test]
fn wildcards_skip_the_references_an_edge_consumed() {
let dir = fixture("wild");
assert_eq!(
lines(&dir, "flights.model", "/airports/LHR->* @| count"),
["3"]
);
assert_eq!(
lines(&dir, "flights.model", "/airports/LHR<-* @| count"),
["2"]
);
assert_eq!(
lines(&dir, "flights.model", "/airports/LHR--* @| count"),
["5"]
);
let mut from = lines(&dir, "flights.model", "/airports/LHR<-*::code");
from.sort();
assert_eq!(from, ["CDG", "JFK"]);
assert_eq!(
lines(&dir, "flights.model", "/airports/LHR<-origin @| count"),
["3"]
);
assert_eq!(
lines(&dir, "flights.model", "/airports/LHR<-dest @| count"),
["2"]
);
assert_eq!(
lines(&dir, "flights.model", "/flights/1->* @| count"),
["0"]
);
assert_eq!(
lines(&dir, "flights.model", "/flights/1->origin::code"),
["LHR"]
);
assert_eq!(
lines(&dir, "flights.model", "/flights/1->dest::code"),
["JFK"]
);
assert_eq!(
lines(&dir, "flights.model", "/airports/LHR~*[::no] @| count"),
["0"]
);
assert_eq!(
lines(&dir, "carriers.model", "/airports/LHR<-* @| count"),
["1"]
);
assert_eq!(
lines(&dir, "carriers.model", "/airports/LHR--* @| count"),
["4"]
);
assert_eq!(
lines(&dir, "carriers.model", "/airports/LHR<-origin @| count"),
["3"]
);
assert_eq!(
lines(&dir, "by-carrier.model", "/airports/LHR<-* @| count"),
["1"]
);
assert_eq!(
lines(&dir, "by-carrier.model", "/airports/LHR->* @| count"),
["3"]
);
assert_eq!(
lines(&dir, "ref-only.model", "/airports/LHR<-* @| count"),
["5"]
);
assert_eq!(
lines(&dir, "ref-only.model", "/flights/1->* @| count"),
["2"]
);
assert_eq!(
lines(&dir, "flights.model", "/airports/* @| edges(--*) | count"),
["5"]
);
}