use quarb::QueryResult;
use quarb_sqlite::SqliteAdapter;
use rusqlite::Connection;
const SCHEMA: &str = r#"
PRAGMA foreign_keys = OFF;
CREATE TABLE authors (id INTEGER PRIMARY KEY, name TEXT, code TEXT UNIQUE);
CREATE TABLE books (
id INTEGER PRIMARY KEY, title TEXT,
author_id INTEGER REFERENCES authors(id),
editor_id INTEGER REFERENCES authors(id),
code TEXT REFERENCES authors(code));
CREATE TABLE staff (id INTEGER PRIMARY KEY, name TEXT,
boss INTEGER REFERENCES staff(id));
CREATE TABLE notes (n TEXT, about INTEGER);
INSERT INTO authors VALUES (1,'Austen','A'), (2,'Twain','T'), (3,'Homer','H'), (4,'Gibbon','G');
INSERT INTO books VALUES
(1,'Emma',1,2,'A'), (2,'Tom Sawyer',2,NULL,'T'), (3,'The Iliad',3,1,'H'),
(4,'Persuasion',1,1,'A'), (5,'Huckleberry Finn',2,3,'T'), (6,'Anonymous',NULL,NULL,NULL),
(7,'Orphan',99,NULL,'Z');
INSERT INTO staff VALUES (1,'ada',NULL), (2,'bo',1), (3,'cy',1), (4,'di',2), (5,'ed',2), (6,'flo',4);
INSERT INTO notes VALUES ('x', 1), ('y', 2);
"#;
fn file_db(tag: &str) -> std::path::PathBuf {
let path = std::env::temp_dir().join(format!("quarb-keyed-{}-{tag}.db", std::process::id()));
let _ = std::fs::remove_file(&path);
let conn = Connection::open(&path).unwrap();
conn.execute_batch(SCHEMA).unwrap();
path
}
fn eager() -> SqliteAdapter {
let conn = Connection::open_in_memory().unwrap();
conn.execute_batch(SCHEMA).unwrap();
SqliteAdapter::load(&conn).unwrap()
}
fn show(a: &SqliteAdapter, q: &str) -> Vec<String> {
match quarb::run(q, a).unwrap_or_else(|e| panic!("{q}: {e}")) {
QueryResult::Values(vs) => vs.iter().map(|v| v.to_string()).collect(),
QueryResult::Nodes(ns) => ns.into_iter().map(|n| a.locator(n)).collect(),
}
}
const QUERIES: &[&str] = &[
"/books/3::author_id-->::name",
"/books/3->author_id",
"/books/3->*",
"/books/5::code-->::name",
"/books/5::code-->authors",
"/books/7::author_id-->",
"/books/6::author_id-->",
"/books/99::title",
"/authors/1<-author_id",
"/authors/1<-*",
"/authors/1<--(::author_id)::title",
"/authors/1<--(::editor_id)::title",
"/authors/1<--::title",
"/authors/1<--(/books/*::author_id)::title",
"/authors/2<--(/books/*)::title",
"/authors/*<--(::author_id) @| count",
"/authors/* | %(::name; n = (<-author_id @| count))",
"/books/* | %(::title; a = ::author_id-->::name)",
"/books/*[::id < 4]->author_id",
"/staff/1(<-boss){1;3}",
"/staff/6(->boss)+",
"/staff/*[<-boss]::name",
"/authors/1--*",
"/notes/*::n",
"/authors/1<--authors(/notes/*::about)::n",
];
#[test]
fn a_lazy_database_answers_as_the_eager_one() {
let e = eager();
let path = file_db("same");
for q in QUERIES {
let l = SqliteAdapter::open(&path).unwrap();
assert_eq!(show(&l, q), show(&e, q), "{q}");
}
let l = SqliteAdapter::open(&path).unwrap();
for q in QUERIES.iter().chain(QUERIES.iter().rev()) {
assert_eq!(show(&l, q), show(&e, q), "{q} (shared adapter)");
}
let _ = std::fs::remove_file(path);
}
#[test]
fn a_hop_reads_the_rows_it_lands_on() {
let path = file_db("rows");
let loaded = |q: &str| {
let l = SqliteAdapter::open(&path).unwrap();
show(&l, q)
};
assert_eq!(
loaded(
"/books/3::author_id--> | %(::name; b = (^/books::::loaded); \
a = (^/authors::::loaded); nb = (^/books::::\"n-loaded\"); \
na = (^/authors::::\"n-loaded\"))"
),
[r#"%(name = "Homer"; b = false; a = false; nb = 1; na = 1)"#]
);
assert_eq!(
loaded(
"/authors/1<-author_id @| count | %(n = $_; b = (^/books::::loaded); \
few = (^/books::::\"n-loaded\") < 7)"
),
[r#"%(n = 2; b = false; few = true)"#]
);
assert_eq!(
loaded(
"/authors/2<--(/books/*::author_id) @| count \
| %(n = $_; b = (^/books::::loaded))"
),
[r#"%(n = 2; b = false)"#]
);
assert_eq!(
loaded("/books/* @| count | %(n = $_; b = (^/books::::loaded))"),
[r#"%(n = 7; b = true)"#]
);
let _ = std::fs::remove_file(path);
}
#[test]
fn a_pushed_down_filter_lists_only_and_hops_stay_exact() {
let e = eager();
let path = file_db("filter");
for (q, table, where_sql) in [
("/staff/*[::id < 3]<-boss", "staff", "id < 3"),
("/staff/*[::id > 3]->boss", "staff", "id > 3"),
("/staff/*[::id > 3]::boss-->::name", "staff", "id > 3"),
("/staff/*[::id < 3]<-boss<-boss", "staff", "id < 3"),
("/books/*[::id < 4]->author_id", "books", "id < 4"),
(
"/staff/*[::id = 2] | %(::name; all = (::::table))",
"staff",
"id = 2",
),
] {
let l = SqliteAdapter::open_filtered(&path, table, where_sql).unwrap();
assert_eq!(show(&l, q), show(&e, q), "{q}");
}
let keyed = |q: &str| quarb_sql::partial_pushdown_keyed(q, Some(quarb_sql::Dialect::Sqlite));
assert!(keyed("/staff/*[::id < 3]<-boss").is_ok());
assert!(keyed("/books/*[::id < 4]::author_id-->::name").is_ok());
assert!(keyed("/authors/*[::id < 3]<--(/books/*::author_id)").is_ok());
assert!(keyed("/books/*[::id < 4]::author_id=>(/books/*::author_id)").is_err());
assert!(keyed("/authors/*[::id < 3]::id=>(::author_id)").is_err());
assert!(keyed("/books/*[::id < 4] | .n((^/books/* @| count))").is_err());
assert!(
quarb_sql::partial_pushdown("/staff/*[::id < 3]<-boss", Some(quarb_sql::Dialect::Sqlite))
.is_none()
);
let _ = std::fs::remove_file(path);
}
#[test]
fn a_column_with_no_key_resolves_only_under_a_label() {
let path = file_db("label");
let l = SqliteAdapter::open(&path).unwrap();
assert_eq!(show(&l, "/notes/1::about-->authors::name"), ["Austen"]);
let err = quarb::run("/notes/1::about-->::name", &l).unwrap_err();
assert!(err.to_string().contains("needs a reference label"), "{err}");
assert_eq!(show(&l, "/books/1::author_id-->::name"), ["Austen"]);
assert!(show(&l, "/books/1::nosuch-->").is_empty());
let _ = std::fs::remove_file(path);
}