A from-scratch, dependency-free SQLite implementation: file format reader and writer (b-tree read and write paths, pager, journal, WAL), a SQL lexer, parser and AST, a planner, and an executor — plus locking, integrity checking and a `sqlq` CLI. It exists because the asset store needs a database on every platform the app ships to, without a C toolchain in the build and without a system library whose version is somebody else's decision. The test suite is the argument: DML, DDL, concurrency, crash recovery, a query corpus and a DML fuzzer, all checked against real SQLite behaviour rather than against our own reading of the spec.
147 lines
4.8 KiB
Rust
147 lines
4.8 KiB
Rust
//! Scale: enough rows for deep b-trees, index rebalancing and overflow chains,
|
|
//! written by this engine and validated by SQLite.
|
|
//!
|
|
//! Set `MAKEPAD_SQLITE_BIG=1` for the million-row version (release build
|
|
//! recommended); the default size keeps a debug `cargo test` quick.
|
|
|
|
mod common;
|
|
|
|
use common::*;
|
|
use makepad_sqlite::{Connection, Database, Value};
|
|
use std::time::{Duration, Instant};
|
|
|
|
fn rows_target() -> i64 {
|
|
if std::env::var("MAKEPAD_SQLITE_BIG").is_ok() {
|
|
1_000_000
|
|
} else {
|
|
20_000
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn many_rows_stay_consistent() {
|
|
if !have_sqlite3() {
|
|
return;
|
|
}
|
|
let n = rows_target();
|
|
let scratch = Scratch::new("scale");
|
|
let path = scratch.path("big.db");
|
|
let started = Instant::now();
|
|
{
|
|
let mut db = Connection::open(&path, Duration::from_secs(30)).unwrap();
|
|
db.execute("PRAGMA page_size=4096", &[]).ok();
|
|
db.execute(
|
|
"CREATE TABLE t(id INTEGER PRIMARY KEY, k TEXT, n INTEGER, b BLOB)",
|
|
&[],
|
|
)
|
|
.unwrap();
|
|
db.execute("CREATE INDEX t_by_k ON t(k, n)", &[]).unwrap();
|
|
db.execute("BEGIN IMMEDIATE", &[]).unwrap();
|
|
for i in 1..=n {
|
|
let b = if i % 5000 == 0 {
|
|
Value::Blob(vec![(i % 251) as u8; 20_000])
|
|
} else {
|
|
Value::Blob(vec![7u8; (i % 16) as usize])
|
|
};
|
|
db.execute(
|
|
"INSERT INTO t(id, k, n, b) VALUES (?1, ?2, ?3, ?4)",
|
|
&[
|
|
Value::Integer(i),
|
|
// Keys arrive out of order so the index really rebalances.
|
|
Value::text(format!("k{:08}", (i * 2_654_435_761u64 as i64) % n)),
|
|
Value::Integer(i % 977),
|
|
b,
|
|
],
|
|
)
|
|
.unwrap();
|
|
}
|
|
db.execute("COMMIT", &[]).unwrap();
|
|
}
|
|
let insert_time = started.elapsed();
|
|
|
|
// SQLite must be happy with the file, and see exactly what we wrote.
|
|
assert_eq!(sqlite3(&path, "PRAGMA integrity_check;\n").trim(), "ok");
|
|
assert_eq!(
|
|
sqlite3(&path, ".mode list\nSELECT COUNT(*), SUM(n) FROM t;\n").trim(),
|
|
{
|
|
let mut db = Database::open(&path).unwrap();
|
|
let r = db.query("SELECT COUNT(*), SUM(n) FROM t", &[]).unwrap();
|
|
format!(
|
|
"{}|{}",
|
|
r.rows[0][0].as_integer().unwrap(),
|
|
r.rows[0][1].as_integer().unwrap()
|
|
)
|
|
}
|
|
);
|
|
|
|
// The b-tree must be more than two levels deep at this size.
|
|
let mut db = Database::open(&path).unwrap();
|
|
let pages = db.pager().page_count();
|
|
assert!(pages > 100, "only {pages} pages");
|
|
|
|
// Deleting most rows keeps both engines in agreement.
|
|
{
|
|
let mut db = Connection::open(&path, Duration::from_secs(30)).unwrap();
|
|
let deleted = db
|
|
.execute("DELETE FROM t WHERE id % 3 <> 0", &[])
|
|
.unwrap();
|
|
assert_eq!(deleted, n as u64 - (n / 3) as u64);
|
|
}
|
|
assert_eq!(sqlite3(&path, "PRAGMA integrity_check;\n").trim(), "ok");
|
|
assert_eq!(
|
|
sqlite3(&path, ".mode list\nSELECT COUNT(*) FROM t;\n").trim(),
|
|
(n / 3).to_string()
|
|
);
|
|
let free: i64 = sqlite3(&path, ".mode list\nPRAGMA freelist_count;\n")
|
|
.trim()
|
|
.parse()
|
|
.unwrap_or(0);
|
|
assert!(free > 0, "deleting two thirds of the rows freed no pages");
|
|
|
|
eprintln!(
|
|
"scale: {n} rows inserted in {:.1}s, {pages} pages, {free} free after delete",
|
|
insert_time.as_secs_f64()
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn index_lookups_stay_fast_as_the_table_grows() {
|
|
if !have_sqlite3() {
|
|
return;
|
|
}
|
|
let scratch = Scratch::new("scale-seek");
|
|
let path = scratch.path("seek.db");
|
|
{
|
|
let mut db = Connection::open(&path, Duration::from_secs(30)).unwrap();
|
|
db.execute("CREATE TABLE t(id INTEGER PRIMARY KEY, k TEXT UNIQUE)", &[])
|
|
.unwrap();
|
|
db.execute("BEGIN IMMEDIATE", &[]).unwrap();
|
|
for i in 1..=20_000i64 {
|
|
db.execute(
|
|
"INSERT INTO t(id, k) VALUES (?1, ?2)",
|
|
&[Value::Integer(i), Value::text(format!("key-{i:06}"))],
|
|
)
|
|
.unwrap();
|
|
}
|
|
db.execute("COMMIT", &[]).unwrap();
|
|
}
|
|
let mut db = Database::open(&path).unwrap();
|
|
let stmt = db.prepare("SELECT id FROM t WHERE k = ?1").unwrap();
|
|
assert!(
|
|
stmt.explain().contains("SEARCH USING INDEX"),
|
|
"{}",
|
|
stmt.explain()
|
|
);
|
|
let started = Instant::now();
|
|
for i in (1..=20_000i64).step_by(97) {
|
|
let out = stmt
|
|
.query(&mut db, &[Value::text(format!("key-{i:06}"))])
|
|
.unwrap();
|
|
assert_eq!(out.rows[0][0].as_integer(), Some(i));
|
|
}
|
|
let elapsed = started.elapsed();
|
|
assert!(
|
|
elapsed < Duration::from_secs(10),
|
|
"207 index lookups took {elapsed:?}"
|
|
);
|
|
}
|