Skip to main content

sqlrite/
connection.rs

1//! Public `Connection` / `Statement` / `Rows` / `Row` API (Phase 5a + SQLR-23).
2//!
3//! This is the stable surface external consumers bind against — Rust
4//! callers use it directly, language SDKs (Python, Node.js, Go) bind
5//! against the C FFI wrapper over these same types in Phase 5b, and
6//! the WASM build in Phase 5g re-exposes them via `wasm-bindgen`.
7//!
8//! The shape mirrors `rusqlite` / Python's `sqlite3` so users
9//! familiar with either can pick it up immediately:
10//!
11//! ```no_run
12//! use sqlrite::Connection;
13//!
14//! let mut conn = Connection::open("foo.sqlrite")?;
15//! conn.execute("CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT)")?;
16//! conn.execute("INSERT INTO users (name) VALUES ('alice')")?;
17//!
18//! let mut stmt = conn.prepare("SELECT id, name FROM users")?;
19//! let mut rows = stmt.query()?;
20//! while let Some(row) = rows.next()? {
21//!     let id: i64 = row.get(0)?;
22//!     let name: String = row.get(1)?;
23//!     println!("{id}: {name}");
24//! }
25//! # Ok::<(), sqlrite::SQLRiteError>(())
26//! ```
27//!
28//! **Relationship to the internal engine.** A `Connection` owns a
29//! `Database` (which owns a `Pager` for file-backed connections).
30//! `execute` and `query` go through the same `process_command`
31//! pipeline the REPL uses, just with typed row return instead of
32//! pre-rendered tables. The internal `Database` / `Pager` stay
33//! accessible via `sqlrite::sql::...` for the engine's own tests
34//! and for the desktop app — but those paths aren't considered
35//! stable API.
36//!
37//! # Prepared statements & parameter binding (SQLR-23)
38//!
39//! `Connection::prepare` parses the SQL once and stashes the AST on
40//! the returned `Statement`. Subsequent calls to `Statement::query` /
41//! `Statement::run` execute against the cached AST without re-running
42//! sqlparser. Bound versions ([`Statement::query_with_params`] /
43//! [`Statement::execute_with_params`]) accept a `&[Value]` slice that is
44//! substituted into the cached AST at execute time — including
45//! `Value::Vector(...)` for HNSW-eligible KNN queries, where binding
46//! the query vector skips per-iter lexing of the 4 KB bracket-array
47//! literal.
48//!
49//! [`Connection::prepare_cached`] adds a small per-connection LRU
50//! (default cap 16) so a hot SQL string is parsed exactly once across
51//! every call, not once per `prepare()`. Matches the rusqlite pattern.
52
53use std::collections::VecDeque;
54use std::path::Path;
55use std::sync::Arc;
56
57use crate::sql::dialect::SqlriteDialect;
58use sqlparser::ast::Statement as AstStatement;
59use sqlparser::parser::Parser;
60
61use crate::error::{Result, SQLRiteError};
62use crate::sql::db::database::Database;
63use crate::sql::db::table::Value;
64use crate::sql::executor::execute_select_rows;
65use crate::sql::pager::{AccessMode, open_database_with_mode, save_database};
66use crate::sql::params::{rewrite_placeholders, substitute_params};
67use crate::sql::parser::select::SelectQuery;
68use crate::sql::process_ast_with_render;
69
70/// Default capacity of the per-connection prepared-statement plan cache.
71/// Matches rusqlite's default; tweak with [`Connection::set_prepared_cache_capacity`].
72const DEFAULT_PREP_CACHE_CAP: usize = 16;
73
74/// A handle to a SQLRite database. Opens a file or an in-memory DB;
75/// drop it to close. Every mutating statement auto-saves (except inside
76/// an explicit `BEGIN`/`COMMIT` block — see [Transactions](#transactions)).
77///
78/// ## Transactions
79///
80/// ```no_run
81/// # use sqlrite::Connection;
82/// let mut conn = Connection::open("foo.sqlrite")?;
83/// conn.execute("BEGIN")?;
84/// conn.execute("INSERT INTO users (name) VALUES ('alice')")?;
85/// conn.execute("INSERT INTO users (name) VALUES ('bob')")?;
86/// conn.execute("COMMIT")?;
87/// # Ok::<(), sqlrite::SQLRiteError>(())
88/// ```
89///
90/// `Connection` is `Send` but not `Sync` — clone it (it's currently
91/// unclonable) or share via a `Mutex<Connection>` if you need
92/// multi-threaded access.
93pub struct Connection {
94    db: Database,
95    /// SQLR-23 — small SQL→cached-plan LRU. Keyed by the verbatim SQL
96    /// string the caller passed to `prepare_cached`. Stored as a
97    /// `VecDeque` rather than a HashMap+linked-list because the
98    /// expected capacity is small (default 16) — linear scan is fine
99    /// and the implementation stays dependency-free.
100    prep_cache: VecDeque<(String, Arc<CachedPlan>)>,
101    prep_cache_cap: usize,
102}
103
104impl Connection {
105    /// Opens (or creates) a database file for read-write access.
106    ///
107    /// If the file doesn't exist, an empty one is materialized with the
108    /// current format version. Takes an exclusive advisory lock on the
109    /// file and its `-wal` sidecar; returns `Err` if either is already
110    /// locked by another process.
111    pub fn open<P: AsRef<Path>>(path: P) -> Result<Self> {
112        let path = path.as_ref();
113        let db_name = path
114            .file_stem()
115            .and_then(|s| s.to_str())
116            .unwrap_or("db")
117            .to_string();
118        let db = if path.exists() {
119            open_database_with_mode(path, db_name, AccessMode::ReadWrite)?
120        } else {
121            // Fresh file: materialize on disk and keep the attached
122            // pager. Setting `source_path` before `save_database` lets
123            // its `same_path` branch create the pager and stash it
124            // back on the Database — no reopen needed (and trying to
125            // reopen here would hit the file's own lock).
126            let mut fresh = Database::new(db_name);
127            fresh.source_path = Some(path.to_path_buf());
128            save_database(&mut fresh, path)?;
129            fresh
130        };
131        Ok(Self::wrap(db))
132    }
133
134    /// Opens an existing database file for read-only access. Takes a
135    /// shared advisory lock, so multiple read-only connections can
136    /// coexist on the same file; any open writer excludes them.
137    /// Mutating statements return `cannot execute: database is opened
138    /// read-only`.
139    pub fn open_read_only<P: AsRef<Path>>(path: P) -> Result<Self> {
140        let path = path.as_ref();
141        let db_name = path
142            .file_stem()
143            .and_then(|s| s.to_str())
144            .unwrap_or("db")
145            .to_string();
146        let db = open_database_with_mode(path, db_name, AccessMode::ReadOnly)?;
147        Ok(Self::wrap(db))
148    }
149
150    /// Opens a transient in-memory database. No file is touched and no
151    /// locks are taken; state lives for the lifetime of the
152    /// `Connection` and is discarded on drop.
153    pub fn open_in_memory() -> Result<Self> {
154        Ok(Self::wrap(Database::new("memdb".to_string())))
155    }
156
157    fn wrap(db: Database) -> Self {
158        Self {
159            db,
160            prep_cache: VecDeque::new(),
161            prep_cache_cap: DEFAULT_PREP_CACHE_CAP,
162        }
163    }
164
165    /// Parses and executes one SQL statement. For DDL (`CREATE TABLE`,
166    /// `CREATE INDEX`), DML (`INSERT`, `UPDATE`, `DELETE`) and
167    /// transaction control (`BEGIN`, `COMMIT`, `ROLLBACK`). Returns
168    /// the status message the engine produced (e.g.
169    /// `"INSERT Statement executed."`).
170    ///
171    /// For `SELECT`, `execute` works but discards the row data and
172    /// just returns the rendered status — use [`Connection::prepare`]
173    /// and [`Statement::query`] to iterate typed rows.
174    pub fn execute(&mut self, sql: &str) -> Result<String> {
175        crate::sql::process_command(sql, &mut self.db)
176    }
177
178    /// Prepares a statement for repeated execution or row iteration.
179    /// SQLR-23: the SQL is parsed once at prepare time (sqlparser walk
180    /// plus placeholder rewriting), and the resulting AST is cached
181    /// on the [`Statement`] for re-execution without further parsing.
182    ///
183    /// Use [`Statement::query`] / [`Statement::run`] for unbound
184    /// execution, or [`Statement::query_with_params`] /
185    /// [`Statement::execute_with_params`] to substitute `?`
186    /// placeholders.
187    pub fn prepare<'c>(&'c mut self, sql: &str) -> Result<Statement<'c>> {
188        let plan = Arc::new(CachedPlan::compile(sql)?);
189        Ok(Statement { conn: self, plan })
190    }
191
192    /// Same as [`Connection::prepare`], but consults a small
193    /// per-connection LRU first. SQLR-23 — for hot statements
194    /// (the body of an INSERT loop, a frequently-rerun lookup) the
195    /// sqlparser walk is amortized to once across the connection's
196    /// lifetime, not once per `prepare()`.
197    ///
198    /// Default cache capacity is 16; tune with
199    /// [`Connection::set_prepared_cache_capacity`].
200    pub fn prepare_cached<'c>(&'c mut self, sql: &str) -> Result<Statement<'c>> {
201        // Lookup-or-insert. Found entries are also moved to the back
202        // (most-recently-used) so capacity-eviction runs LRU.
203        let plan = if let Some(pos) = self.prep_cache.iter().position(|(k, _)| k == sql) {
204            let (k, v) = self.prep_cache.remove(pos).unwrap();
205            self.prep_cache.push_back((k, Arc::clone(&v)));
206            v
207        } else {
208            let plan = Arc::new(CachedPlan::compile(sql)?);
209            self.prep_cache
210                .push_back((sql.to_string(), Arc::clone(&plan)));
211            while self.prep_cache.len() > self.prep_cache_cap {
212                self.prep_cache.pop_front();
213            }
214            plan
215        };
216        Ok(Statement { conn: self, plan })
217    }
218
219    /// SQLR-23 — sets the maximum number of cached prepared plans
220    /// (matches `prepare_cached`'s default 16). Reducing below the
221    /// current size evicts the oldest entries; setting to 0 disables
222    /// caching but `prepare_cached` still works (it just always
223    /// re-parses).
224    pub fn set_prepared_cache_capacity(&mut self, cap: usize) {
225        self.prep_cache_cap = cap;
226        while self.prep_cache.len() > cap {
227            self.prep_cache.pop_front();
228        }
229    }
230
231    /// SQLR-23 — current number of plans held by the prepared-statement
232    /// cache. Useful for tests / introspection; not load-bearing for
233    /// the public API.
234    pub fn prepared_cache_len(&self) -> usize {
235        self.prep_cache.len()
236    }
237
238    /// Returns `true` while a `BEGIN … COMMIT/ROLLBACK` block is open
239    /// against this connection.
240    pub fn in_transaction(&self) -> bool {
241        self.db.in_transaction()
242    }
243
244    /// Returns the current auto-VACUUM threshold (SQLR-10). After a
245    /// page-releasing DDL (DROP TABLE / DROP INDEX / ALTER TABLE DROP
246    /// COLUMN) commits, the engine compacts the file in place if the
247    /// freelist exceeds this fraction of `page_count`. New connections
248    /// default to `Some(0.25)` (SQLite parity); `None` means the
249    /// trigger is disabled. See [`Connection::set_auto_vacuum_threshold`].
250    pub fn auto_vacuum_threshold(&self) -> Option<f32> {
251        self.db.auto_vacuum_threshold()
252    }
253
254    /// Sets the auto-VACUUM threshold (SQLR-10). `Some(t)` with `t` in
255    /// `0.0..=1.0` arms the trigger; `None` disables it. Values outside
256    /// `0.0..=1.0` (or NaN / infinite) return a typed error rather than
257    /// silently saturating. The setting is per-connection runtime
258    /// state — closing the connection drops it; new connections start
259    /// at the default `Some(0.25)`.
260    ///
261    /// Calling this on an in-memory or read-only database is allowed
262    /// (it just won't fire — there's nothing to compact / no writes
263    /// will reach the trigger).
264    pub fn set_auto_vacuum_threshold(&mut self, threshold: Option<f32>) -> Result<()> {
265        self.db.set_auto_vacuum_threshold(threshold)
266    }
267
268    /// Returns `true` if the connection was opened read-only. Mutating
269    /// statements on a read-only connection return a typed error.
270    pub fn is_read_only(&self) -> bool {
271        self.db.is_read_only()
272    }
273
274    /// Escape hatch for advanced callers — the internal `Database`
275    /// backing this connection. Not part of the stable API; will move
276    /// or change as Phase 5's cursor abstraction lands.
277    #[doc(hidden)]
278    pub fn database(&self) -> &Database {
279        &self.db
280    }
281
282    #[doc(hidden)]
283    pub fn database_mut(&mut self) -> &mut Database {
284        &mut self.db
285    }
286}
287
288impl std::fmt::Debug for Connection {
289    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
290        f.debug_struct("Connection")
291            .field("in_transaction", &self.db.in_transaction())
292            .field("read_only", &self.db.is_read_only())
293            .field("tables", &self.db.tables.len())
294            .field("prep_cache_len", &self.prep_cache.len())
295            .finish()
296    }
297}
298
299/// SQLR-23 — the parse-once-execute-many representation. Built by
300/// `CachedPlan::compile` (sqlparser walk + placeholder rewriting +
301/// SELECT narrowing) and shared between every `Statement` that hits
302/// the same SQL string in `prepare_cached`.
303#[derive(Debug)]
304struct CachedPlan {
305    /// Original SQL — kept for diagnostic output.
306    #[allow(dead_code)]
307    sql: String,
308    /// AST after `?` → `?N` placeholder rewriting. Cloned per execute
309    /// so the substitution pass leaves the cached copy intact.
310    ast: AstStatement,
311    /// Total `?` placeholder count in the source SQL. Strict bind
312    /// validation in `query_with_params` / `execute_with_params`
313    /// uses this.
314    param_count: usize,
315    /// SELECT narrowing — cached so `query()` doesn't redo the
316    /// `SelectQuery::new` walk for unbound SELECTs. `None` for
317    /// non-SELECT statements.
318    select: Option<SelectQuery>,
319}
320
321impl CachedPlan {
322    fn compile(sql: &str) -> Result<Self> {
323        let dialect = SqlriteDialect::new();
324        let mut ast = Parser::parse_sql(&dialect, sql).map_err(SQLRiteError::from)?;
325        let Some(mut stmt) = ast.pop() else {
326            return Err(SQLRiteError::General("no statement to prepare".to_string()));
327        };
328        if !ast.is_empty() {
329            return Err(SQLRiteError::General(
330                "prepare() accepts a single statement; found more than one".to_string(),
331            ));
332        }
333        let param_count = rewrite_placeholders(&mut stmt);
334        let select = match &stmt {
335            AstStatement::Query(_) => Some(SelectQuery::new(&stmt)?),
336            _ => None,
337        };
338        Ok(Self {
339            sql: sql.to_string(),
340            ast: stmt,
341            param_count,
342            select,
343        })
344    }
345}
346
347/// A prepared statement bound to a specific connection lifetime.
348///
349/// SQLR-23 — `Statement` carries the parsed AST (parsed exactly once
350/// at prepare time), not just the raw SQL. `query` / `run` execute
351/// against the cached AST; `query_with_params` / `execute_with_params`
352/// clone the AST and substitute `?` placeholders before dispatch.
353pub struct Statement<'c> {
354    conn: &'c mut Connection,
355    plan: Arc<CachedPlan>,
356}
357
358impl std::fmt::Debug for Statement<'_> {
359    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
360        f.debug_struct("Statement")
361            .field("sql", &self.plan.sql)
362            .field("param_count", &self.plan.param_count)
363            .field(
364                "kind",
365                &match self.plan.select {
366                    Some(_) => "Select",
367                    None => "Other",
368                },
369            )
370            .finish()
371    }
372}
373
374impl<'c> Statement<'c> {
375    /// Number of `?` placeholders detected in the source SQL. Strict
376    /// arity validation: passing a slice of a different length to
377    /// `query_with_params` / `execute_with_params` returns a typed
378    /// error.
379    pub fn parameter_count(&self) -> usize {
380        self.plan.param_count
381    }
382
383    /// Executes a prepared non-query statement. Equivalent to
384    /// [`Connection::execute`] — included for parity with the
385    /// typed-row `query()` so callers who want `Statement::run` /
386    /// `Statement::query` symmetry get it.
387    ///
388    /// Errors if the prepared SQL contains `?` placeholders — use
389    /// [`Statement::execute_with_params`] for those.
390    pub fn run(&mut self) -> Result<String> {
391        if self.plan.param_count > 0 {
392            return Err(SQLRiteError::General(format!(
393                "statement has {} `?` placeholder(s); call execute_with_params()",
394                self.plan.param_count
395            )));
396        }
397        let ast = self.plan.ast.clone();
398        process_ast_with_render(ast, &mut self.conn.db).map(|o| o.status)
399    }
400
401    /// SQLR-23 — executes a prepared non-SELECT statement after binding
402    /// `?` placeholders to `params` (positional, in source order).
403    ///
404    /// Use this for parameterized INSERT / UPDATE / DELETE — the
405    /// substitution clones the cached AST, fills in the `?` slots
406    /// from `params`, and dispatches without re-running sqlparser.
407    /// For SELECT, prefer [`Statement::query_with_params`].
408    pub fn execute_with_params(&mut self, params: &[Value]) -> Result<String> {
409        self.check_arity(params)?;
410        let mut ast = self.plan.ast.clone();
411        if !params.is_empty() {
412            substitute_params(&mut ast, params)?;
413        }
414        process_ast_with_render(ast, &mut self.conn.db).map(|o| o.status)
415    }
416
417    /// Runs a SELECT and returns a [`Rows`] iterator over typed rows.
418    /// Errors if the prepared statement isn't a SELECT.
419    ///
420    /// SQLR-23 — uses the SELECT narrowing cached at prepare time;
421    /// no per-call sqlparser walk. Errors if the prepared SQL
422    /// contains `?` placeholders — use [`Statement::query_with_params`]
423    /// for those.
424    pub fn query(&self) -> Result<Rows> {
425        if self.plan.param_count > 0 {
426            return Err(SQLRiteError::General(format!(
427                "statement has {} `?` placeholder(s); call query_with_params()",
428                self.plan.param_count
429            )));
430        }
431        let Some(sq) = self.plan.select.as_ref() else {
432            return Err(SQLRiteError::General(
433                "query() only works on SELECT statements; use run() for DDL/DML".to_string(),
434            ));
435        };
436        let result = execute_select_rows(sq.clone(), &self.conn.db)?;
437        Ok(Rows {
438            columns: result.columns,
439            rows: result.rows.into_iter(),
440        })
441    }
442
443    /// SQLR-23 — runs a SELECT and returns a [`Rows`] iterator after
444    /// binding `?` placeholders to `params`. Positional, source-order
445    /// indexing — `params[0]` is `?1`, `params[1]` is `?2`, etc.
446    ///
447    /// Vector parameters (`Value::Vector(...)`) substitute as the
448    /// in-band bracket-array shape the executor recognizes, so a
449    /// bound query vector still triggers the HNSW probe optimizer
450    /// (Phase 7d.2 KNN shortcut).
451    pub fn query_with_params(&self, params: &[Value]) -> Result<Rows> {
452        self.check_arity(params)?;
453        if self.plan.select.is_none() {
454            return Err(SQLRiteError::General(
455                "query_with_params() only works on SELECT statements; use execute_with_params() \
456                 for DDL/DML"
457                    .to_string(),
458            ));
459        }
460        // Re-narrow against the substituted AST. The narrow walk is
461        // cheap (it pulls projection/WHERE/ORDER BY into typed
462        // structs), and rerunning it ensures the substituted literals
463        // (e.g. a bracket-array vector) flow through `SelectQuery`.
464        let mut ast = self.plan.ast.clone();
465        if !params.is_empty() {
466            substitute_params(&mut ast, params)?;
467        }
468        let sq = SelectQuery::new(&ast)?;
469        let result = execute_select_rows(sq, &self.conn.db)?;
470        Ok(Rows {
471            columns: result.columns,
472            rows: result.rows.into_iter(),
473        })
474    }
475
476    fn check_arity(&self, params: &[Value]) -> Result<()> {
477        if params.len() != self.plan.param_count {
478            return Err(SQLRiteError::General(format!(
479                "expected {} parameter{}, got {}",
480                self.plan.param_count,
481                if self.plan.param_count == 1 { "" } else { "s" },
482                params.len()
483            )));
484        }
485        Ok(())
486    }
487
488    /// Column names this statement will produce, in projection order.
489    /// `None` for non-SELECT statements.
490    pub fn column_names(&self) -> Option<Vec<String>> {
491        match &self.plan.select {
492            Some(_) => {
493                // We can't know the concrete column list without
494                // running the query (it depends on the table schema
495                // and the projection). Callers who need it up front
496                // should call query() and inspect Rows::columns.
497                None
498            }
499            None => None,
500        }
501    }
502}
503
504/// Iterator of typed [`Row`] values produced by a `SELECT` query.
505///
506/// Today `Rows` is backed by an eager `Vec<Vec<Value>>` — the cursor
507/// abstraction in Phase 5a's follow-up will swap this for a lazy
508/// walker that streams rows off the B-Tree without materializing
509/// them upfront. The `Rows::next` API is designed for that: it
510/// returns `Result<Option<Row>>` rather than `Option<Result<Row>>`,
511/// so a mid-stream I/O error surfaces cleanly.
512pub struct Rows {
513    columns: Vec<String>,
514    rows: std::vec::IntoIter<Vec<Value>>,
515}
516
517impl std::fmt::Debug for Rows {
518    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
519        f.debug_struct("Rows")
520            .field("columns", &self.columns)
521            .field("remaining", &self.rows.len())
522            .finish()
523    }
524}
525
526impl Rows {
527    /// Column names in projection order.
528    pub fn columns(&self) -> &[String] {
529        &self.columns
530    }
531
532    /// Advances to the next row. Returns `Ok(None)` when the query is
533    /// exhausted, `Ok(Some(row))` otherwise, `Err(_)` on an I/O or
534    /// decode failure (relevant once Phase 5a's cursor work lands —
535    /// today this is always `Ok(_)`).
536    pub fn next(&mut self) -> Result<Option<Row<'_>>> {
537        Ok(self.rows.next().map(|values| Row {
538            columns: &self.columns,
539            values,
540        }))
541    }
542
543    /// Collects every remaining row into a `Vec<Row>`. Convenient for
544    /// small result sets; avoid on large queries — that's what the
545    /// streaming [`Rows::next`] API is for.
546    pub fn collect_all(mut self) -> Result<Vec<OwnedRow>> {
547        let mut out = Vec::new();
548        while let Some(r) = self.next()? {
549            out.push(r.to_owned_row());
550        }
551        Ok(out)
552    }
553}
554
555/// A single row borrowed from a [`Rows`] iterator. Lives only as long
556/// as the iterator; call `Row::to_owned_row` to detach it if you need
557/// to keep it past the next `next()` call.
558pub struct Row<'r> {
559    columns: &'r [String],
560    values: Vec<Value>,
561}
562
563impl<'r> Row<'r> {
564    /// Value at column index `idx`. Returns a clean error if out of
565    /// bounds or the type conversion fails.
566    pub fn get<T: FromValue>(&self, idx: usize) -> Result<T> {
567        let v = self.values.get(idx).ok_or_else(|| {
568            SQLRiteError::General(format!(
569                "column index {idx} out of bounds (row has {} columns)",
570                self.values.len()
571            ))
572        })?;
573        T::from_value(v)
574    }
575
576    /// Value at column named `name`. Case-sensitive.
577    pub fn get_by_name<T: FromValue>(&self, name: &str) -> Result<T> {
578        let idx = self
579            .columns
580            .iter()
581            .position(|c| c == name)
582            .ok_or_else(|| SQLRiteError::General(format!("no column named '{name}' in row")))?;
583        self.get(idx)
584    }
585
586    /// Column names for this row.
587    pub fn columns(&self) -> &[String] {
588        self.columns
589    }
590
591    /// Detaches from the parent `Rows` iterator. Useful when you want
592    /// to keep rows past the next `Rows::next()` call.
593    pub fn to_owned_row(&self) -> OwnedRow {
594        OwnedRow {
595            columns: self.columns.to_vec(),
596            values: self.values.clone(),
597        }
598    }
599}
600
601/// A row detached from the `Rows` iterator — owns its data, no
602/// borrow ties it to the parent iterator.
603#[derive(Debug, Clone)]
604pub struct OwnedRow {
605    pub columns: Vec<String>,
606    pub values: Vec<Value>,
607}
608
609impl OwnedRow {
610    pub fn get<T: FromValue>(&self, idx: usize) -> Result<T> {
611        let v = self.values.get(idx).ok_or_else(|| {
612            SQLRiteError::General(format!(
613                "column index {idx} out of bounds (row has {} columns)",
614                self.values.len()
615            ))
616        })?;
617        T::from_value(v)
618    }
619
620    pub fn get_by_name<T: FromValue>(&self, name: &str) -> Result<T> {
621        let idx = self
622            .columns
623            .iter()
624            .position(|c| c == name)
625            .ok_or_else(|| SQLRiteError::General(format!("no column named '{name}' in row")))?;
626        self.get(idx)
627    }
628}
629
630/// Conversion from SQLRite's internal [`Value`] enum into a typed Rust
631/// value. Implementations cover the common built-ins — `i64`, `f64`,
632/// `String`, `bool`, and `Option<T>` for nullable columns. Extend on
633/// demand.
634pub trait FromValue: Sized {
635    fn from_value(v: &Value) -> Result<Self>;
636}
637
638impl FromValue for i64 {
639    fn from_value(v: &Value) -> Result<Self> {
640        match v {
641            Value::Integer(n) => Ok(*n),
642            Value::Null => Err(SQLRiteError::General(
643                "expected Integer, got NULL".to_string(),
644            )),
645            other => Err(SQLRiteError::General(format!(
646                "cannot convert {other:?} to i64"
647            ))),
648        }
649    }
650}
651
652impl FromValue for f64 {
653    fn from_value(v: &Value) -> Result<Self> {
654        match v {
655            Value::Real(f) => Ok(*f),
656            Value::Integer(n) => Ok(*n as f64),
657            Value::Null => Err(SQLRiteError::General("expected Real, got NULL".to_string())),
658            other => Err(SQLRiteError::General(format!(
659                "cannot convert {other:?} to f64"
660            ))),
661        }
662    }
663}
664
665impl FromValue for String {
666    fn from_value(v: &Value) -> Result<Self> {
667        match v {
668            Value::Text(s) => Ok(s.clone()),
669            Value::Null => Err(SQLRiteError::General("expected Text, got NULL".to_string())),
670            other => Err(SQLRiteError::General(format!(
671                "cannot convert {other:?} to String"
672            ))),
673        }
674    }
675}
676
677impl FromValue for bool {
678    fn from_value(v: &Value) -> Result<Self> {
679        match v {
680            Value::Bool(b) => Ok(*b),
681            Value::Integer(n) => Ok(*n != 0),
682            Value::Null => Err(SQLRiteError::General("expected Bool, got NULL".to_string())),
683            other => Err(SQLRiteError::General(format!(
684                "cannot convert {other:?} to bool"
685            ))),
686        }
687    }
688}
689
690/// Nullable columns: `Option<T>` maps `NULL → None` and everything else
691/// through the inner type's `FromValue` impl.
692impl<T: FromValue> FromValue for Option<T> {
693    fn from_value(v: &Value) -> Result<Self> {
694        match v {
695            Value::Null => Ok(None),
696            other => Ok(Some(T::from_value(other)?)),
697        }
698    }
699}
700
701/// Identity impl so `row.get::<_, Value>(0)` works when you want
702/// untyped access.
703impl FromValue for Value {
704    fn from_value(v: &Value) -> Result<Self> {
705        Ok(v.clone())
706    }
707}
708
709#[cfg(test)]
710mod tests {
711    use super::*;
712
713    fn tmp_path(name: &str) -> std::path::PathBuf {
714        let mut p = std::env::temp_dir();
715        let pid = std::process::id();
716        let nanos = std::time::SystemTime::now()
717            .duration_since(std::time::UNIX_EPOCH)
718            .map(|d| d.as_nanos())
719            .unwrap_or(0);
720        p.push(format!("sqlrite-conn-{pid}-{nanos}-{name}.sqlrite"));
721        p
722    }
723
724    fn cleanup(path: &std::path::Path) {
725        let _ = std::fs::remove_file(path);
726        let mut wal = path.as_os_str().to_owned();
727        wal.push("-wal");
728        let _ = std::fs::remove_file(std::path::PathBuf::from(wal));
729    }
730
731    #[test]
732    fn in_memory_roundtrip() {
733        let mut conn = Connection::open_in_memory().unwrap();
734        conn.execute("CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT, age INTEGER);")
735            .unwrap();
736        conn.execute("INSERT INTO users (name, age) VALUES ('alice', 30);")
737            .unwrap();
738        conn.execute("INSERT INTO users (name, age) VALUES ('bob', 25);")
739            .unwrap();
740
741        let stmt = conn.prepare("SELECT id, name, age FROM users;").unwrap();
742        let mut rows = stmt.query().unwrap();
743        assert_eq!(rows.columns(), &["id", "name", "age"]);
744        let mut collected: Vec<(i64, String, i64)> = Vec::new();
745        while let Some(row) = rows.next().unwrap() {
746            collected.push((
747                row.get::<i64>(0).unwrap(),
748                row.get::<String>(1).unwrap(),
749                row.get::<i64>(2).unwrap(),
750            ));
751        }
752        assert_eq!(collected.len(), 2);
753        assert!(collected.iter().any(|(_, n, a)| n == "alice" && *a == 30));
754        assert!(collected.iter().any(|(_, n, a)| n == "bob" && *a == 25));
755    }
756
757    #[test]
758    fn file_backed_persists_across_connections() {
759        let path = tmp_path("persist");
760        {
761            let mut c1 = Connection::open(&path).unwrap();
762            c1.execute("CREATE TABLE items (id INTEGER PRIMARY KEY, label TEXT);")
763                .unwrap();
764            c1.execute("INSERT INTO items (label) VALUES ('one');")
765                .unwrap();
766        }
767        {
768            let mut c2 = Connection::open(&path).unwrap();
769            let stmt = c2.prepare("SELECT label FROM items;").unwrap();
770            let mut rows = stmt.query().unwrap();
771            let first = rows.next().unwrap().expect("one row");
772            assert_eq!(first.get::<String>(0).unwrap(), "one");
773            assert!(rows.next().unwrap().is_none());
774        }
775        cleanup(&path);
776    }
777
778    #[test]
779    fn read_only_connection_rejects_writes() {
780        let path = tmp_path("ro_reject");
781        {
782            let mut c = Connection::open(&path).unwrap();
783            c.execute("CREATE TABLE t (id INTEGER PRIMARY KEY);")
784                .unwrap();
785            c.execute("INSERT INTO t (id) VALUES (1);").unwrap();
786        } // writer drops → releases exclusive lock
787
788        let mut ro = Connection::open_read_only(&path).unwrap();
789        assert!(ro.is_read_only());
790        let err = ro.execute("INSERT INTO t (id) VALUES (2);").unwrap_err();
791        assert!(format!("{err}").contains("read-only"));
792        cleanup(&path);
793    }
794
795    #[test]
796    fn transactions_work_through_connection() {
797        let mut conn = Connection::open_in_memory().unwrap();
798        conn.execute("CREATE TABLE t (id INTEGER PRIMARY KEY, x INTEGER);")
799            .unwrap();
800        conn.execute("INSERT INTO t (x) VALUES (1);").unwrap();
801
802        conn.execute("BEGIN;").unwrap();
803        assert!(conn.in_transaction());
804        conn.execute("INSERT INTO t (x) VALUES (2);").unwrap();
805        conn.execute("ROLLBACK;").unwrap();
806        assert!(!conn.in_transaction());
807
808        let stmt = conn.prepare("SELECT x FROM t;").unwrap();
809        let rows = stmt.query().unwrap().collect_all().unwrap();
810        assert_eq!(rows.len(), 1);
811        assert_eq!(rows[0].get::<i64>(0).unwrap(), 1);
812    }
813
814    #[test]
815    fn get_by_name_works() {
816        let mut conn = Connection::open_in_memory().unwrap();
817        conn.execute("CREATE TABLE t (a INTEGER, b TEXT);").unwrap();
818        conn.execute("INSERT INTO t (a, b) VALUES (42, 'hello');")
819            .unwrap();
820
821        let stmt = conn.prepare("SELECT a, b FROM t;").unwrap();
822        let mut rows = stmt.query().unwrap();
823        let row = rows.next().unwrap().unwrap();
824        assert_eq!(row.get_by_name::<i64>("a").unwrap(), 42);
825        assert_eq!(row.get_by_name::<String>("b").unwrap(), "hello");
826    }
827
828    #[test]
829    fn null_column_maps_to_none() {
830        let mut conn = Connection::open_in_memory().unwrap();
831        conn.execute("CREATE TABLE t (id INTEGER PRIMARY KEY, note TEXT);")
832            .unwrap();
833        // id INTEGER PRIMARY KEY autoincrements; `note` is left unspecified.
834        conn.execute("INSERT INTO t (id) VALUES (1);").unwrap();
835
836        let stmt = conn.prepare("SELECT id, note FROM t;").unwrap();
837        let mut rows = stmt.query().unwrap();
838        let row = rows.next().unwrap().unwrap();
839        assert_eq!(row.get::<i64>(0).unwrap(), 1);
840        // note is NULL → Option<String> resolves to None.
841        assert_eq!(row.get::<Option<String>>(1).unwrap(), None);
842    }
843
844    #[test]
845    fn prepare_rejects_multiple_statements() {
846        let mut conn = Connection::open_in_memory().unwrap();
847        let err = conn.prepare("SELECT 1; SELECT 2;").unwrap_err();
848        assert!(format!("{err}").contains("single statement"));
849    }
850
851    #[test]
852    fn query_on_non_select_errors() {
853        let mut conn = Connection::open_in_memory().unwrap();
854        conn.execute("CREATE TABLE t (id INTEGER PRIMARY KEY);")
855            .unwrap();
856        let stmt = conn.prepare("INSERT INTO t VALUES (1);").unwrap();
857        let err = stmt.query().unwrap_err();
858        assert!(format!("{err}").contains("SELECT"));
859    }
860
861    /// SQLR-10: fresh connections expose the SQLite-parity 25% default,
862    /// the setter validates its input, and `None` opts out cleanly.
863    #[test]
864    fn auto_vacuum_threshold_default_and_setter() {
865        let mut conn = Connection::open_in_memory().unwrap();
866        assert_eq!(
867            conn.auto_vacuum_threshold(),
868            Some(0.25),
869            "fresh connection should ship with the SQLite-parity default"
870        );
871
872        conn.set_auto_vacuum_threshold(None).unwrap();
873        assert_eq!(conn.auto_vacuum_threshold(), None);
874
875        conn.set_auto_vacuum_threshold(Some(0.5)).unwrap();
876        assert_eq!(conn.auto_vacuum_threshold(), Some(0.5));
877
878        // Out-of-range values must be rejected with a typed error and
879        // must not stomp the previously-set value.
880        let err = conn.set_auto_vacuum_threshold(Some(1.5)).unwrap_err();
881        assert!(
882            format!("{err}").contains("auto_vacuum_threshold"),
883            "expected typed range error, got: {err}"
884        );
885        assert_eq!(
886            conn.auto_vacuum_threshold(),
887            Some(0.5),
888            "rejected setter call must not mutate the threshold"
889        );
890    }
891
892    #[test]
893    fn index_out_of_bounds_errors_cleanly() {
894        let mut conn = Connection::open_in_memory().unwrap();
895        conn.execute("CREATE TABLE t (a INTEGER PRIMARY KEY);")
896            .unwrap();
897        conn.execute("INSERT INTO t (a) VALUES (1);").unwrap();
898        let stmt = conn.prepare("SELECT a FROM t;").unwrap();
899        let mut rows = stmt.query().unwrap();
900        let row = rows.next().unwrap().unwrap();
901        let err = row.get::<i64>(99).unwrap_err();
902        assert!(format!("{err}").contains("out of bounds"));
903    }
904
905    // -----------------------------------------------------------------
906    // SQLR-23 — prepared-statement plan cache + parameter binding
907    // -----------------------------------------------------------------
908
909    #[test]
910    fn parameter_count_reflects_question_marks() {
911        let mut conn = Connection::open_in_memory().unwrap();
912        conn.execute("CREATE TABLE t (a INTEGER, b TEXT);").unwrap();
913        let stmt = conn.prepare("SELECT a, b FROM t WHERE a = ?").unwrap();
914        assert_eq!(stmt.parameter_count(), 1);
915        let stmt = conn
916            .prepare("SELECT a, b FROM t WHERE a = ? AND b = ?")
917            .unwrap();
918        assert_eq!(stmt.parameter_count(), 2);
919        let stmt = conn.prepare("SELECT a FROM t").unwrap();
920        assert_eq!(stmt.parameter_count(), 0);
921    }
922
923    #[test]
924    fn query_with_params_binds_scalars() {
925        let mut conn = Connection::open_in_memory().unwrap();
926        conn.execute("CREATE TABLE t (a INTEGER PRIMARY KEY, b TEXT);")
927            .unwrap();
928        conn.execute("INSERT INTO t (a, b) VALUES (1, 'alice');")
929            .unwrap();
930        conn.execute("INSERT INTO t (a, b) VALUES (2, 'bob');")
931            .unwrap();
932        conn.execute("INSERT INTO t (a, b) VALUES (3, 'carol');")
933            .unwrap();
934
935        let stmt = conn.prepare("SELECT b FROM t WHERE a = ?").unwrap();
936        let rows = stmt
937            .query_with_params(&[Value::Integer(2)])
938            .unwrap()
939            .collect_all()
940            .unwrap();
941        assert_eq!(rows.len(), 1);
942        assert_eq!(rows[0].get::<String>(0).unwrap(), "bob");
943    }
944
945    #[test]
946    fn execute_with_params_binds_insert_values() {
947        let mut conn = Connection::open_in_memory().unwrap();
948        conn.execute("CREATE TABLE t (a INTEGER, b TEXT);").unwrap();
949
950        let mut stmt = conn.prepare("INSERT INTO t (a, b) VALUES (?, ?)").unwrap();
951        stmt.execute_with_params(&[Value::Integer(7), Value::Text("hi".into())])
952            .unwrap();
953        stmt.execute_with_params(&[Value::Integer(8), Value::Text("yo".into())])
954            .unwrap();
955
956        let stmt = conn.prepare("SELECT a, b FROM t").unwrap();
957        let rows = stmt.query().unwrap().collect_all().unwrap();
958        assert_eq!(rows.len(), 2);
959        assert!(
960            rows.iter()
961                .any(|r| r.get::<i64>(0).unwrap() == 7 && r.get::<String>(1).unwrap() == "hi")
962        );
963        assert!(
964            rows.iter()
965                .any(|r| r.get::<i64>(0).unwrap() == 8 && r.get::<String>(1).unwrap() == "yo")
966        );
967    }
968
969    #[test]
970    fn arity_mismatch_returns_clean_error() {
971        let mut conn = Connection::open_in_memory().unwrap();
972        conn.execute("CREATE TABLE t (a INTEGER, b TEXT);").unwrap();
973        let stmt = conn
974            .prepare("SELECT * FROM t WHERE a = ? AND b = ?")
975            .unwrap();
976        let err = stmt.query_with_params(&[Value::Integer(1)]).unwrap_err();
977        assert!(format!("{err}").contains("expected 2 parameter"));
978    }
979
980    #[test]
981    fn run_and_query_reject_when_placeholders_present() {
982        let mut conn = Connection::open_in_memory().unwrap();
983        conn.execute("CREATE TABLE t (a INTEGER);").unwrap();
984        let mut stmt_select = conn.prepare("SELECT a FROM t WHERE a = ?").unwrap();
985        let err = stmt_select.query().unwrap_err();
986        assert!(format!("{err}").contains("query_with_params"));
987        let err = stmt_select.run().unwrap_err();
988        assert!(format!("{err}").contains("execute_with_params"));
989    }
990
991    #[test]
992    fn null_param_compares_against_null() {
993        // a = NULL is *false* in SQL three-valued logic; binding NULL
994        // must match SQLite's behavior so callers can rely on the same
995        // semantics.
996        let mut conn = Connection::open_in_memory().unwrap();
997        conn.execute("CREATE TABLE t (a INTEGER);").unwrap();
998        conn.execute("INSERT INTO t (a) VALUES (1);").unwrap();
999        let stmt = conn.prepare("SELECT a FROM t WHERE a = ?").unwrap();
1000        let rows = stmt
1001            .query_with_params(&[Value::Null])
1002            .unwrap()
1003            .collect_all()
1004            .unwrap();
1005        assert_eq!(rows.len(), 0);
1006    }
1007
1008    #[test]
1009    fn vector_param_substitutes_through_select() {
1010        // Non-HNSW path: a small VECTOR table + brute-force ORDER BY
1011        // exercises the substitution into the ORDER BY expression
1012        // and the bracket-array shape eval_expr_scope expects.
1013        let mut conn = Connection::open_in_memory().unwrap();
1014        conn.execute("CREATE TABLE v (id INTEGER PRIMARY KEY, e VECTOR(3));")
1015            .unwrap();
1016        conn.execute("INSERT INTO v (id, e) VALUES (1, [1.0, 0.0, 0.0]);")
1017            .unwrap();
1018        conn.execute("INSERT INTO v (id, e) VALUES (2, [0.0, 1.0, 0.0]);")
1019            .unwrap();
1020        conn.execute("INSERT INTO v (id, e) VALUES (3, [0.0, 0.0, 1.0]);")
1021            .unwrap();
1022
1023        let stmt = conn
1024            .prepare("SELECT id FROM v ORDER BY vec_distance_l2(e, ?) ASC LIMIT 1")
1025            .unwrap();
1026        let rows = stmt
1027            .query_with_params(&[Value::Vector(vec![1.0, 0.0, 0.0])])
1028            .unwrap()
1029            .collect_all()
1030            .unwrap();
1031        assert_eq!(rows.len(), 1);
1032        assert_eq!(rows[0].get::<i64>(0).unwrap(), 1);
1033    }
1034
1035    #[test]
1036    fn prepare_cached_reuses_plans() {
1037        let mut conn = Connection::open_in_memory().unwrap();
1038        conn.execute("CREATE TABLE t (a INTEGER);").unwrap();
1039        for n in 1..=3 {
1040            conn.execute(&format!("INSERT INTO t (a) VALUES ({n});"))
1041                .unwrap();
1042        }
1043
1044        // First call populates the cache; second hits the same entry.
1045        let _ = conn.prepare_cached("SELECT a FROM t WHERE a = ?").unwrap();
1046        let _ = conn.prepare_cached("SELECT a FROM t WHERE a = ?").unwrap();
1047        assert_eq!(conn.prepared_cache_len(), 1);
1048
1049        // Distinct SQL widens the cache.
1050        let _ = conn.prepare_cached("SELECT a FROM t").unwrap();
1051        assert_eq!(conn.prepared_cache_len(), 2);
1052    }
1053
1054    #[test]
1055    fn prepare_cached_evicts_when_over_capacity() {
1056        let mut conn = Connection::open_in_memory().unwrap();
1057        conn.execute("CREATE TABLE t (a INTEGER);").unwrap();
1058        conn.set_prepared_cache_capacity(2);
1059        let _ = conn.prepare_cached("SELECT a FROM t").unwrap();
1060        let _ = conn.prepare_cached("SELECT a FROM t WHERE a = ?").unwrap();
1061        assert_eq!(conn.prepared_cache_len(), 2);
1062        // Third distinct SQL evicts the oldest entry (the FROM-only SELECT).
1063        let _ = conn.prepare_cached("SELECT a FROM t WHERE a > ?").unwrap();
1064        assert_eq!(conn.prepared_cache_len(), 2);
1065    }
1066
1067    /// SQLR-23 — the headline VECTOR-binding case. With an HNSW index
1068    /// attached, the optimizer hook recognizes
1069    /// `ORDER BY vec_distance_l2(col, ?) LIMIT k` even when the second
1070    /// arg is a bound parameter, because substitution lowers
1071    /// `Value::Vector` into the same bracket-array shape an inline
1072    /// `[…]` literal produces. Self-query: querying for one of the
1073    /// corpus's own vectors must return that vector as the nearest.
1074    #[test]
1075    fn vector_bind_through_hnsw_optimizer() {
1076        let mut conn = Connection::open_in_memory().unwrap();
1077        conn.execute("CREATE TABLE v (id INTEGER PRIMARY KEY, e VECTOR(4));")
1078            .unwrap();
1079        let corpus: [(i64, [f32; 4]); 5] = [
1080            (1, [1.0, 0.0, 0.0, 0.0]),
1081            (2, [0.0, 1.0, 0.0, 0.0]),
1082            (3, [0.0, 0.0, 1.0, 0.0]),
1083            (4, [0.0, 0.0, 0.0, 1.0]),
1084            (5, [0.5, 0.5, 0.5, 0.5]),
1085        ];
1086        for (id, vec) in corpus {
1087            conn.execute(&format!(
1088                "INSERT INTO v (id, e) VALUES ({id}, [{}, {}, {}, {}]);",
1089                vec[0], vec[1], vec[2], vec[3]
1090            ))
1091            .unwrap();
1092        }
1093        conn.execute("CREATE INDEX v_hnsw ON v USING hnsw (e);")
1094            .unwrap();
1095
1096        let stmt = conn
1097            .prepare("SELECT id FROM v ORDER BY vec_distance_l2(e, ?) ASC LIMIT 1")
1098            .unwrap();
1099        // Query with id=3's vector — expect id=3 back.
1100        let rows = stmt
1101            .query_with_params(&[Value::Vector(vec![0.0, 0.0, 1.0, 0.0])])
1102            .unwrap()
1103            .collect_all()
1104            .unwrap();
1105        assert_eq!(rows.len(), 1);
1106        assert_eq!(rows[0].get::<i64>(0).unwrap(), 3);
1107
1108        // Query with id=1's vector — expect id=1.
1109        let rows = stmt
1110            .query_with_params(&[Value::Vector(vec![1.0, 0.0, 0.0, 0.0])])
1111            .unwrap()
1112            .collect_all()
1113            .unwrap();
1114        assert_eq!(rows.len(), 1);
1115        assert_eq!(rows[0].get::<i64>(0).unwrap(), 1);
1116    }
1117
1118    /// SQLR-28 — cosine probe: an HNSW index built `WITH (metric =
1119    /// 'cosine')` must serve `ORDER BY vec_distance_cosine(col, [...])`
1120    /// from the graph. Self-query: querying for one of the corpus's
1121    /// own vectors must come back as the nearest under cosine
1122    /// distance.
1123    #[test]
1124    fn cosine_self_query_through_hnsw_optimizer() {
1125        let mut conn = Connection::open_in_memory().unwrap();
1126        conn.execute("CREATE TABLE v (id INTEGER PRIMARY KEY, e VECTOR(4));")
1127            .unwrap();
1128        let corpus: [(i64, [f32; 4]); 5] = [
1129            (1, [1.0, 0.0, 0.0, 0.0]),
1130            (2, [0.0, 1.0, 0.0, 0.0]),
1131            (3, [0.0, 0.0, 1.0, 0.0]),
1132            (4, [0.0, 0.0, 0.0, 1.0]),
1133            (5, [0.5, 0.5, 0.5, 0.5]),
1134        ];
1135        for (id, vec) in corpus {
1136            conn.execute(&format!(
1137                "INSERT INTO v (id, e) VALUES ({id}, [{}, {}, {}, {}]);",
1138                vec[0], vec[1], vec[2], vec[3]
1139            ))
1140            .unwrap();
1141        }
1142        conn.execute("CREATE INDEX v_hnsw ON v USING hnsw (e) WITH (metric = 'cosine');")
1143            .unwrap();
1144
1145        // Self-query for id=2's vector — expected nearest under cosine
1146        // distance is id=2 itself (cos distance 0).
1147        let rows = conn
1148            .prepare("SELECT id FROM v ORDER BY vec_distance_cosine(e, [0.0, 1.0, 0.0, 0.0]) ASC LIMIT 1")
1149            .unwrap()
1150            .query_with_params(&[])
1151            .unwrap()
1152            .collect_all()
1153            .unwrap();
1154        assert_eq!(rows.len(), 1);
1155        assert_eq!(rows[0].get::<i64>(0).unwrap(), 2);
1156    }
1157
1158    /// SQLR-28 — dot probe: same shape as the cosine test, but the
1159    /// index is built `WITH (metric = 'dot')` and the query uses
1160    /// `vec_distance_dot`. Confirms the third metric variant lights up
1161    /// the graph shortcut, not just l2 / cosine.
1162    #[test]
1163    fn dot_self_query_through_hnsw_optimizer() {
1164        let mut conn = Connection::open_in_memory().unwrap();
1165        conn.execute("CREATE TABLE v (id INTEGER PRIMARY KEY, e VECTOR(3));")
1166            .unwrap();
1167        // Data: distinguishable magnitudes so the dot metric resolves
1168        // a clear winner. `vec_distance_dot(a, b) = -(a·b)` — smaller
1169        // (more negative) is closer.
1170        let corpus: [(i64, [f32; 3]); 4] = [
1171            (1, [1.0, 0.0, 0.0]),
1172            (2, [2.0, 0.0, 0.0]),
1173            (3, [0.0, 1.0, 0.0]),
1174            (4, [0.0, 0.0, 1.0]),
1175        ];
1176        for (id, vec) in corpus {
1177            conn.execute(&format!(
1178                "INSERT INTO v (id, e) VALUES ({id}, [{}, {}, {}]);",
1179                vec[0], vec[1], vec[2]
1180            ))
1181            .unwrap();
1182        }
1183        conn.execute("CREATE INDEX v_hnsw ON v USING hnsw (e) WITH (metric = 'dot');")
1184            .unwrap();
1185
1186        // Query [3, 0, 0]: dot products are 3, 6, 0, 0 → distances
1187        // -3, -6, 0, 0. id=2 has the smallest (most negative) distance.
1188        let rows = conn
1189            .prepare("SELECT id FROM v ORDER BY vec_distance_dot(e, [3.0, 0.0, 0.0]) ASC LIMIT 1")
1190            .unwrap()
1191            .query_with_params(&[])
1192            .unwrap()
1193            .collect_all()
1194            .unwrap();
1195        assert_eq!(rows.len(), 1);
1196        assert_eq!(rows[0].get::<i64>(0).unwrap(), 2);
1197    }
1198
1199    /// SQLR-28 — metric mismatch must NOT take the graph shortcut.
1200    /// An L2-built index queried with `vec_distance_cosine` falls
1201    /// through to brute-force, which still returns the correct
1202    /// answer. We confirm the answer is correct; the slow-path
1203    /// behaviour itself is implicit (no error, no panic, no wrong
1204    /// result), which is the user-visible contract that matters.
1205    #[test]
1206    fn metric_mismatch_falls_back_to_brute_force() {
1207        let mut conn = Connection::open_in_memory().unwrap();
1208        conn.execute("CREATE TABLE v (id INTEGER PRIMARY KEY, e VECTOR(2));")
1209            .unwrap();
1210        let half_sqrt2 = std::f32::consts::FRAC_1_SQRT_2;
1211        let corpus: [(i64, [f32; 2]); 3] = [
1212            (1, [1.0, 0.0]),
1213            (2, [half_sqrt2, half_sqrt2]),
1214            (3, [0.0, 1.0]),
1215        ];
1216        for (id, vec) in corpus {
1217            conn.execute(&format!(
1218                "INSERT INTO v (id, e) VALUES ({id}, [{}, {}]);",
1219                vec[0], vec[1]
1220            ))
1221            .unwrap();
1222        }
1223        // Default L2 index — no WITH clause.
1224        conn.execute("CREATE INDEX v_hnsw_l2 ON v USING hnsw (e);")
1225            .unwrap();
1226
1227        // Query with cosine. Index can't help; brute-force still
1228        // returns the correct nearest by cosine: id=1 (cos dist 0).
1229        let rows = conn
1230            .prepare("SELECT id FROM v ORDER BY vec_distance_cosine(e, [1.0, 0.0]) ASC LIMIT 1")
1231            .unwrap()
1232            .query_with_params(&[])
1233            .unwrap()
1234            .collect_all()
1235            .unwrap();
1236        assert_eq!(rows.len(), 1);
1237        assert_eq!(rows[0].get::<i64>(0).unwrap(), 1);
1238    }
1239
1240    /// SQLR-28 — a typo in the metric name must error at CREATE INDEX
1241    /// time. Falling back to L2 silently is the bug we're fixing here,
1242    /// not the behaviour to preserve.
1243    #[test]
1244    fn unknown_metric_name_is_rejected() {
1245        let mut conn = Connection::open_in_memory().unwrap();
1246        conn.execute("CREATE TABLE v (id INTEGER PRIMARY KEY, e VECTOR(2));")
1247            .unwrap();
1248        let err = conn
1249            .execute("CREATE INDEX bad ON v USING hnsw (e) WITH (metric = 'cosin');")
1250            .unwrap_err();
1251        let msg = format!("{err}");
1252        assert!(msg.contains("unknown HNSW metric"), "got: {msg}");
1253    }
1254
1255    /// SQLR-28 — WITH options on a non-HNSW index must error rather
1256    /// than be silently ignored. An option that has no effect on the
1257    /// resulting index is a footgun.
1258    #[test]
1259    fn with_metric_on_btree_is_rejected() {
1260        let mut conn = Connection::open_in_memory().unwrap();
1261        conn.execute("CREATE TABLE t (a INTEGER PRIMARY KEY, b TEXT);")
1262            .unwrap();
1263        let err = conn
1264            .execute("CREATE INDEX bad ON t (b) WITH (metric = 'cosine');")
1265            .unwrap_err();
1266        let msg = format!("{err}");
1267        assert!(msg.contains("doesn't support any options"), "got: {msg}");
1268    }
1269
1270    #[test]
1271    fn prepare_cached_executes_the_same_as_prepare() {
1272        let mut conn = Connection::open_in_memory().unwrap();
1273        conn.execute("CREATE TABLE t (a INTEGER PRIMARY KEY, b TEXT);")
1274            .unwrap();
1275        let mut ins = conn
1276            .prepare_cached("INSERT INTO t (a, b) VALUES (?, ?)")
1277            .unwrap();
1278        ins.execute_with_params(&[Value::Integer(1), Value::Text("alpha".into())])
1279            .unwrap();
1280        ins.execute_with_params(&[Value::Integer(2), Value::Text("beta".into())])
1281            .unwrap();
1282
1283        let stmt = conn.prepare_cached("SELECT b FROM t WHERE a = ?").unwrap();
1284        let rows = stmt
1285            .query_with_params(&[Value::Integer(2)])
1286            .unwrap()
1287            .collect_all()
1288            .unwrap();
1289        assert_eq!(rows.len(), 1);
1290        assert_eq!(rows[0].get::<String>(0).unwrap(), "beta");
1291    }
1292}