• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

joaoh82 / rust_sqlite / 25455403967

06 May 2026 07:05PM UTC coverage: 64.728% (-0.6%) from 65.306%
25455403967

push

github

web-flow
feat(bench): harness scaffolding + W1 read-by-PK (SQLR-16, sub-phase 9.1) (#102)

First executable code for the SQLRite-vs-SQLite benchmark suite designed
in #101. Stands up a `benchmarks/` workspace member with a small
`Driver` trait, two engine implementations (SQLRite via `Connection`,
SQLite via `rusqlite`-bundled), one workload end-to-end (W1 read-by-PK),
a criterion entry point, and an aggregator that collapses
`target/criterion/**/estimates.json` into a single results envelope
under `benchmarks/results/`. Excluded from CI alongside `sqlrite-desktop`
+ the SDK cdylibs; runs locally with `make bench`.

Plan-doc:
  - Q1-Q8 resolved on 2026-05-06 and converted to a "Decisions
    (was: open questions)" section, mirroring the post-resolution
    shape of `docs/phase-7-plan.md`. Status moved to
    "approved 2026-05-06 -- 9.1 in flight".
  - Cleaned up the muddled "Not added to the default exclude list"
    sentence in the Repository layout section.

Harness:
  - `Driver` trait is small enough to express every planned workload:
    open / execute / execute_with_params / query_one / query_all.
    `&self` so drivers are cheap to clone into criterion closures.
  - SQLRite driver inlines `?` params via SQL formatting (SQLite-side
    quoting / arity checks unit-tested) -- the engine has no parameter
    binding yet; that lands as a post-9.6 follow-up.
  - SQLite driver runs the Q3-tuned profile at open time
    (WAL + synchronous=NORMAL + 64 MB cache + temp_store=MEMORY) and
    `prepare_cached` on every hot-path SELECT so we measure execution
    cost, not per-iter parse cost.
  - Per-run `TempDir` -- no page-cache / WAL bleed across iterations.
  - Correctness gate: every workload runs `correctness_check` against
    sample keys before timing, so a divergent-result "speedup" can't
    silently land.
  - Q8 versioning baked in: workloads carry a `WorkloadId { id, name,
    version }` and the JSON envelope tags every sample with `W{n}.v{v}`.

Output:
  ... (continued)

0 of 119 new or added lines in 3 files covered. (0.0%)

8704 of 13447 relevant lines covered (64.73%)

1.2 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

0.0
/benchmarks/src/drivers/sqlite.rs
1
//! SQLite driver via [`rusqlite`] (bundled libsqlite3).
2
//!
3
//! Per Q3, the driver applies the **headline tuned profile** at open
4
//! time:
5
//!
6
//! ```sql
7
//! PRAGMA journal_mode = WAL;
8
//! PRAGMA synchronous  = NORMAL;
9
//! PRAGMA temp_store   = MEMORY;
10
//! PRAGMA cache_size   = -65536;  -- 64 MB
11
//! ```
12
//!
13
//! Rationale: SQLRite's WAL is mandatory + always-on, so SQLite-default
14
//! (`journal_mode=DELETE`, `synchronous=FULL`) is *not* apples-to-apples.
15
//! `synchronous=NORMAL` matches SQLRite's commit fsync semantics. A
16
//! "SQLite-default" comparator column is a future opt-in (post-9.6).
17
//!
18
//! Driver-bias mitigation (Q3 risk in the plan): every hot SELECT path
19
//! goes through `prepare_cached` so we measure the engine's execution
20
//! cost, not per-iter parse cost. That's how a perf-conscious rusqlite
21
//! user would write this.
22

23
use std::path::Path;
24

25
use anyhow::{Context, Result};
26

27
use crate::{Driver, Value};
28

29
pub struct SQLiteDriver;
30

31
impl Driver for SQLiteDriver {
32
    type Conn = rusqlite::Connection;
33

NEW
34
    fn name(&self) -> &'static str {
×
NEW
35
        "sqlite"
×
36
    }
37

NEW
38
    fn open(&self, path: &Path) -> Result<Self::Conn> {
×
NEW
39
        let conn = rusqlite::Connection::open(path)
×
NEW
40
            .with_context(|| format!("rusqlite open({})", path.display()))?;
×
41
        // Tuned profile (Q3). Each PRAGMA returns one row of feedback
42
        // we discard; query_row is the right shape (`pragma_update`
43
        // doesn't accept the negative-value cache_size form).
NEW
44
        conn.pragma_update(None, "journal_mode", "WAL")
×
NEW
45
            .context("PRAGMA journal_mode=WAL")?;
×
NEW
46
        conn.pragma_update(None, "synchronous", "NORMAL")
×
NEW
47
            .context("PRAGMA synchronous=NORMAL")?;
×
NEW
48
        conn.pragma_update(None, "temp_store", "MEMORY")
×
NEW
49
            .context("PRAGMA temp_store=MEMORY")?;
×
NEW
50
        conn.pragma_update(None, "cache_size", -65536i64)
×
NEW
51
            .context("PRAGMA cache_size=-65536")?;
×
NEW
52
        Ok(conn)
×
53
    }
54

NEW
55
    fn execute(&self, conn: &mut Self::Conn, sql: &str) -> Result<()> {
×
NEW
56
        conn.execute_batch(sql)
×
NEW
57
            .with_context(|| format!("rusqlite execute_batch: {sql}"))?;
×
NEW
58
        Ok(())
×
59
    }
60

NEW
61
    fn execute_with_params(
×
62
        &self,
63
        conn: &mut Self::Conn,
64
        sql: &str,
65
        params: &[Value],
66
    ) -> Result<()> {
NEW
67
        let bound: Vec<rusqlite::types::Value> = params.iter().map(to_rusqlite).collect();
×
NEW
68
        conn.execute(sql, rusqlite::params_from_iter(bound.iter()))
×
NEW
69
            .with_context(|| format!("rusqlite execute: {sql}"))?;
×
NEW
70
        Ok(())
×
71
    }
72

NEW
73
    fn query_one(&self, conn: &mut Self::Conn, sql: &str, params: &[Value]) -> Result<Vec<Value>> {
×
74
        // prepare_cached is the standard rusqlite hot-path idiom — it
75
        // hits the connection's per-statement LRU cache, so the same
76
        // SQL string only pays the parse cost once across all bench
77
        // iterations (cache size defaults to 16, plenty for our
78
        // workloads).
NEW
79
        let mut stmt = conn
×
NEW
80
            .prepare_cached(sql)
×
NEW
81
            .with_context(|| format!("rusqlite prepare_cached: {sql}"))?;
×
NEW
82
        let bound: Vec<rusqlite::types::Value> = params.iter().map(to_rusqlite).collect();
×
NEW
83
        let cols = stmt.column_count();
×
NEW
84
        let mut rows = stmt
×
NEW
85
            .query(rusqlite::params_from_iter(bound.iter()))
×
NEW
86
            .with_context(|| format!("rusqlite query: {sql}"))?;
×
NEW
87
        let row = rows
×
NEW
88
            .next()
×
NEW
89
            .with_context(|| format!("rusqlite row read: {sql}"))?
×
NEW
90
            .context("rusqlite query_one: zero rows returned")?;
×
NEW
91
        let mut out = Vec::with_capacity(cols);
×
NEW
92
        for i in 0..cols {
×
NEW
93
            out.push(extract_column(row, i)?);
×
94
        }
NEW
95
        if rows
×
NEW
96
            .next()
×
NEW
97
            .with_context(|| format!("rusqlite row read: {sql}"))?
×
98
            .is_some()
99
        {
NEW
100
            anyhow::bail!("rusqlite query_one: >1 rows returned");
×
101
        }
NEW
102
        Ok(out)
×
103
    }
104

NEW
105
    fn query_all(
×
106
        &self,
107
        conn: &mut Self::Conn,
108
        sql: &str,
109
        params: &[Value],
110
    ) -> Result<Vec<Vec<Value>>> {
NEW
111
        let mut stmt = conn
×
NEW
112
            .prepare_cached(sql)
×
NEW
113
            .with_context(|| format!("rusqlite prepare_cached: {sql}"))?;
×
NEW
114
        let bound: Vec<rusqlite::types::Value> = params.iter().map(to_rusqlite).collect();
×
NEW
115
        let cols = stmt.column_count();
×
NEW
116
        let mut rows = stmt
×
NEW
117
            .query(rusqlite::params_from_iter(bound.iter()))
×
NEW
118
            .with_context(|| format!("rusqlite query: {sql}"))?;
×
NEW
119
        let mut out = Vec::new();
×
NEW
120
        while let Some(row) = rows
×
NEW
121
            .next()
×
NEW
122
            .with_context(|| format!("rusqlite row read: {sql}"))?
×
123
        {
NEW
124
            let mut buf = Vec::with_capacity(cols);
×
NEW
125
            for i in 0..cols {
×
NEW
126
                buf.push(extract_column(row, i)?);
×
127
            }
NEW
128
            out.push(buf);
×
129
        }
NEW
130
        Ok(out)
×
131
    }
132
}
133

134
fn to_rusqlite(v: &Value) -> rusqlite::types::Value {
135
    match v {
136
        Value::Null => rusqlite::types::Value::Null,
137
        Value::Integer(i) => rusqlite::types::Value::Integer(*i),
138
        Value::Real(f) => rusqlite::types::Value::Real(*f),
139
        Value::Text(s) => rusqlite::types::Value::Text(s.clone()),
140
    }
141
}
142

143
fn extract_column(row: &rusqlite::Row<'_>, idx: usize) -> Result<Value> {
144
    let raw: rusqlite::types::Value = row.get(idx)?;
145
    Ok(match raw {
146
        rusqlite::types::Value::Null => Value::Null,
147
        rusqlite::types::Value::Integer(i) => Value::Integer(i),
148
        rusqlite::types::Value::Real(f) => Value::Real(f),
149
        rusqlite::types::Value::Text(s) => Value::Text(s),
150
        rusqlite::types::Value::Blob(_) => {
151
            anyhow::bail!("rusqlite extract_column: BLOB not yet supported by bench harness")
152
        }
153
    })
154
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc