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

joaoh82 / rust_sqlite / 25672443658

11 May 2026 01:14PM UTC coverage: 68.857% (-0.2%) from 69.041%
25672443658

push

github

web-flow
bench(W13): Phase 11.11b — concurrent-writers workload (SQLR-22 / SQLR-16) (#133)

Adds the headline Phase-11 differentiator workload to the SQLR-16
benchmark harness. Splits the original 11.11 scope: this slice ships
the bench workload only; the Go SDK cross-pool sibling shape is now
Phase 11.11c (a separate slice because it touches the Go binding
architecture, not the bench harness).

Workload shape (`benchmarks/src/workloads/concurrent_writers.rs`):

  CREATE TABLE counters (id INTEGER PRIMARY KEY, n INTEGER NOT NULL);
  -- preload 1_000 rows with n=0
  -- 4 worker threads × 50 BEGIN/UPDATE/COMMIT cycles each
  --   BEGIN <CONCURRENT|IMMEDIATE>;
  --   UPDATE counters SET n = n + 1 WHERE id = ?;  -- random in 1..=1000
  --   COMMIT;                                     -- retry on Busy/Locked

Per-engine specifics flow through four new `Driver` trait methods,
each with a sensible default so existing workloads (W1..W12) and
drivers are untouched:

- `connect_sibling(primary, path)` — SQLRite overrides to call
  `Connection::connect()` so workers share the primary's
  `Arc<Mutex<Database>>` (SQLRite's `Connection::open` takes
  `flock(LOCK_EX)`, so workers can't re-open). Default opens a
  fresh connection at `path` — works for SQLite (separate
  connections coexist under WAL).
- `enable_concurrent_mode(conn)` — SQLRite runs
  `PRAGMA journal_mode = mvcc;`. Default no-op (SQLite's WAL +
  busy_timeout setup happens at `open`).
- `concurrent_begin_sql()` — SQLRite returns `"BEGIN CONCURRENT"`,
  SQLite returns `"BEGIN IMMEDIATE"`. Default `"BEGIN"`.
- `is_retryable_busy(err)` — SQLRite downcasts to
  `SQLRiteError::is_retryable()`; SQLite walks the anyhow chain for
  rusqlite's `DatabaseBusy` / `DatabaseLocked`. Default false.

SQLite driver gains a `busy_timeout = 5s` pragma at open so its
`BEGIN IMMEDIATE` blocks rather than fails on contention. SQLRite
driver's `.map_err(|e| anyhow::anyhow!(...))` pattern in `execute`
/ `execute_with_params` was dro... (continued)

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

3 existing lines in 2 files now uncovered.

11148 of 16190 relevant lines covered (68.86%)

1.23 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
#[derive(Clone, Copy)]
30
pub struct SQLiteDriver;
31

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

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

39
    fn open(&self, path: &Path) -> Result<Self::Conn> {
×
40
        let conn = rusqlite::Connection::open(path)
×
41
            .with_context(|| format!("rusqlite open({})", path.display()))?;
×
42
        // Tuned profile (Q3). Each PRAGMA returns one row of feedback
43
        // we discard; query_row is the right shape (`pragma_update`
44
        // doesn't accept the negative-value cache_size form).
45
        conn.pragma_update(None, "journal_mode", "WAL")
×
46
            .context("PRAGMA journal_mode=WAL")?;
×
47
        conn.pragma_update(None, "synchronous", "NORMAL")
×
48
            .context("PRAGMA synchronous=NORMAL")?;
×
49
        conn.pragma_update(None, "temp_store", "MEMORY")
×
50
            .context("PRAGMA temp_store=MEMORY")?;
×
51
        conn.pragma_update(None, "cache_size", -65536i64)
×
52
            .context("PRAGMA cache_size=-65536")?;
×
53
        // Phase 11.11b — `busy_timeout = 5s` so the W13 multi-writer
54
        // workload's concurrent COMMITs wait for the writer lock
55
        // rather than immediately returning SQLITE_BUSY. Without
56
        // this, the bench measures lock-collision rate, not
57
        // throughput. Single-writer workloads (W1..W12) never hit
58
        // contention so the pragma is a no-op for them.
NEW
59
        conn.busy_timeout(std::time::Duration::from_secs(5))
×
NEW
60
            .context("busy_timeout=5s")?;
×
UNCOV
61
        Ok(conn)
×
62
    }
63

64
    fn execute(&self, conn: &mut Self::Conn, sql: &str) -> Result<()> {
×
65
        conn.execute_batch(sql)
×
66
            .with_context(|| format!("rusqlite execute_batch: {sql}"))?;
×
67
        Ok(())
×
68
    }
69

70
    fn execute_with_params(
×
71
        &self,
72
        conn: &mut Self::Conn,
73
        sql: &str,
74
        params: &[Value],
75
    ) -> Result<()> {
76
        let bound = to_rusqlite_params(params)?;
×
77
        conn.execute(sql, rusqlite::params_from_iter(bound.iter()))
×
78
            .with_context(|| format!("rusqlite execute: {sql}"))?;
×
79
        Ok(())
×
80
    }
81

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

114
    fn query_all(
×
115
        &self,
116
        conn: &mut Self::Conn,
117
        sql: &str,
118
        params: &[Value],
119
    ) -> Result<Vec<Vec<Value>>> {
120
        let mut stmt = conn
×
121
            .prepare_cached(sql)
×
122
            .with_context(|| format!("rusqlite prepare_cached: {sql}"))?;
×
123
        let bound = to_rusqlite_params(params)?;
×
124
        let cols = stmt.column_count();
×
125
        let mut rows = stmt
×
126
            .query(rusqlite::params_from_iter(bound.iter()))
×
127
            .with_context(|| format!("rusqlite query: {sql}"))?;
×
128
        let mut out = Vec::new();
×
129
        while let Some(row) = rows
×
130
            .next()
×
131
            .with_context(|| format!("rusqlite row read: {sql}"))?
×
132
        {
133
            let mut buf = Vec::with_capacity(cols);
×
134
            for i in 0..cols {
×
135
                buf.push(extract_column(row, i)?);
×
136
            }
137
            out.push(buf);
×
138
        }
139
        Ok(out)
×
140
    }
141

NEW
142
    fn concurrent_begin_sql(&self) -> &'static str {
×
143
        // SQLite's recommended multi-writer shape: acquire the
144
        // write lock at BEGIN so two writers don't race into
145
        // SQLITE_BUSY at COMMIT. The `busy_timeout` set at open
146
        // makes the BEGIN wait for the lock rather than failing
147
        // immediately.
NEW
148
        "BEGIN IMMEDIATE"
×
149
    }
150

NEW
151
    fn is_retryable_busy(&self, err: &anyhow::Error) -> bool {
×
152
        // SQLite returns SQLITE_BUSY / SQLITE_BUSY_RETRY /
153
        // SQLITE_LOCKED on contention. With `busy_timeout` we
154
        // mostly *don't* see these — the call blocks instead —
155
        // but if the timeout elapses (very rare with our 5s
156
        // setting on the W13 workload sizes) we should retry.
NEW
157
        err.chain()
×
NEW
158
            .filter_map(|e| e.downcast_ref::<rusqlite::Error>())
×
NEW
159
            .any(|e| {
×
NEW
160
                use rusqlite::ErrorCode::*;
×
NEW
161
                matches!(
×
NEW
162
                    e.sqlite_error_code(),
×
NEW
163
                    Some(DatabaseBusy) | Some(DatabaseLocked)
×
164
                )
165
            })
166
    }
167
}
168

