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

joaoh82 / rust_sqlite / 25559195914

08 May 2026 01:48PM UTC coverage: 65.094% (+0.6%) from 64.474%
25559195914

push

github

web-flow
feat(engine): prepared-statement plan cache + parameter binding (SQLR-23) (#110)

`Connection::prepare_cached` (default 16-entry per-connection LRU) +
`Statement::query_with_params` / `Statement::execute_with_params`
substitute `?` placeholders into a cached AST at execute time, skipping
the per-call sqlparser walk. `Value::Vector(Vec<f32>)` is a first-class
bind type — a bound vector substitutes into the same in-band bracket-
array shape an inline `[…]` literal would, so the HNSW probe optimizer
still recognizes it on prepared queries.

Bench harness flipped from per-call SQL formatting (`inline_params`) to
the bound + cached path; every workload's `WorkloadId.version` bumped
`v1 → v2` in lockstep so the methodology change is captured explicitly.
Old v1 envelopes stay readable; the comparison script flags cross-
version pairs.

Smoke run confirms parser-bound wins on the workloads the plan
predicted: W1 -52%, W6 -61%, W3 -44%, W11 -26%. Republishing the
official pinned-host envelope is SQLR-25.

Surfaced one pre-existing limitation: `try_hnsw_probe` is L2-only, so
W10's `vec_distance_cosine` query has been silently brute-forcing the
HNSW variant the entire time. SQLR-28 tracks widening the probe to
cosine + dot.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

307 of 358 new or added lines in 6 files covered. (85.75%)

484 existing lines in 2 files now uncovered.

8983 of 13800 relevant lines covered (65.09%)

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

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

38
    fn open(&self, path: &Path) -> Result<Self::Conn> {
×
39
        let conn = rusqlite::Connection::open(path)
×
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).
44
        conn.pragma_update(None, "journal_mode", "WAL")
×
45
            .context("PRAGMA journal_mode=WAL")?;
×
46
        conn.pragma_update(None, "synchronous", "NORMAL")
×
47
            .context("PRAGMA synchronous=NORMAL")?;
×
48
        conn.pragma_update(None, "temp_store", "MEMORY")
×
49
            .context("PRAGMA temp_store=MEMORY")?;
×
50
        conn.pragma_update(None, "cache_size", -65536i64)
×
51
            .context("PRAGMA cache_size=-65536")?;
×
52
        Ok(conn)
×
53
    }
54

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

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

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).
79
        let mut stmt = conn
×
80
            .prepare_cached(sql)
×
81
            .with_context(|| format!("rusqlite prepare_cached: {sql}"))?;
×
NEW
82
        let bound = to_rusqlite_params(params)?;
×
83
        let cols = stmt.column_count();
×
84
        let mut rows = stmt
×
85
            .query(rusqlite::params_from_iter(bound.iter()))
×
86
            .with_context(|| format!("rusqlite query: {sql}"))?;
×
87
        let row = rows
×
88
            .next()
×
89
            .with_context(|| format!("rusqlite row read: {sql}"))?
×
90
            .context("rusqlite query_one: zero rows returned")?;
×
91
        let mut out = Vec::with_capacity(cols);
×
92
        for i in 0..cols {
×
93
            out.push(extract_column(row, i)?);
×
94
        }
95
        if rows
×
96
            .next()
×
97
            .with_context(|| format!("rusqlite row read: {sql}"))?
×
98
            .is_some()
99
        {
100
            anyhow::bail!("rusqlite query_one: >1 rows returned");
×
101
        }
102
        Ok(out)
×
103
    }
104

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

134
fn to_rusqlite_params(params: &[Value]) -> Result<Vec<rusqlite::types::Value>> {
135
    params.iter().map(to_rusqlite).collect()
136
}
137

138
fn to_rusqlite(v: &Value) -> Result<rusqlite::types::Value> {
139
    match v {
140
        Value::Null => Ok(rusqlite::types::Value::Null),
141
        Value::Integer(i) => Ok(rusqlite::types::Value::Integer(*i)),
142
        Value::Real(f) => Ok(rusqlite::types::Value::Real(*f)),
143
        Value::Text(s) => Ok(rusqlite::types::Value::Text(s.clone())),
144
        // VECTOR is SQLRite-only; the W10/W12 workloads gate on
145
        // `driver_supports("sqlrite")` so this branch indicates a
146
        // bug in workload registration. Fail loudly rather than
147
        // silently coercing.
148
        Value::Vector(_) => anyhow::bail!(
149
            "rusqlite driver: VECTOR params are SQLRite-only; this workload should not register \
150
             against sqlite"
151
        ),
152
    }
153
}
154

155
fn extract_column(row: &rusqlite::Row<'_>, idx: usize) -> Result<Value> {
156
    let raw: rusqlite::types::Value = row.get(idx)?;
157
    Ok(match raw {
158
        rusqlite::types::Value::Null => Value::Null,
159
        rusqlite::types::Value::Integer(i) => Value::Integer(i),
160
        rusqlite::types::Value::Real(f) => Value::Real(f),
161
        rusqlite::types::Value::Text(s) => Value::Text(s),
162
        rusqlite::types::Value::Blob(_) => {
163
            anyhow::bail!("rusqlite extract_column: BLOB not yet supported by bench harness")
164
        }
165
    })
166
}
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