#![cfg(feature = "std")]
use graphitesql::{Connection, Value};
fn rows(c: &Connection, sql: &str) -> Vec<Vec<Value>> {
c.query(sql).unwrap().rows
}
#[test]
fn spatial_filter_and_rowid_alias() {
let mut c = Connection::open_memory().unwrap();
c.execute("CREATE VIRTUAL TABLE r USING rtree(id, minX, maxX, minY, maxY)")
.unwrap();
c.execute("INSERT INTO r VALUES (1, 0,1, 0,1), (2, 5,6, 5,6), (3, 0.5,2, 0.5,2)")
.unwrap();
assert_eq!(
rows(
&c,
"SELECT id FROM r WHERE minX <= 1.5 AND maxX >= 0.5 ORDER BY id"
),
[vec![Value::Integer(1)], vec![Value::Integer(3)]]
);
assert_eq!(
rows(&c, "SELECT rowid, id FROM r WHERE id = 2"),
[vec![Value::Integer(2), Value::Integer(2)]]
);
assert_eq!(
rows(&c, "SELECT minX, maxX FROM r WHERE id = 1"),
[vec![Value::Real(0.0), Value::Real(1.0)]]
);
}
#[test]
fn spatial_pushdown_prunes_but_keeps_every_match() {
let mut c = Connection::open_memory().unwrap();
c.execute("CREATE VIRTUAL TABLE r USING rtree(id, minX, maxX, minY, maxY)")
.unwrap();
let n = 300;
let mut sql = String::from("INSERT INTO r VALUES ");
for i in 1..=n {
if i > 1 {
sql.push(',');
}
let y = (i * 7) % 100;
sql.push_str(&format!("({i},{i},{i},{y},{y})"));
}
c.execute(&sql).unwrap();
let ids = |q: &str| -> Vec<i64> {
rows(&c, q)
.iter()
.map(|r| match r[0] {
Value::Integer(v) => v,
ref o => panic!("not int: {o:?}"),
})
.collect()
};
assert_eq!(
ids("SELECT id FROM r WHERE minX>=50 AND maxX<=60 ORDER BY id"),
(50..=60).collect::<Vec<_>>()
);
assert_eq!(
ids("SELECT id FROM r WHERE maxX<=5 ORDER BY id"),
vec![1, 2, 3, 4, 5]
);
assert_eq!(
ids("SELECT id FROM r WHERE minX>=296 ORDER BY id"),
vec![296, 297, 298, 299, 300]
);
assert_eq!(
ids("SELECT id FROM r WHERE minX=137 ORDER BY id"),
vec![137]
);
let expect_xy: Vec<i64> = (1..=n)
.filter(|&i| (40..=60).contains(&i) && ((i * 7) % 100) <= 30)
.collect();
assert_eq!(
ids("SELECT id FROM r WHERE minX>=40 AND maxX<=60 AND maxY<=30 ORDER BY id"),
expect_xy
);
assert!(ids("SELECT id FROM r WHERE minX>=10000").is_empty());
assert_eq!(
ids("SELECT id FROM r WHERE minX>=50 AND maxX<=60 AND id%2=0 ORDER BY id"),
vec![50, 52, 54, 56, 58, 60]
);
}
#[test]
fn coordinates_round_to_f32_like_sqlite() {
let mut c = Connection::open_memory().unwrap();
c.execute("CREATE VIRTUAL TABLE r USING rtree(id, lo, hi)")
.unwrap();
c.execute("INSERT INTO r VALUES (10, 0.1, 0.3)").unwrap();
assert_eq!(
rows(&c, "SELECT lo, hi FROM r"),
[vec![
Value::Real(0.09999998658895493),
Value::Real(0.30000001192092896),
]]
);
}
#[test]
fn update_and_delete() {
let mut c = Connection::open_memory().unwrap();
c.execute("CREATE VIRTUAL TABLE r USING rtree(id, a, b)")
.unwrap();
c.execute("INSERT INTO r VALUES (1, 0,10), (2, 20,30)")
.unwrap();
c.execute("UPDATE r SET b = 100 WHERE id = 1").unwrap();
assert_eq!(
rows(&c, "SELECT id, b FROM r WHERE id = 1"),
[vec![Value::Integer(1), Value::Real(100.0)]]
);
c.execute("DELETE FROM r WHERE id = 2").unwrap();
assert_eq!(
rows(&c, "SELECT id FROM r ORDER BY id"),
[vec![Value::Integer(1)]]
);
}
#[test]
fn rejects_min_greater_than_max_and_bad_arity() {
let mut c = Connection::open_memory().unwrap();
c.execute("CREATE VIRTUAL TABLE r USING rtree(id, a, b)")
.unwrap();
assert!(c.execute("INSERT INTO r VALUES (1, 5, 2)").is_err());
assert!(c
.execute("CREATE VIRTUAL TABLE bad USING rtree(id, a)")
.is_err());
}
#[test]
fn rows_persist_in_the_node_store() {
let mut c = Connection::open_memory().unwrap();
c.execute("CREATE VIRTUAL TABLE r USING rtree(id, a, b)")
.unwrap();
c.execute("INSERT INTO r VALUES (7, 1, 2)").unwrap();
assert_eq!(
rows(&c, "SELECT id, a, b FROM r"),
[vec![Value::Integer(7), Value::Real(1.0), Value::Real(2.0)]]
);
assert!(c.query("SELECT data FROM r_node WHERE nodeno=1").is_ok());
assert!(c.query("SELECT nodeno FROM r_rowid WHERE rowid=7").is_ok());
assert!(c.query("SELECT * FROM r_data").is_err());
}
#[test]
fn alter_and_index_on_a_virtual_table() {
let mut c = Connection::open_memory().unwrap();
c.execute("CREATE VIRTUAL TABLE r USING rtree(id, a, b)")
.unwrap();
c.execute("INSERT INTO r VALUES (1, 0, 5), (2, 10, 15)")
.unwrap();
assert!(c.execute("ALTER TABLE r ADD COLUMN z").is_err());
assert!(c.execute("CREATE INDEX i ON r(a)").is_err());
c.execute("ALTER TABLE r RENAME TO r2").unwrap();
assert_eq!(
c.query("SELECT id, a, b FROM r2 ORDER BY id").unwrap().rows,
[
vec![Value::Integer(1), Value::Real(0.0), Value::Real(5.0)],
vec![Value::Integer(2), Value::Real(10.0), Value::Real(15.0)],
]
);
assert!(c.query("SELECT * FROM r").is_err());
assert!(c.query("SELECT * FROM r2_node").is_ok());
assert!(c.query("SELECT * FROM r_node").is_err());
}
#[test]
fn drop_removes_the_backing_table() {
let mut c = Connection::open_memory().unwrap();
c.execute("CREATE VIRTUAL TABLE r USING rtree(id, a, b)")
.unwrap();
c.execute("INSERT INTO r VALUES (1, 0, 5)").unwrap();
assert!(c.query("SELECT * FROM r_node").is_ok());
c.execute("DROP TABLE r").unwrap();
assert!(c.query("SELECT * FROM r").is_err());
assert!(c.query("SELECT * FROM r_node").is_err());
assert!(c.query("SELECT * FROM r_rowid").is_err());
assert_eq!(
c.query("SELECT count(*) FROM sqlite_master").unwrap().rows[0][0],
Value::Integer(0)
);
}
#[test]
fn integrity_check_passes_with_a_virtual_table() {
let mut c = Connection::open_memory().unwrap();
c.execute("CREATE TABLE t(x INTEGER PRIMARY KEY, y)")
.unwrap();
c.execute("CREATE INDEX iy ON t(y)").unwrap();
c.execute("INSERT INTO t VALUES (1,'a'),(2,'b')").unwrap();
c.execute("CREATE VIRTUAL TABLE r USING rtree(id, a, b)")
.unwrap();
c.execute("INSERT INTO r VALUES (1, 0, 5)").unwrap();
assert_eq!(
c.query("PRAGMA integrity_check").unwrap().rows[0][0],
Value::Text("ok".into())
);
}
#[test]
fn vacuum_preserves_a_persistent_virtual_table() {
let mut c = Connection::open_memory().unwrap();
c.execute("CREATE TABLE t(x)").unwrap();
c.execute("INSERT INTO t VALUES (1),(2)").unwrap();
c.execute("CREATE VIRTUAL TABLE r USING rtree(id, a, b)")
.unwrap();
c.execute("INSERT INTO r VALUES (1, 0, 5), (2, 3, 8)")
.unwrap();
c.execute("VACUUM").unwrap();
assert_eq!(
rows(&c, "SELECT x FROM t ORDER BY x"),
[vec![Value::Integer(1)], vec![Value::Integer(2)]]
);
assert_eq!(
rows(&c, "SELECT id, a, b FROM r ORDER BY id"),
[
vec![Value::Integer(1), Value::Real(0.0), Value::Real(5.0)],
vec![Value::Integer(2), Value::Real(3.0), Value::Real(8.0)],
]
);
assert_eq!(
c.query("PRAGMA integrity_check").unwrap().rows[0][0],
Value::Text("ok".into())
);
}
#[test]
fn explain_query_plan_matches_sqlite_rtree_index() {
let mut c = Connection::open_memory().unwrap();
c.execute("CREATE VIRTUAL TABLE r USING rtree(id, minX, maxX, minY, maxY)")
.unwrap();
let plan = |sql: &str| match c.query(sql).unwrap().rows.last().unwrap().last() {
Some(Value::Text(s)) => s.clone(),
o => panic!("not text: {o:?}"),
};
assert_eq!(
plan("EXPLAIN QUERY PLAN SELECT id FROM r WHERE minX>=0 AND maxX<=3"),
"SCAN r VIRTUAL TABLE INDEX 2:D0B1"
);
assert_eq!(
plan(
"EXPLAIN QUERY PLAN SELECT id FROM r WHERE minX>=0 AND minY>=0 AND maxX<=3 AND maxY<=3"
),
"SCAN r VIRTUAL TABLE INDEX 2:D0D2B1B3"
);
assert_eq!(
plan("EXPLAIN QUERY PLAN SELECT id FROM r WHERE id = 2"),
"SCAN r VIRTUAL TABLE INDEX 1:"
);
assert_eq!(
plan("EXPLAIN QUERY PLAN SELECT id FROM r"),
"SCAN r VIRTUAL TABLE INDEX 2:"
);
}
#[test]
fn foreign_key_list_on_a_vtab_is_empty() {
let mut c = Connection::open_memory().unwrap();
c.execute("CREATE VIRTUAL TABLE r USING rtree(id, a, b)")
.unwrap();
assert!(c
.query("PRAGMA foreign_key_list(r)")
.unwrap()
.rows
.is_empty());
}
#[test]
fn table_info_reports_rtree_column_types() {
let mut c = Connection::open_memory().unwrap();
c.execute("CREATE VIRTUAL TABLE r USING rtree(id, minX, maxX)")
.unwrap();
let r = c.query("PRAGMA table_info(r)").unwrap();
let ty = |i: usize| match &r.rows[i][2] {
Value::Text(s) => s.clone(),
o => panic!("not text: {o:?}"),
};
assert_eq!(ty(0), "INT");
assert_eq!(ty(1), "REAL");
assert_eq!(ty(2), "REAL");
}
#[test]
fn auxiliary_columns_store_and_query() {
let mut c = Connection::open_memory().unwrap();
c.execute("CREATE VIRTUAL TABLE r USING rtree(id, minX, maxX, +label TEXT, +n)")
.unwrap();
c.execute("INSERT INTO r VALUES (1, 0,10, 'alpha', 42), (2, 20,30, 'beta', 7)")
.unwrap();
assert_eq!(
rows(&c, "SELECT id, label, n FROM r ORDER BY id"),
[
vec![
Value::Integer(1),
Value::Text("alpha".into()),
Value::Integer(42)
],
vec![
Value::Integer(2),
Value::Text("beta".into()),
Value::Integer(7)
],
]
);
assert_eq!(
rows(&c, "SELECT label FROM r WHERE minX <= 5"),
[vec![Value::Text("alpha".into())]]
);
assert_eq!(
rows(&c, "SELECT id FROM r WHERE n = 7"),
[vec![Value::Integer(2)]]
);
let info = c.query("PRAGMA table_info(r)").unwrap();
assert_eq!(info.rows[3][1], Value::Text("label".into()));
assert_eq!(info.rows[3][2], Value::Text("".into()));
}
#[test]
fn rtree_i32_stores_integer_coordinates() {
let mut c = Connection::open_memory().unwrap();
c.execute("CREATE VIRTUAL TABLE r USING rtree_i32(id, x0, x1)")
.unwrap();
c.execute("INSERT INTO r VALUES (1, 5,15), (2, 2.7,8.2), (3, -3.9,4.1)")
.unwrap();
assert_eq!(
rows(&c, "SELECT id, x0, x1 FROM r ORDER BY id"),
[
vec![Value::Integer(1), Value::Integer(5), Value::Integer(15)],
vec![Value::Integer(2), Value::Integer(2), Value::Integer(8)],
vec![Value::Integer(3), Value::Integer(-3), Value::Integer(4)],
]
);
assert_eq!(
rows(&c, "SELECT id FROM r WHERE x0 <= 4 ORDER BY id"),
[vec![Value::Integer(2)], vec![Value::Integer(3)]]
);
let info = c.query("PRAGMA table_info(r)").unwrap();
assert_eq!(info.rows[1][2], Value::Text("INT".into()));
}
#[test]
fn duplicate_id_is_rejected() {
let mut c = Connection::open_memory().unwrap();
c.execute("CREATE VIRTUAL TABLE r USING rtree(id, x0, x1)")
.unwrap();
c.execute("INSERT INTO r VALUES (1, 0, 5)").unwrap();
assert!(c.execute("INSERT INTO r VALUES (1, 9, 9)").is_err());
assert_eq!(rows(&c, "SELECT x0 FROM r"), [vec![Value::Real(0.0)]]);
c.execute("INSERT OR IGNORE INTO r VALUES (1, 9, 9)")
.unwrap();
assert_eq!(rows(&c, "SELECT x0 FROM r"), [vec![Value::Real(0.0)]]);
c.execute("INSERT OR REPLACE INTO r VALUES (1, 9, 9)")
.unwrap();
assert_eq!(rows(&c, "SELECT x0 FROM r"), [vec![Value::Real(9.0)]]);
c.execute("INSERT INTO r(x0, x1) VALUES (2, 7)").unwrap();
assert_eq!(
rows(&c, "SELECT count(*) FROM r"),
[vec![Value::Integer(2)]]
);
}
#[test]
fn reads_sqlite_written_rtree_node_format() {
use std::process::Command;
if Command::new("sqlite3").arg("--version").output().is_err() {
eprintln!("sqlite3 not found; skipping");
return;
}
let path = std::env::temp_dir().join(format!("gsql-rtree-d3c-{}.db", std::process::id()));
let path = path.to_string_lossy().into_owned();
let _ = std::fs::remove_file(&path);
let mut script =
String::from("CREATE VIRTUAL TABLE rt USING rtree(id, minx, maxx, miny, maxy);");
for i in 0..250 {
script.push_str(&format!(
"INSERT INTO rt VALUES({i},{}.5,{}.5,{}.0,{}.0);",
i,
i + 1,
i * 2,
i * 2 + 1
));
}
let o = Command::new("sqlite3")
.arg(&path)
.arg(&script)
.output()
.unwrap();
assert!(o.status.success(), "sqlite build failed: {o:?}");
let c = Connection::open(&path).unwrap();
assert!(
c.query("SELECT 1 FROM sqlite_master WHERE name='rt_node'")
.unwrap()
.rows
.len()
== 1
);
assert!(c
.query("SELECT 1 FROM sqlite_master WHERE name='rt_data'")
.unwrap()
.rows
.is_empty());
let sqlite = |q: &str| {
let o = Command::new("sqlite3").arg(&path).arg(q).output().unwrap();
let mut v: Vec<String> = String::from_utf8_lossy(&o.stdout)
.lines()
.map(str::to_string)
.collect();
v.sort();
v.join("~")
};
let graph = |q: &str| {
let mut v: Vec<String> = c
.query(q)
.unwrap()
.rows
.iter()
.map(|r| {
r.iter()
.map(|x| match x {
Value::Integer(i) => i.to_string(),
Value::Real(f) => graphitesql::exec::eval::format_real(*f),
Value::Null => String::new(),
Value::Text(s) => s.clone(),
Value::Blob(_) => "b".into(),
})
.collect::<Vec<_>>()
.join("|")
})
.collect();
v.sort();
v.join("~")
};
for q in [
"SELECT count(*) FROM rt",
"SELECT id FROM rt WHERE minx>=100 AND minx<105 ORDER BY id",
"SELECT id, minx, maxx, miny, maxy FROM rt WHERE id=137",
"SELECT max(maxx), min(minx) FROM rt",
"SELECT id FROM rt WHERE minx<2 ORDER BY id",
] {
assert_eq!(graph(q), sqlite(q), "diverged: {q}");
}
assert_eq!(
c.query("PRAGMA integrity_check").unwrap().rows[0][0],
Value::Text("ok".into())
);
let _ = std::fs::remove_file(&path);
}
#[test]
fn reads_sqlite_written_rtree_i32_node_format() {
use std::process::Command;
if Command::new("sqlite3").arg("--version").output().is_err() {
eprintln!("sqlite3 not found; skipping");
return;
}
let path = std::env::temp_dir().join(format!("gsql-rtreei32-d3c-{}.db", std::process::id()));
let path = path.to_string_lossy().into_owned();
let _ = std::fs::remove_file(&path);
let o = Command::new("sqlite3")
.arg(&path)
.arg("CREATE VIRTUAL TABLE ri USING rtree_i32(id, x0, x1); INSERT INTO ri VALUES(1,-5,5),(2,10,20),(3,0,1);")
.output()
.unwrap();
assert!(o.status.success(), "{o:?}");
let c = Connection::open(&path).unwrap();
let r = c.query("SELECT id, x0, x1 FROM ri ORDER BY id").unwrap();
assert_eq!(r.rows.len(), 3);
assert_eq!(
r.rows[0],
vec![Value::Integer(1), Value::Integer(-5), Value::Integer(5)]
);
assert_eq!(
r.rows[1],
vec![Value::Integer(2), Value::Integer(10), Value::Integer(20)]
);
let _ = std::fs::remove_file(&path);
}
#[test]
fn graphite_written_rtree_is_read_by_sqlite3() {
use std::process::Command;
if Command::new("sqlite3").arg("--version").output().is_err() {
eprintln!("sqlite3 not found; skipping");
return;
}
let path = std::env::temp_dir().join(format!("gsql-rtree-m2-{}.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 rt USING rtree(id, minx, maxx, miny, maxy)")
.unwrap();
let vals = (0..150)
.map(|i| format!("({i},{}.0,{}.0,{}.0,{}.0)", i, i + 1, i * 2, i * 2 + 1))
.collect::<Vec<_>>()
.join(",");
c.execute(&format!("INSERT INTO rt VALUES {vals}")).unwrap();
c.execute("DELETE FROM rt WHERE id=77").unwrap();
c.execute("UPDATE rt SET maxx=999 WHERE id=12").unwrap();
}
let chk = |q: &str| {
let o = Command::new("sqlite3").arg(&path).arg(q).output().unwrap();
String::from_utf8_lossy(&o.stdout).trim().to_string()
};
assert_eq!(chk("SELECT rtreecheck('rt')"), "ok", "rtreecheck");
assert_eq!(chk("PRAGMA integrity_check"), "ok");
assert_eq!(chk("SELECT count(*) FROM rt"), "149");
assert_eq!(chk("SELECT id FROM rt WHERE id=77"), "");
assert_eq!(chk("SELECT maxx FROM rt WHERE id=12"), "999.0");
assert_eq!(
chk("SELECT group_concat(id) FROM (SELECT id FROM rt WHERE minx>=40 AND minx<=43 ORDER BY id)"),
"40,41,42,43"
);
assert_eq!(
chk("SELECT count(*) FROM sqlite_master WHERE name IN('rt_node','rt_rowid','rt_parent')"),
"3"
);
assert_eq!(
chk("SELECT count(*) FROM sqlite_master WHERE name='rt_data'"),
"0"
);
let _ = std::fs::remove_file(&path);
}