#![cfg(feature = "std")]
#![cfg(feature = "fts5")]
use graphitesql::Connection;
use std::process::Command;
use std::sync::atomic::{AtomicU64, Ordering};
fn tmp_path(tag: &str) -> String {
static SEQ: AtomicU64 = AtomicU64::new(0);
let p = std::env::temp_dir().join(format!(
"gsql-fts5-ms-{}-{}-{}.db",
tag,
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 have_fts5_sqlite() -> bool {
let o = Command::new("sqlite3")
.arg(":memory:")
.arg("CREATE VIRTUAL TABLE t USING fts5(a); SELECT 1;")
.output();
matches!(o, Ok(o) if o.status.success())
}
fn sqlite_raw(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)
);
String::from_utf8_lossy(&o.stdout).trim_end().to_string()
}
fn sqlite_integrity_ok(path: &str) -> bool {
Command::new("sqlite3")
.arg(path)
.arg("INSERT INTO ft(ft) VALUES('integrity-check');")
.output()
.map(|o| o.status.success())
.unwrap_or(false)
}
fn dump_shadows_sqlite(path: &str) -> String {
let data = sqlite_raw(path, "SELECT id, quote(block) FROM ft_data ORDER BY id;");
let idx = sqlite_raw(
path,
"SELECT segid, quote(term), pgno FROM ft_idx ORDER BY segid, term, pgno;",
);
let ds = sqlite_raw(path, "SELECT id, quote(sz) FROM ft_docsize ORDER BY id;");
format!("DATA\n{data}\nIDX\n{idx}\nDOCSIZE\n{ds}")
}
fn dump_shadows_graphite(c: &Connection) -> String {
let fmt = |r: graphitesql::QueryResult| -> String {
r.rows
.iter()
.map(|row| {
row.iter()
.map(|v| match v {
graphitesql::Value::Integer(i) => i.to_string(),
graphitesql::Value::Text(t) => String::from(t.as_str()),
graphitesql::Value::Null => String::new(),
other => format!("{other:?}"),
})
.collect::<Vec<_>>()
.join("|")
})
.collect::<Vec<_>>()
.join("\n")
};
let data = fmt(c
.query("SELECT id, quote(block) FROM ft_data ORDER BY id")
.unwrap());
let idx = fmt(c
.query("SELECT segid, quote(term), pgno FROM ft_idx ORDER BY segid, term, pgno")
.unwrap());
let ds = fmt(c
.query("SELECT id, quote(sz) FROM ft_docsize ORDER BY id")
.unwrap());
format!("DATA\n{data}\nIDX\n{idx}\nDOCSIZE\n{ds}")
}
fn assert_seq_identical(tag: &str, docs: &[&str]) {
let g = tmp_path(&format!("{tag}-g"));
let s = tmp_path(&format!("{tag}-s"));
let mut c = Connection::create(&g).unwrap();
c.execute("CREATE VIRTUAL TABLE ft USING fts5(x)").unwrap();
for d in docs {
c.execute(&format!("INSERT INTO ft(x) VALUES('{d}')"))
.unwrap();
}
sqlite_raw(&s, "CREATE VIRTUAL TABLE ft USING fts5(x);");
for d in docs {
sqlite_raw(&s, &format!("INSERT INTO ft(x) VALUES('{d}');"));
}
let gs = dump_shadows_graphite(&c);
let ss = dump_shadows_sqlite(&s);
assert_eq!(gs, ss, "shadow-table bytes diverge for {tag}");
assert!(
sqlite_integrity_ok(&g),
"sqlite integrity-check rejected graphite's file for {tag}"
);
for d in docs {
let term = d.split_whitespace().next().unwrap_or("");
if term.is_empty() {
continue;
}
let gq = c
.query(&format!(
"SELECT rowid FROM ft WHERE ft MATCH '{term}' ORDER BY rowid"
))
.unwrap();
let grow: Vec<i64> = gq
.rows
.iter()
.map(|r| match &r[0] {
graphitesql::Value::Integer(i) => *i,
_ => -1,
})
.collect();
let srow_raw = sqlite_raw(
&s,
&format!("SELECT rowid FROM ft WHERE ft MATCH '{term}' ORDER BY rowid;"),
);
let srow: Vec<i64> = srow_raw
.lines()
.filter(|l| !l.is_empty())
.map(|l| l.parse().unwrap())
.collect();
assert_eq!(grow, srow, "MATCH '{term}' rows diverge for {tag}");
}
}
#[test]
fn multiseg_two_inserts_append_second_segment() {
if !have_fts5_sqlite() {
return;
}
assert_seq_identical("two", &["alpha beta", "gamma delta"]);
}
#[test]
fn multiseg_five_inserts_five_level0_segments() {
if !have_fts5_sqlite() {
return;
}
assert_seq_identical(
"five",
&[
"alpha beta",
"gamma delta",
"epsilon zeta",
"eta theta",
"iota kappa",
],
);
}
#[test]
fn multiseg_below_crisis_fifteen_segments() {
if !have_fts5_sqlite() {
return;
}
let docs: Vec<String> = (1..=15).map(|i| format!("word{i} tok{i}")).collect();
let refs: Vec<&str> = docs.iter().map(String::as_str).collect();
assert_seq_identical("fifteen", &refs);
}
#[test]
fn multiseg_crisis_merge_at_sixteen() {
if !have_fts5_sqlite() {
return;
}
let docs: Vec<String> = (1..=16).map(|i| format!("word{i} tok{i}")).collect();
let refs: Vec<&str> = docs.iter().map(String::as_str).collect();
assert_seq_identical("sixteen", &refs);
}
#[test]
fn multiseg_crisis_then_more_appends() {
if !have_fts5_sqlite() {
return;
}
let docs: Vec<String> = (1..=20).map(|i| format!("word{i} tok{i}")).collect();
let refs: Vec<&str> = docs.iter().map(String::as_str).collect();
assert_seq_identical("twenty", &refs);
}
#[test]
fn multiseg_second_crisis_and_beyond() {
if !have_fts5_sqlite() {
return;
}
let docs: Vec<String> = (1..=33).map(|i| format!("word{i} tok{i}")).collect();
let refs: Vec<&str> = docs.iter().map(String::as_str).collect();
assert_seq_identical("thirtythree", &refs);
}
#[test]
fn multiseg_multicolumn_appends() {
if !have_fts5_sqlite() {
return;
}
let g = tmp_path("mc-g");
let s = tmp_path("mc-s");
let mut c = Connection::create(&g).unwrap();
c.execute("CREATE VIRTUAL TABLE ft USING fts5(a, b)")
.unwrap();
sqlite_raw(&s, "CREATE VIRTUAL TABLE ft USING fts5(a, b);");
let rows = [
("alpha one", "beta two"),
("gamma three", "delta four"),
("epsilon five", "zeta six"),
];
for (a, b) in rows {
c.execute(&format!("INSERT INTO ft(a, b) VALUES('{a}', '{b}')"))
.unwrap();
sqlite_raw(&s, &format!("INSERT INTO ft(a, b) VALUES('{a}', '{b}');"));
}
assert_eq!(
dump_shadows_graphite(&c),
dump_shadows_sqlite(&s),
"multi-column shadow bytes diverge"
);
assert!(sqlite_integrity_ok(&g), "integrity-check rejected multicol");
}
#[test]
fn multiseg_single_insert_is_one_segment() {
if !have_fts5_sqlite() {
return;
}
assert_seq_identical("one", &["alpha beta gamma"]);
}