#![cfg(feature = "std")]
#![cfg(feature = "fts5")]
use graphitesql::Connection;
use std::io::Write;
use std::process::{Command, Stdio};
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-mt-{}-{}-{}.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_stdin(path: &str, sql: &str) {
let mut child = Command::new("sqlite3")
.arg(path)
.stdin(Stdio::piped())
.stdout(Stdio::null())
.spawn()
.unwrap();
child
.stdin
.take()
.unwrap()
.write_all(sql.as_bytes())
.unwrap();
assert!(child.wait().unwrap().success());
}
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 build_pair(g: &str, s: &str, values: &str) {
{
let mut c = Connection::create(g).unwrap();
c.execute("CREATE VIRTUAL TABLE ft USING fts5(body)")
.unwrap();
c.execute("BEGIN").ok();
c.execute(&format!("INSERT INTO ft(rowid,body) VALUES {values}"))
.unwrap();
c.execute("COMMIT").ok();
}
sqlite_stdin(
s,
&format!(
"CREATE VIRTUAL TABLE ft USING fts5(body);\n\
INSERT INTO ft(rowid,body) VALUES {values};\n\
INSERT INTO ft(ft) VALUES('optimize');"
),
);
}
fn multiterm_values(n_docs: usize, terms_per_doc: usize, vocab: usize) -> String {
let mut s = String::new();
for d in 1..=n_docs {
if d > 1 {
s.push(',');
}
let mut body = String::new();
for w in 0..terms_per_doc {
if w > 0 {
body.push(' ');
}
let t = (d * 7 + w * 13) % vocab;
body.push_str(&format!("term{t:04}"));
}
s.push_str(&format!("({d},'{body}')"));
}
s
}
fn varlen_values(n_docs: usize, terms_per_doc: usize, vocab: usize) -> String {
let mut s = String::new();
for d in 1..=n_docs {
if d > 1 {
s.push(',');
}
let mut body = String::new();
for w in 0..terms_per_doc {
if w > 0 {
body.push(' ');
}
let t = (d * 7 + w * 13) % vocab;
body.push_str(&"z".repeat(1 + (t % 9)));
body.push_str(&format!("{t}"));
}
s.push_str(&format!("({d},'{body}')"));
}
s
}
fn assert_byte_identical(g: &str, s: &str, min_leaves: usize) {
let n_leaves: usize = sqlite_raw(g, "SELECT count(*) FROM ft_data WHERE id>10;")
.parse()
.unwrap();
assert!(
n_leaves >= min_leaves,
"corpus produced only {n_leaves} leaves; expected >= {min_leaves}"
);
assert!(
sqlite_integrity_ok(g),
"sqlite integrity-check rejected graphite's multi-term file"
);
assert_eq!(sqlite_raw(g, "PRAGMA integrity_check;"), "ok");
let data = "SELECT id||':'||quote(block) FROM ft_data ORDER BY id;";
assert_eq!(
sqlite_raw(g, data),
sqlite_raw(s, data),
"%_data bytes diverge from sqlite ({n_leaves} leaves)"
);
let idx = "SELECT segid||':'||quote(term)||':'||pgno FROM ft_idx ORDER BY segid, term;";
assert_eq!(
sqlite_raw(g, idx),
sqlite_raw(s, idx),
"%_idx bytes diverge from sqlite ({n_leaves} leaves)"
);
let m = "SELECT count(*) FROM ft WHERE ft MATCH 'term0000';";
assert_eq!(sqlite_raw(g, m), sqlite_raw(s, m), "MATCH count diverges");
}
#[test]
fn multiterm_leaf_fill_is_byte_identical() {
if !have_fts5_sqlite() {
eprintln!("sqlite3 with FTS5 not found; skipping");
return;
}
for &(nd, tpd, vocab, min_leaves) in &[
(200usize, 10usize, 400usize, 2usize),
(500, 12, 800, 5),
(1000, 15, 1500, 10),
(3000, 8, 6000, 20),
] {
let g = tmp_path("mt-g");
let s = tmp_path("mt-s");
build_pair(&g, &s, &multiterm_values(nd, tpd, vocab));
assert_byte_identical(&g, &s, min_leaves);
let _ = std::fs::remove_file(&g);
let _ = std::fs::remove_file(&s);
}
}
#[test]
fn multiterm_varlen_leaf_fill_is_byte_identical() {
if !have_fts5_sqlite() {
eprintln!("sqlite3 with FTS5 not found; skipping");
return;
}
for &(nd, tpd, vocab, min_leaves) in &[
(300usize, 15usize, 600usize, 3usize),
(800, 20, 1500, 10),
(1500, 10, 3000, 15),
] {
let g = tmp_path("vl-g");
let s = tmp_path("vl-s");
{
let mut c = Connection::create(&g).unwrap();
c.execute("CREATE VIRTUAL TABLE ft USING fts5(body)")
.unwrap();
c.execute("BEGIN").ok();
c.execute(&format!(
"INSERT INTO ft(rowid,body) VALUES {}",
varlen_values(nd, tpd, vocab)
))
.unwrap();
c.execute("COMMIT").ok();
}
sqlite_stdin(
&s,
&format!(
"CREATE VIRTUAL TABLE ft USING fts5(body);\n\
INSERT INTO ft(rowid,body) VALUES {};\n\
INSERT INTO ft(ft) VALUES('optimize');",
varlen_values(nd, tpd, vocab)
),
);
let n_leaves: usize = sqlite_raw(&g, "SELECT count(*) FROM ft_data WHERE id>10;")
.parse()
.unwrap();
assert!(
n_leaves >= min_leaves,
"varlen corpus produced only {n_leaves} leaves; expected >= {min_leaves}"
);
assert!(sqlite_integrity_ok(&g));
assert_eq!(sqlite_raw(&g, "PRAGMA integrity_check;"), "ok");
let data = "SELECT id||':'||quote(block) FROM ft_data ORDER BY id;";
assert_eq!(
sqlite_raw(&g, data),
sqlite_raw(&s, data),
"varlen %_data bytes diverge ({n_leaves} leaves)"
);
let idx = "SELECT segid||':'||quote(term)||':'||pgno FROM ft_idx ORDER BY segid, term;";
assert_eq!(
sqlite_raw(&g, idx),
sqlite_raw(&s, idx),
"varlen %_idx bytes diverge ({n_leaves} leaves)"
);
let _ = std::fs::remove_file(&g);
let _ = std::fs::remove_file(&s);
}
}