169
fn to_rusqlite_params(params: &[Value]) -> Result<Vec<rusqlite::types::Value>> {
170
    params.iter().map(to_rusqlite).collect()
171
}
172

173
fn to_rusqlite(v: &Value) -> Result<rusqlite::types::Value> {
174
    match v {
175
        Value::Null => Ok(rusqlite::types::Value::Null),
176
        Value::Integer(i) => Ok(rusqlite::types::Value::Integer(*i)),
177
        Value::Real(f) => Ok(rusqlite::types::Value::Real(*f)),
178
        Value::Text(s) => Ok(rusqlite::types::Value::Text(s.clone())),
179
        // VECTOR is SQLRite-only; the W10/W12 workloads gate on
180
        // `driver_supports("sqlrite")` so this branch indicates a
181
        // bug in workload registration. Fail loudly rather than
182
        // silently coercing.
183
        Value::Vector(_) => anyhow::bail!(
184
            "rusqlite driver: VECTOR params are SQLRite-only; this workload should not register \
185
             against sqlite"
186
        ),
187
    }
188
}
189

190
fn extract_column(row: &rusqlite::Row<'_>, idx: usize) -> Result<Value> {
191
    let raw: rusqlite::types::Value = row.get(idx)?;
192
    Ok(match raw {
193
        rusqlite::types::Value::Null => Value::Null,
194
        rusqlite::types::Value::Integer(i) => Value::Integer(i),
195
        rusqlite::types::Value::Real(f) => Value::Real(f),
196
        rusqlite::types::Value::Text(s) => Value::Text(s),
197
        rusqlite::types::Value::Blob(_) => {
198
            anyhow::bail!("rusqlite extract_column: BLOB not yet supported by bench harness")
199
        }
200
    })
201
}
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