use quarb::AstAdapter as _;
use quarb_sqlite::SqliteAdapter;
use rusqlite::Connection;
fn adapter() -> SqliteAdapter {
let conn = Connection::open_in_memory().unwrap();
conn.execute_batch(
r#"
CREATE TABLE artists (
id INTEGER PRIMARY KEY,
name TEXT
);
CREATE TABLE albums (
id INTEGER PRIMARY KEY,
title TEXT,
artist_id INTEGER REFERENCES artists(id)
);
CREATE TABLE tracks (
id INTEGER PRIMARY KEY,
title TEXT,
album_id INTEGER REFERENCES albums(id),
price REAL,
secs INTEGER
);
INSERT INTO artists VALUES (1, 'Holst'), (2, 'Bartok'), (3, 'Satie');
INSERT INTO albums VALUES
(1, 'The Planets', 1),
(2, 'Mikrokosmos', 2),
(3, 'Gymnopedies', 3),
(4, 'Quartets', 2);
INSERT INTO tracks VALUES
(1, 'Mars', 1, 1.29, 430),
(2, 'Venus', 1, 0.99, 480),
(3, 'Jupiter', 1, 1.29, 470),
(4, 'Bourree', 2, 0.99, 95),
(5, 'Ostinato', 2, 1.29, 140),
(6, 'Gymnopedie No.1', 3, 0.99, 210),
(7, 'Quartet No.4', 4, 1.29, 1500),
(8, 'Unfiled Sketch', NULL, 0.50, 60);
"#,
)
.unwrap();
SqliteAdapter::load(&conn).unwrap()
}
fn values(query: &str) -> Vec<String> {
let a = adapter();
match quarb::run(query, &a).unwrap() {
quarb::QueryResult::Values(vs) => vs.iter().map(|v| v.to_string()).collect(),
quarb::QueryResult::Nodes(_) => panic!("expected values"),
}
}
fn nodes(query: &str) -> Vec<String> {
let a = adapter();
match quarb::run(query, &a).unwrap() {
quarb::QueryResult::Nodes(ns) => ns.into_iter().map(|n| a.locator(n)).collect(),
quarb::QueryResult::Values(_) => panic!("expected nodes"),
}
}
#[test]
fn tables_and_rows() {
assert_eq!(nodes("/*"), vec!["/albums", "/artists", "/tracks"]);
assert_eq!(values("/tracks;;;n-rows"), vec!["8"]);
assert_eq!(nodes("/tracks/7"), vec!["/tracks/7"]);
assert_eq!(values("/artists/2::name"), vec!["Bartok"]);
assert_eq!(values("/tracks/*[::price > 1]::title @| count"), vec!["4"]);
assert_eq!(values("/tracks/*[::album_id] @| count"), vec!["7"]);
assert_eq!(values("/tracks/8;;;table"), vec!["tracks"]);
}
#[test]
fn fk_resolution() {
assert_eq!(values("/tracks/1::album_id~>::title"), vec!["The Planets"]);
assert_eq!(
values("/tracks/*[::title = \"Bourree\"]::album_id~>::artist_id~>::name"),
vec!["Bartok"]
);
assert_eq!(values("/tracks/8::album_id~>::title @| count"), vec!["0"]);
assert_eq!(
values("/tracks/1::album_id~>albums::title"),
vec!["The Planets"]
);
}
#[test]
fn fk_links() {
assert_eq!(nodes("/tracks/4->album_id"), vec!["/albums/2"]);
assert_eq!(nodes("/tracks/4->album_id->artist_id"), vec!["/artists/2"]);
assert_eq!(
nodes("/albums/1<-album_id"),
vec!["/tracks/1", "/tracks/2", "/tracks/3"]
);
assert_eq!(
values("/artists/2::artist_id<~::title"),
vec!["Mikrokosmos", "Quartets"]
);
}
#[test]
fn sql_shapes() {
assert_eq!(
values("/tracks/*[::price < 1] @| sort_by(::title) | rec(::title, ::price)"),
vec![
r#"{"title": "Bourree", "price": 0.99}"#,
r#"{"title": "Gymnopedie No.1", "price": 0.99}"#,
r#"{"title": "Unfiled Sketch", "price": 0.5}"#,
r#"{"title": "Venus", "price": 0.99}"#
]
);
assert_eq!(
values(
"/tracks/*[::album_id] @| group(\"album\", ::album_id~>::title) \
| count | .n | [$_ > 1] | %."
),
vec![
r#"{"album": "The Planets", "n": 3}"#,
r#"{"album": "Mikrokosmos", "n": 2}"#
]
);
assert_eq!(
values(
"/tracks/*[::secs > 400] <=> /albums/*[::id = $$::album_id] \
| rec(\"album\", $*1::title, ::title)"
),
vec![
r#"{"album": "The Planets", "title": "Mars"}"#,
r#"{"album": "The Planets", "title": "Venus"}"#,
r#"{"album": "The Planets", "title": "Jupiter"}"#,
r#"{"album": "Quartets", "title": "Quartet No.4"}"#
]
);
assert_eq!(
values(
"/albums/* | .id(::id) \
| .rev(^/tracks/*[::album_id = $$.id]::price @| sum) \
| [$.rev > 2] | rec(::title, \"revenue\", $.rev)"
),
vec![
r#"{"title": "The Planets", "revenue": 3.5700000000000003}"#,
r#"{"title": "Mikrokosmos", "revenue": 2.2800000000000002}"#
]
);
}
#[test]
fn tricky_names_and_without_rowid() {
let conn = Connection::open_in_memory().unwrap();
conn.execute_batch(
r#"
CREATE TABLE "Order" (id INTEGER PRIMARY KEY, note TEXT);
CREATE TABLE "my table" (id INTEGER PRIMARY KEY, note TEXT);
CREATE TABLE kv (k TEXT PRIMARY KEY, v TEXT) WITHOUT ROWID;
INSERT INTO "Order" VALUES (1, 'a'), (2, 'b');
INSERT INTO "my table" VALUES (1, 'x');
INSERT INTO kv VALUES ('one', '1'), ('two', '2'), ('three', '3');
"#,
)
.unwrap();
let a = SqliteAdapter::load(&conn).unwrap();
let node = |table: &str| {
a.children(a.root())
.into_iter()
.find(|n| a.name(*n).as_deref() == Some(table))
.unwrap_or_else(|| panic!("table {table} missing"))
};
assert_eq!(a.children(node("Order")).len(), 2);
assert_eq!(a.children(node("my table")).len(), 1);
let kv_keys: Vec<String> = a
.children(node("kv"))
.iter()
.filter_map(|n| a.name(*n))
.collect();
assert_eq!(kv_keys, vec!["one", "three", "two"]);
}
#[test]
fn pushdown_matches_scan() {
use std::io::Write as _;
let dir = std::env::temp_dir().join("quarb-sqlite-pushdown-test");
std::fs::create_dir_all(&dir).unwrap();
let path = dir.join("music.db");
let _ = std::fs::remove_file(&path);
{
let conn = Connection::open(&path).unwrap();
conn.execute_batch(
"CREATE TABLE tracks (id INTEGER PRIMARY KEY, title TEXT, secs INTEGER, price REAL);
INSERT INTO tracks VALUES (1,'Mars',430,1.29), (2,'Venus',480,0.99),
(3,'Jupiter',470,1.29), (4,'Bourree',95,0.99);",
)
.unwrap();
std::io::stdout().flush().unwrap();
}
let cases = [
"/tracks/* @| count",
"/tracks/* | ::price @| sum",
"/tracks/*[::price < 1] | rec(::title, ::secs)",
"/tracks/*[::secs > 100 and ::price >= 1] | ::title",
];
for q in cases {
let plan = quarb_sql::pushdown(q, Some(quarb_sql::Dialect::Sqlite))
.unwrap_or_else(|| panic!("expected pushdown for {q}"));
let (cols, rows) = quarb_sqlite::raw_query(
&path,
&plan.sql,
plan.order_table.as_deref(),
plan.join_left
.as_ref()
.map(|(t, c)| (t.as_str(), c.as_slice())),
)
.unwrap();
let pushed: Vec<String> = rows
.into_iter()
.map(|row| {
if cols.len() <= 1 {
row[0].to_string()
} else {
quarb::Value::Record(cols.iter().cloned().zip(row).collect()).to_string()
}
})
.collect();
let adapter = SqliteAdapter::open(&path).unwrap();
let scanned: Vec<String> = match quarb::run(q, &adapter).unwrap() {
quarb::QueryResult::Values(vs) => vs.iter().map(|v| v.to_string()).collect(),
quarb::QueryResult::Nodes(_) => panic!("expected values for {q}"),
};
assert_eq!(pushed, scanned, "pushdown/scan divergence for: {q}");
}
}
#[test]
fn filtered_open_matches_scan() {
let dir = std::env::temp_dir().join("quarb-sqlite-partial-test");
std::fs::create_dir_all(&dir).unwrap();
let path = dir.join("events.db");
let _ = std::fs::remove_file(&path);
{
let conn = Connection::open(&path).unwrap();
conn.execute_batch(
"CREATE TABLE events (id INTEGER PRIMARY KEY, kind TEXT, amount REAL);
INSERT INTO events VALUES
(1,'a',10.0),(2,'b',20.0),(3,'a',30.0),(4,'b',40.0),(5,'a',50.0);",
)
.unwrap();
}
let q = "/events/*[::kind = \"a\"] | ::amount @| group(\"half\", ::amount idiv 25) \
| count | .n | %.";
let plan = quarb_sql::partial_pushdown(q).expect("partial plan");
let run = |a: &SqliteAdapter| -> Vec<String> {
match quarb::run(q, a).unwrap() {
quarb::QueryResult::Values(vs) => vs.iter().map(|v| v.to_string()).collect(),
_ => panic!("expected values"),
}
};
let filtered = SqliteAdapter::open_filtered(&path, &plan.table, &plan.where_sql).unwrap();
let plain = SqliteAdapter::open(&path).unwrap();
assert_eq!(run(&filtered), run(&plain));
}
#[test]
fn raw_query_join_uniqueness_gate() {
let path = std::env::temp_dir().join(format!(
"quarb-sqlite-unique-gate-{}.db",
std::process::id()
));
let _ = std::fs::remove_file(&path);
let conn = Connection::open(&path).unwrap();
conn.execute_batch(
r#"
CREATE TABLE a (id INTEGER PRIMARY KEY, name TEXT);
CREATE TABLE b (id INTEGER PRIMARY KEY, a_id INTEGER REFERENCES a(id));
INSERT INTO a VALUES (1, 'one'), (2, 'two');
INSERT INTO b VALUES (10, 1), (11, 1), (12, 2);
"#,
)
.unwrap();
drop(conn);
let sql = "SELECT a.name, b.id FROM a JOIN b ON b.a_id = a.id";
let ok = quarb_sqlite::raw_query(&path, sql, Some("b"), Some(("a", &["id".to_string()][..])));
assert_eq!(ok.expect("PK-bound join executes").1.len(), 3);
let refused = quarb_sqlite::raw_query(
&path,
"SELECT b.id FROM b JOIN a ON a.a_id = b.a_id",
Some("a"),
Some(("b", &["a_id".to_string()][..])),
);
assert!(
refused.is_err(),
"non-key binding must refuse: the JOIN multiplies rows"
);
let _ = std::fs::remove_file(&path);
}