Skip to main content

sqlmodel_sqlite/
connection.rs

1//! SQLite connection implementation.
2//!
3//! This module provides safe wrappers around SQLite's C API and implements
4//! the Connection trait from sqlmodel-core.
5//!
6//! # Console Integration
7//!
8//! When the `console` feature is enabled, the connection can report status
9//! during operations. Use the `ConsoleAware` trait to attach a console.
10//!
11//! ```rust,ignore
12//! use sqlmodel_sqlite::SqliteConnection;
13//! use sqlmodel_console::{SqlModelConsole, ConsoleAware};
14//! use std::sync::Arc;
15//!
16//! let console = Arc::new(SqlModelConsole::new());
17//! let mut conn = SqliteConnection::open_memory().unwrap();
18//! conn.set_console(Some(console));
19//! ```
20
21// Allow casts in FFI code where we need to match C types exactly
22#![allow(clippy::cast_possible_truncation)]
23#![allow(clippy::cast_sign_loss)]
24#![allow(clippy::cast_lossless)]
25#![allow(clippy::result_large_err)] // Error type is defined in sqlmodel-core
26#![allow(clippy::borrow_as_ptr)] // FFI requires raw pointers
27#![allow(clippy::if_not_else)] // Clearer for error handling
28#![allow(clippy::implicit_clone)] // Minor optimization
29#![allow(clippy::map_unwrap_or)] // Clearer for optional formatting
30#![allow(clippy::redundant_closure)] // format_value requires context
31
32use crate::ffi;
33use crate::types;
34use sqlmodel_core::{
35    Connection, Cx, Error, IsolationLevel, Outcome, PreparedStatement, Row, TransactionOps, Value,
36    error::{ConfigError, ConnectionError, ConnectionErrorKind, QueryError, QueryErrorKind},
37    row::ColumnInfo,
38};
39use std::ffi::{CStr, CString, c_int};
40use std::future::Future;
41use std::ptr;
42use std::sync::{Arc, Mutex};
43use std::time::{Duration, Instant};
44
45/// Exact SQLite result codes retained on errors produced by the native driver.
46///
47/// SQLite extended result codes preserve the primary result in their low byte.
48/// Callers that need a fail-closed contract can compare [`Self::primary`] with
49/// constants from [`crate::ffi`] while retaining [`Self::extended`] for more
50/// precise diagnostics.
51#[derive(Debug, Clone, Copy, PartialEq, Eq)]
52pub struct SqliteErrorCode {
53    primary: c_int,
54    extended: c_int,
55}
56
57impl SqliteErrorCode {
58    const fn from_result_codes(result: c_int, extended: c_int) -> Self {
59        Self {
60            primary: result & 0xff,
61            extended,
62        }
63    }
64
65    /// SQLite primary result code, such as `SQLITE_READONLY`.
66    #[must_use]
67    pub const fn primary(self) -> c_int {
68        self.primary
69    }
70
71    /// SQLite extended result code, such as `SQLITE_READONLY_CANTLOCK`.
72    #[must_use]
73    pub const fn extended(self) -> c_int {
74        self.extended
75    }
76}
77
78impl std::fmt::Display for SqliteErrorCode {
79    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
80        write!(
81            formatter,
82            "SQLite result code {} (extended {})",
83            self.primary, self.extended
84        )
85    }
86}
87
88impl std::error::Error for SqliteErrorCode {}
89
90/// Return exact native SQLite result codes retained on a driver error.
91///
92/// Errors produced before SQLite is called (for example, SQL containing a NUL
93/// byte) and SQLModel lifecycle errors have no native result code and return
94/// `None`.
95#[must_use]
96pub fn sqlite_error_code(error: &Error) -> Option<SqliteErrorCode> {
97    std::error::Error::source(error)?
98        .downcast_ref::<SqliteErrorCode>()
99        .copied()
100}
101
102#[cfg(feature = "console")]
103use sqlmodel_console::{ConsoleAware, SqlModelConsole};
104
105/// Configuration for opening SQLite connections.
106#[derive(Debug, Clone)]
107pub struct SqliteConfig {
108    /// Path to the database file, or ":memory:" for in-memory database.
109    pub path: String,
110    /// Open flags (read-only, read-write, create, etc.)
111    pub flags: OpenFlags,
112    /// Busy timeout in milliseconds.
113    pub busy_timeout_ms: u32,
114}
115
116/// Flags controlling how the database is opened.
117#[derive(Debug, Clone, Copy, Default)]
118pub struct OpenFlags {
119    /// Open for reading only.
120    pub read_only: bool,
121    /// Open for reading and writing.
122    pub read_write: bool,
123    /// Create the database if it doesn't exist.
124    pub create: bool,
125    /// Enable URI filename interpretation.
126    pub uri: bool,
127    /// Open in multi-thread mode (connections not shared between threads).
128    pub no_mutex: bool,
129    /// Open in serialized mode (connections can be shared).
130    pub full_mutex: bool,
131    /// Enable shared cache mode (except for a plain `:memory:` database, which
132    /// SQLite always keeps private).
133    pub shared_cache: bool,
134    /// Explicitly disable shared cache mode. Private cache is also the default
135    /// when `shared_cache` is false, so process-global SQLite configuration
136    /// cannot silently change a connection's cache mode.
137    pub private_cache: bool,
138}
139
140impl OpenFlags {
141    /// Create flags for read-only access.
142    pub fn read_only() -> Self {
143        Self {
144            read_only: true,
145            ..Default::default()
146        }
147    }
148
149    /// Create flags for read-write access (database must exist).
150    pub fn read_write() -> Self {
151        Self {
152            read_write: true,
153            ..Default::default()
154        }
155    }
156
157    /// Create flags for read-write access with creation if needed.
158    pub fn create_read_write() -> Self {
159        Self {
160            read_write: true,
161            create: true,
162            ..Default::default()
163        }
164    }
165
166    fn to_sqlite_flags(self) -> c_int {
167        let mut flags = 0;
168
169        if self.read_only {
170            flags |= ffi::SQLITE_OPEN_READONLY;
171        }
172        if self.read_write {
173            flags |= ffi::SQLITE_OPEN_READWRITE;
174        }
175        if self.create {
176            flags |= ffi::SQLITE_OPEN_CREATE;
177        }
178        if self.uri {
179            flags |= ffi::SQLITE_OPEN_URI;
180        }
181        if self.no_mutex {
182            flags |= ffi::SQLITE_OPEN_NOMUTEX;
183        }
184        if self.full_mutex {
185            flags |= ffi::SQLITE_OPEN_FULLMUTEX;
186        }
187        if self.shared_cache {
188            flags |= ffi::SQLITE_OPEN_SHAREDCACHE;
189        } else {
190            // A private cache is the fail-closed default. Besides matching
191            // SQLite's normal default, an explicit flag prevents a process-wide
192            // sqlite3_enable_shared_cache() call elsewhere from silently
193            // changing this connection's backup-safety contract. A URI
194            // `cache=` parameter can still override this flag and is inspected
195            // from SQLite's parsed filename after open.
196            flags |= ffi::SQLITE_OPEN_PRIVATECACHE;
197        }
198
199        // Default to read-write if no mode specified
200        if flags & (ffi::SQLITE_OPEN_READONLY | ffi::SQLITE_OPEN_READWRITE) == 0 {
201            flags |= ffi::SQLITE_OPEN_READWRITE | ffi::SQLITE_OPEN_CREATE;
202        }
203
204        flags
205    }
206}
207
208impl Default for SqliteConfig {
209    fn default() -> Self {
210        Self {
211            path: ":memory:".to_string(),
212            flags: OpenFlags::create_read_write(),
213            busy_timeout_ms: 5000,
214        }
215    }
216}
217
218impl SqliteConfig {
219    /// Create a new config for a file-based database.
220    pub fn file(path: impl Into<String>) -> Self {
221        Self {
222            path: path.into(),
223            flags: OpenFlags::create_read_write(),
224            busy_timeout_ms: 5000,
225        }
226    }
227
228    /// Create a new config for an in-memory database.
229    pub fn memory() -> Self {
230        Self::default()
231    }
232
233    /// Set open flags.
234    pub fn flags(mut self, flags: OpenFlags) -> Self {
235        self.flags = flags;
236        self
237    }
238
239    /// Set busy timeout.
240    pub fn busy_timeout(mut self, ms: u32) -> Self {
241        self.busy_timeout_ms = ms;
242        self
243    }
244}
245
246/// Inner state of the SQLite connection, protected by a mutex for thread safety.
247struct SqliteInner {
248    db: *mut ffi::sqlite3,
249    in_transaction: bool,
250}
251
252// SAFETY: SQLite handles can be safely sent between threads when using
253// SQLITE_OPEN_FULLMUTEX (serialized mode) or when properly synchronized.
254// We use a Mutex to ensure synchronization.
255unsafe impl Send for SqliteInner {}
256
257/// A connection to a SQLite database.
258///
259/// This is a thread-safe wrapper around a SQLite database handle.
260pub struct SqliteConnection {
261    inner: Mutex<SqliteInner>,
262    path: String,
263    uses_shared_cache: bool,
264    /// Optional console for rich output
265    #[cfg(feature = "console")]
266    console: Option<Arc<SqlModelConsole>>,
267}
268
269// SqliteConnection is Send + Sync because all access goes through the Mutex
270unsafe impl Send for SqliteConnection {}
271unsafe impl Sync for SqliteConnection {}
272
273impl SqliteConnection {
274    /// Open a new SQLite connection with the given configuration.
275    pub fn open(config: &SqliteConfig) -> Result<Self, Error> {
276        if config.flags.shared_cache && config.flags.private_cache {
277            return Err(Error::Config(ConfigError {
278                message: "SQLite shared_cache and private_cache flags are mutually exclusive"
279                    .to_string(),
280                source: None,
281            }));
282        }
283        let busy_timeout_ms = c_int::try_from(config.busy_timeout_ms).map_err(|_| {
284            Error::Config(ConfigError {
285                message: format!(
286                    "SQLite busy timeout {}ms exceeds the native {}ms limit",
287                    config.busy_timeout_ms,
288                    c_int::MAX
289                ),
290                source: None,
291            })
292        })?;
293        let c_path = CString::new(config.path.as_str()).map_err(|_| {
294            Error::Connection(ConnectionError {
295                kind: ConnectionErrorKind::Connect,
296                message: "Invalid path: contains null byte".to_string(),
297                source: None,
298            })
299        })?;
300
301        let mut db: *mut ffi::sqlite3 = ptr::null_mut();
302        let flags = config.flags.to_sqlite_flags();
303
304        // SAFETY: We pass valid pointers and check the return value
305        let rc = unsafe { ffi::sqlite3_open_v2(c_path.as_ptr(), &mut db, flags, ptr::null()) };
306
307        if rc != ffi::SQLITE_OK {
308            let error_code = sqlite_error_code_from_db(db, rc);
309            let msg = if !db.is_null() {
310                // SAFETY: db is valid, errmsg returns a valid C string
311                unsafe {
312                    let err_ptr = ffi::sqlite3_errmsg(db);
313                    let msg = CStr::from_ptr(err_ptr).to_string_lossy().into_owned();
314                    ffi::sqlite3_close(db);
315                    msg
316                }
317            } else {
318                ffi::error_string(rc).to_string()
319            };
320
321            return Err(Error::Connection(ConnectionError {
322                kind: ConnectionErrorKind::Connect,
323                message: format!("Failed to open database: {}", msg),
324                source: Some(Box::new(error_code)),
325            }));
326        }
327
328        // Set busy timeout
329        if busy_timeout_ms > 0 {
330            // SAFETY: db is valid
331            let busy_rc = unsafe { ffi::sqlite3_busy_timeout(db, busy_timeout_ms) };
332            if busy_rc != ffi::SQLITE_OK {
333                let error_code = sqlite_error_code_from_db(db, busy_rc);
334                let msg = unsafe { CStr::from_ptr(ffi::sqlite3_errmsg(db)) }
335                    .to_string_lossy()
336                    .into_owned();
337                // SAFETY: db was opened successfully above and is not shared yet.
338                unsafe { ffi::sqlite3_close(db) };
339                return Err(Error::Connection(ConnectionError {
340                    kind: ConnectionErrorKind::Connect,
341                    message: format!("Failed to configure SQLite busy timeout: {msg}"),
342                    source: Some(Box::new(error_code)),
343                }));
344            }
345        }
346
347        Ok(Self {
348            inner: Mutex::new(SqliteInner {
349                db,
350                in_transaction: false,
351            }),
352            path: config.path.clone(),
353            uses_shared_cache: connection_uses_shared_cache(config),
354            #[cfg(feature = "console")]
355            console: None,
356        })
357    }
358
359    /// Open an in-memory database.
360    pub fn open_memory() -> Result<Self, Error> {
361        Self::open(&SqliteConfig::memory())
362    }
363
364    /// Open a file-based database.
365    pub fn open_file(path: impl Into<String>) -> Result<Self, Error> {
366        Self::open(&SqliteConfig::file(path))
367    }
368
369    /// Get the database path.
370    pub fn path(&self) -> &str {
371        &self.path
372    }
373
374    /// Execute SQL directly without preparing (for DDL, etc.)
375    pub fn execute_raw(&self, sql: &str) -> Result<(), Error> {
376        let inner = self.inner.lock().unwrap_or_else(|e| e.into_inner());
377        let c_sql = CString::new(sql).map_err(|_| {
378            Error::Query(QueryError {
379                kind: QueryErrorKind::Syntax,
380                sql: Some(sql.to_string()),
381                sqlstate: None,
382                message: "SQL contains null byte".to_string(),
383                detail: None,
384                hint: None,
385                position: None,
386                source: None,
387            })
388        })?;
389
390        let mut errmsg: *mut std::ffi::c_char = ptr::null_mut();
391
392        // SAFETY: All pointers are valid
393        let rc = unsafe {
394            ffi::sqlite3_exec(inner.db, c_sql.as_ptr(), None, ptr::null_mut(), &mut errmsg)
395        };
396
397        if rc != ffi::SQLITE_OK {
398            let error_code = sqlite_error_code_from_db(inner.db, rc);
399            let msg = if !errmsg.is_null() {
400                // SAFETY: errmsg is valid
401                let msg = unsafe { CStr::from_ptr(errmsg).to_string_lossy().into_owned() };
402                unsafe { ffi::sqlite3_free(errmsg.cast()) };
403                msg
404            } else {
405                ffi::error_string(rc).to_string()
406            };
407
408            return Err(Error::Query(QueryError {
409                kind: error_code_to_kind(rc),
410                sql: Some(sql.to_string()),
411                sqlstate: None,
412                message: msg,
413                detail: None,
414                hint: None,
415                position: None,
416                source: Some(Box::new(error_code)),
417            }));
418        }
419
420        Ok(())
421    }
422
423    /// Backup the current database to a destination path using the SQLite backup API.
424    ///
425    /// This opens (or creates) the destination database and performs an online backup
426    /// from this connection's `main` database into the destination's `main` database.
427    pub fn backup_to_path(&self, dest_path: impl AsRef<str>) -> Result<(), Error> {
428        let dest = SqliteConnection::open(
429            &SqliteConfig::file(dest_path.as_ref()).flags(OpenFlags::create_read_write()),
430        )?;
431        self.backup_to_connection(&dest)
432    }
433
434    /// Backup the current database to another open SQLite connection.
435    ///
436    /// The destination must not use SQLite shared-cache mode because this
437    /// wrapper cannot coordinate other connections attached to that cache.
438    pub fn backup_to_connection(&self, dest: &SqliteConnection) -> Result<(), Error> {
439        if std::ptr::eq(self, dest) {
440            return Err(Error::Connection(ConnectionError {
441                kind: ConnectionErrorKind::Connect,
442                message: "SQLite backup source and destination must be different connections"
443                    .to_string(),
444                source: None,
445            }));
446        }
447        // SQLite requires exclusive in-process access to a shared-cache
448        // destination for the entire backup operation. This wrapper can lock
449        // the two participating connections, but it cannot discover or lock a
450        // third connection attached to the same shared cache. Reject that
451        // configuration instead of exposing SQLite's documented mutex
452        // deadlock/malfunction surface.
453        if dest.uses_shared_cache {
454            return Err(Error::Connection(ConnectionError {
455                kind: ConnectionErrorKind::Connect,
456                message: "SQLite backup destinations cannot use shared-cache mode".to_string(),
457                source: None,
458            }));
459        }
460        let self_first = (std::ptr::from_ref(self) as usize) <= (std::ptr::from_ref(dest) as usize);
461        let (source_guard, dest_guard) = if self_first {
462            let source_guard = self.inner.lock().unwrap_or_else(|e| e.into_inner());
463            let dest_guard = dest.inner.lock().unwrap_or_else(|e| e.into_inner());
464            (source_guard, dest_guard)
465        } else {
466            let dest_guard = dest.inner.lock().unwrap_or_else(|e| e.into_inner());
467            let source_guard = self.inner.lock().unwrap_or_else(|e| e.into_inner());
468            (source_guard, dest_guard)
469        };
470
471        let source_db = source_guard.db;
472        let dest_db = dest_guard.db;
473        let source_busy_timeout_ms = sqlite_busy_timeout_ms(source_db)?;
474        let dest_busy_timeout_ms = sqlite_busy_timeout_ms(dest_db)?;
475
476        let main = CString::new("main").expect("static sqlite db name");
477
478        // SAFETY: We hold locks on both connections; db pointers are valid.
479        let backup =
480            unsafe { ffi::sqlite3_backup_init(dest_db, main.as_ptr(), source_db, main.as_ptr()) };
481        if backup.is_null() {
482            let result_code = unsafe { ffi::sqlite3_errcode(dest_db) };
483            let error_code = sqlite_error_code_from_db(dest_db, result_code);
484            let msg = unsafe { CStr::from_ptr(ffi::sqlite3_errmsg(dest_db)) }
485                .to_string_lossy()
486                .into_owned();
487            return Err(Error::Connection(ConnectionError {
488                kind: ConnectionErrorKind::Connect,
489                message: format!("SQLite backup init failed: {msg}"),
490                source: Some(Box::new(error_code)),
491            }));
492        }
493
494        let busy_deadline = Instant::now()
495            + Duration::from_millis(
496                u64::try_from(source_busy_timeout_ms.max(dest_busy_timeout_ms))
497                    .expect("SQLite busy timeout is non-negative"),
498            );
499        // sqlite3_backup_step() may invoke either connection's configured
500        // busy handler before returning SQLITE_BUSY. Temporarily disable those
501        // native waits and apply the deadline in this loop instead; otherwise
502        // a retry started just before the deadline could block for another
503        // complete busy-timeout interval. The guard restores both connection
504        // settings before their mutex guards are released.
505        let _busy_timeout_guard = BackupBusyTimeoutGuard::disable(
506            source_db,
507            dest_db,
508            source_busy_timeout_ms,
509            dest_busy_timeout_ms,
510        );
511        let mut rc = unsafe { ffi::sqlite3_backup_step(backup, 100) };
512        loop {
513            if rc == ffi::SQLITE_DONE {
514                break;
515            }
516            if rc == ffi::SQLITE_OK {
517                rc = unsafe { ffi::sqlite3_backup_step(backup, 100) };
518                continue;
519            }
520            if rc == ffi::SQLITE_BUSY || rc == ffi::SQLITE_LOCKED {
521                let now = Instant::now();
522                if now >= busy_deadline {
523                    break;
524                }
525                std::thread::sleep(
526                    Duration::from_millis(50).min(busy_deadline.saturating_duration_since(now)),
527                );
528                if Instant::now() >= busy_deadline {
529                    break;
530                }
531                rc = unsafe { ffi::sqlite3_backup_step(backup, 100) };
532                continue;
533            }
534            break;
535        }
536
537        let backup_error = if rc != ffi::SQLITE_DONE && rc != ffi::SQLITE_OK {
538            // sqlite3_backup_step returns the authoritative result directly
539            // and does not promise to replace the destination connection's
540            // error state. Preserve that direct (possibly extended) code;
541            // consulting sqlite3_extended_errcode/sqlite3_errmsg here could
542            // substitute an unrelated stale error from the same family.
543            Some(backup_step_error(dest_db, rc))
544        } else {
545            None
546        };
547
548        let finish_rc = unsafe { ffi::sqlite3_backup_finish(backup) };
549
550        if let Some(error) = backup_error {
551            return Err(error);
552        }
553
554        if finish_rc != ffi::SQLITE_OK {
555            let error_code = sqlite_error_code_from_db(dest_db, finish_rc);
556            let msg = unsafe { CStr::from_ptr(ffi::sqlite3_errmsg(dest_db)) }
557                .to_string_lossy()
558                .into_owned();
559            return Err(Error::Connection(ConnectionError {
560                kind: ConnectionErrorKind::Connect,
561                message: format!(
562                    "SQLite backup finish failed: {} ({})",
563                    msg,
564                    ffi::error_string(finish_rc)
565                ),
566                source: Some(Box::new(error_code)),
567            }));
568        }
569
570        Ok(())
571    }
572
573    /// Get the last insert rowid.
574    pub fn last_insert_rowid(&self) -> i64 {
575        let inner = self.inner.lock().unwrap_or_else(|e| e.into_inner());
576        // SAFETY: db is valid
577        unsafe { ffi::sqlite3_last_insert_rowid(inner.db) }
578    }
579
580    /// Get the number of rows changed by the last statement.
581    pub fn changes(&self) -> i32 {
582        let inner = self.inner.lock().unwrap_or_else(|e| e.into_inner());
583        // SAFETY: db is valid
584        unsafe { ffi::sqlite3_changes(inner.db) }
585    }
586
587    /// Prepare and execute a query synchronously, returning all rows.
588    ///
589    /// This is a blocking operation suitable for simple use cases.
590    /// For async usage, use the `Connection` trait methods instead.
591    pub fn query_sync(&self, sql: &str, params: &[Value]) -> Result<Vec<Row>, Error> {
592        #[cfg(feature = "console")]
593        let start = std::time::Instant::now();
594
595        let inner = self.inner.lock().unwrap_or_else(|e| e.into_inner());
596        let stmt = prepare_stmt(inner.db, sql)?;
597
598        // Bind parameters
599        for (i, param) in params.iter().enumerate() {
600            // SAFETY: stmt is valid, index is 1-based
601            let rc = unsafe { types::bind_value(stmt, (i + 1) as c_int, param) };
602            if rc != ffi::SQLITE_OK {
603                let error = bind_error(inner.db, sql, i + 1, rc);
604                // SAFETY: stmt is valid
605                unsafe { ffi::sqlite3_finalize(stmt) };
606                return Err(error);
607            }
608        }
609
610        // Fetch column names
611        // SAFETY: stmt is valid
612        let col_count = unsafe { ffi::sqlite3_column_count(stmt) };
613        let mut col_names = Vec::with_capacity(col_count as usize);
614        for i in 0..col_count {
615            let name =
616                unsafe { types::column_name(stmt, i) }.unwrap_or_else(|| format!("col{}", i));
617            col_names.push(name);
618        }
619        let columns = Arc::new(ColumnInfo::new(col_names.clone()));
620
621        // Fetch rows
622        let mut rows = Vec::new();
623        loop {
624            // SAFETY: stmt is valid
625            let rc = unsafe { ffi::sqlite3_step(stmt) };
626            match rc {
627                ffi::SQLITE_ROW => {
628                    let mut values = Vec::with_capacity(col_count as usize);
629                    for i in 0..col_count {
630                        // SAFETY: stmt is valid, we just got SQLITE_ROW
631                        let value = unsafe { types::read_column(stmt, i) };
632                        values.push(value);
633                    }
634                    rows.push(Row::with_columns(Arc::clone(&columns), values));
635                }
636                ffi::SQLITE_DONE => break,
637                _ => {
638                    let error = step_error(inner.db, sql, rc);
639                    // SAFETY: stmt is valid
640                    unsafe { ffi::sqlite3_finalize(stmt) };
641                    return Err(error);
642                }
643            }
644        }
645
646        // SAFETY: stmt is valid
647        unsafe { ffi::sqlite3_finalize(stmt) };
648
649        // Emit console output for PRAGMA queries and timing
650        #[cfg(feature = "console")]
651        {
652            let elapsed_ms = start.elapsed().as_secs_f64() * 1000.0;
653            self.emit_query_result(sql, &col_names, &rows, elapsed_ms);
654        }
655
656        Ok(rows)
657    }
658
659    /// Prepare and execute a statement synchronously, returning rows affected.
660    ///
661    /// This is a blocking operation suitable for simple use cases.
662    /// For async usage, use the `Connection` trait methods instead.
663    pub fn execute_sync(&self, sql: &str, params: &[Value]) -> Result<u64, Error> {
664        #[cfg(feature = "console")]
665        let start = std::time::Instant::now();
666
667        let inner = self.inner.lock().unwrap_or_else(|e| e.into_inner());
668        let stmt = prepare_stmt(inner.db, sql)?;
669
670        // Bind parameters
671        for (i, param) in params.iter().enumerate() {
672            // SAFETY: stmt is valid
673            let rc = unsafe { types::bind_value(stmt, (i + 1) as c_int, param) };
674            if rc != ffi::SQLITE_OK {
675                let error = bind_error(inner.db, sql, i + 1, rc);
676                // SAFETY: stmt is valid
677                unsafe { ffi::sqlite3_finalize(stmt) };
678                return Err(error);
679            }
680        }
681
682        // Execute through SQLITE_DONE. DML with RETURNING can yield one or
683        // more SQLITE_ROW results before a later commit-time failure, so the
684        // first row is not proof that the statement completed successfully.
685        let execution_error = loop {
686            // SAFETY: stmt is valid until it reaches DONE or an error below.
687            let rc = unsafe { ffi::sqlite3_step(stmt) };
688            match rc {
689                ffi::SQLITE_ROW => continue,
690                ffi::SQLITE_DONE => break None,
691                _ => break Some(step_error(inner.db, sql, rc)),
692            }
693        };
694
695        // SAFETY: stmt is valid
696        unsafe { ffi::sqlite3_finalize(stmt) };
697
698        if let Some(error) = execution_error {
699            return Err(error);
700        }
701
702        // SAFETY: db is valid
703        let changes = unsafe { ffi::sqlite3_changes(inner.db) };
704
705        #[cfg(feature = "console")]
706        {
707            let elapsed_ms = start.elapsed().as_secs_f64() * 1000.0;
708            self.emit_execute_timing(sql, changes as u64, elapsed_ms);
709        }
710
711        Ok(changes as u64)
712    }
713
714    /// Execute an INSERT and return the last inserted rowid.
715    fn insert_sync(&self, sql: &str, params: &[Value]) -> Result<i64, Error> {
716        self.execute_sync(sql, params)?;
717        Ok(self.last_insert_rowid())
718    }
719
720    /// Begin a transaction.
721    fn begin_sync(&self, isolation: IsolationLevel) -> Result<(), Error> {
722        let inner = self.inner.lock().unwrap_or_else(|e| e.into_inner());
723        if inner.in_transaction {
724            return Err(Error::Query(QueryError {
725                kind: QueryErrorKind::Database,
726                sql: None,
727                sqlstate: None,
728                message: "Already in a transaction".to_string(),
729                detail: None,
730                hint: None,
731                position: None,
732                source: None,
733            }));
734        }
735
736        // SQLite doesn't support isolation levels in the same way as PostgreSQL,
737        // but we can approximate with different transaction types
738        let begin_sql = match isolation {
739            IsolationLevel::Serializable => "BEGIN EXCLUSIVE",
740            IsolationLevel::RepeatableRead | IsolationLevel::ReadCommitted => "BEGIN IMMEDIATE",
741            IsolationLevel::ReadUncommitted => "BEGIN DEFERRED",
742        };
743
744        drop(inner); // Release lock before calling execute_raw
745        self.execute_raw(begin_sql)?;
746
747        let mut inner = self.inner.lock().unwrap_or_else(|e| e.into_inner());
748        inner.in_transaction = true;
749        self.emit_transaction_state("BEGIN");
750        Ok(())
751    }
752
753    /// Commit the current transaction.
754    fn commit_sync(&self) -> Result<(), Error> {
755        let inner = self.inner.lock().unwrap_or_else(|e| e.into_inner());
756        if !inner.in_transaction {
757            return Err(Error::Query(QueryError {
758                kind: QueryErrorKind::Database,
759                sql: None,
760                sqlstate: None,
761                message: "Not in a transaction".to_string(),
762                detail: None,
763                hint: None,
764                position: None,
765                source: None,
766            }));
767        }
768
769        drop(inner);
770        self.execute_raw("COMMIT")?;
771
772        let mut inner = self.inner.lock().unwrap_or_else(|e| e.into_inner());
773        inner.in_transaction = false;
774        self.emit_transaction_state("COMMIT");
775        Ok(())
776    }
777
778    /// Rollback the current transaction.
779    fn rollback_sync(&self) -> Result<(), Error> {
780        let inner = self.inner.lock().unwrap_or_else(|e| e.into_inner());
781        if !inner.in_transaction {
782            return Err(Error::Query(QueryError {
783                kind: QueryErrorKind::Database,
784                sql: None,
785                sqlstate: None,
786                message: "Not in a transaction".to_string(),
787                detail: None,
788                hint: None,
789                position: None,
790                source: None,
791            }));
792        }
793
794        drop(inner);
795        self.execute_raw("ROLLBACK")?;
796
797        let mut inner = self.inner.lock().unwrap_or_else(|e| e.into_inner());
798        inner.in_transaction = false;
799        self.emit_transaction_state("ROLLBACK");
800        Ok(())
801    }
802}
803
804impl Drop for SqliteConnection {
805    fn drop(&mut self) {
806        if let Ok(inner) = self.inner.lock()
807            && !inner.db.is_null()
808        {
809            // SAFETY: db is valid
810            unsafe {
811                ffi::sqlite3_close_v2(inner.db);
812            }
813        }
814    }
815}
816
817/// A SQLite transaction.
818pub struct SqliteTransaction<'conn> {
819    conn: &'conn SqliteConnection,
820    committed: bool,
821}
822
823impl<'conn> SqliteTransaction<'conn> {
824    fn new(conn: &'conn SqliteConnection) -> Self {
825        Self {
826            conn,
827            committed: false,
828        }
829    }
830}
831
832impl Drop for SqliteTransaction<'_> {
833    fn drop(&mut self) {
834        if !self.committed {
835            // Auto-rollback on drop if not committed
836            let _ = self.conn.rollback_sync();
837        }
838    }
839}
840
841// Implement Connection trait for SqliteConnection
842impl Connection for SqliteConnection {
843    type Tx<'conn>
844        = SqliteTransaction<'conn>
845    where
846        Self: 'conn;
847
848    fn dialect(&self) -> sqlmodel_core::Dialect {
849        sqlmodel_core::Dialect::Sqlite
850    }
851
852    fn query(
853        &self,
854        _cx: &Cx,
855        sql: &str,
856        params: &[Value],
857    ) -> impl Future<Output = Outcome<Vec<Row>, Error>> + Send {
858        let result = self.query_sync(sql, params);
859        async move { result.map_or_else(Outcome::Err, Outcome::Ok) }
860    }
861
862    fn query_one(
863        &self,
864        _cx: &Cx,
865        sql: &str,
866        params: &[Value],
867    ) -> impl Future<Output = Outcome<Option<Row>, Error>> + Send {
868        let result = self.query_sync(sql, params).map(|mut rows| rows.pop());
869        async move { result.map_or_else(Outcome::Err, Outcome::Ok) }
870    }
871
872    fn execute(
873        &self,
874        _cx: &Cx,
875        sql: &str,
876        params: &[Value],
877    ) -> impl Future<Output = Outcome<u64, Error>> + Send {
878        let result = self.execute_sync(sql, params);
879        async move { result.map_or_else(Outcome::Err, Outcome::Ok) }
880    }
881
882    fn insert(
883        &self,
884        _cx: &Cx,
885        sql: &str,
886        params: &[Value],
887    ) -> impl Future<Output = Outcome<i64, Error>> + Send {
888        let result = self.insert_sync(sql, params);
889        async move { result.map_or_else(Outcome::Err, Outcome::Ok) }
890    }
891
892    fn batch(
893        &self,
894        _cx: &Cx,
895        statements: &[(String, Vec<Value>)],
896    ) -> impl Future<Output = Outcome<Vec<u64>, Error>> + Send {
897        let mut results = Vec::with_capacity(statements.len());
898        let mut error = None;
899
900        for (sql, params) in statements {
901            match self.execute_sync(sql, params) {
902                Ok(n) => results.push(n),
903                Err(e) => {
904                    error = Some(e);
905                    break;
906                }
907            }
908        }
909
910        async move {
911            match error {
912                Some(e) => Outcome::Err(e),
913                None => Outcome::Ok(results),
914            }
915        }
916    }
917
918    fn begin(&self, cx: &Cx) -> impl Future<Output = Outcome<Self::Tx<'_>, Error>> + Send {
919        self.begin_with(cx, IsolationLevel::default())
920    }
921
922    fn begin_with(
923        &self,
924        _cx: &Cx,
925        isolation: IsolationLevel,
926    ) -> impl Future<Output = Outcome<Self::Tx<'_>, Error>> + Send {
927        let result = self
928            .begin_sync(isolation)
929            .map(|()| SqliteTransaction::new(self));
930        async move { result.map_or_else(Outcome::Err, Outcome::Ok) }
931    }
932
933    fn prepare(
934        &self,
935        _cx: &Cx,
936        sql: &str,
937    ) -> impl Future<Output = Outcome<PreparedStatement, Error>> + Send {
938        let inner = self.inner.lock().unwrap_or_else(|e| e.into_inner());
939        let result = prepare_stmt(inner.db, sql).map(|stmt| {
940            // SAFETY: stmt is valid
941            let param_count = unsafe { ffi::sqlite3_bind_parameter_count(stmt) } as usize;
942            let col_count = unsafe { ffi::sqlite3_column_count(stmt) } as c_int;
943
944            let mut columns = Vec::with_capacity(col_count as usize);
945            for i in 0..col_count {
946                if let Some(name) = unsafe { types::column_name(stmt, i) } {
947                    columns.push(name);
948                }
949            }
950
951            // SAFETY: stmt is valid
952            unsafe { ffi::sqlite3_finalize(stmt) };
953
954            // Use address as pseudo-ID since we don't cache statements yet
955            let id = sql.as_ptr() as u64;
956            PreparedStatement::with_columns(id, sql.to_string(), param_count, columns)
957        });
958
959        async move { result.map_or_else(Outcome::Err, Outcome::Ok) }
960    }
961
962    fn query_prepared(
963        &self,
964        cx: &Cx,
965        stmt: &PreparedStatement,
966        params: &[Value],
967    ) -> impl Future<Output = Outcome<Vec<Row>, Error>> + Send {
968        // For now, just re-execute the SQL
969        // Future optimization: cache prepared statements
970        self.query(cx, stmt.sql(), params)
971    }
972
973    fn execute_prepared(
974        &self,
975        cx: &Cx,
976        stmt: &PreparedStatement,
977        params: &[Value],
978    ) -> impl Future<Output = Outcome<u64, Error>> + Send {
979        self.execute(cx, stmt.sql(), params)
980    }
981
982    fn ping(&self, _cx: &Cx) -> impl Future<Output = Outcome<(), Error>> + Send {
983        // Simple ping: execute a trivial query
984        let result = self.query_sync("SELECT 1", &[]).map(|_| ());
985        async move { result.map_or_else(Outcome::Err, Outcome::Ok) }
986    }
987
988    fn close(self, _cx: &Cx) -> impl Future<Output = sqlmodel_core::Result<()>> + Send {
989        // Connection is closed on drop
990        std::future::ready(Ok(()))
991    }
992}
993
994// Implement TransactionOps for SqliteTransaction
995impl TransactionOps for SqliteTransaction<'_> {
996    fn query(
997        &self,
998        _cx: &Cx,
999        sql: &str,
1000        params: &[Value],
1001    ) -> impl Future<Output = Outcome<Vec<Row>, Error>> + Send {
1002        let result = self.conn.query_sync(sql, params);
1003        async move { result.map_or_else(Outcome::Err, Outcome::Ok) }
1004    }
1005
1006    fn query_one(
1007        &self,
1008        _cx: &Cx,
1009        sql: &str,
1010        params: &[Value],
1011    ) -> impl Future<Output = Outcome<Option<Row>, Error>> + Send {
1012        let result = self.conn.query_sync(sql, params).map(|mut rows| rows.pop());
1013        async move { result.map_or_else(Outcome::Err, Outcome::Ok) }
1014    }
1015
1016    fn execute(
1017        &self,
1018        _cx: &Cx,
1019        sql: &str,
1020        params: &[Value],
1021    ) -> impl Future<Output = Outcome<u64, Error>> + Send {
1022        let result = self.conn.execute_sync(sql, params);
1023        async move { result.map_or_else(Outcome::Err, Outcome::Ok) }
1024    }
1025
1026    fn savepoint(&self, _cx: &Cx, name: &str) -> impl Future<Output = Outcome<(), Error>> + Send {
1027        // Quote identifier to prevent SQL injection
1028        let quoted_name = format!("\"{}\"", name.replace('"', "\"\""));
1029        let sql = format!("SAVEPOINT {}", quoted_name);
1030        let result = self.conn.execute_raw(&sql);
1031        async move { result.map_or_else(Outcome::Err, Outcome::Ok) }
1032    }
1033
1034    fn rollback_to(&self, _cx: &Cx, name: &str) -> impl Future<Output = Outcome<(), Error>> + Send {
1035        // Quote identifier to prevent SQL injection
1036        let quoted_name = format!("\"{}\"", name.replace('"', "\"\""));
1037        let sql = format!("ROLLBACK TO {}", quoted_name);
1038        let result = self.conn.execute_raw(&sql);
1039        async move { result.map_or_else(Outcome::Err, Outcome::Ok) }
1040    }
1041
1042    fn release(&self, _cx: &Cx, name: &str) -> impl Future<Output = Outcome<(), Error>> + Send {
1043        // Quote identifier to prevent SQL injection
1044        let quoted_name = format!("\"{}\"", name.replace('"', "\"\""));
1045        let sql = format!("RELEASE {}", quoted_name);
1046        let result = self.conn.execute_raw(&sql);
1047        async move { result.map_or_else(Outcome::Err, Outcome::Ok) }
1048    }
1049
1050    fn commit(mut self, _cx: &Cx) -> impl Future<Output = Outcome<(), Error>> + Send {
1051        self.committed = true;
1052        std::future::ready(
1053            self.conn
1054                .commit_sync()
1055                .map_or_else(Outcome::Err, Outcome::Ok),
1056        )
1057    }
1058
1059    fn rollback(mut self, _cx: &Cx) -> impl Future<Output = Outcome<(), Error>> + Send {
1060        self.committed = true; // Prevent double rollback in drop
1061        std::future::ready(
1062            self.conn
1063                .rollback_sync()
1064                .map_or_else(Outcome::Err, Outcome::Ok),
1065        )
1066    }
1067}
1068
1069// Helper functions
1070
1071fn connection_uses_shared_cache(config: &SqliteConfig) -> bool {
1072    // A plain :memory: database is always private even if SHAREDCACHE was
1073    // requested. Named in-memory databases can share only through URI mode and
1074    // are handled by the URI cache-mode parser below.
1075    if config.path == ":memory:" || config.path.is_empty() {
1076        return false;
1077    }
1078
1079    let uri_mode = sqlite_uri_cache_mode(&config.path);
1080    if config.flags.uri {
1081        // SQLITE_OPEN_URI guarantees that SQLite interpreted this exact
1082        // file: URI, so its final cache parameter is authoritative.
1083        uri_mode.unwrap_or(config.flags.shared_cache)
1084    } else {
1085        // URI parsing can also be enabled process-wide by third-party code.
1086        // Without visibility into that global setting, reject a connection as
1087        // shared if either interpretation could be shared. This may reject a
1088        // safe backup but can never admit an unsafe one.
1089        config.flags.shared_cache || uri_mode == Some(true)
1090    }
1091}
1092
1093struct BackupBusyTimeoutGuard {
1094    source_db: *mut ffi::sqlite3,
1095    dest_db: *mut ffi::sqlite3,
1096    source_timeout_ms: c_int,
1097    dest_timeout_ms: c_int,
1098}
1099
1100impl BackupBusyTimeoutGuard {
1101    fn disable(
1102        source_db: *mut ffi::sqlite3,
1103        dest_db: *mut ffi::sqlite3,
1104        source_timeout_ms: c_int,
1105        dest_timeout_ms: c_int,
1106    ) -> Self {
1107        // SAFETY: backup_to_connection holds both connection mutexes and both
1108        // database handles remain valid for this guard's lifetime.
1109        let source_rc = unsafe { ffi::sqlite3_busy_timeout(source_db, 0) };
1110        let dest_rc = unsafe { ffi::sqlite3_busy_timeout(dest_db, 0) };
1111        debug_assert_eq!(source_rc, ffi::SQLITE_OK);
1112        debug_assert_eq!(dest_rc, ffi::SQLITE_OK);
1113
1114        Self {
1115            source_db,
1116            dest_db,
1117            source_timeout_ms,
1118            dest_timeout_ms,
1119        }
1120    }
1121}
1122
1123fn sqlite_busy_timeout_ms(db: *mut ffi::sqlite3) -> Result<c_int, Error> {
1124    const SQL: &str = "PRAGMA busy_timeout";
1125    let stmt = prepare_stmt(db, SQL)?;
1126
1127    // SAFETY: stmt is valid and PRAGMA busy_timeout returns exactly one row.
1128    let row_rc = unsafe { ffi::sqlite3_step(stmt) };
1129    if row_rc != ffi::SQLITE_ROW {
1130        let error = step_error(db, SQL, row_rc);
1131        unsafe { ffi::sqlite3_finalize(stmt) };
1132        return Err(error);
1133    }
1134    let timeout_ms = unsafe { ffi::sqlite3_column_int(stmt, 0) };
1135
1136    let done_rc = unsafe { ffi::sqlite3_step(stmt) };
1137    if done_rc != ffi::SQLITE_DONE {
1138        let error = step_error(db, SQL, done_rc);
1139        unsafe { ffi::sqlite3_finalize(stmt) };
1140        return Err(error);
1141    }
1142    unsafe { ffi::sqlite3_finalize(stmt) };
1143
1144    Ok(timeout_ms.max(0))
1145}
1146
1147impl Drop for BackupBusyTimeoutGuard {
1148    fn drop(&mut self) {
1149        // sqlite3_busy_timeout returns SQLITE_OK for valid handles. Both
1150        // handles are still protected by their connection mutexes here.
1151        let source_rc =
1152            unsafe { ffi::sqlite3_busy_timeout(self.source_db, self.source_timeout_ms) };
1153        let dest_rc = unsafe { ffi::sqlite3_busy_timeout(self.dest_db, self.dest_timeout_ms) };
1154        debug_assert_eq!(source_rc, ffi::SQLITE_OK);
1155        debug_assert_eq!(dest_rc, ffi::SQLITE_OK);
1156    }
1157}
1158
1159fn sqlite_uri_cache_mode(path: &str) -> Option<bool> {
1160    let query = path.strip_prefix("file:")?.split_once('?')?.1;
1161    let query = query.split_once('#').map_or(query, |(query, _)| query);
1162    let mut cache_mode = None;
1163
1164    for parameter in query.split('&') {
1165        let (name, value) = parameter.split_once('=').unwrap_or((parameter, ""));
1166        let name = percent_decode_uri_component(name);
1167        if name != b"cache" {
1168            continue;
1169        }
1170
1171        match percent_decode_uri_component(value).as_slice() {
1172            b"shared" => cache_mode = Some(true),
1173            b"private" => cache_mode = Some(false),
1174            _ => {
1175                // With URI parsing enabled SQLite rejects unknown cache modes,
1176                // so this branch cannot describe a successfully opened URI.
1177                // If URI parsing was disabled, the text is only a filename and
1178                // the explicit open flag remains authoritative.
1179            }
1180        }
1181    }
1182
1183    cache_mode
1184}
1185
1186fn percent_decode_uri_component(component: &str) -> Vec<u8> {
1187    let bytes = component.as_bytes();
1188    let mut decoded = Vec::with_capacity(bytes.len());
1189    let mut index = 0;
1190    while index < bytes.len() {
1191        if bytes[index] == b'%'
1192            && let (Some(high), Some(low)) = (bytes.get(index + 1), bytes.get(index + 2))
1193            && let (Some(high), Some(low)) = (hex_nibble(*high), hex_nibble(*low))
1194        {
1195            let byte = (high << 4) | low;
1196            if byte == 0 {
1197                // SQLite truncates the current URI component at an
1198                // encoded NUL and resumes at its next raw separator.
1199                break;
1200            }
1201            decoded.push(byte);
1202            index += 3;
1203            continue;
1204        }
1205
1206        decoded.push(bytes[index]);
1207        index += 1;
1208    }
1209    decoded
1210}
1211
1212const fn hex_nibble(byte: u8) -> Option<u8> {
1213    match byte {
1214        b'0'..=b'9' => Some(byte - b'0'),
1215        b'a'..=b'f' => Some(byte - b'a' + 10),
1216        b'A'..=b'F' => Some(byte - b'A' + 10),
1217        _ => None,
1218    }
1219}
1220
1221fn sqlite_error_code_from_db(db: *mut ffi::sqlite3, result: c_int) -> SqliteErrorCode {
1222    let primary = result & 0xff;
1223    let observed_extended = if db.is_null() {
1224        result
1225    } else {
1226        // SAFETY: every non-null pointer passed here is an open SQLite handle
1227        // held by the caller for the duration of this observation.
1228        unsafe { ffi::sqlite3_extended_errcode(db) }
1229    };
1230    // Some APIs return an error directly without replacing the connection's
1231    // previous error state. Never expose a contradictory primary/extended pair;
1232    // the direct result remains the authoritative fallback in that case.
1233    let extended = if observed_extended & 0xff == primary {
1234        observed_extended
1235    } else {
1236        result
1237    };
1238    SqliteErrorCode::from_result_codes(result, extended)
1239}
1240
1241fn direct_sqlite_error_code(result: c_int) -> SqliteErrorCode {
1242    SqliteErrorCode::from_result_codes(result, result)
1243}
1244
1245fn backup_step_error(db: *mut ffi::sqlite3, result: c_int) -> Error {
1246    let detail = if db.is_null() {
1247        ffi::error_string(result).to_string()
1248    } else {
1249        // SQLite documents backup routine failures on the destination
1250        // connection. Capture its detailed message while retaining `result`
1251        // itself as the authoritative exact code.
1252        unsafe { CStr::from_ptr(ffi::sqlite3_errmsg(db)) }
1253            .to_string_lossy()
1254            .into_owned()
1255    };
1256    Error::Connection(ConnectionError {
1257        kind: ConnectionErrorKind::Connect,
1258        message: format!(
1259            "SQLite backup failed: {detail} ({})",
1260            ffi::error_string(result)
1261        ),
1262        source: Some(Box::new(direct_sqlite_error_code(result))),
1263    })
1264}
1265
1266fn prepare_stmt(db: *mut ffi::sqlite3, sql: &str) -> Result<*mut ffi::sqlite3_stmt, Error> {
1267    let c_sql = CString::new(sql).map_err(|_| {
1268        Error::Query(QueryError {
1269            kind: QueryErrorKind::Syntax,
1270            sql: Some(sql.to_string()),
1271            sqlstate: None,
1272            message: "SQL contains null byte".to_string(),
1273            detail: None,
1274            hint: None,
1275            position: None,
1276            source: None,
1277        })
1278    })?;
1279
1280    let mut stmt: *mut ffi::sqlite3_stmt = ptr::null_mut();
1281
1282    // SAFETY: All pointers are valid
1283    let rc = unsafe {
1284        ffi::sqlite3_prepare_v2(
1285            db,
1286            c_sql.as_ptr(),
1287            c_sql.as_bytes().len() as c_int,
1288            &mut stmt,
1289            ptr::null_mut(),
1290        )
1291    };
1292
1293    if rc != ffi::SQLITE_OK {
1294        return Err(prepare_error(db, sql, rc));
1295    }
1296
1297    if stmt.is_null() {
1298        return Err(Error::Query(QueryError {
1299            kind: QueryErrorKind::Syntax,
1300            sql: Some(sql.to_string()),
1301            sqlstate: None,
1302            message: "SQL contains no executable statement".to_string(),
1303            detail: None,
1304            hint: None,
1305            position: None,
1306            source: None,
1307        }));
1308    }
1309
1310    Ok(stmt)
1311}
1312
1313fn prepare_error(db: *mut ffi::sqlite3, sql: &str, code: c_int) -> Error {
1314    // SAFETY: db is valid
1315    let msg = unsafe {
1316        let ptr = ffi::sqlite3_errmsg(db);
1317        CStr::from_ptr(ptr).to_string_lossy().into_owned()
1318    };
1319    let error_code = sqlite_error_code_from_db(db, code);
1320
1321    Error::Query(QueryError {
1322        kind: error_code_to_kind(code),
1323        sql: Some(sql.to_string()),
1324        sqlstate: None,
1325        message: msg,
1326        detail: None,
1327        hint: None,
1328        position: None,
1329        source: Some(Box::new(error_code)),
1330    })
1331}
1332
1333fn bind_error(db: *mut ffi::sqlite3, sql: &str, param_index: usize, code: c_int) -> Error {
1334    // SAFETY: db is valid
1335    let msg = unsafe {
1336        let ptr = ffi::sqlite3_errmsg(db);
1337        CStr::from_ptr(ptr).to_string_lossy().into_owned()
1338    };
1339    let error_code = sqlite_error_code_from_db(db, code);
1340
1341    Error::Query(QueryError {
1342        kind: error_code_to_kind(code),
1343        sql: Some(sql.to_string()),
1344        sqlstate: None,
1345        message: format!("Failed to bind parameter {}: {}", param_index, msg),
1346        detail: None,
1347        hint: None,
1348        position: None,
1349        source: Some(Box::new(error_code)),
1350    })
1351}
1352
1353fn step_error(db: *mut ffi::sqlite3, sql: &str, code: c_int) -> Error {
1354    // SAFETY: db is valid
1355    let msg = unsafe {
1356        let ptr = ffi::sqlite3_errmsg(db);
1357        CStr::from_ptr(ptr).to_string_lossy().into_owned()
1358    };
1359    let error_code = sqlite_error_code_from_db(db, code);
1360
1361    Error::Query(QueryError {
1362        kind: error_code_to_kind(code),
1363        sql: Some(sql.to_string()),
1364        sqlstate: None,
1365        message: msg,
1366        detail: None,
1367        hint: None,
1368        position: None,
1369        source: Some(Box::new(error_code)),
1370    })
1371}
1372
1373fn error_code_to_kind(code: c_int) -> QueryErrorKind {
1374    match code & 0xff {
1375        ffi::SQLITE_CONSTRAINT => QueryErrorKind::Constraint,
1376        ffi::SQLITE_BUSY | ffi::SQLITE_LOCKED => QueryErrorKind::Deadlock,
1377        ffi::SQLITE_PERM | ffi::SQLITE_READONLY | ffi::SQLITE_AUTH => QueryErrorKind::Permission,
1378        ffi::SQLITE_NOTFOUND => QueryErrorKind::NotFound,
1379        ffi::SQLITE_TOOBIG => QueryErrorKind::DataTruncation,
1380        ffi::SQLITE_INTERRUPT => QueryErrorKind::Cancelled,
1381        _ => QueryErrorKind::Database,
1382    }
1383}
1384
1385/// Format a Value for display in console output.
1386#[allow(dead_code)]
1387fn format_value(value: &Value) -> String {
1388    match value {
1389        Value::Null => "NULL".to_string(),
1390        Value::Bool(b) => if *b { "true" } else { "false" }.to_string(),
1391        Value::TinyInt(n) => n.to_string(),
1392        Value::SmallInt(n) => n.to_string(),
1393        Value::Int(n) => n.to_string(),
1394        Value::BigInt(n) => n.to_string(),
1395        Value::Float(n) => format!("{:.6}", n),
1396        Value::Double(n) => format!("{:.6}", n),
1397        Value::Text(s) => s.clone(),
1398        Value::Bytes(b) => format!("[BLOB: {} bytes]", b.len()),
1399        Value::Date(d) => d.to_string(),
1400        Value::Time(t) => t.to_string(),
1401        Value::Timestamp(ts) => ts.to_string(),
1402        Value::TimestampTz(ts) => ts.to_string(),
1403        Value::Json(j) => j.to_string(),
1404        Value::Uuid(u) => {
1405            // Format UUID as hex string: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
1406            format!(
1407                "{:02x}{:02x}{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}",
1408                u[0],
1409                u[1],
1410                u[2],
1411                u[3],
1412                u[4],
1413                u[5],
1414                u[6],
1415                u[7],
1416                u[8],
1417                u[9],
1418                u[10],
1419                u[11],
1420                u[12],
1421                u[13],
1422                u[14],
1423                u[15]
1424            )
1425        }
1426        Value::Decimal(d) => d.to_string(),
1427        Value::Array(arr) => format!("[{} items]", arr.len()),
1428        Value::Default => "DEFAULT".to_string(),
1429    }
1430}
1431
1432// ==================== Console Support ====================
1433
1434#[cfg(feature = "console")]
1435impl ConsoleAware for SqliteConnection {
1436    fn set_console(&mut self, console: Option<Arc<SqlModelConsole>>) {
1437        self.console = console;
1438        // Emit database status when console is attached
1439        self.emit_open_status();
1440    }
1441
1442    fn console(&self) -> Option<&Arc<SqlModelConsole>> {
1443        self.console.as_ref()
1444    }
1445
1446    fn has_console(&self) -> bool {
1447        self.console.is_some()
1448    }
1449}
1450
1451impl SqliteConnection {
1452    /// Emit database open status to console if available.
1453    #[cfg(feature = "console")]
1454    fn emit_open_status(&self) {
1455        if let Some(console) = &self.console {
1456            // Get database info
1457            let mode = if self.path == ":memory:" {
1458                "in-memory"
1459            } else {
1460                "file"
1461            };
1462
1463            // Query journal mode if we can
1464            let journal_mode = self
1465                .query_sync("PRAGMA journal_mode", &[])
1466                .ok()
1467                .and_then(|rows| rows.first().and_then(|r| r.get_as::<String>(0).ok()));
1468
1469            let page_size = self
1470                .query_sync("PRAGMA page_size", &[])
1471                .ok()
1472                .and_then(|rows| rows.first().and_then(|r| r.get_as::<i64>(0).ok()));
1473
1474            if console.mode().is_plain() {
1475                // Plain text output for agents
1476                let journal = journal_mode.as_deref().unwrap_or("unknown");
1477                console.status(&format!(
1478                    "Opened SQLite database: {} ({} mode, journal: {})",
1479                    self.path, mode, journal
1480                ));
1481            } else {
1482                // Rich output
1483                console.status(&format!("SQLite database: {}", self.path));
1484                console.status(&format!("  Mode: {}", mode));
1485                if let Some(journal) = journal_mode {
1486                    console.status(&format!("  Journal: {}", journal.to_uppercase()));
1487                }
1488                if let Some(size) = page_size {
1489                    console.status(&format!("  Page size: {} bytes", size));
1490                }
1491            }
1492        }
1493    }
1494
1495    /// Emit transaction state to console if available.
1496    #[cfg(feature = "console")]
1497    fn emit_transaction_state(&self, state: &str) {
1498        if let Some(console) = &self.console {
1499            if console.mode().is_plain() {
1500                console.status(&format!("Transaction: {}", state));
1501            } else {
1502                console.status(&format!("[{}] Transaction {}", state, state.to_lowercase()));
1503            }
1504        }
1505    }
1506
1507    /// Emit query timing to console if available.
1508    #[cfg(feature = "console")]
1509    fn emit_query_timing(&self, elapsed_ms: f64, rows: usize) {
1510        if let Some(console) = &self.console {
1511            console.status(&format!("Query: {:.1}ms, {} rows", elapsed_ms, rows));
1512        }
1513    }
1514
1515    /// Emit query results with PRAGMA-aware formatting.
1516    #[cfg(feature = "console")]
1517    fn emit_query_result(&self, sql: &str, col_names: &[String], rows: &[Row], elapsed_ms: f64) {
1518        if let Some(console) = &self.console {
1519            // Check if this is a PRAGMA query for special formatting
1520            let sql_upper = sql.trim().to_uppercase();
1521            let is_pragma = sql_upper.starts_with("PRAGMA");
1522
1523            if is_pragma && !rows.is_empty() {
1524                // Format PRAGMA results as a table
1525                if console.mode().is_plain() {
1526                    // Plain text format for agents
1527                    console.status(&format!("{}:", sql.trim()));
1528                    // Header
1529                    console.status(&format!("  {}", col_names.join("|")));
1530                    // Rows
1531                    for row in rows.iter().take(20) {
1532                        let values: Vec<String> = (0..col_names.len())
1533                            .map(|i| {
1534                                row.get(i)
1535                                    .map(|v| format_value(v))
1536                                    .unwrap_or_else(|| "NULL".to_string())
1537                            })
1538                            .collect();
1539                        console.status(&format!("  {}", values.join("|")));
1540                    }
1541                    if rows.len() > 20 {
1542                        console.status(&format!("  ... and {} more rows", rows.len() - 20));
1543                    }
1544                    console.status(&format!("  ({:.1}ms)", elapsed_ms));
1545                } else {
1546                    // Rich format with table rendering
1547                    let mut table_output = String::new();
1548                    table_output.push_str(&format!("PRAGMA Query Results ({:.1}ms)\n", elapsed_ms));
1549
1550                    // Calculate column widths
1551                    let mut widths: Vec<usize> = col_names.iter().map(|c| c.len()).collect();
1552                    for row in rows.iter().take(20) {
1553                        for (i, w) in widths.iter_mut().enumerate() {
1554                            let val_len = row.get(i).map(|v| format_value(v).len()).unwrap_or(4); // "NULL".len()
1555                            if val_len > *w {
1556                                *w = val_len;
1557                            }
1558                        }
1559                    }
1560
1561                    // Build header separator
1562                    let sep: String = widths
1563                        .iter()
1564                        .map(|w| "-".repeat(*w + 2))
1565                        .collect::<Vec<_>>()
1566                        .join("+");
1567                    table_output.push_str(&format!("+{}+\n", sep));
1568
1569                    // Header row
1570                    let header: String = col_names
1571                        .iter()
1572                        .enumerate()
1573                        .map(|(i, name)| format!(" {:width$} ", name, width = widths[i]))
1574                        .collect::<Vec<_>>()
1575                        .join("|");
1576                    table_output.push_str(&format!("|{}|\n", header));
1577                    table_output.push_str(&format!("+{}+\n", sep));
1578
1579                    // Data rows
1580                    for row in rows.iter().take(20) {
1581                        let data: String = (0..col_names.len())
1582                            .map(|i| {
1583                                let val = row
1584                                    .get(i)
1585                                    .map(|v| format_value(v))
1586                                    .unwrap_or_else(|| "NULL".to_string());
1587                                format!(" {:width$} ", val, width = widths[i])
1588                            })
1589                            .collect::<Vec<_>>()
1590                            .join("|");
1591                        table_output.push_str(&format!("|{}|\n", data));
1592                    }
1593                    table_output.push_str(&format!("+{}+", sep));
1594
1595                    if rows.len() > 20 {
1596                        table_output.push_str(&format!("\n... and {} more rows", rows.len() - 20));
1597                    }
1598
1599                    console.status(&table_output);
1600                }
1601            } else {
1602                // Regular query timing
1603                self.emit_query_timing(elapsed_ms, rows.len());
1604            }
1605        }
1606    }
1607
1608    /// Emit execute operation timing to console.
1609    #[cfg(feature = "console")]
1610    fn emit_execute_timing(&self, sql: &str, rows_affected: u64, elapsed_ms: f64) {
1611        if let Some(console) = &self.console {
1612            let sql_upper = sql.trim().to_uppercase();
1613
1614            // Provide contextual message based on operation type
1615            let op_type = if sql_upper.starts_with("INSERT") {
1616                "Insert"
1617            } else if sql_upper.starts_with("UPDATE") {
1618                "Update"
1619            } else if sql_upper.starts_with("DELETE") {
1620                "Delete"
1621            } else if sql_upper.starts_with("CREATE") {
1622                "Create"
1623            } else if sql_upper.starts_with("DROP") {
1624                "Drop"
1625            } else if sql_upper.starts_with("ALTER") {
1626                "Alter"
1627            } else {
1628                "Execute"
1629            };
1630
1631            if console.mode().is_plain() {
1632                console.status(&format!(
1633                    "{}: {} rows affected ({:.1}ms)",
1634                    op_type, rows_affected, elapsed_ms
1635                ));
1636            } else {
1637                console.status(&format!(
1638                    "[{}] {} rows affected ({:.1}ms)",
1639                    op_type.to_uppercase(),
1640                    rows_affected,
1641                    elapsed_ms
1642                ));
1643            }
1644        }
1645    }
1646
1647    /// Emit busy waiting status to console.
1648    #[cfg(feature = "console")]
1649    pub fn emit_busy_waiting(&self, elapsed_secs: f64) {
1650        if let Some(console) = &self.console {
1651            if console.mode().is_plain() {
1652                console.status(&format!(
1653                    "Waiting for database lock... ({:.1}s)",
1654                    elapsed_secs
1655                ));
1656            } else {
1657                console.status(&format!(
1658                    "[..] Waiting for database lock... ({:.1}s)",
1659                    elapsed_secs
1660                ));
1661            }
1662        }
1663    }
1664
1665    /// Emit WAL checkpoint progress to console.
1666    #[cfg(feature = "console")]
1667    pub fn emit_checkpoint_progress(&self, pages_done: u32, pages_total: u32) {
1668        if let Some(console) = &self.console {
1669            let pct = if pages_total > 0 {
1670                (pages_done as f64 / pages_total as f64) * 100.0
1671            } else {
1672                100.0
1673            };
1674
1675            if console.mode().is_plain() {
1676                console.status(&format!(
1677                    "WAL checkpoint: {:.0}% ({}/{} pages)",
1678                    pct, pages_done, pages_total
1679                ));
1680            } else {
1681                // ASCII progress bar for rich mode
1682                let bar_width: usize = 20;
1683                let filled = ((pct / 100.0) * bar_width as f64).round() as usize;
1684                let empty = bar_width.saturating_sub(filled);
1685                let bar = format!("[{}{}]", "=".repeat(filled), " ".repeat(empty));
1686                console.status(&format!(
1687                    "WAL checkpoint: {} {:.0}% ({}/{} pages)",
1688                    bar, pct, pages_done, pages_total
1689                ));
1690            }
1691        }
1692    }
1693
1694    /// No-op when console feature is disabled.
1695    #[cfg(not(feature = "console"))]
1696    #[allow(dead_code)]
1697    fn emit_open_status(&self) {}
1698
1699    /// No-op when console feature is disabled.
1700    #[cfg(not(feature = "console"))]
1701    fn emit_transaction_state(&self, _state: &str) {}
1702
1703    /// No-op when console feature is disabled.
1704    #[cfg(not(feature = "console"))]
1705    #[allow(dead_code)]
1706    fn emit_query_timing(&self, _elapsed_ms: f64, _rows: usize) {}
1707
1708    /// No-op when console feature is disabled.
1709    #[cfg(not(feature = "console"))]
1710    #[allow(dead_code)]
1711    fn emit_query_result(
1712        &self,
1713        _sql: &str,
1714        _col_names: &[String],
1715        _rows: &[Row],
1716        _elapsed_ms: f64,
1717    ) {
1718    }
1719
1720    /// No-op when console feature is disabled.
1721    #[cfg(not(feature = "console"))]
1722    #[allow(dead_code)]
1723    fn emit_execute_timing(&self, _sql: &str, _rows_affected: u64, _elapsed_ms: f64) {}
1724
1725    /// No-op when console feature is disabled.
1726    #[cfg(not(feature = "console"))]
1727    pub fn emit_busy_waiting(&self, _elapsed_secs: f64) {}
1728
1729    /// No-op when console feature is disabled.
1730    #[cfg(not(feature = "console"))]
1731    pub fn emit_checkpoint_progress(&self, _pages_done: u32, _pages_total: u32) {}
1732}
1733
1734#[cfg(test)]
1735mod tests {
1736    use super::*;
1737
1738    static NEXT_TEMP_DB: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
1739
1740    fn unique_temp_db_path(label: &str) -> std::path::PathBuf {
1741        let nonce = NEXT_TEMP_DB.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
1742        std::env::temp_dir().join(format!(
1743            "sqlmodel_{label}_{}_{}.db",
1744            std::process::id(),
1745            nonce
1746        ))
1747    }
1748
1749    #[test]
1750    fn test_open_memory() {
1751        let conn = SqliteConnection::open_memory().unwrap();
1752        assert_eq!(conn.path(), ":memory:");
1753    }
1754
1755    #[test]
1756    fn test_execute_raw() {
1757        let conn = SqliteConnection::open_memory().unwrap();
1758        conn.execute_raw("CREATE TABLE test (id INTEGER PRIMARY KEY, name TEXT)")
1759            .unwrap();
1760        conn.execute_raw("INSERT INTO test (name) VALUES ('Alice')")
1761            .unwrap();
1762        assert_eq!(conn.changes(), 1);
1763        assert_eq!(conn.last_insert_rowid(), 1);
1764
1765        let pre_sqlite_error = conn
1766            .execute_raw("SELECT \0")
1767            .expect_err("NUL-bearing SQL must fail before SQLite");
1768        assert_eq!(
1769            sqlite_error_code(&pre_sqlite_error),
1770            None,
1771            "errors produced before the native call must not invent a SQLite result code"
1772        );
1773    }
1774
1775    #[test]
1776    fn test_query_sync() {
1777        let conn = SqliteConnection::open_memory().unwrap();
1778        conn.execute_raw("CREATE TABLE test (id INTEGER PRIMARY KEY, name TEXT)")
1779            .unwrap();
1780        conn.execute_raw("INSERT INTO test (name) VALUES ('Alice'), ('Bob')")
1781            .unwrap();
1782
1783        let rows = conn
1784            .query_sync("SELECT * FROM test ORDER BY id", &[])
1785            .unwrap();
1786        assert_eq!(rows.len(), 2);
1787
1788        assert_eq!(rows[0].get_named::<i32>("id").unwrap(), 1);
1789        assert_eq!(rows[0].get_named::<String>("name").unwrap(), "Alice");
1790        assert_eq!(rows[1].get_named::<i32>("id").unwrap(), 2);
1791        assert_eq!(rows[1].get_named::<String>("name").unwrap(), "Bob");
1792    }
1793
1794    #[test]
1795    fn test_parameterized_query() {
1796        let conn = SqliteConnection::open_memory().unwrap();
1797        conn.execute_raw("CREATE TABLE test (id INTEGER PRIMARY KEY, name TEXT, age INTEGER)")
1798            .unwrap();
1799
1800        conn.execute_sync(
1801            "INSERT INTO test (name, age) VALUES (?, ?)",
1802            &[Value::Text("Alice".to_string()), Value::Int(30)],
1803        )
1804        .unwrap();
1805
1806        let rows = conn
1807            .query_sync(
1808                "SELECT * FROM test WHERE name = ?",
1809                &[Value::Text("Alice".to_string())],
1810            )
1811            .unwrap();
1812
1813        assert_eq!(rows.len(), 1);
1814        assert_eq!(rows[0].get_named::<String>("name").unwrap(), "Alice");
1815        assert_eq!(rows[0].get_named::<i32>("age").unwrap(), 30);
1816    }
1817
1818    #[test]
1819    fn test_prepared_errors_retain_exact_native_codes() {
1820        let conn = SqliteConnection::open_memory().unwrap();
1821
1822        let prepare_error = conn
1823            .query_sync("SELEC 1", &[])
1824            .expect_err("invalid SQL must fail during prepare");
1825        let prepare_code = sqlite_error_code(&prepare_error)
1826            .expect("prepare failures must retain their native SQLite result code");
1827        assert_eq!(prepare_code.primary(), ffi::SQLITE_ERROR);
1828        assert_eq!(prepare_code.extended(), ffi::SQLITE_ERROR);
1829
1830        let query_bind_error = conn
1831            .query_sync("SELECT ?1", &[Value::Int(1), Value::Int(2)])
1832            .expect_err("binding beyond the statement parameter count must fail");
1833        let execute_bind_error = conn
1834            .execute_sync("SELECT ?1", &[Value::Int(1), Value::Int(2)])
1835            .expect_err("execute_sync must retain the same bind failure");
1836        for error in [&query_bind_error, &execute_bind_error] {
1837            let code = sqlite_error_code(error)
1838                .expect("bind failures must survive statement finalization");
1839            assert_eq!(code.primary(), ffi::SQLITE_RANGE);
1840            assert_eq!(code.extended(), ffi::SQLITE_RANGE);
1841        }
1842
1843        conn.execute_raw("CREATE TABLE exact_codes (value INTEGER UNIQUE)")
1844            .unwrap();
1845        conn.execute_sync("INSERT INTO exact_codes VALUES (1)", &[])
1846            .unwrap();
1847        let execute_step_error = conn
1848            .execute_sync("INSERT INTO exact_codes VALUES (1)", &[])
1849            .expect_err("duplicate prepared insert must fail during step");
1850        let query_step_error = conn
1851            .query_sync("INSERT INTO exact_codes VALUES (1) RETURNING value", &[])
1852            .expect_err("query_sync must retain a step failure before finalization");
1853        for error in [&execute_step_error, &query_step_error] {
1854            let code = sqlite_error_code(error)
1855                .expect("step failures must retain their extended SQLite result code");
1856            assert_eq!(code.primary(), ffi::SQLITE_CONSTRAINT);
1857            assert_eq!(code.extended(), ffi::SQLITE_CONSTRAINT_UNIQUE);
1858            assert!(
1859                matches!(error, Error::Query(query) if query.kind == QueryErrorKind::Constraint),
1860                "unique violations should map to the constraint error family: {error}"
1861            );
1862        }
1863    }
1864
1865    #[test]
1866    fn test_empty_prepared_sql_is_rejected_before_statement_ffi() {
1867        let conn = SqliteConnection::open_memory().unwrap();
1868
1869        for sql in ["", " \n\t", "-- comment only\n", "/* comment only */"] {
1870            for error in [
1871                conn.query_sync(sql, &[])
1872                    .expect_err("empty query SQL must not produce a null statement"),
1873                conn.execute_sync(sql, &[])
1874                    .expect_err("empty execute SQL must not produce a null statement"),
1875            ] {
1876                assert!(
1877                    matches!(error, Error::Query(ref query) if query.kind == QueryErrorKind::Syntax),
1878                    "empty prepared SQL should be a typed syntax error: {error}"
1879                );
1880                assert!(error.to_string().contains("no executable statement"));
1881                assert_eq!(sqlite_error_code(&error), None);
1882            }
1883        }
1884    }
1885
1886    #[test]
1887    fn test_execute_returning_steps_until_done_and_retains_late_busy() {
1888        let path = unique_temp_db_path("returning_busy");
1889        let _ = std::fs::remove_file(&path);
1890        let config = SqliteConfig::file(path.to_string_lossy().into_owned()).busy_timeout(0);
1891        let writer = SqliteConnection::open(&config).unwrap();
1892        writer.execute_raw("PRAGMA journal_mode=DELETE").unwrap();
1893        writer
1894            .execute_raw("CREATE TABLE returning_rows (value INTEGER)")
1895            .unwrap();
1896        let reader = SqliteConnection::open(&config).unwrap();
1897        reader.execute_raw("BEGIN DEFERRED").unwrap();
1898        reader
1899            .query_sync("SELECT COUNT(*) FROM returning_rows", &[])
1900            .unwrap();
1901
1902        let error = writer
1903            .execute_sync("INSERT INTO returning_rows VALUES (7) RETURNING value", &[])
1904            .expect_err("the read lock must surface after RETURNING rows but before DONE");
1905        let code = sqlite_error_code(&error)
1906            .expect("late RETURNING completion failure must retain its native code");
1907        assert_eq!(code.primary(), ffi::SQLITE_BUSY);
1908        assert!(
1909            matches!(error, Error::Query(ref query) if query.kind == QueryErrorKind::Deadlock),
1910            "late SQLITE_BUSY should map to the deadlock family: {error}"
1911        );
1912
1913        reader.execute_raw("ROLLBACK").unwrap();
1914        let rows = writer
1915            .query_sync("SELECT COUNT(*) AS row_count FROM returning_rows", &[])
1916            .unwrap();
1917        assert_eq!(rows[0].get_named::<i64>("row_count").unwrap(), 0);
1918        drop(reader);
1919        drop(writer);
1920        let _ = std::fs::remove_file(path);
1921    }
1922
1923    #[test]
1924    fn test_non_native_connection_preflights_have_no_sqlite_code() {
1925        let invalid_timeout = SqliteConfig::memory().busy_timeout(c_int::MAX as u32 + 1);
1926        let Err(timeout_error) = SqliteConnection::open(&invalid_timeout) else {
1927            panic!("out-of-range native busy timeout must fail closed");
1928        };
1929        assert!(matches!(timeout_error, Error::Config(_)));
1930        assert_eq!(sqlite_error_code(&timeout_error), None);
1931
1932        let conn = SqliteConnection::open_memory().unwrap();
1933        let backup_error = conn
1934            .backup_to_connection(&conn)
1935            .expect_err("backing a connection up onto itself must fail before locking");
1936        assert!(
1937            backup_error
1938                .to_string()
1939                .contains("source and destination must be different")
1940        );
1941        assert_eq!(sqlite_error_code(&backup_error), None);
1942
1943        let contradictory_cache_flags = SqliteConfig::memory().flags(OpenFlags {
1944            shared_cache: true,
1945            private_cache: true,
1946            ..OpenFlags::create_read_write()
1947        });
1948        let Err(cache_error) = SqliteConnection::open(&contradictory_cache_flags) else {
1949            panic!("contradictory SQLite cache flags must fail before native open");
1950        };
1951        assert!(matches!(cache_error, Error::Config(_)));
1952        assert_eq!(sqlite_error_code(&cache_error), None);
1953        assert_ne!(
1954            OpenFlags::create_read_write().to_sqlite_flags() & ffi::SQLITE_OPEN_PRIVATECACHE,
1955            0,
1956            "ordinary opens must override process-global shared-cache mode"
1957        );
1958    }
1959
1960    #[test]
1961    fn test_backup_copies_data_and_opposite_directions_do_not_deadlock() {
1962        let left = Arc::new(SqliteConnection::open_memory().unwrap());
1963        let right = Arc::new(SqliteConnection::open_memory().unwrap());
1964        left.execute_raw("CREATE TABLE backup_rows (value INTEGER)")
1965            .unwrap();
1966        left.execute_raw("INSERT INTO backup_rows VALUES (7)")
1967            .unwrap();
1968
1969        left.backup_to_connection(&right)
1970            .expect("ordinary backup should copy the source database");
1971        let copied = right
1972            .query_sync("SELECT value FROM backup_rows", &[])
1973            .expect("copied table should be readable");
1974        assert_eq!(copied[0].get_named::<i64>("value").unwrap(), 7);
1975
1976        let barrier = Arc::new(std::sync::Barrier::new(3));
1977        let (completed_tx, completed_rx) = std::sync::mpsc::channel();
1978        let mut workers = Vec::new();
1979        for (source, destination) in [
1980            (Arc::clone(&left), Arc::clone(&right)),
1981            (Arc::clone(&right), Arc::clone(&left)),
1982        ] {
1983            let worker_barrier = Arc::clone(&barrier);
1984            let worker_tx = completed_tx.clone();
1985            workers.push(std::thread::spawn(move || {
1986                worker_barrier.wait();
1987                let result = source.backup_to_connection(&destination);
1988                worker_tx.send(result).expect("test receiver remains live");
1989            }));
1990        }
1991        drop(completed_tx);
1992        barrier.wait();
1993        for _ in 0..2 {
1994            completed_rx
1995                .recv_timeout(Duration::from_secs(5))
1996                .expect("opposing backups must not deadlock")
1997                .expect("serialized opposing backup should succeed");
1998        }
1999        for worker in workers {
2000            worker.join().expect("backup worker should not panic");
2001        }
2002    }
2003
2004    #[test]
2005    fn test_backup_lock_retries_respect_deadline_and_restore_busy_timeouts() {
2006        let source = SqliteConnection::open(&SqliteConfig::memory().busy_timeout(500)).unwrap();
2007        source
2008            .execute_raw("CREATE TABLE backup_rows (value INTEGER)")
2009            .unwrap();
2010        source
2011            .execute_raw("INSERT INTO backup_rows VALUES (7)")
2012            .unwrap();
2013
2014        let path = unique_temp_db_path("backup_deadline");
2015        let _ = std::fs::remove_file(&path);
2016        let destination_config =
2017            SqliteConfig::file(path.to_string_lossy().into_owned()).busy_timeout(450);
2018        let destination = SqliteConnection::open(&destination_config).unwrap();
2019        destination
2020            .execute_raw("CREATE TABLE old_rows (value INTEGER)")
2021            .unwrap();
2022        source.execute_raw("PRAGMA busy_timeout=520").unwrap();
2023        destination.execute_raw("PRAGMA busy_timeout=470").unwrap();
2024        let blocker = SqliteConnection::open(&destination_config).unwrap();
2025        blocker.execute_raw("BEGIN EXCLUSIVE").unwrap();
2026
2027        let started = Instant::now();
2028        let error = source
2029            .backup_to_connection(&destination)
2030            .expect_err("an exclusive destination lock must block the backup");
2031        let elapsed = started.elapsed();
2032        let code = sqlite_error_code(&error)
2033            .expect("a lock-blocked backup must retain its native SQLite result code");
2034        assert!(
2035            matches!(code.primary(), ffi::SQLITE_BUSY | ffi::SQLITE_LOCKED),
2036            "unexpected lock failure code: {code}"
2037        );
2038        assert!(
2039            elapsed < Duration::from_millis(800),
2040            "backup retry deadline overran by a native busy-timeout interval: {elapsed:?}"
2041        );
2042
2043        blocker.execute_raw("ROLLBACK").unwrap();
2044        for (connection, expected_timeout) in [(&source, 520), (&destination, 470)] {
2045            let rows = connection.query_sync("PRAGMA busy_timeout", &[]).unwrap();
2046            assert_eq!(
2047                rows[0].get_named::<i32>("timeout").unwrap(),
2048                expected_timeout
2049            );
2050        }
2051
2052        drop(blocker);
2053        drop(destination);
2054        let _ = std::fs::remove_file(path);
2055    }
2056
2057    #[test]
2058    fn test_backup_rejects_shared_cache_destination_before_locking() {
2059        let source = SqliteConnection::open_memory().unwrap();
2060        let shared_uri = format!(
2061            "file:sqlmodel_backup_shared_{}?mode=memory&cache=shared",
2062            std::process::id()
2063        );
2064        let destination =
2065            SqliteConnection::open(&SqliteConfig::file(shared_uri).flags(OpenFlags {
2066                uri: true,
2067                ..OpenFlags::create_read_write()
2068            }))
2069            .expect("shared-cache connection should open for the preflight test");
2070        assert!(destination.uses_shared_cache);
2071
2072        let error = source
2073            .backup_to_connection(&destination)
2074            .expect_err("shared-cache backup destination must fail closed");
2075        assert!(error.to_string().contains("shared-cache mode"));
2076        assert_eq!(sqlite_error_code(&error), None);
2077
2078        let plain_memory = SqliteConnection::open(&SqliteConfig::memory().flags(OpenFlags {
2079            shared_cache: true,
2080            ..OpenFlags::create_read_write()
2081        }))
2082        .expect("plain :memory: remains private even with SHAREDCACHE requested");
2083        assert!(!plain_memory.uses_shared_cache);
2084        source
2085            .backup_to_connection(&plain_memory)
2086            .expect("a truly private in-memory destination is backup-safe");
2087
2088        let private_uri = format!(
2089            "file:sqlmodel_backup_private_{}?mode=memory&cache=private",
2090            std::process::id()
2091        );
2092        let uri_overrides_flag =
2093            SqliteConnection::open(&SqliteConfig::file(private_uri).flags(OpenFlags {
2094                uri: true,
2095                shared_cache: true,
2096                ..OpenFlags::create_read_write()
2097            }))
2098            .expect("URI cache=private should override the shared-cache open flag");
2099        assert!(!uri_overrides_flag.uses_shared_cache);
2100        source
2101            .backup_to_connection(&uri_overrides_flag)
2102            .expect("an effectively private URI destination is backup-safe");
2103    }
2104
2105    #[test]
2106    fn test_sqlite_uri_cache_mode_matches_sqlite_uri_rules() {
2107        assert_eq!(
2108            sqlite_uri_cache_mode("file:memory?mode=memory&cache=shared"),
2109            Some(true)
2110        );
2111        assert_eq!(
2112            sqlite_uri_cache_mode("file:memory?cache=private&cache=shared"),
2113            Some(true),
2114            "SQLite applies duplicate cache parameters in order, so the last one wins"
2115        );
2116        assert_eq!(
2117            sqlite_uri_cache_mode("file:memory?%63ache=%70rivate"),
2118            Some(false),
2119            "SQLite percent-decodes URI parameter names and values"
2120        );
2121        assert_eq!(
2122            sqlite_uri_cache_mode("file:memory?cache%00ignored=shared%00ignored"),
2123            Some(true),
2124            "SQLite truncates URI components at encoded NUL bytes"
2125        );
2126        assert_eq!(
2127            sqlite_uri_cache_mode("file:memory?CACHE=shared"),
2128            None,
2129            "SQLite URI parameter names are case-sensitive"
2130        );
2131        assert_eq!(
2132            sqlite_uri_cache_mode("file:memory?cache=shared#cache=private"),
2133            Some(true),
2134            "SQLite ignores URI fragments"
2135        );
2136    }
2137
2138    #[test]
2139    fn test_backup_failure_retains_native_destination_error() {
2140        let source = SqliteConnection::open_memory().unwrap();
2141        source
2142            .execute_raw("CREATE TABLE backup_source (value INTEGER)")
2143            .unwrap();
2144
2145        let path = unique_temp_db_path("readonly_backup");
2146        let writable = SqliteConnection::open_file(path.to_string_lossy().into_owned()).unwrap();
2147        writable
2148            .execute_raw("CREATE TABLE backup_destination (value INTEGER)")
2149            .unwrap();
2150        drop(writable);
2151        let destination = SqliteConnection::open(
2152            &SqliteConfig::file(path.to_string_lossy().into_owned()).flags(OpenFlags::read_only()),
2153        )
2154        .unwrap();
2155
2156        let error = source
2157            .backup_to_connection(&destination)
2158            .expect_err("read-only destination must reject backup writes");
2159        let code = sqlite_error_code(&error)
2160            .expect("native backup failure must retain its SQLite result code");
2161        assert_eq!(code.primary(), ffi::SQLITE_READONLY);
2162        assert!(error.to_string().to_ascii_lowercase().contains("readonly"));
2163
2164        drop(destination);
2165        let _ = std::fs::remove_file(path);
2166    }
2167
2168    #[test]
2169    fn test_backup_step_error_preserves_direct_extended_result() {
2170        let direct_extended = ffi::SQLITE_IOERR | (42 << 8);
2171        let error = backup_step_error(std::ptr::null_mut(), direct_extended);
2172        let code = sqlite_error_code(&error)
2173            .expect("a backup-step failure must retain its direct native result");
2174        assert_eq!(code.primary(), ffi::SQLITE_IOERR);
2175        assert_eq!(code.extended(), direct_extended);
2176    }
2177
2178    #[test]
2179    fn test_null_handling() {
2180        let conn = SqliteConnection::open_memory().unwrap();
2181        conn.execute_raw("CREATE TABLE test (id INTEGER PRIMARY KEY, name TEXT)")
2182            .unwrap();
2183
2184        conn.execute_sync("INSERT INTO test (name) VALUES (?)", &[Value::Null])
2185            .unwrap();
2186
2187        let rows = conn.query_sync("SELECT * FROM test", &[]).unwrap();
2188        assert_eq!(rows.len(), 1);
2189        assert_eq!(rows[0].get_named::<Option<String>>("name").unwrap(), None);
2190    }
2191
2192    #[test]
2193    fn test_transaction() {
2194        let conn = SqliteConnection::open_memory().unwrap();
2195        conn.execute_raw("CREATE TABLE test (id INTEGER PRIMARY KEY, name TEXT)")
2196            .unwrap();
2197
2198        // Start transaction, insert, rollback
2199        conn.begin_sync(IsolationLevel::default()).unwrap();
2200        conn.execute_sync(
2201            "INSERT INTO test (name) VALUES (?)",
2202            &[Value::Text("Alice".to_string())],
2203        )
2204        .unwrap();
2205        conn.rollback_sync().unwrap();
2206
2207        // Verify rollback worked
2208        let rows = conn.query_sync("SELECT * FROM test", &[]).unwrap();
2209        assert_eq!(rows.len(), 0);
2210
2211        // Start transaction, insert, commit
2212        conn.begin_sync(IsolationLevel::default()).unwrap();
2213        conn.execute_sync(
2214            "INSERT INTO test (name) VALUES (?)",
2215            &[Value::Text("Bob".to_string())],
2216        )
2217        .unwrap();
2218        conn.commit_sync().unwrap();
2219
2220        // Verify commit worked
2221        let rows = conn.query_sync("SELECT * FROM test", &[]).unwrap();
2222        assert_eq!(rows.len(), 1);
2223        assert_eq!(rows[0].get_named::<String>("name").unwrap(), "Bob");
2224    }
2225
2226    #[test]
2227    fn test_insert_rowid() {
2228        let conn = SqliteConnection::open_memory().unwrap();
2229        conn.execute_raw("CREATE TABLE test (id INTEGER PRIMARY KEY, name TEXT)")
2230            .unwrap();
2231
2232        let rowid = conn
2233            .insert_sync(
2234                "INSERT INTO test (name) VALUES (?)",
2235                &[Value::Text("Alice".to_string())],
2236            )
2237            .unwrap();
2238        assert_eq!(rowid, 1);
2239
2240        let rowid = conn
2241            .insert_sync(
2242                "INSERT INTO test (name) VALUES (?)",
2243                &[Value::Text("Bob".to_string())],
2244            )
2245            .unwrap();
2246        assert_eq!(rowid, 2);
2247    }
2248
2249    #[test]
2250    #[allow(clippy::approx_constant)]
2251    fn test_type_conversions() {
2252        let conn = SqliteConnection::open_memory().unwrap();
2253        conn.execute_raw(
2254            "CREATE TABLE types (
2255                b BOOLEAN,
2256                i INTEGER,
2257                f REAL,
2258                t TEXT,
2259                bl BLOB
2260            )",
2261        )
2262        .unwrap();
2263
2264        conn.execute_sync(
2265            "INSERT INTO types VALUES (?, ?, ?, ?, ?)",
2266            &[
2267                Value::Bool(true),
2268                Value::BigInt(42),
2269                Value::Double(3.14),
2270                Value::Text("hello".to_string()),
2271                Value::Bytes(vec![1, 2, 3]),
2272            ],
2273        )
2274        .unwrap();
2275
2276        let rows = conn.query_sync("SELECT * FROM types", &[]).unwrap();
2277        assert_eq!(rows.len(), 1);
2278
2279        // SQLite stores booleans as integers
2280        let b: i32 = rows[0].get_named("b").unwrap();
2281        assert_eq!(b, 1);
2282
2283        let i: i32 = rows[0].get_named("i").unwrap();
2284        assert_eq!(i, 42);
2285
2286        let f: f64 = rows[0].get_named("f").unwrap();
2287        assert!((f - 3.14).abs() < 0.001);
2288
2289        let t: String = rows[0].get_named("t").unwrap();
2290        assert_eq!(t, "hello");
2291
2292        let bl: Vec<u8> = rows[0].get_named("bl").unwrap();
2293        assert_eq!(bl, vec![1, 2, 3]);
2294    }
2295
2296    #[test]
2297    fn test_open_flags() {
2298        // Test creating a database with create flag
2299        let tmp = unique_temp_db_path("open_flags");
2300        let _ = std::fs::remove_file(&tmp); // Ensure it doesn't exist
2301
2302        let config = SqliteConfig::file(tmp.to_string_lossy().to_string())
2303            .flags(OpenFlags::create_read_write());
2304        let conn = SqliteConnection::open(&config).unwrap();
2305        conn.execute_raw("CREATE TABLE test (id INTEGER)").unwrap();
2306        drop(conn);
2307
2308        // Open as read-only
2309        let config =
2310            SqliteConfig::file(tmp.to_string_lossy().to_string()).flags(OpenFlags::read_only());
2311        let conn = SqliteConnection::open(&config).unwrap();
2312
2313        // Reading should work
2314        let rows = conn.query_sync("SELECT * FROM test", &[]).unwrap();
2315        assert_eq!(rows.len(), 0);
2316
2317        // Writing should fail
2318        let error = conn
2319            .execute_raw("INSERT INTO test VALUES (1)")
2320            .expect_err("read-only connection must reject writes");
2321        let error_code = sqlite_error_code(&error)
2322            .expect("native write rejection must retain its exact SQLite result code");
2323        assert_eq!(error_code.primary(), ffi::SQLITE_READONLY);
2324        assert_eq!(error_code.extended() & 0xff, ffi::SQLITE_READONLY);
2325        assert!(
2326            matches!(error, Error::Query(ref query) if query.kind == QueryErrorKind::Permission),
2327            "SQLITE_READONLY should map to the permission error family: {error}"
2328        );
2329
2330        let prepared_error = conn
2331            .execute_sync("INSERT INTO test VALUES (1)", &[])
2332            .expect_err("prepared writes must also retain SQLITE_READONLY");
2333        let prepared_code = sqlite_error_code(&prepared_error)
2334            .expect("prepared write rejection must retain its native result code");
2335        assert_eq!(prepared_code.primary(), ffi::SQLITE_READONLY);
2336        assert!(
2337            matches!(prepared_error, Error::Query(ref query) if query.kind == QueryErrorKind::Permission),
2338            "prepared SQLITE_READONLY should map to permission: {prepared_error}"
2339        );
2340
2341        drop(conn);
2342        let _ = std::fs::remove_file(&tmp);
2343    }
2344
2345    // ==================== Console Integration Tests ====================
2346
2347    #[cfg(feature = "console")]
2348    mod console_tests {
2349        use super::*;
2350
2351        /// Test that ConsoleAware trait is properly implemented.
2352        #[test]
2353        fn test_console_aware_trait_impl() {
2354            let mut conn = SqliteConnection::open_memory().unwrap();
2355
2356            // Initially no console
2357            assert!(!conn.has_console());
2358            assert!(conn.console().is_none());
2359
2360            // Attach console
2361            let console = Arc::new(SqlModelConsole::with_mode(
2362                sqlmodel_console::OutputMode::Plain,
2363            ));
2364            conn.set_console(Some(console.clone()));
2365
2366            // Verify console is attached
2367            assert!(conn.has_console());
2368            assert!(conn.console().is_some());
2369
2370            // Detach console
2371            conn.set_console(None);
2372            assert!(!conn.has_console());
2373        }
2374
2375        /// Test database open feedback is emitted when console is attached.
2376        #[test]
2377        fn test_database_open_feedback() {
2378            let mut conn = SqliteConnection::open_memory().unwrap();
2379
2380            // Attaching console should emit open status
2381            // (output goes to stderr, we just verify no panic)
2382            let console = Arc::new(SqlModelConsole::with_mode(
2383                sqlmodel_console::OutputMode::Plain,
2384            ));
2385            conn.set_console(Some(console));
2386
2387            // No panic means success
2388        }
2389
2390        /// Test PRAGMA query formatting.
2391        #[test]
2392        fn test_pragma_formatting() {
2393            let mut conn = SqliteConnection::open_memory().unwrap();
2394
2395            // Create a table to have something in pragma_table_info
2396            conn.execute_raw("CREATE TABLE test (id INTEGER PRIMARY KEY, name TEXT)")
2397                .unwrap();
2398
2399            // Attach console for formatted output
2400            let console = Arc::new(SqlModelConsole::with_mode(
2401                sqlmodel_console::OutputMode::Plain,
2402            ));
2403            conn.set_console(Some(console));
2404
2405            // Execute PRAGMA query - should format as table
2406            let rows = conn.query_sync("PRAGMA table_info(test)", &[]).unwrap();
2407
2408            // Verify we got the expected columns
2409            assert!(!rows.is_empty());
2410        }
2411
2412        /// Test transaction state display.
2413        #[test]
2414        fn test_transaction_state() {
2415            let mut conn = SqliteConnection::open_memory().unwrap();
2416            conn.execute_raw("CREATE TABLE test (id INTEGER PRIMARY KEY)")
2417                .unwrap();
2418
2419            // Attach console
2420            let console = Arc::new(SqlModelConsole::with_mode(
2421                sqlmodel_console::OutputMode::Plain,
2422            ));
2423            conn.set_console(Some(console));
2424
2425            // Transaction operations should emit state
2426            conn.begin_sync(IsolationLevel::default()).unwrap();
2427            conn.execute_sync("INSERT INTO test (id) VALUES (?)", &[Value::Int(1)])
2428                .unwrap();
2429            conn.commit_sync().unwrap();
2430
2431            // Verify the transaction worked
2432            let rows = conn.query_sync("SELECT * FROM test", &[]).unwrap();
2433            assert_eq!(rows.len(), 1);
2434        }
2435
2436        /// Test WAL checkpoint progress output.
2437        #[test]
2438        fn test_wal_checkpoint_progress() {
2439            let conn = SqliteConnection::open_memory().unwrap();
2440
2441            // emit_checkpoint_progress should not panic
2442            conn.emit_checkpoint_progress(50, 100);
2443            conn.emit_checkpoint_progress(100, 100);
2444            conn.emit_checkpoint_progress(0, 0);
2445        }
2446
2447        /// Test busy timeout feedback output.
2448        #[test]
2449        fn test_busy_timeout_feedback() {
2450            let conn = SqliteConnection::open_memory().unwrap();
2451
2452            // emit_busy_waiting should not panic
2453            conn.emit_busy_waiting(0.5);
2454            conn.emit_busy_waiting(2.1);
2455        }
2456
2457        /// Test that console disabled produces no output (no panic).
2458        #[test]
2459        fn test_console_disabled_no_output() {
2460            let conn = SqliteConnection::open_memory().unwrap();
2461
2462            // Without console, all emit methods should be no-ops
2463            conn.emit_busy_waiting(1.0);
2464            conn.emit_checkpoint_progress(10, 100);
2465
2466            // Query should work without console
2467            conn.execute_raw("CREATE TABLE test (id INTEGER PRIMARY KEY)")
2468                .unwrap();
2469            let rows = conn.query_sync("SELECT * FROM test", &[]).unwrap();
2470            assert_eq!(rows.len(), 0);
2471        }
2472
2473        /// Test plain mode output format (parseable by agents).
2474        #[test]
2475        fn test_plain_mode_output() {
2476            let mut conn = SqliteConnection::open_memory().unwrap();
2477
2478            // Attach plain mode console
2479            let console = Arc::new(SqlModelConsole::with_mode(
2480                sqlmodel_console::OutputMode::Plain,
2481            ));
2482            conn.set_console(Some(console.clone()));
2483
2484            // Verify plain mode is active
2485            assert!(conn.console().unwrap().is_plain());
2486
2487            // Execute operations (output should be plain text)
2488            conn.execute_raw("CREATE TABLE test (id INTEGER PRIMARY KEY, name TEXT)")
2489                .unwrap();
2490            conn.execute_sync(
2491                "INSERT INTO test (name) VALUES (?)",
2492                &[Value::Text("Alice".to_string())],
2493            )
2494            .unwrap();
2495
2496            let rows = conn.query_sync("PRAGMA table_info(test)", &[]).unwrap();
2497            assert!(!rows.is_empty());
2498        }
2499
2500        /// Test rich mode output format.
2501        #[test]
2502        fn test_rich_mode_output() {
2503            let mut conn = SqliteConnection::open_memory().unwrap();
2504
2505            // Attach rich mode console
2506            let console = Arc::new(SqlModelConsole::with_mode(
2507                sqlmodel_console::OutputMode::Rich,
2508            ));
2509            conn.set_console(Some(console.clone()));
2510
2511            // Verify rich mode is active
2512            assert!(conn.console().unwrap().is_rich());
2513
2514            // Execute operations (output should have formatting)
2515            conn.execute_raw("CREATE TABLE test (id INTEGER PRIMARY KEY)")
2516                .unwrap();
2517            conn.emit_checkpoint_progress(50, 100);
2518        }
2519    }
2520}