#![allow(clippy::unwrap_used, clippy::expect_used)]
use std::path::{Path, PathBuf};
use std::process::Command;
use sqlite_core::{Database, Value};
fn sqlite3_bin() -> Option<String> {
let bin = std::env::var("SQLITE3_BIN").unwrap_or_else(|_| "sqlite3".to_string());
Command::new(&bin)
.arg("--version")
.output()
.ok()
.filter(|o| o.status.success())
.map(|_| bin)
}
#[test]
fn reads_live_without_rowid_table_rows() {
let Some(bin) = sqlite3_bin() else {
eprintln!("SKIP without_rowid: no sqlite3 (set SQLITE3_BIN)");
return;
};
let dir = std::env::temp_dir().join("sqlite4n6-without-rowid");
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
let db_path: PathBuf = dir.join("wr.db");
let sql = "PRAGMA page_size=4096;
CREATE TABLE kv(k TEXT PRIMARY KEY, v INTEGER, note TEXT) WITHOUT ROWID;
INSERT INTO kv VALUES('alpha',1,'first'),('bravo',2,'second'),('charlie',3,'third');
CREATE TABLE plain(id INTEGER PRIMARY KEY, x TEXT);
INSERT INTO plain VALUES(1,'ordinary');";
assert!(
Command::new(&bin)
.arg(&db_path)
.arg(sql)
.status()
.unwrap()
.success(),
"sqlite3 construction failed"
);
let db = Database::open(std::fs::read(&db_path).unwrap()).unwrap();
let tables = db.without_rowid_table_rows();
assert_eq!(
tables.iter().map(|t| t.name.as_str()).collect::<Vec<_>>(),
vec!["kv"],
"only the WITHOUT ROWID table is returned: {tables:?}"
);
let kv = &tables[0];
let keys: Vec<&str> = kv
.rows
.iter()
.filter_map(|r| match r.first() {
Some(Value::Text(t)) => Some(t.as_str()),
_ => None,
})
.collect();
assert_eq!(keys, vec!["alpha", "bravo", "charlie"], "rows: {kv:?}");
assert!(
kv.rows.iter().all(|r| r.len() == 3),
"each row carries all 3 columns: {kv:?}"
);
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn walks_a_multi_page_without_rowid_btree() {
let Some(bin) = sqlite3_bin() else {
return;
};
let dir = std::env::temp_dir().join("sqlite4n6-without-rowid-big");
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
let db_path = dir.join("big.db");
let sql = "PRAGMA page_size=512;
CREATE TABLE kv(k TEXT PRIMARY KEY, v TEXT) WITHOUT ROWID;
WITH RECURSIVE c(n) AS (SELECT 1 UNION ALL SELECT n+1 FROM c WHERE n<400)
INSERT INTO kv SELECT printf('key-%05d', n), printf('val-%05d', n) FROM c;";
assert!(Command::new(&bin)
.arg(&db_path)
.arg(sql)
.status()
.unwrap()
.success());
let db = Database::open(std::fs::read(&db_path).unwrap()).unwrap();
let tables = db.without_rowid_table_rows();
assert_eq!(tables.len(), 1);
assert_eq!(
tables[0].rows.len(),
400,
"every row across all leaves of the multi-page b-tree is read"
);
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn parses_a_handbuilt_without_rowid_table() {
let db = build_handbuilt_without_rowid();
let tables = db.without_rowid_table_rows();
assert_eq!(tables.len(), 1, "one WITHOUT ROWID table");
assert_eq!(tables[0].name, "wr");
assert_eq!(tables[0].rows, vec![vec![Value::Integer(42)]]);
}
#[test]
fn row_histories_carries_without_rowid_live_rows() {
let db = build_handbuilt_without_rowid();
let wr = db
.row_histories()
.into_iter()
.find(|h| h.table == "wr")
.expect("the WITHOUT ROWID table appears in row_histories");
assert!(wr.without_rowid, "flagged WITHOUT ROWID");
assert!(wr.versions.is_empty(), "no rowid version history");
assert_eq!(
wr.without_rowid_rows,
vec![vec![Value::Integer(42)]],
"its live rows are carried"
);
}
#[allow(dead_code)]
fn _uses(_: &Path) {}
fn build_handbuilt_without_rowid() -> Database {
let ps = 4096usize;
let mut b = vec![0u8; ps * 3];
b[..16].copy_from_slice(b"SQLite format 3\0");
b[16] = 0x10; b[17] = 0x00;
b[56..60].copy_from_slice(&1u32.to_be_bytes()); b[28..32].copy_from_slice(&3u32.to_be_bytes());
let p1 = 0usize;
b[p1 + 100] = 0x0d; b[p1 + 100 + 3..p1 + 100 + 5].copy_from_slice(&1u16.to_be_bytes()); let sql = "CREATE TABLE wr(k INTEGER PRIMARY KEY) WITHOUT ROWID";
let record = schema_record(b"table", b"wr", b"wr", 3, sql.as_bytes());
let cell_off = ps - record.len() - 2;
let mut cell = Vec::new();
push_varint(&mut cell, record.len() as u64); push_varint(&mut cell, 1); cell.extend_from_slice(&record);
b[p1 + cell_off..p1 + cell_off + cell.len()].copy_from_slice(&cell);
let ptr = p1 + 100 + 8;
b[ptr..ptr + 2].copy_from_slice(&u16::try_from(cell_off).unwrap().to_be_bytes());
let p3 = 2 * ps;
b[p3] = 0x0a; b[p3 + 3..p3 + 5].copy_from_slice(&1u16.to_be_bytes()); let icell_off = 4000usize;
b[p3 + icell_off] = 0x03; b[p3 + icell_off + 1] = 0x02; b[p3 + icell_off + 2] = 0x01; b[p3 + icell_off + 3] = 42; let iptr = p3 + 8;
b[iptr..iptr + 2].copy_from_slice(&u16::try_from(icell_off).unwrap().to_be_bytes());
Database::open(b).expect("hand-built db opens")
}
fn schema_record(typ: &[u8], name: &[u8], tbl: &[u8], rootpage: u8, sql: &[u8]) -> Vec<u8> {
let mut header = Vec::new();
let mut body = Vec::new();
for s in [typ, name, tbl] {
push_varint(&mut header, (13 + 2 * s.len()) as u64);
body.extend_from_slice(s);
}
push_varint(&mut header, 1); body.push(rootpage);
push_varint(&mut header, (13 + 2 * sql.len()) as u64);
body.extend_from_slice(sql);
let mut header_len = header.len() + 1;
while varint_len(header_len as u64) + header.len() != header_len {
header_len += 1;
}
let mut record = Vec::new();
push_varint(&mut record, header_len as u64);
record.extend_from_slice(&header);
record.extend_from_slice(&body);
record
}
fn push_varint(out: &mut Vec<u8>, mut v: u64) {
let mut bytes = Vec::new();
loop {
bytes.push((v & 0x7f) as u8);
v >>= 7;
if v == 0 {
break;
}
}
bytes.reverse();
for (i, byte) in bytes.iter().enumerate() {
if i + 1 < bytes.len() {
out.push(byte | 0x80);
} else {
out.push(*byte);
}
}
}
fn varint_len(v: u64) -> usize {
let mut n = 1;
let mut x = v >> 7;
while x != 0 {
n += 1;
x >>= 7;
}
n
}