#![cfg(feature = "std")]
#![cfg(feature = "fts5")]
use graphitesql::Connection;
use std::process::Command;
use std::sync::atomic::{AtomicU64, Ordering};
fn tmp_path() -> String {
static SEQ: AtomicU64 = AtomicU64::new(0);
let p = std::env::temp_dir().join(format!(
"gsql-fts5-m2b-{}-{}.db",
std::process::id(),
SEQ.fetch_add(1, Ordering::Relaxed)
));
let p = p.to_string_lossy().into_owned();
let _ = std::fs::remove_file(&p);
p
}
fn sqlite_run(path: &str, q: &str) -> String {
let o = Command::new("sqlite3").arg(path).arg(q).output().unwrap();
assert!(
o.status.success(),
"sqlite3 failed for {q:?}: {}",
String::from_utf8_lossy(&o.stderr)
);
let mut v: Vec<String> = String::from_utf8_lossy(&o.stdout)
.lines()
.map(str::to_string)
.collect();
v.sort();
v.join("|")
}
fn have_sqlite() -> bool {
Command::new("sqlite3").arg("--version").output().is_ok()
}
#[test]
fn sqlite_reads_and_matches_graphite_written_fts5() {
if Command::new("sqlite3").arg("--version").output().is_err() {
eprintln!("sqlite3 not found; skipping");
return;
}
let path = std::env::temp_dir().join(format!("gsql-fts5-m2b-{}.db", std::process::id()));
let path = path.to_string_lossy().into_owned();
let _ = std::fs::remove_file(&path);
{
let mut c = Connection::create(&path).unwrap();
c.execute("CREATE VIRTUAL TABLE t USING fts5(title, body)")
.unwrap();
c.execute(
"INSERT INTO t(rowid, title, body) VALUES \
(1,'hello world','the quick brown fox'),\
(2,'goodbye moon','a lazy dog runs'),\
(3,'hello again','the fox and the dog')",
)
.unwrap();
}
let sqlite = |q: &str| {
let o = Command::new("sqlite3").arg(&path).arg(q).output().unwrap();
assert!(
o.status.success(),
"sqlite3 failed for {q:?}: {}",
String::from_utf8_lossy(&o.stderr)
);
let mut v: Vec<String> = String::from_utf8_lossy(&o.stdout)
.lines()
.map(str::to_string)
.collect();
v.sort();
v.join("|")
};
assert_eq!(
sqlite("SELECT rowid, title FROM t ORDER BY rowid"),
"1|hello world|2|goodbye moon|3|hello again"
);
assert_eq!(
sqlite("SELECT rowid FROM t WHERE t MATCH 'fox' ORDER BY rowid"),
"1|3"
);
assert_eq!(
sqlite("SELECT rowid FROM t WHERE t MATCH 'dog' ORDER BY rowid"),
"2|3"
);
assert_eq!(
sqlite("SELECT rowid FROM t WHERE t MATCH 'hello' ORDER BY rowid"),
"1|3"
);
assert_eq!(
sqlite("SELECT rowid FROM t WHERE t MATCH 'title:goodbye'"),
"2"
);
assert_eq!(
sqlite("SELECT rowid FROM t WHERE t MATCH '\"quick brown\"'"),
"1"
);
let chk = Command::new("sqlite3")
.arg(&path)
.arg("INSERT INTO t(t) VALUES('integrity-check');")
.output()
.unwrap();
assert!(
chk.status.success(),
"fts5 integrity-check failed: {}",
String::from_utf8_lossy(&chk.stderr)
);
assert_eq!(sqlite("PRAGMA integrity_check"), "ok");
let _ = std::fs::remove_file(&path);
}
#[test]
fn sqlite_matches_multi_leaf_graphite_fts5() {
if !have_sqlite() {
eprintln!("sqlite3 not found; skipping");
return;
}
let path = tmp_path();
{
let mut c = Connection::create(&path).unwrap();
c.execute("CREATE VIRTUAL TABLE t USING fts5(body)")
.unwrap();
let mut sql = String::from("INSERT INTO t(rowid, body) VALUES ");
for i in 1..=400 {
if i > 1 {
sql.push(',');
}
sql.push_str(&format!("({i},'common word{i:04}')"));
}
c.execute(&sql).unwrap();
}
assert_eq!(
sqlite_run(&path, "SELECT count(*) FROM t WHERE t MATCH 'common'"),
"400"
);
assert_eq!(
sqlite_run(&path, "SELECT rowid FROM t WHERE t MATCH 'word0377'"),
"377"
);
assert_eq!(
sqlite_run(&path, "SELECT rowid FROM t WHERE t MATCH 'word0001'"),
"1"
);
assert_eq!(sqlite_run(&path, "PRAGMA integrity_check"), "ok");
let chk = Command::new("sqlite3")
.arg(&path)
.arg("INSERT INTO t(t) VALUES('integrity-check');")
.output()
.unwrap();
assert!(
chk.status.success(),
"fts5 integrity-check: {}",
String::from_utf8_lossy(&chk.stderr)
);
let _ = std::fs::remove_file(&path);
}
#[test]
fn sqlite_matches_porter_graphite_fts5() {
if !have_sqlite() {
eprintln!("sqlite3 not found; skipping");
return;
}
let path = tmp_path();
{
let mut c = Connection::create(&path).unwrap();
c.execute("CREATE VIRTUAL TABLE t USING fts5(body, tokenize='porter')")
.unwrap();
c.execute(
"INSERT INTO t(rowid, body) VALUES \
(1,'the runners are running'),(2,'a connection was connected')",
)
.unwrap();
}
assert_eq!(
sqlite_run(&path, "SELECT rowid FROM t WHERE t MATCH 'run'"),
"1"
);
assert_eq!(
sqlite_run(&path, "SELECT rowid FROM t WHERE t MATCH 'connect'"),
"2"
);
assert_eq!(sqlite_run(&path, "PRAGMA integrity_check"), "ok");
let _ = std::fs::remove_file(&path);
}
#[test]
fn sqlite_reads_graphite_fts5_after_update_delete() {
if !have_sqlite() {
eprintln!("sqlite3 not found; skipping");
return;
}
let path = tmp_path();
{
let mut c = Connection::create(&path).unwrap();
c.execute("CREATE VIRTUAL TABLE t USING fts5(body)")
.unwrap();
c.execute(
"INSERT INTO t(rowid, body) VALUES (1,'alpha beta'),(2,'gamma delta'),(3,'epsilon')",
)
.unwrap();
c.execute("UPDATE t SET body='zeta eta' WHERE rowid=2")
.unwrap();
c.execute("DELETE FROM t WHERE rowid=3").unwrap();
}
assert_eq!(
sqlite_run(&path, "SELECT rowid FROM t WHERE t MATCH 'gamma'"),
""
);
assert_eq!(
sqlite_run(&path, "SELECT rowid FROM t WHERE t MATCH 'zeta'"),
"2"
);
assert_eq!(
sqlite_run(&path, "SELECT rowid FROM t WHERE t MATCH 'epsilon'"),
""
);
assert_eq!(
sqlite_run(&path, "SELECT rowid FROM t WHERE t MATCH 'alpha'"),
"1"
);
assert_eq!(
sqlite_run(&path, "SELECT rowid, body FROM t ORDER BY rowid"),
"1|alpha beta|2|zeta eta"
);
assert_eq!(sqlite_run(&path, "PRAGMA integrity_check"), "ok");
let _ = std::fs::remove_file(&path);
}
#[test]
fn sqlite_matches_accented_graphite_fts5() {
if !have_sqlite() {
eprintln!("sqlite3 not found; skipping");
return;
}
let path = tmp_path();
{
let mut c = Connection::create(&path).unwrap();
c.execute("CREATE VIRTUAL TABLE t USING fts5(body)")
.unwrap();
c.execute(
"INSERT INTO t(rowid, body) VALUES \
(1,'café résumé'),(2,'naïve über'),(3,'Pâté à la française')",
)
.unwrap();
}
assert_eq!(sqlite_run(&path, "PRAGMA integrity_check"), "ok");
assert_eq!(
sqlite_run(&path, "SELECT rowid FROM t WHERE t MATCH 'cafe'"),
"1"
);
assert_eq!(
sqlite_run(&path, "SELECT rowid FROM t WHERE t MATCH 'resume'"),
"1"
);
assert_eq!(
sqlite_run(&path, "SELECT rowid FROM t WHERE t MATCH 'naive'"),
"2"
);
assert_eq!(
sqlite_run(&path, "SELECT rowid FROM t WHERE t MATCH 'francaise'"),
"3"
);
let _ = std::fs::remove_file(&path);
}
#[test]
fn sqlite_matches_extended_latin_graphite_fts5() {
if !have_sqlite() {
eprintln!("sqlite3 not found; skipping");
return;
}
let path = tmp_path();
{
let mut c = Connection::create(&path).unwrap();
c.execute("CREATE VIRTUAL TABLE t USING fts5(body)")
.unwrap();
c.execute(
"INSERT INTO t(rowid, body) VALUES \
(1,'zażółć gęślą jaźń'),(2,'Dvořák Antonín'),\
(3,'București România'),(4,'Tiếng Việt Nam mạ lủ')",
)
.unwrap();
}
assert_eq!(sqlite_run(&path, "PRAGMA integrity_check"), "ok");
assert_eq!(
sqlite_run(&path, "SELECT rowid FROM t WHERE t MATCH 'gesla'"),
"1"
);
assert_eq!(
sqlite_run(&path, "SELECT rowid FROM t WHERE t MATCH 'jazn'"),
"1"
);
assert_eq!(
sqlite_run(&path, "SELECT rowid FROM t WHERE t MATCH 'dvorak'"),
"2"
);
assert_eq!(
sqlite_run(&path, "SELECT rowid FROM t WHERE t MATCH 'bucuresti'"),
"3"
);
assert_eq!(
sqlite_run(&path, "SELECT rowid FROM t WHERE t MATCH 'ma lu'"),
"4"
);
let _ = std::fs::remove_file(&path);
}
#[test]
fn sqlite_matches_remove_diacritics_levels_graphite_fts5() {
if !have_sqlite() {
eprintln!("sqlite3 not found; skipping");
return;
}
let p0 = tmp_path();
{
let mut c = Connection::create(&p0).unwrap();
c.execute("CREATE VIRTUAL TABLE t USING fts5(b, tokenize='unicode61 remove_diacritics 0')")
.unwrap();
c.execute("INSERT INTO t(rowid,b) VALUES (1,'café crème')")
.unwrap();
}
assert_eq!(sqlite_run(&p0, "PRAGMA integrity_check"), "ok");
assert_eq!(
sqlite_run(&p0, "SELECT rowid FROM t WHERE t MATCH 'café'"),
"1"
);
assert_eq!(
sqlite_run(&p0, "SELECT rowid FROM t WHERE t MATCH 'cafe'"),
""
);
{
let c = Connection::open(&p0).unwrap();
let hit = c.query("SELECT rowid FROM t WHERE t MATCH 'café'").unwrap();
assert_eq!(hit.rows.len(), 1);
let miss = c.query("SELECT rowid FROM t WHERE t MATCH 'cafe'").unwrap();
assert_eq!(miss.rows.len(), 0);
}
let _ = std::fs::remove_file(&p0);
let p2 = tmp_path();
{
let mut c = Connection::create(&p2).unwrap();
c.execute("CREATE VIRTUAL TABLE t USING fts5(b, tokenize='unicode61 remove_diacritics 2')")
.unwrap();
c.execute("INSERT INTO t(rowid,b) VALUES (1,'Tiếng Việt')")
.unwrap();
}
assert_eq!(sqlite_run(&p2, "PRAGMA integrity_check"), "ok");
assert_eq!(
sqlite_run(&p2, "SELECT rowid FROM t WHERE t MATCH 'tieng viet'"),
"1"
);
let _ = std::fs::remove_file(&p2);
}
#[test]
fn sqlite_matches_tokenchars_separators_graphite_fts5() {
if !have_sqlite() {
eprintln!("sqlite3 not found; skipping");
return;
}
let pc = tmp_path();
{
let mut c = Connection::create(&pc).unwrap();
c.execute("CREATE VIRTUAL TABLE t USING fts5(b, tokenize=\"unicode61 tokenchars '-_'\")")
.unwrap();
c.execute("INSERT INTO t(rowid,b) VALUES (1,'foo-bar a_b'),(2,'x@y plain')")
.unwrap();
}
assert_eq!(sqlite_run(&pc, "PRAGMA integrity_check"), "ok");
assert_eq!(
sqlite_run(&pc, "SELECT rowid FROM t WHERE t MATCH '\"foo-bar\"'"),
"1"
);
assert_eq!(
sqlite_run(&pc, "SELECT rowid FROM t WHERE t MATCH '\"a_b\"'"),
"1"
);
assert_eq!(
sqlite_run(&pc, "SELECT rowid FROM t WHERE t MATCH 'x AND y'"),
"2"
);
let _ = std::fs::remove_file(&pc);
let ps = tmp_path();
{
let mut c = Connection::create(&ps).unwrap();
c.execute("CREATE VIRTUAL TABLE t USING fts5(b, tokenize=\"unicode61 separators 'x'\")")
.unwrap();
c.execute("INSERT INTO t(rowid,b) VALUES (1,'axbxc hello')")
.unwrap();
}
assert_eq!(sqlite_run(&ps, "PRAGMA integrity_check"), "ok");
assert_eq!(
sqlite_run(&ps, "SELECT rowid FROM t WHERE t MATCH 'b'"),
"1"
);
assert_eq!(
sqlite_run(&ps, "SELECT rowid FROM t WHERE t MATCH 'c'"),
"1"
);
let _ = std::fs::remove_file(&ps);
}