makepad/libs/sqlite_query/tests/scale.rs
Admin 19c37a3df8 libs/sqlite_query: an SQLite engine of our own
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.
2026-08-23 00:43:20 +02:00

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:?}"
);
}