mod common;
use common::Sha256;
use quantydb_sqlite::{
Cell, FileSource, MappedCell, Reader, RowLayout, Rows, SliceSource, SqliteValue,
};
fn data_path(name: &str) -> String {
format!("{}/tests/data/{name}", env!("CARGO_MANIFEST_DIR"))
}
fn fixture(name: &str) -> Vec<u8> {
std::fs::read(data_path(name)).unwrap_or_else(|e| panic!("fixture {name}: {e}"))
}
fn oracle() -> (Vec<(String, usize, String)>, usize) {
let text = std::fs::read_to_string(data_path("without_rowid.oracle"))
.expect("the oracle is checked in");
let mut tables = Vec::new();
let mut total = 0;
for line in text.lines() {
if let Some(rest) = line.strip_prefix("# total rows ") {
total = rest.trim().parse().expect("the total is a number");
continue;
}
let mut parts = line.split_whitespace();
match (parts.next(), parts.next(), parts.next()) {
(Some(name), Some(rows), Some(digest)) => {
tables.push((name.to_string(), rows.parse().unwrap(), digest.to_string()))
}
_ => panic!("unreadable oracle line: {line}"),
}
}
(tables, total)
}
fn render(value: &SqliteValue) -> Vec<u8> {
match value {
SqliteValue::Null => b"null".to_vec(),
SqliteValue::Integer(n) => format!("i:{n}").into_bytes(),
SqliteValue::Real(f) => {
let mut out = String::from("f:");
for b in f.to_be_bytes() {
out.push_str(&format!("{b:02x}"));
}
out.into_bytes()
}
SqliteValue::Text(t) => {
let mut out = b"t:".to_vec();
out.extend_from_slice(t.as_bytes());
out
}
SqliteValue::Blob(b) => {
let mut out = String::from("b:");
for byte in b {
out.push_str(&format!("{byte:02x}"));
}
out.into_bytes()
}
}
}
#[test]
fn every_row_matches_what_sqlite_reports() {
let (expected, expected_total) = oracle();
let bytes = fixture("without_rowid.sqlite");
let reader = Reader::open(SliceSource::new(&bytes)).unwrap();
let schema = reader.schema().unwrap();
let mut seen_total = 0;
for (name, rows_expected, digest_expected) in &expected {
let object = schema
.object(name)
.unwrap_or_else(|| panic!("{name} is not in the schema"));
let def = object.table_def().unwrap();
assert!(def.without_rowid, "{name} should be a without rowid table");
let layout = RowLayout::new(&def);
let root = object.root_page.unwrap();
let mut hasher = Sha256::new();
let mut count = 0;
for row in reader.rows(root).unwrap() {
let row = row.unwrap_or_else(|e| panic!("{name}: {e}"));
assert_eq!(row.rowid, None, "{name}: these rows have no rowid");
let mut line = Vec::new();
for index in 0..def.columns.len() {
if index > 0 {
line.push(0x1f);
}
match layout.cell(&row, index) {
MappedCell::Value(value) => line.extend_from_slice(&render(value)),
MappedCell::Missing => line.extend_from_slice(b"missing"),
other => panic!("{name}: unexpected cell {other:?}"),
}
}
line.push(b'\n');
hasher.update(&line);
count += 1;
}
assert_eq!(count, *rows_expected, "{name}: wrong number of rows");
assert_eq!(
hasher.finish(),
*digest_expected,
"{name}: {count} rows read, but their contents differ from sqlite's"
);
seen_total += count;
}
assert_eq!(seen_total, expected_total);
}
#[test]
fn the_record_puts_the_key_columns_first() {
let bytes = fixture("without_rowid.sqlite");
let reader = Reader::open(SliceSource::new(&bytes)).unwrap();
let object = reader.schema().unwrap().object("two").unwrap().clone();
let root = object.root_page.unwrap();
let raw = reader
.index_scan(root)
.unwrap()
.next()
.expect("the table is not empty")
.unwrap();
assert_eq!(
raw,
vec![
SqliteValue::Text("c0".into()),
SqliteValue::Text("a0".into()),
SqliteValue::Integer(0),
SqliteValue::Text("d0".into()),
],
"stored order is the key columns first"
);
let def = object.table_def().unwrap();
let layout = RowLayout::new(&def);
let row = reader.rows(root).unwrap().next().unwrap().unwrap();
let declared: Vec<String> = (0..def.columns.len())
.map(|i| match layout.cell(&row, i) {
MappedCell::Value(SqliteValue::Text(t)) => t.clone(),
MappedCell::Value(SqliteValue::Integer(n)) => n.to_string(),
other => panic!("unexpected cell {other:?}"),
})
.collect();
assert_eq!(declared, vec!["a0", "0", "c0", "d0"]);
}
#[test]
fn an_integer_key_is_not_a_rowid_alias_here() {
let bytes = fixture("without_rowid.sqlite");
let reader = Reader::open(SliceSource::new(&bytes)).unwrap();
let object = reader.schema().unwrap().object("int_pk").unwrap().clone();
let def = object.table_def().unwrap();
assert!(
def.rowid_alias().is_none(),
"a without rowid table has none"
);
let first = reader
.index_scan(object.root_page.unwrap())
.unwrap()
.next()
.unwrap()
.unwrap();
assert_eq!(first[0], SqliteValue::Integer(1));
}
#[test]
fn a_descending_key_comes_back_in_descending_order() {
let bytes = fixture("without_rowid.sqlite");
let reader = Reader::open(SliceSource::new(&bytes)).unwrap();
let root = reader
.schema()
.unwrap()
.object("desc_key")
.unwrap()
.root_page
.unwrap();
let keys: Vec<i64> = reader
.index_scan(root)
.unwrap()
.map(|entry| match entry.unwrap()[0] {
SqliteValue::Integer(n) => n,
ref other => panic!("unexpected key {other:?}"),
})
.collect();
assert_eq!(keys, vec![5, 4, 3, 2, 1, 0]);
}
#[test]
fn a_column_added_later_is_missing_from_the_older_rows() {
let bytes = fixture("without_rowid.sqlite");
let reader = Reader::open(SliceSource::new(&bytes)).unwrap();
let object = reader.schema().unwrap().object("grown_wr").unwrap().clone();
let def = object.table_def().unwrap();
let layout = RowLayout::new(&def);
let extra = def.column_index("extra").unwrap();
let mut missing = 0;
let mut present = 0;
for row in reader.rows(object.root_page.unwrap()).unwrap() {
match layout.cell(&row.unwrap(), extra) {
MappedCell::Missing => missing += 1,
MappedCell::Value(_) => present += 1,
other => panic!("unexpected cell {other:?}"),
}
}
assert_eq!((missing, present), (2, 2), "two rows predate the column");
}
#[test]
fn a_large_table_walks_through_its_interior_pages() {
let bytes = fixture("without_rowid.sqlite");
let reader = Reader::open(SliceSource::new(&bytes)).unwrap();
let root = reader
.schema()
.unwrap()
.object("kv")
.unwrap()
.root_page
.unwrap();
assert!(reader.btree_page(root).unwrap().kind.is_interior());
let keys: Vec<String> = reader
.index_scan(root)
.unwrap()
.map(|entry| match &entry.unwrap()[0] {
SqliteValue::Text(t) => t.clone(),
other => panic!("unexpected key {other:?}"),
})
.collect();
assert_eq!(keys.len(), 500);
assert_eq!(keys[0], "key-0000");
assert_eq!(keys[499], "key-0499");
assert!(
keys.windows(2).all(|w| w[0] < w[1]),
"an index walk is in key order"
);
}
#[test]
fn rows_picks_the_walk_from_the_page_rather_than_the_sql() {
let chinook = fixture("chinook.sqlite");
let reader =
Reader::open(FileSource::open(data_path("without_rowid.sqlite")).unwrap()).unwrap();
let keyed = reader
.rows(
reader
.schema()
.unwrap()
.object("kv")
.unwrap()
.root_page
.unwrap(),
)
.unwrap();
assert!(matches!(keyed, Rows::Keyed(_)));
let reader = Reader::open(SliceSource::new(&chinook)).unwrap();
let rowid = reader
.rows(
reader
.schema()
.unwrap()
.object("Album")
.unwrap()
.root_page
.unwrap(),
)
.unwrap();
assert!(matches!(rowid, Rows::Rowid(_)));
}
#[test]
fn a_table_scan_of_an_index_tree_is_refused_by_name() {
let bytes = fixture("without_rowid.sqlite");
let reader = Reader::open(SliceSource::new(&bytes)).unwrap();
let root = reader
.schema()
.unwrap()
.object("kv")
.unwrap()
.root_page
.unwrap();
let err = reader
.table_scan(root)
.err()
.expect("a table scan of an index b-tree was accepted");
assert!(
err.to_string().contains("index_scan") || err.to_string().contains("rows"),
"the message should point at the right call: {err}"
);
let page = reader.btree_page(root).unwrap();
assert!(matches!(
reader.cell(&page, 0).unwrap(),
Cell::IndexInterior { .. } | Cell::IndexLeaf { .. }
));
}