#![cfg(feature = "std")]
use graphitesql::exec::eval::Params;
use graphitesql::{Connection, Value};
use std::process::Command;
struct Rng(u64);
impl Rng {
fn new(seed: u64) -> Rng {
Rng(seed ^ 0x9E37_79B9_7F4A_7C15)
}
fn next_u64(&mut self) -> u64 {
self.0 = self.0.wrapping_add(0x9E37_79B9_7F4A_7C15);
let mut z = self.0;
z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
z ^ (z >> 31)
}
}
fn env_usize(key: &str, default: usize) -> usize {
std::env::var(key)
.ok()
.and_then(|v| v.parse().ok())
.unwrap_or(default)
}
fn stress_db(tag: &str) -> String {
let dir = std::env::temp_dir().join(format!("gsql-stress-{}", std::process::id()));
let _ = std::fs::create_dir_all(&dir);
dir.join(format!("{tag}.db")).to_string_lossy().into_owned()
}
fn rm_db(path: &str) {
for suffix in ["", "-journal", "-wal", "-shm"] {
let _ = std::fs::remove_file(format!("{path}{suffix}"));
}
}
fn sqlite3_scalar(path: &str, sql: &str) -> Option<String> {
let out = Command::new("sqlite3").arg(path).arg(sql).output().ok()?;
Some(if out.status.success() {
String::from_utf8_lossy(&out.stdout).trim().to_string()
} else {
String::from_utf8_lossy(&out.stderr).trim().to_string()
})
}
fn graphite_scalar(c: &Connection, sql: &str) -> String {
match &c.query(sql).unwrap().rows[0][0] {
Value::Integer(i) => i.to_string(),
Value::Text(t) => t.to_string(),
Value::Real(r) => r.to_string(),
Value::Null => String::new(),
Value::Blob(b) => format!("<blob {} bytes>", b.len()),
}
}
fn assert_integrity(c: &Connection, path: &str) {
assert_eq!(
c.query("PRAGMA integrity_check").unwrap().rows[0][0],
Value::Text("ok".into()),
"graphite integrity_check failed"
);
if let Some(ok) = sqlite3_scalar(path, "PRAGMA integrity_check;") {
assert_eq!(ok, "ok", "sqlite3 integrity_check failed on graphite file");
}
}
#[test]
#[ignore = "heavy: large row count; run via the stress workflow"]
fn stress_volume_many_rows() {
let rows = env_usize("GSQL_VOLUME_ROWS", 100_000);
let path = stress_db("volume-many-rows");
rm_db(&path);
let mut c = Connection::create(&path).unwrap();
c.execute("PRAGMA journal_mode=WAL").unwrap();
c.execute("CREATE TABLE t(id INTEGER PRIMARY KEY, k INTEGER, s TEXT, data BLOB)")
.unwrap();
c.execute("CREATE INDEX t_k ON t(k)").unwrap();
let mut rng = Rng::new(0xF00D);
let mut key_sum: i64 = 0;
let batch = 5_000;
let mut i = 0usize;
while i < rows {
c.execute("BEGIN").unwrap();
let end = (i + batch).min(rows);
for j in i..end {
let k = (rng.next_u64() % 1_000_000) as i64;
key_sum += k;
if j % 500 == 0 {
c.execute_params(
"INSERT INTO t(k, s, data) VALUES(?1, ?2, randomblob(3000))",
&Params {
positional: vec![Value::Integer(k), Value::Text(format!("row-{j}").into())],
named: vec![],
},
)
.unwrap();
} else {
c.execute_params(
"INSERT INTO t(k, s) VALUES(?1, ?2)",
&Params {
positional: vec![Value::Integer(k), Value::Text(format!("row-{j}").into())],
named: vec![],
},
)
.unwrap();
}
}
c.execute("COMMIT").unwrap();
i = end;
}
assert_eq!(
graphite_scalar(&c, "SELECT count(*) FROM t"),
rows.to_string()
);
assert_eq!(
graphite_scalar(&c, "SELECT COALESCE(SUM(k),0) FROM t"),
key_sum.to_string()
);
let via_index = graphite_scalar(
&c,
"SELECT count(*) FROM t WHERE k BETWEEN 100000 AND 200000",
);
if let Some(s) = sqlite3_scalar(
&path,
"SELECT count(*) FROM t WHERE k BETWEEN 100000 AND 200000;",
) {
assert_eq!(via_index, s, "indexed range count differs from sqlite3");
}
if let Some(s) = sqlite3_scalar(&path, "SELECT count(*) FROM t;") {
assert_eq!(s, rows.to_string(), "sqlite3 row count differs");
}
if let Some(s) = sqlite3_scalar(&path, "SELECT COALESCE(SUM(k),0) FROM t;") {
assert_eq!(s, key_sum.to_string(), "sqlite3 key sum differs");
}
assert_integrity(&c, &path);
drop(c);
rm_db(&path);
}
#[test]
#[ignore = "heavy: multi-MiB overflow blobs; run via the stress workflow"]
fn stress_volume_overlarge_blobs() {
let mb = env_usize("GSQL_VOLUME_BLOB_MB", 4);
let count = env_usize("GSQL_VOLUME_BLOBS", 6);
let path = stress_db("volume-blobs");
rm_db(&path);
let mut c = Connection::create(&path).unwrap();
c.execute("CREATE TABLE big(id INTEGER PRIMARY KEY, n INTEGER, data BLOB)")
.unwrap();
let mut rng = Rng::new(0xBEEF);
let make_blob = |id: usize, len: usize| -> Vec<u8> {
let mut v = Vec::with_capacity(len);
let mut r = Rng::new(0xABCD_0000 ^ id as u64);
for _ in 0..len {
v.push((r.next_u64() & 0xFF) as u8);
}
v
};
for id in 0..count {
let len = mb * 1024 * 1024 + (rng.next_u64() as usize % 4096);
let blob = make_blob(id, len);
c.execute_params(
"INSERT INTO big(id, n, data) VALUES(?1, ?2, ?3)",
&Params {
positional: vec![
Value::Integer(id as i64),
Value::Integer(len as i64),
Value::Blob(blob),
],
named: vec![],
},
)
.unwrap();
}
for id in 0..count {
let r = c
.query(&format!("SELECT n, data FROM big WHERE id={id}"))
.unwrap();
let n = match r.rows[0][0] {
Value::Integer(n) => n as usize,
_ => panic!("n not integer"),
};
let data = match &r.rows[0][1] {
Value::Blob(b) => b.clone(),
other => panic!("data not blob: {other:?}"),
};
assert_eq!(
data.len(),
n,
"blob {id}: length column disagrees with data"
);
assert_eq!(
data,
make_blob(id, n),
"blob {id}: overflow chain content corrupted"
);
}
for id in 0..count {
if let Some(s) = sqlite3_scalar(
&path,
&format!("SELECT length(data) FROM big WHERE id={id};"),
) {
let n = graphite_scalar(&c, &format!("SELECT n FROM big WHERE id={id}"));
assert_eq!(s, n, "sqlite3 blob {id} length differs");
}
}
assert_integrity(&c, &path);
drop(c);
rm_db(&path);
}
#[test]
#[ignore = "heavy: random-key fragmentation shape; run via the stress workflow"]
fn stress_volume_random_key_fragmentation() {
let keys = env_usize("GSQL_VOLUME_FRAG_KEYS", 50_000);
let path = stress_db("volume-frag");
rm_db(&path);
let mut c = Connection::create(&path).unwrap();
c.execute("CREATE TABLE frag(k TEXT PRIMARY KEY, v INTEGER) WITHOUT ROWID")
.unwrap();
c.execute("CREATE INDEX frag_v ON frag(v)").unwrap();
let mut rng = Rng::new(0x5EED);
let batch = 5_000;
let mut i = 0usize;
while i < keys {
c.execute("BEGIN").unwrap();
let end = (i + batch).min(keys);
for _ in i..end {
let key = format!("{:016x}", rng.next_u64());
let v = (rng.next_u64() % 1_000_000) as i64;
c.execute_params(
"INSERT OR IGNORE INTO frag(k, v) VALUES(?1, ?2)",
&Params {
positional: vec![Value::Text(key.into()), Value::Integer(v)],
named: vec![],
},
)
.unwrap();
}
c.execute("COMMIT").unwrap();
i = end;
}
assert_integrity(&c, &path);
let gp: i64 = c.query("PRAGMA page_count").unwrap().rows[0][0]
.clone()
.into_i64();
if let Some(sp) = sqlite3_scalar(&path, "PRAGMA page_count;")
&& let Ok(sp) = sp.parse::<i64>()
{
assert!(
gp <= sp * 2 + 16,
"fragmentation regression: graphite {gp} pages vs sqlite {sp}"
);
}
drop(c);
rm_db(&path);
}
#[test]
#[ignore = "heavy: big transaction + VACUUM; run via the stress workflow"]
fn stress_volume_big_txn_and_vacuum() {
let rows = env_usize("GSQL_VOLUME_ROWS", 100_000);
let path = stress_db("volume-vacuum");
rm_db(&path);
let mut c = Connection::create(&path).unwrap();
c.execute("CREATE TABLE t(id INTEGER PRIMARY KEY, v INTEGER, pad TEXT)")
.unwrap();
c.execute("BEGIN").unwrap();
for i in 0..rows {
c.execute_params(
"INSERT INTO t(id, v, pad) VALUES(?1, ?2, ?3)",
&Params {
positional: vec![
Value::Integer(i as i64),
Value::Integer((i as i64 * 7) % 1000),
Value::Text(format!("padding-value-for-row-{i}").into()),
],
named: vec![],
},
)
.unwrap();
}
c.execute("COMMIT").unwrap();
assert_eq!(
graphite_scalar(&c, "SELECT count(*) FROM t"),
rows.to_string()
);
assert_integrity(&c, &path);
c.execute("DELETE FROM t WHERE id % 2 = 0").unwrap();
let remaining = graphite_scalar(&c, "SELECT count(*) FROM t");
c.execute("VACUUM").unwrap();
assert_eq!(
graphite_scalar(&c, "SELECT count(*) FROM t"),
remaining,
"VACUUM changed the row count"
);
assert_integrity(&c, &path);
if let Some(s) = sqlite3_scalar(&path, "SELECT count(*) FROM t;") {
assert_eq!(s, remaining, "sqlite3 row count differs after VACUUM");
}
drop(c);
rm_db(&path);
}
trait IntoI64 {
fn into_i64(self) -> i64;
}
impl IntoI64 for Value {
fn into_i64(self) -> i64 {
match self {
Value::Integer(i) => i,
Value::Real(r) => r as i64,
Value::Text(t) => t.to_string().parse().unwrap_or(0),
_ => 0,
}
}
}