use qua::{OpenOpts, open_target};
use quarb::{AstAdapter, NodeId, Value};
struct Dyn(Box<dyn AstAdapter>);
impl AstAdapter for Dyn {
fn root(&self) -> NodeId {
self.0.root()
}
fn children(&self, n: NodeId) -> Vec<NodeId> {
self.0.children(n)
}
fn name(&self, n: NodeId) -> Option<String> {
self.0.name(n)
}
fn parent(&self, n: NodeId) -> Option<NodeId> {
self.0.parent(n)
}
fn property(&self, n: NodeId, key: &str) -> Option<Value> {
self.0.property(n, key)
}
fn default_value(&self, n: NodeId) -> Option<Value> {
self.0.default_value(n)
}
fn traits(&self, n: NodeId) -> Vec<String> {
self.0.traits(n)
}
fn has_trait(&self, n: NodeId, name: &str) -> bool {
self.0.has_trait(n, name)
}
fn metadata(&self, n: NodeId, key: &str) -> Option<Value> {
self.0.metadata(n, key)
}
}
#[test]
fn mounted_sqlite_grafts_json_columns() {
let dir = std::env::temp_dir().join("qua-mount-graft-test");
std::fs::create_dir_all(&dir).unwrap();
let path = dir.join("g.db");
let _ = std::fs::remove_file(&path);
{
let conn = rusqlite::Connection::open(&path).unwrap();
conn.execute_batch(
r#"CREATE TABLE s (id INTEGER PRIMARY KEY, meta JSON, note TEXT);
INSERT INTO s VALUES
(1,'{"device":"phone","geo":{"city":"London"}}','{"geo":{"city":"Paris"}}'),
(2,'{"device":"laptop","geo":{"city":"Berlin"}}','{"geo":{"city":"Rome"}}');"#,
)
.unwrap();
}
let adapter = open_target(path.to_str().unwrap(), &OpenOpts::default())
.expect("mount")
.adapter;
let adapter = Dyn(adapter);
let run = |q: &str| -> Vec<String> {
match quarb::run(q, &adapter).unwrap() {
quarb::QueryResult::Values(vs) => vs.iter().map(|v| v.to_string()).collect(),
_ => panic!("expected values"),
}
};
assert_eq!(run("/s/1/meta/geo/city::"), ["London"]);
assert_eq!(run("/s/*[/meta/geo/city:: = \"Berlin\"]::id"), ["2"]);
assert_eq!(run("/s/2/meta::device"), ["laptop"]);
assert_eq!(run("/s/1/note/geo/city::"), Vec::<String>::new());
assert_eq!(run("/s/1/note::"), [r#"{"geo":{"city":"Paris"}}"#]);
assert_eq!(run("/s/1/note<json>/geo/city::"), ["Paris"]);
assert_eq!(run("/s/*/note<json>/geo/city::"), ["Paris", "Rome"]);
}
#[test]
fn rows_read_as_documents_with_doc() {
let dir = std::env::temp_dir().join("qua-mount-doc-test");
std::fs::create_dir_all(&dir).unwrap();
let path = dir.join("blog.db");
let _ = std::fs::remove_file(&path);
{
let conn = rusqlite::Connection::open(&path).unwrap();
conn.execute_batch(
r#"CREATE TABLE posts (id INTEGER PRIMARY KEY, title TEXT, author TEXT, body TEXT);
INSERT INTO posts VALUES
(1,'Ithaca','Homer','Tell me, Muse, of the man of many ways. He wandered far.'),
(2,'Troy','Homer','Sing, goddess, the anger of Achilles.');"#,
)
.unwrap();
}
let opts = OpenOpts::default();
let target = format!("corpus:{}?doc=posts/*/body", path.display());
let a = Dyn(open_target(&target, &opts).expect("mount").adapter);
assert_eq!(run_q(&a, "/posts/*/body:::title"), ["Ithaca", "Troy"]);
assert_eq!(run_q(&a, "/posts/1/body:::author"), ["Homer"]);
assert_eq!(run_q(&a, "/posts/1/body//sentence @| count"), ["2"]);
assert_eq!(run_q(&a, "/posts/2/body//token @| count"), ["9"]);
assert_eq!(run_q(&a, "/posts/*/title::"), ["Ithaca", "Troy"]);
let t = Dyn(
open_target(&format!("text:{}?doc=posts/*/body", path.display()), &opts)
.expect("mount")
.adapter,
);
assert_eq!(
run_q(&t, "/posts/2/body//paragraph::"),
["Sing, goddess, the anger of Achilles."]
);
assert_eq!(run_q(&t, "/posts/2/body//token @| count"), ["0"]);
let err = open_target(
&format!("corpus:{}?doc=posts/*/nothing", path.display()),
&opts,
)
.err()
.map(|e| e.to_string())
.unwrap_or_default();
assert!(err.contains("names no leaf"), "{err}");
}
#[test]
fn a_leaf_grafts_only_as_declared() {
let dir = std::env::temp_dir().join("qua-mount-declared-test");
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
std::fs::write(dir.join("a.json"), r#"{"k":"named"}"#).unwrap();
std::fs::write(dir.join("b"), r#"{"k":"bare"}"#).unwrap();
std::fs::write(
dir.join("c"),
"<?xml version=\"1.0\"?><root><x>decl</x></root>",
)
.unwrap();
std::fs::write(
dir.join("d"),
"<!DOCTYPE html><html><body><p>doc</p></body></html>",
)
.unwrap();
std::fs::write(dir.join("e"), "<root><x>guess</x></root>").unwrap();
let opts = OpenOpts {
graft: true,
..OpenOpts::default()
};
let mount = |target: &str| Dyn(open_target(target, &opts).expect("mount").adapter);
let a = mount(dir.to_str().unwrap());
assert_eq!(run_q(&a, "/a.json/k::"), ["named"]);
assert_eq!(run_q(&a, "/b/k::"), Vec::<String>::new());
assert_eq!(run_q(&a, "/b::"), [r#"{"k":"bare"}"#]);
assert_eq!(run_q(&a, "/b<json>/k::"), ["bare"]);
assert_eq!(run_q(&a, "/c/root/x::"), ["decl"]);
assert_eq!(run_q(&a, "//p::"), ["doc"]);
assert_eq!(run_q(&a, "/e/root/x::"), Vec::<String>::new());
assert_eq!(run_q(&a, "/e<xml>/root/x::"), ["guess"]);
let j = mount(&format!("{}?format=json", dir.display()));
assert_eq!(run_q(&j, "/b/k::"), ["bare"]);
assert_eq!(run_q(&j, "/a.json/k::"), ["named"]);
let err = open_target(&format!("{}?format=jsonx", dir.display()), &opts)
.err()
.map(|e| e.to_string())
.unwrap_or_default();
assert!(
err.contains("names no notation") && err.contains("json, jsonl"),
"{err}"
);
}
fn run_q(adapter: &Dyn, q: &str) -> Vec<String> {
match quarb::run(q, adapter).unwrap() {
quarb::QueryResult::Values(vs) => vs.iter().map(|v| v.to_string()).collect(),
_ => panic!("expected values"),
}
}
#[test]
fn dir_mount_grafts_only_with_graft() {
let dir = std::env::temp_dir().join("qua-mount-dir-graft-test");
std::fs::create_dir_all(&dir).unwrap();
std::fs::write(dir.join("cfg.json"), r#"{"port": 8080}"#).unwrap();
let a = open_target(dir.to_str().unwrap(), &OpenOpts::default())
.expect("mount")
.adapter;
assert!(
run_q(&Dyn(a), "/cfg.json/port::").is_empty(),
"bare dir must !graft"
);
let opts = OpenOpts {
graft: true,
..OpenOpts::default()
};
let a = open_target(dir.to_str().unwrap(), &opts)
.expect("mount")
.adapter;
assert_eq!(run_q(&Dyn(a), "/cfg.json/port::"), ["8080"]);
}
#[test]
fn archive_no_graft_holds_members_opaque() {
use std::io::Write;
let dir = std::env::temp_dir().join("qua-mount-archive-graft-test");
std::fs::create_dir_all(&dir).unwrap();
let path = dir.join("a.zip");
let f = std::fs::File::create(&path).unwrap();
let mut z = zip::ZipWriter::new(f);
let o = zip::write::SimpleFileOptions::default();
z.start_file("cfg.json", o).unwrap();
z.write_all(br#"{"port": 8080}"#).unwrap();
z.finish().unwrap();
let a = open_target(path.to_str().unwrap(), &OpenOpts::default())
.expect("mount")
.adapter;
assert_eq!(run_q(&Dyn(a), "/cfg.json/port::"), ["8080"]);
let opts = OpenOpts {
no_graft: true,
..OpenOpts::default()
};
let a = open_target(path.to_str().unwrap(), &opts)
.expect("mount")
.adapter;
let a = Dyn(a);
assert!(
run_q(&a, "/cfg.json/port::").is_empty(),
"no_graft must !cross the boundary"
);
assert_eq!(
run_q(&a, "/cfg.json:::size").len(),
1,
"the member stays listable && sizable"
);
}
#[test]
fn sqlite_no_graft_keeps_columns_flat() {
let dir = std::env::temp_dir().join("qua-mount-no-graft-sql-test");
std::fs::create_dir_all(&dir).unwrap();
let path = dir.join("g.db");
let _ = std::fs::remove_file(&path);
{
let conn = rusqlite::Connection::open(&path).unwrap();
conn.execute_batch(
r#"CREATE TABLE s (id INTEGER PRIMARY KEY, meta TEXT);
INSERT INTO s VALUES (1,'{"device":"phone","geo":{"city":"London"}}');"#,
)
.unwrap();
}
let opts = OpenOpts {
no_graft: true,
..OpenOpts::default()
};
let a = open_target(path.to_str().unwrap(), &opts)
.expect("mount")
.adapter;
let a = Dyn(a);
assert!(run_q(&a, "/s/1/meta/geo/city::").is_empty());
assert_eq!(
run_q(&a, "/s/1/meta::"),
[r#"{"device":"phone","geo":{"city":"London"}}"#]
);
}
#[test]
fn no_graft_refuses_code_prefix() {
let dir = std::env::temp_dir().join("qua-mount-no-graft-code-test");
std::fs::create_dir_all(&dir).unwrap();
let target = format!("code:{}", dir.display());
let opts = OpenOpts {
no_graft: true,
..OpenOpts::default()
};
let err = match open_target(&target, &opts) {
Err(e) => e,
Ok(_) => panic!("must refuse"),
};
assert!(err.to_string().contains("code:"), "unexpected error: {err}");
}