#![cfg(all(feature = "std", feature = "fts5"))]
use graphitesql::{Connection, Value};
use std::process::Command;
fn sqlite3(path: &str, sql: &str) -> String {
let o = Command::new("sqlite3").arg(path).arg(sql).output().unwrap();
String::from_utf8_lossy(&o.stdout).trim_end().to_string()
}
fn render(result: &graphitesql::QueryResult) -> String {
result
.rows
.iter()
.map(|row| {
row.iter()
.map(|v| match v {
Value::Null => String::new(),
Value::Integer(i) => i.to_string(),
Value::Text(s) => s.clone(),
Value::Real(r) => graphitesql::exec::eval::format_real(*r),
Value::Blob(b) => b.iter().map(|x| format!("{x:02x}")).collect(),
})
.collect::<Vec<_>>()
.join("|")
})
.collect::<Vec<_>>()
.join("\n")
}
const SETUP: &str = "CREATE VIRTUAL TABLE ft USING fts5(a,b,c);\
INSERT INTO ft VALUES('quick fox','lazy dog','red car'),\
('slow fox','quick dog','blue car'),\
('big fox','small dog','quick car');";
#[test]
fn braced_column_filter_matches_sqlite3() {
if Command::new("sqlite3").arg("--version").output().is_err() {
eprintln!("sqlite3 not found; skipping");
return;
}
let path = std::env::temp_dir().join(format!("gsql-fts5colfilter-{}.db", std::process::id()));
let path = path.to_string_lossy().into_owned();
let _ = std::fs::remove_file(&path);
sqlite3(&path, SETUP);
let mut g = Connection::open_memory().unwrap();
for s in SETUP.split(';') {
if !s.trim().is_empty() {
g.execute(s).unwrap();
}
}
let queries = [
"SELECT rowid FROM ft WHERE ft MATCH '{a}:quick' ORDER BY rowid",
"SELECT rowid FROM ft WHERE ft MATCH 'a:quick' ORDER BY rowid",
"SELECT rowid FROM ft WHERE ft MATCH '{a b}:quick' ORDER BY rowid",
"SELECT rowid FROM ft WHERE ft MATCH '{a c}:quick' ORDER BY rowid",
"SELECT rowid FROM ft WHERE ft MATCH '{b c}:quick' ORDER BY rowid",
"SELECT rowid FROM ft WHERE ft MATCH '{a b c}:quick' ORDER BY rowid",
"SELECT rowid FROM ft WHERE ft MATCH '-{a}:quick' ORDER BY rowid",
"SELECT rowid FROM ft WHERE ft MATCH '-{a b}:quick' ORDER BY rowid",
"SELECT rowid FROM ft WHERE ft MATCH '-{b c}:quick' ORDER BY rowid",
"SELECT rowid FROM ft WHERE ft MATCH '-a:quick' ORDER BY rowid",
"SELECT rowid FROM ft WHERE ft MATCH '{A B}:quick' ORDER BY rowid",
"SELECT rowid FROM ft WHERE ft MATCH '{a b} : quick' ORDER BY rowid",
"SELECT rowid FROM ft WHERE ft MATCH '{a b}:quick OR {c}:car' ORDER BY rowid",
"SELECT rowid FROM ft WHERE ft MATCH '{a}:quick AND {c}:red' ORDER BY rowid",
"SELECT rowid FROM ft WHERE ft MATCH '{a b c}:fox NOT {a}:quick' ORDER BY rowid",
"SELECT rowid FROM ft WHERE ft MATCH '{a b}:(quick OR fox)' ORDER BY rowid",
"SELECT rowid FROM ft WHERE ft MATCH '-{a}:(quick OR car)' ORDER BY rowid",
"SELECT rowid FROM ft WHERE ft MATCH '{a b}:(c:quick)' ORDER BY rowid",
"SELECT rowid FROM ft WHERE ft MATCH '{a b c}:(a:quick)' ORDER BY rowid",
"SELECT rowid FROM ft WHERE ft MATCH '-{a}:(b:quick)' ORDER BY rowid",
"SELECT rowid FROM ft WHERE ft MATCH '{a}:qui*' ORDER BY rowid",
"SELECT rowid FROM ft WHERE ft MATCH '{a b}:qui*' ORDER BY rowid",
"SELECT rowid FROM ft WHERE ft MATCH '{a}:NEAR(quick fox)' ORDER BY rowid",
"SELECT rowid FROM ft WHERE ft MATCH '{b}:NEAR(quick fox)' ORDER BY rowid",
"SELECT rowid FROM ft WHERE ft MATCH 'a:quick OR c:quick' ORDER BY rowid",
"SELECT rowid FROM ft WHERE ft MATCH 'b:dog' ORDER BY rowid",
];
for q in queries {
let want = sqlite3(&path, q);
let got = render(&g.query(q).unwrap());
assert_eq!(got, want, "mismatch for `{q}`");
}
let _ = std::fs::remove_file(&path);
}
#[test]
fn unknown_column_in_filter_errors() {
if Command::new("sqlite3").arg("--version").output().is_err() {
eprintln!("sqlite3 not found; skipping");
return;
}
let path = std::env::temp_dir().join(format!("gsql-fts5colerr-{}.db", std::process::id()));
let path = path.to_string_lossy().into_owned();
let _ = std::fs::remove_file(&path);
sqlite3(&path, SETUP);
let mut g = Connection::open_memory().unwrap();
for s in SETUP.split(';') {
if !s.trim().is_empty() {
g.execute(s).unwrap();
}
}
let cases = [
("SELECT rowid FROM ft WHERE ft MATCH '{z}:quick'", "z"),
("SELECT rowid FROM ft WHERE ft MATCH '{a z}:quick'", "z"),
("SELECT rowid FROM ft WHERE ft MATCH '-{z}:quick'", "z"),
("SELECT rowid FROM ft WHERE ft MATCH 'z:quick'", "z"),
];
for (q, missing) in cases {
let sqlite_out = {
let o = Command::new("sqlite3").arg(&path).arg(q).output().unwrap();
let err = String::from_utf8_lossy(&o.stderr);
assert!(
err.contains(&format!("no such column: {missing}")),
"sqlite3 should report the missing column for `{q}`, got stderr: {err}"
);
String::from_utf8_lossy(&o.stdout).trim_end().to_string()
};
assert_eq!(sqlite_out, "", "sqlite3 should yield no rows for `{q}`");
let err = g.query(q).expect_err(&format!("`{q}` should error"));
assert!(
err.to_string()
.contains(&format!("no such column: {missing}")),
"graphite error message for `{q}`: {err}"
);
}
let _ = std::fs::remove_file(&path);
}
#[test]
fn unknown_column_errors_on_empty_table() {
let mut g = Connection::open_memory().unwrap();
g.execute("CREATE VIRTUAL TABLE ft USING fts5(a,b,c)")
.unwrap();
let err = g
.query("SELECT rowid FROM ft WHERE ft MATCH '{z}:quick'")
.expect_err("should error even with no rows");
assert!(
err.to_string().contains("no such column: z"),
"expected a no-such-column error, got: {err}"
);
}
#[test]
fn malformed_brace_is_a_syntax_error() {
if Command::new("sqlite3").arg("--version").output().is_err() {
eprintln!("sqlite3 not found; skipping");
return;
}
let path = std::env::temp_dir().join(format!("gsql-fts5colsyn-{}.db", std::process::id()));
let path = path.to_string_lossy().into_owned();
let _ = std::fs::remove_file(&path);
sqlite3(&path, SETUP);
let mut g = Connection::open_memory().unwrap();
for s in SETUP.split(';') {
if !s.trim().is_empty() {
g.execute(s).unwrap();
}
}
let cases = [
"SELECT rowid FROM ft WHERE ft MATCH '{a}:{b}:quick'",
"SELECT rowid FROM ft WHERE ft MATCH 'a:{b}:quick'",
"SELECT rowid FROM ft WHERE ft MATCH '{a b} quick'",
"SELECT rowid FROM ft WHERE ft MATCH '{}:quick'",
];
for q in cases {
let o = Command::new("sqlite3").arg(&path).arg(q).output().unwrap();
let err = String::from_utf8_lossy(&o.stderr);
assert!(
err.contains("fts5: syntax error"),
"sqlite3 should report a syntax error for `{q}`, got stderr: {err}"
);
let g_err = g.query(q).expect_err(&format!("`{q}` should error"));
assert!(
g_err.to_string().contains("fts5: syntax error"),
"graphite should report a syntax error for `{q}`, got: {g_err}"
);
}
let _ = std::fs::remove_file(&path);
}