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            if !inner.db.is_null() {
808                // SAFETY: db is valid
809                unsafe {
810                    ffi::sqlite3_close_v2(inner.db);
811                }
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            if let (Some(high), Some(low)) = (bytes.get(index + 1), bytes.get(index + 2)) {
1193                if let (Some(high), Some(low)) = (hex_nibble(*high), hex_nibble(*low)) {
1194                    let byte = (high << 4) | low;
1195                    if byte == 0 {
1196                        // SQLite truncates the current URI component at an
1197                        // encoded NUL and resumes at its next raw separator.
1198                        break;
1199                    }
1200                    decoded.push(byte);
1201                    index += 3;
1202                    continue;
1203                }
1204            }
1205        }
1206
1207        decoded.push(bytes[index]);
1208        index += 1;
1209    }
1210    decoded
1211}
1212
1213const fn hex_nibble(byte: u8) -> Option<u8> {
1214    match byte {
1215        b'0'..=b'9' => Some(byte - b'0'),
1216        b'a'..=b'f' => Some(byte - b'a' + 10),
1217        b'A'..=b'F' => Some(byte - b'A' + 10),
1218        _ => None,
1219    }
1220}
1221
1222fn sqlite_error_code_from_db(db: *mut ffi::sqlite3, result: c_int) -> SqliteErrorCode {
1223    let primary = result & 0xff;
1224    let observed_extended = if db.is_null() {
1225        result
1226    } else {
1227        // SAFETY: every non-null pointer passed here is an open SQLite handle
1228        // held by the caller for the duration of this observation.
1229        unsafe { ffi::sqlite3_extended_errcode(db) }
1230    };
1231    // Some APIs return an error directly without replacing the connection's
1232    // previous error state. Never expose a contradictory primary/extended pair;
1233    // the direct result remains the authoritative fallback in that case.
1234    let extended = if observed_extended & 0xff == primary {
1235        observed_extended
1236    } else {
1237        result
1238    };
1239    SqliteErrorCode::from_result_codes(result, extended)
1240}
1241
1242fn direct_sqlite_error_code(result: c_int) -> SqliteErrorCode {
1243    SqliteErrorCode::from_result_codes(result, result)
1244}
1245
1246fn backup_step_error(db: *mut ffi::sqlite3, result: c_int) -> Error {
1247    let detail = if db.is_null() {
1248        ffi::error_string(result).to_string()
1249    } else {
1250        // SQLite documents backup routine failures on the destination
1251        // connection. Capture its detailed message while retaining `result`
1252        // itself as the authoritative exact code.
1253        unsafe { CStr::from_ptr(ffi::sqlite3_errmsg(db)) }
1254            .to_string_lossy()
1255            .into_owned()
1256    };
1257    Error::Connection(ConnectionError {
1258        kind: ConnectionErrorKind::Connect,
1259        message: format!(
1260            "SQLite backup failed: {detail} ({})",
1261            ffi::error_string(result)
1262        ),
1263        source: Some(Box::new(direct_sqlite_error_code(result))),
1264    })
1265}
1266
1267fn prepare_stmt(db: *mut ffi::sqlite3, sql: &str) -> Result<*mut ffi::sqlite3_stmt, Error> {
1268    let c_sql = CString::new(sql).map_err(|_| {
1269        Error::Query(QueryError {
1270            kind: QueryErrorKind::Syntax,
1271            sql: Some(sql.to_string()),
1272            sqlstate: None,
1273            message: "SQL contains null byte".to_string(),
1274            detail: None,
1275            hint: None,
1276            position: None,
1277            source: None,
1278        })
1279    })?;
1280
1281    let mut stmt: *mut ffi::sqlite3_stmt = ptr::null_mut();
1282
1283    // SAFETY: All pointers are valid
1284    let rc = unsafe {
1285        ffi::sqlite3_prepare_v2(
1286            db,
1287            c_sql.as_ptr(),
1288            c_sql.as_bytes().len() as c_int,
1289            &mut stmt,
1290            ptr::null_mut(),
1291        )
1292    };
1293
1294    if rc != ffi::SQLITE_OK {
1295        return Err(prepare_error(db, sql, rc));
1296    }
1297
1298    if stmt.is_null() {
1299        return Err(Error::Query(QueryError {
1300            kind: QueryErrorKind::Syntax,
1301            sql: Some(sql.to_string()),
1302            sqlstate: None,
1303            message: "SQL contains no executable statement".to_string(),
1304            detail: None,
1305            hint: None,
1306            position: None,
1307            source: None,
1308        }));
1309    }
1310
1311    Ok(stmt)
1312}
1313
1314fn prepare_error(db: *mut ffi::sqlite3, sql: &str, code: c_int) -> Error {
1315    // SAFETY: db is valid
1316    let msg = unsafe {
1317        let ptr = ffi::sqlite3_errmsg(db);
1318        CStr::from_ptr(ptr).to_string_lossy().into_owned()
1319    };
1320    let error_code = sqlite_error_code_from_db(db, code);
1321
1322    Error::Query(QueryError {
1323        kind: error_code_to_kind(code),
1324        sql: Some(sql.to_string()),
1325        sqlstate: None,
1326        message: msg,
1327        detail: None,
1328        hint: None,
1329        position: None,
1330        source: Some(Box::new(error_code)),
1331    })
1332}
1333
1334fn bind_error(db: *mut ffi::sqlite3, sql: &str, param_index: usize, code: c_int) -> Error {
1335    // SAFETY: db is valid
1336    let msg = unsafe {
1337        let ptr = ffi::sqlite3_errmsg(db);
1338        CStr::from_ptr(ptr).to_string_lossy().into_owned()
1339    };
1340    let error_code = sqlite_error_code_from_db(db, code);
1341
1342    Error::Query(QueryError {
1343        kind: error_code_to_kind(code),
1344        sql: Some(sql.to_string()),
1345        sqlstate: None,
1346        message: format!("Failed to bind parameter {}: {}", param_index, msg),
1347        detail: None,
1348        hint: None,
1349        position: None,
1350        source: Some(Box::new(error_code)),
1351    })
1352}
1353
1354fn step_error(db: *mut ffi::sqlite3, sql: &str, code: c_int) -> Error {
1355    // SAFETY: db is valid
1356    let msg = unsafe {
1357        let ptr = ffi::sqlite3_errmsg(db);
1358        CStr::from_ptr(ptr).to_string_lossy().into_owned()
1359    };
1360    let error_code = sqlite_error_code_from_db(db, code);
1361
1362    Error::Query(QueryError {
1363        kind: error_code_to_kind(code),
1364        sql: Some(sql.to_string()),
1365        sqlstate: None,
1366        message: msg,
1367        detail: None,
1368        hint: None,
1369        position: None,
1370        source: Some(Box::new(error_code)),
1371    })
1372}
1373
1374fn error_code_to_kind(code: c_int) -> QueryErrorKind {
1375    match code & 0xff {
1376        ffi::SQLITE_CONSTRAINT => QueryErrorKind::Constraint,
1377        ffi::SQLITE_BUSY | ffi::SQLITE_LOCKED => QueryErrorKind::Deadlock,
1378        ffi::SQLITE_PERM | ffi::SQLITE_READONLY | ffi::SQLITE_AUTH => QueryErrorKind::Permission,
1379        ffi::SQLITE_NOTFOUND => QueryErrorKind::NotFound,
1380        ffi::SQLITE_TOOBIG => QueryErrorKind::DataTruncation,
1381        ffi::SQLITE_INTERRUPT => QueryErrorKind::Cancelled,
1382        _ => QueryErrorKind::Database,
1383    }
1384}
1385
1386/// Format a Value for display in console output.
1387#[allow(dead_code)]
1388fn format_value(value: &Value) -> String {
1389    match value {
1390        Value::Null => "NULL".to_string(),
1391        Value::Bool(b) => if *b { "true" } else { "false" }.to_string(),
1392        Value::TinyInt(n) => n.to_string(),
1393        Value::SmallInt(n) => n.to_string(),
1394        Value::Int(n) => n.to_string(),
1395        Value::BigInt(n) => n.to_string(),
1396        Value::Float(n) => format!("{:.6}", n),
1397        Value::Double(n) => format!("{:.6}", n),
1398        Value::Text(s) => s.clone(),
1399        Value::Bytes(b) => format!("[BLOB: {} bytes]", b.len()),
1400        Value::Date(d) => d.to_string(),
1401        Value::Time(t) => t.to_string(),
1402        Value::Timestamp(ts) => ts.to_string(),
1403        Value::TimestampTz(ts) => ts.to_string(),
1404        Value::Json(j) => j.to_string(),
1405        Value::Uuid(u) => {
1406            // Format UUID as hex string: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
1407            format!(
1408                "{:02x}{:02x}{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}",
1409                u[0],
1410                u[1],
1411                u[2],
1412                u[3],
1413                u[4],
1414                u[5],
1415                u[6],
1416                u[7],
1417                u[8],
1418                u[9],
1419                u[10],
1420                u[11],
1421                u[12],
1422                u[13],
1423                u[14],
1424                u[15]
1425            )
1426        }
1427        Value::Decimal(d) => d.to_string(),
1428        Value::Array(arr) => format!("[{} items]", arr.len()),
1429        Value::Default => "DEFAULT".to_string(),
1430    }
1431}
1432
1433// ==================== Console Support ====================
1434
1435#[cfg(feature = "console")]
1436impl ConsoleAware for SqliteConnection {
1437    fn set_console(&mut self, console: Option<Arc<SqlModelConsole>>) {
1438        self.console = console;
1439        // Emit database status when console is attached
1440        self.emit_open_status();
1441    }
1442
1443    fn console(&self) -> Option<&Arc<SqlModelConsole>> {
1444        self.console.as_ref()
1445    }
1446
1447    fn has_console(&self) -> bool {
1448        self.console.is_some()
1449    }
1450}
1451
1452impl SqliteConnection {
1453    /// Emit database open status to console if available.
1454    #[cfg(feature = "console")]
1455    fn emit_open_status(&self) {
1456        if let Some(console) = &self.console {
1457            // Get database info
1458            let mode = if self.path == ":memory:" {
1459                "in-memory"
1460            } else {
1461                "file"
1462            };
1463
1464            // Query journal mode if we can
1465            let journal_mode = self
1466                .query_sync("PRAGMA journal_mode", &[])
1467                .ok()
1468                .and_then(|rows| rows.first().and_then(|r| r.get_as::<String>(0).ok()));
1469
1470            let page_size = self
1471                .query_sync("PRAGMA page_size", &[])
1472                .ok()
1473                .and_then(|rows| rows.first().and_then(|r| r.get_as::<i64>(0).ok()));
1474
1475            if console.mode().is_plain() {
1476                // Plain text output for agents
1477                let journal = journal_mode.as_deref().unwrap_or("unknown");
1478                console.status(&format!(
1479                    "Opened SQLite database: {} ({} mode, journal: {})",
1480                    self.path, mode, journal
1481                ));
1482            } else {
1483                // Rich output
1484                console.status(&format!("SQLite database: {}", self.path));
1485                console.status(&format!("  Mode: {}", mode));
1486                if let Some(journal) = journal_mode {
1487                    console.status(&format!("  Journal: {}", journal.to_uppercase()));
1488                }
1489                if let Some(size) = page_size {
1490                    console.status(&format!("  Page size: {} bytes", size));
1491                }
1492            }
1493        }
1494    }
1495
1496    /// Emit transaction state to console if available.
1497    #[cfg(feature = "console")]
1498    fn emit_transaction_state(&self, state: &str) {
1499        if let Some(console) = &self.console {
1500            if console.mode().is_plain() {
1501                console.status(&format!("Transaction: {}", state));
1502            } else {
1503                console.status(&format!("[{}] Transaction {}", state, state.to_lowercase()));
1504            }
1505        }
1506    }
1507
1508    /// Emit query timing to console if available.
1509    #[cfg(feature = "console")]
1510    fn emit_query_timing(&self, elapsed_ms: f64, rows: usize) {
1511        if let Some(console) = &self.console {
1512            console.status(&format!("Query: {:.1}ms, {} rows", elapsed_ms, rows));
1513        }
1514    }
1515
1516    /// Emit query results with PRAGMA-aware formatting.
1517    #[cfg(feature = "console")]
1518    fn emit_query_result(&self, sql: &str, col_names: &[String], rows: &[Row], elapsed_ms: f64) {
1519        if let Some(console) = &self.console {
1520            // Check if this is a PRAGMA query for special formatting
1521            let sql_upper = sql.trim().to_uppercase();
1522            let is_pragma = sql_upper.starts_with("PRAGMA");
1523
1524            if is_pragma && !rows.is_empty() {
1525                // Format PRAGMA results as a table
1526                if console.mode().is_plain() {
1527                    // Plain text format for agents
1528                    console.status(&format!("{}:", sql.trim()));
1529                    // Header
1530                    console.status(&format!("  {}", col_names.join("|")));
1531                    // Rows
1532                    for row in rows.iter().take(20) {
1533                        let values: Vec<String> = (0..col_names.len())
1534                            .map(|i| {
1535                                row.get(i)
1536                                    .map(|v| format_value(v))
1537                                    .unwrap_or_else(|| "NULL".to_string())
1538                            })
1539                            .collect();
1540                        console.status(&format!("  {}", values.join("|")));
1541                    }
1542                    if rows.len() > 20 {
1543                        console.status(&format!("  ... and {} more rows", rows.len() - 20));
1544                    }
1545                    console.status(&format!("  ({:.1}ms)", elapsed_ms));
1546                } else {
1547                    // Rich format with table rendering
1548                    let mut table_output = String::new();
1549                    table_output.push_str(&format!("PRAGMA Query Results ({:.1}ms)\n", elapsed_ms));
1550
1551                    // Calculate column widths
1552                    let mut widths: Vec<usize> = col_names.iter().map(|c| c.len()).collect();
1553                    for row in rows.iter().take(20) {
1554                        for (i, w) in widths.iter_mut().enumerate() {
1555                            let val_len = row.get(i).map(|v| format_value(v).len()).unwrap_or(4); // "NULL".len()
1556                            if val_len > *w {
1557                                *w = val_len;
1558                            }
1559                        }
1560                    }
1561
1562                    // Build header separator
1563                    let sep: String = widths
1564                        .iter()
1565                        .map(|w| "-".repeat(*w + 2))
1566                        .collect::<Vec<_>>()
1567                        .join("+");
1568                    table_output.push_str(&format!("+{}+\n", sep));
1569
1570                    // Header row
1571                    let header: String = col_names
1572                        .iter()
1573                        .enumerate()
1574                        .map(|(i, name)| format!(" {:width$} ", name, width = widths[i]))
1575                        .collect::<Vec<_>>()
1576                        .join("|");
1577                    table_output.push_str(&format!("|{}|\n", header));
1578                    table_output.push_str(&format!("+{}+\n", sep));
1579
1580                    // Data rows
1581                    for row in rows.iter().take(20) {
1582                        let data: String = (0..col_names.len())
1583                            .map(|i| {
1584                                let val = row
1585                                    .get(i)
1586                                    .map(|v| format_value(v))
1587                                    .unwrap_or_else(|| "NULL".to_string());
1588                                format!(" {:width$} ", val, width = widths[i])
1589                            })
1590                            .collect::<Vec<_>>()
1591                            .join("|");
1592                        table_output.push_str(&format!("|{}|\n", data));
1593                    }
1594                    table_output.push_str(&format!("+{}+", sep));
1595
1596                    if rows.len() > 20 {
1597                        table_output.push_str(&format!("\n... and {} more rows", rows.len() - 20));
1598                    }
1599
1600                    console.status(&table_output);
1601                }
1602            } else {
1603                // Regular query timing
1604                self.emit_query_timing(elapsed_ms, rows.len());
1605            }
1606        }
1607    }
1608
1609    /// Emit execute operation timing to console.
1610    #[cfg(feature = "console")]
1611    fn emit_execute_timing(&self, sql: &str, rows_affected: u64, elapsed_ms: f64) {
1612        if let Some(console) = &self.console {
1613            let sql_upper = sql.trim().to_uppercase();
1614
1615            // Provide contextual message based on operation type
1616            let op_type = if sql_upper.starts_with("INSERT") {
1617                "Insert"
1618            } else if sql_upper.starts_with("UPDATE") {
1619                "Update"
1620            } else if sql_upper.starts_with("DELETE") {
1621                "Delete"
1622            } else if sql_upper.starts_with("CREATE") {
1623                "Create"
1624            } else if sql_upper.starts_with("DROP") {
1625                "Drop"
1626            } else if sql_upper.starts_with("ALTER") {
1627                "Alter"
1628            } else {
1629                "Execute"
1630            };
1631
1632            if console.mode().is_plain() {
1633                console.status(&format!(
1634                    "{}: {} rows affected ({:.1}ms)",
1635                    op_type, rows_affected, elapsed_ms
1636                ));
1637            } else {
1638                console.status(&format!(
1639                    "[{}] {} rows affected ({:.1}ms)",
1640                    op_type.to_uppercase(),
1641                    rows_affected,
1642                    elapsed_ms
1643                ));
1644            }
1645        }
1646    }
1647
1648    /// Emit busy waiting status to console.
1649    #[cfg(feature = "console")]
1650    pub fn emit_busy_waiting(&self, elapsed_secs: f64) {
1651        if let Some(console) = &self.console {
1652            if console.mode().is_plain() {
1653                console.status(&format!(
1654                    "Waiting for database lock... ({:.1}s)",
1655                    elapsed_secs
1656                ));
1657            } else {
1658                console.status(&format!(
1659                    "[..] Waiting for database lock... ({:.1}s)",
1660                    elapsed_secs
1661                ));
1662            }
1663        }
1664    }
1665
1666    /// Emit WAL checkpoint progress to console.
1667    #[cfg(feature = "console")]
1668    pub fn emit_checkpoint_progress(&self, pages_done: u32, pages_total: u32) {
1669        if let Some(console) = &self.console {
1670            let pct = if pages_total > 0 {
1671                (pages_done as f64 / pages_total as f64) * 100.0
1672            } else {
1673                100.0
1674            };
1675
1676            if console.mode().is_plain() {
1677                console.status(&format!(
1678                    "WAL checkpoint: {:.0}% ({}/{} pages)",
1679                    pct, pages_done, pages_total
1680                ));
1681            } else {
1682                // ASCII progress bar for rich mode
1683                let bar_width: usize = 20;
1684                let filled = ((pct / 100.0) * bar_width as f64).round() as usize;
1685                let empty = bar_width.saturating_sub(filled);
1686                let bar = format!("[{}{}]", "=".repeat(filled), " ".repeat(empty));
1687                console.status(&format!(
1688                    "WAL checkpoint: {} {:.0}% ({}/{} pages)",
1689                    bar, pct, pages_done, pages_total
1690                ));
1691            }
1692        }
1693    }
1694
1695    /// No-op when console feature is disabled.
1696    #[cfg(not(feature = "console"))]
1697    #[allow(dead_code)]
1698    fn emit_open_status(&self) {}
1699
1700    /// No-op when console feature is disabled.
1701    #[cfg(not(feature = "console"))]
1702    fn emit_transaction_state(&self, _state: &str) {}
1703
1704    /// No-op when console feature is disabled.
1705    #[cfg(not(feature = "console"))]
1706    #[allow(dead_code)]
1707    fn emit_query_timing(&self, _elapsed_ms: f64, _rows: usize) {}
1708
1709    /// No-op when console feature is disabled.
1710    #[cfg(not(feature = "console"))]
1711    #[allow(dead_code)]
1712    fn emit_query_result(
1713        &self,
1714        _sql: &str,
1715        _col_names: &[String],
1716        _rows: &[Row],
1717        _elapsed_ms: f64,
1718    ) {
1719    }
1720
1721    /// No-op when console feature is disabled.
1722    #[cfg(not(feature = "console"))]
1723    #[allow(dead_code)]
1724    fn emit_execute_timing(&self, _sql: &str, _rows_affected: u64, _elapsed_ms: f64) {}
1725
1726    /// No-op when console feature is disabled.
1727    #[cfg(not(feature = "console"))]
1728    pub fn emit_busy_waiting(&self, _elapsed_secs: f64) {}
1729
1730    /// No-op when console feature is disabled.
1731    #[cfg(not(feature = "console"))]
1732    pub fn emit_checkpoint_progress(&self, _pages_done: u32, _pages_total: u32) {}
1733}
1734
1735#[cfg(test)]
1736mod tests {
1737    use super::*;
1738
1739    static NEXT_TEMP_DB: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
1740
1741    fn unique_temp_db_path(label: &str) -> std::path::PathBuf {
1742        let nonce = NEXT_TEMP_DB.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
1743        std::env::temp_dir().join(format!(
1744            "sqlmodel_{label}_{}_{}.db",
1745            std::process::id(),
1746            nonce
1747        ))
1748    }
1749
1750    #[test]
1751    fn test_open_memory() {
1752        let conn = SqliteConnection::open_memory().unwrap();
1753        assert_eq!(conn.path(), ":memory:");
1754    }
1755
1756    #[test]
1757    fn test_execute_raw() {
1758        let conn = SqliteConnection::open_memory().unwrap();
1759        conn.execute_raw("CREATE TABLE test (id INTEGER PRIMARY KEY, name TEXT)")
1760            .unwrap();
1761        conn.execute_raw("INSERT INTO test (name) VALUES ('Alice')")
1762            .unwrap();
1763        assert_eq!(conn.changes(), 1);
1764        assert_eq!(conn.last_insert_rowid(), 1);
1765
1766        let pre_sqlite_error = conn
1767            .execute_raw("SELECT \0")
1768            .expect_err("NUL-bearing SQL must fail before SQLite");
1769        assert_eq!(
1770            sqlite_error_code(&pre_sqlite_error),
1771            None,
1772            "errors produced before the native call must not invent a SQLite result code"
1773        );
1774    }
1775
1776    #[test]
1777    fn test_query_sync() {
1778        let conn = SqliteConnection::open_memory().unwrap();
1779        conn.execute_raw("CREATE TABLE test (id INTEGER PRIMARY KEY, name TEXT)")
1780            .unwrap();
1781        conn.execute_raw("INSERT INTO test (name) VALUES ('Alice'), ('Bob')")
1782            .unwrap();
1783
1784        let rows = conn
1785            .query_sync("SELECT * FROM test ORDER BY id", &[])
1786            .unwrap();
1787        assert_eq!(rows.len(), 2);
1788
1789        assert_eq!(rows[0].get_named::<i32>("id").unwrap(), 1);
1790        assert_eq!(rows[0].get_named::<String>("name").unwrap(), "Alice");
1791        assert_eq!(rows[1].get_named::<i32>("id").unwrap(), 2);
1792        assert_eq!(rows[1].get_named::<String>("name").unwrap(), "Bob");
1793    }
1794
1795    #[test]
1796    fn test_parameterized_query() {
1797        let conn = SqliteConnection::open_memory().unwrap();
1798        conn.execute_raw("CREATE TABLE test (id INTEGER PRIMARY KEY, name TEXT, age INTEGER)")
1799            .unwrap();
1800
1801        conn.execute_sync(
1802            "INSERT INTO test (name, age) VALUES (?, ?)",
1803            &[Value::Text("Alice".to_string()), Value::Int(30)],
1804        )
1805        .unwrap();
1806
1807        let rows = conn
1808            .query_sync(
1809                "SELECT * FROM test WHERE name = ?",
1810                &[Value::Text("Alice".to_string())],
1811            )
1812            .unwrap();
1813
1814        assert_eq!(rows.len(), 1);
1815        assert_eq!(rows[0].get_named::<String>("name").unwrap(), "Alice");
1816        assert_eq!(rows[0].get_named::<i32>("age").unwrap(), 30);
1817    }
1818
1819    #[test]
1820    fn test_prepared_errors_retain_exact_native_codes() {
1821        let conn = SqliteConnection::open_memory().unwrap();
1822
1823        let prepare_error = conn
1824            .query_sync("SELEC 1", &[])
1825            .expect_err("invalid SQL must fail during prepare");
1826        let prepare_code = sqlite_error_code(&prepare_error)
1827            .expect("prepare failures must retain their native SQLite result code");
1828        assert_eq!(prepare_code.primary(), ffi::SQLITE_ERROR);
1829        assert_eq!(prepare_code.extended(), ffi::SQLITE_ERROR);
1830
1831        let query_bind_error = conn
1832            .query_sync("SELECT ?1", &[Value::Int(1), Value::Int(2)])
1833            .expect_err("binding beyond the statement parameter count must fail");
1834        let execute_bind_error = conn
1835            .execute_sync("SELECT ?1", &[Value::Int(1), Value::Int(2)])
1836            .expect_err("execute_sync must retain the same bind failure");
1837        for error in [&query_bind_error, &execute_bind_error] {
1838            let code = sqlite_error_code(error)
1839                .expect("bind failures must survive statement finalization");
1840            assert_eq!(code.primary(), ffi::SQLITE_RANGE);
1841            assert_eq!(code.extended(), ffi::SQLITE_RANGE);
1842        }
1843
1844        conn.execute_raw("CREATE TABLE exact_codes (value INTEGER UNIQUE)")
1845            .unwrap();
1846        conn.execute_sync("INSERT INTO exact_codes VALUES (1)", &[])
1847            .unwrap();
1848        let execute_step_error = conn
1849            .execute_sync("INSERT INTO exact_codes VALUES (1)", &[])
1850            .expect_err("duplicate prepared insert must fail during step");
1851        let query_step_error = conn
1852            .query_sync("INSERT INTO exact_codes VALUES (1) RETURNING value", &[])
1853            .expect_err("query_sync must retain a step failure before finalization");
1854        for error in [&execute_step_error, &query_step_error] {
1855            let code = sqlite_error_code(error)
1856                .expect("step failures must retain their extended SQLite result code");
1857            assert_eq!(code.primary(), ffi::SQLITE_CONSTRAINT);
1858            assert_eq!(code.extended(), ffi::SQLITE_CONSTRAINT_UNIQUE);
1859            assert!(
1860                matches!(error, Error::Query(query) if query.kind == QueryErrorKind::Constraint),
1861                "unique violations should map to the constraint error family: {error}"
1862            );
1863        }
1864    }
1865
1866    #[test]
1867    fn test_empty_prepared_sql_is_rejected_before_statement_ffi() {
1868        let conn = SqliteConnection::open_memory().unwrap();
1869
1870        for sql in ["", " \n\t", "-- comment only\n", "/* comment only */"] {
1871            for error in [
1872                conn.query_sync(sql, &[])
1873                    .expect_err("empty query SQL must not produce a null statement"),
1874                conn.execute_sync(sql, &[])
1875                    .expect_err("empty execute SQL must not produce a null statement"),
1876            ] {
1877                assert!(
1878                    matches!(error, Error::Query(ref query) if query.kind == QueryErrorKind::Syntax),
1879                    "empty prepared SQL should be a typed syntax error: {error}"
1880                );
1881                assert!(error.to_string().contains("no executable statement"));
1882                assert_eq!(sqlite_error_code(&error), None);
1883            }
1884        }
1885    }
1886
1887    #[test]
1888    fn test_execute_returning_steps_until_done_and_retains_late_busy() {
1889        let path = unique_temp_db_path("returning_busy");
1890        let _ = std::fs::remove_file(&path);
1891        let config = SqliteConfig::file(path.to_string_lossy().into_owned()).busy_timeout(0);
1892        let writer = SqliteConnection::open(&config).unwrap();
1893        writer.execute_raw("PRAGMA journal_mode=DELETE").unwrap();
1894        writer
1895            .execute_raw("CREATE TABLE returning_rows (value INTEGER)")
1896            .unwrap();
1897        let reader = SqliteConnection::open(&config).unwrap();
1898        reader.execute_raw("BEGIN DEFERRED").unwrap();
1899        reader
1900            .query_sync("SELECT COUNT(*) FROM returning_rows", &[])
1901            .unwrap();
1902
1903        let error = writer
1904            .execute_sync("INSERT INTO returning_rows VALUES (7) RETURNING value", &[])
1905            .expect_err("the read lock must surface after RETURNING rows but before DONE");
1906        let code = sqlite_error_code(&error)
1907            .expect("late RETURNING completion failure must retain its native code");
1908        assert_eq!(code.primary(), ffi::SQLITE_BUSY);
1909        assert!(
1910            matches!(error, Error::Query(ref query) if query.kind == QueryErrorKind::Deadlock),
1911            "late SQLITE_BUSY should map to the deadlock family: {error}"
1912        );
1913
1914        reader.execute_raw("ROLLBACK").unwrap();
1915        let rows = writer
1916            .query_sync("SELECT COUNT(*) AS row_count FROM returning_rows", &[])
1917            .unwrap();
1918        assert_eq!(rows[0].get_named::<i64>("row_count").unwrap(), 0);
1919        drop(reader);
1920        drop(writer);
1921        let _ = std::fs::remove_file(path);
1922    }
1923
1924    #[test]
1925    fn test_non_native_connection_preflights_have_no_sqlite_code() {
1926        let invalid_timeout = SqliteConfig::memory().busy_timeout(c_int::MAX as u32 + 1);
1927        let Err(timeout_error) = SqliteConnection::open(&invalid_timeout) else {
1928            panic!("out-of-range native busy timeout must fail closed");
1929        };
1930        assert!(matches!(timeout_error, Error::Config(_)));
1931        assert_eq!(sqlite_error_code(&timeout_error), None);
1932
1933        let conn = SqliteConnection::open_memory().unwrap();
1934        let backup_error = conn
1935            .backup_to_connection(&conn)
1936            .expect_err("backing a connection up onto itself must fail before locking");
1937        assert!(
1938            backup_error
1939                .to_string()
1940                .contains("source and destination must be different")
1941        );
1942        assert_eq!(sqlite_error_code(&backup_error), None);
1943
1944        let contradictory_cache_flags = SqliteConfig::memory().flags(OpenFlags {
1945            shared_cache: true,
1946            private_cache: true,
1947            ..OpenFlags::create_read_write()
1948        });
1949        let Err(cache_error) = SqliteConnection::open(&contradictory_cache_flags) else {
1950            panic!("contradictory SQLite cache flags must fail before native open");
1951        };
1952        assert!(matches!(cache_error, Error::Config(_)));
1953        assert_eq!(sqlite_error_code(&cache_error), None);
1954        assert_ne!(
1955            OpenFlags::create_read_write().to_sqlite_flags() & ffi::SQLITE_OPEN_PRIVATECACHE,
1956            0,
1957            "ordinary opens must override process-global shared-cache mode"
1958        );
1959    }
1960
1961    #[test]
1962    fn test_backup_copies_data_and_opposite_directions_do_not_deadlock() {
1963        let left = Arc::new(SqliteConnection::open_memory().unwrap());
1964        let right = Arc::new(SqliteConnection::open_memory().unwrap());
1965        left.execute_raw("CREATE TABLE backup_rows (value INTEGER)")
1966            .unwrap();
1967        left.execute_raw("INSERT INTO backup_rows VALUES (7)")
1968            .unwrap();
1969
1970        left.backup_to_connection(&right)
1971            .expect("ordinary backup should copy the source database");
1972        let copied = right
1973            .query_sync("SELECT value FROM backup_rows", &[])
1974            .expect("copied table should be readable");
1975        assert_eq!(copied[0].get_named::<i64>("value").unwrap(), 7);
1976
1977        let barrier = Arc::new(std::sync::Barrier::new(3));
1978        let (completed_tx, completed_rx) = std::sync::mpsc::channel();
1979        let mut workers = Vec::new();
1980        for (source, destination) in [
1981            (Arc::clone(&left), Arc::clone(&right)),
1982            (Arc::clone(&right), Arc::clone(&left)),
1983        ] {
1984            let worker_barrier = Arc::clone(&barrier);
1985            let worker_tx = completed_tx.clone();
1986            workers.push(std::thread::spawn(move || {
1987                worker_barrier.wait();
1988                let result = source.backup_to_connection(&destination);
1989                worker_tx.send(result).expect("test receiver remains live");
1990            }));
1991        }
1992        drop(completed_tx);
1993        barrier.wait();
1994        for _ in 0..2 {
1995            completed_rx
1996                .recv_timeout(Duration::from_secs(5))
1997                .expect("opposing backups must not deadlock")
1998                .expect("serialized opposing backup should succeed");
1999        }
2000        for worker in workers {
2001            worker.join().expect("backup worker should not panic");
2002        }
2003    }
2004
2005    #[test]
2006    fn test_backup_lock_retries_respect_deadline_and_restore_busy_timeouts() {
2007        let source = SqliteConnection::open(&SqliteConfig::memory().busy_timeout(500)).unwrap();
2008        source
2009            .execute_raw("CREATE TABLE backup_rows (value INTEGER)")
2010            .unwrap();
2011        source
2012            .execute_raw("INSERT INTO backup_rows VALUES (7)")
2013            .unwrap();
2014
2015        let path = unique_temp_db_path("backup_deadline");
2016        let _ = std::fs::remove_file(&path);
2017        let destination_config =
2018            SqliteConfig::file(path.to_string_lossy().into_owned()).busy_timeout(450);
2019        let destination = SqliteConnection::open(&destination_config).unwrap();
2020        destination
2021            .execute_raw("CREATE TABLE old_rows (value INTEGER)")
2022            .unwrap();
2023        source.execute_raw("PRAGMA busy_timeout=520").unwrap();
2024        destination.execute_raw("PRAGMA busy_timeout=470").unwrap();
2025        let blocker = SqliteConnection::open(&destination_config).unwrap();
2026        blocker.execute_raw("BEGIN EXCLUSIVE").unwrap();
2027
2028        let started = Instant::now();
2029        let error = source
2030            .backup_to_connection(&destination)
2031            .expect_err("an exclusive destination lock must block the backup");
2032        let elapsed = started.elapsed();
2033        let code = sqlite_error_code(&error)
2034            .expect("a lock-blocked backup must retain its native SQLite result code");
2035        assert!(
2036            matches!(code.primary(), ffi::SQLITE_BUSY | ffi::SQLITE_LOCKED),
2037            "unexpected lock failure code: {code}"
2038        );
2039        assert!(
2040            elapsed < Duration::from_millis(800),
2041            "backup retry deadline overran by a native busy-timeout interval: {elapsed:?}"
2042        );
2043
2044        blocker.execute_raw("ROLLBACK").unwrap();
2045        for (connection, expected_timeout) in [(&source, 520), (&destination, 470)] {
2046            let rows = connection.query_sync("PRAGMA busy_timeout", &[]).unwrap();
2047            assert_eq!(
2048                rows[0].get_named::<i32>("timeout").unwrap(),
2049                expected_timeout
2050            );
2051        }
2052
2053        drop(blocker);
2054        drop(destination);
2055        let _ = std::fs::remove_file(path);
2056    }
2057
2058    #[test]
2059    fn test_backup_rejects_shared_cache_destination_before_locking() {
2060        let source = SqliteConnection::open_memory().unwrap();
2061        let shared_uri = format!(
2062            "file:sqlmodel_backup_shared_{}?mode=memory&cache=shared",
2063            std::process::id()
2064        );
2065        let destination =
2066            SqliteConnection::open(&SqliteConfig::file(shared_uri).flags(OpenFlags {
2067                uri: true,
2068                ..OpenFlags::create_read_write()
2069            }))
2070            .expect("shared-cache connection should open for the preflight test");
2071        assert!(destination.uses_shared_cache);
2072
2073        let error = source
2074            .backup_to_connection(&destination)
2075            .expect_err("shared-cache backup destination must fail closed");
2076        assert!(error.to_string().contains("shared-cache mode"));
2077        assert_eq!(sqlite_error_code(&error), None);
2078
2079        let plain_memory = SqliteConnection::open(&SqliteConfig::memory().flags(OpenFlags {
2080            shared_cache: true,
2081            ..OpenFlags::create_read_write()
2082        }))
2083        .expect("plain :memory: remains private even with SHAREDCACHE requested");
2084        assert!(!plain_memory.uses_shared_cache);
2085        source
2086            .backup_to_connection(&plain_memory)
2087            .expect("a truly private in-memory destination is backup-safe");
2088
2089        let private_uri = format!(
2090            "file:sqlmodel_backup_private_{}?mode=memory&cache=private",
2091            std::process::id()
2092        );
2093        let uri_overrides_flag =
2094            SqliteConnection::open(&SqliteConfig::file(private_uri).flags(OpenFlags {
2095                uri: true,
2096                shared_cache: true,
2097                ..OpenFlags::create_read_write()
2098            }))
2099            .expect("URI cache=private should override the shared-cache open flag");
2100        assert!(!uri_overrides_flag.uses_shared_cache);
2101        source
2102            .backup_to_connection(&uri_overrides_flag)
2103            .expect("an effectively private URI destination is backup-safe");
2104    }
2105
2106    #[test]
2107    fn test_sqlite_uri_cache_mode_matches_sqlite_uri_rules() {
2108        assert_eq!(
2109            sqlite_uri_cache_mode("file:memory?mode=memory&cache=shared"),
2110            Some(true)
2111        );
2112        assert_eq!(
2113            sqlite_uri_cache_mode("file:memory?cache=private&cache=shared"),
2114            Some(true),
2115            "SQLite applies duplicate cache parameters in order, so the last one wins"
2116        );
2117        assert_eq!(
2118            sqlite_uri_cache_mode("file:memory?%63ache=%70rivate"),
2119            Some(false),
2120            "SQLite percent-decodes URI parameter names and values"
2121        );
2122        assert_eq!(
2123            sqlite_uri_cache_mode("file:memory?cache%00ignored=shared%00ignored"),
2124            Some(true),
2125            "SQLite truncates URI components at encoded NUL bytes"
2126        );
2127        assert_eq!(
2128            sqlite_uri_cache_mode("file:memory?CACHE=shared"),
2129            None,
2130            "SQLite URI parameter names are case-sensitive"
2131        );
2132        assert_eq!(
2133            sqlite_uri_cache_mode("file:memory?cache=shared#cache=private"),
2134            Some(true),
2135            "SQLite ignores URI fragments"
2136        );
2137    }
2138
2139    #[test]
2140    fn test_backup_failure_retains_native_destination_error() {
2141        let source = SqliteConnection::open_memory().unwrap();
2142        source
2143            .execute_raw("CREATE TABLE backup_source (value INTEGER)")
2144            .unwrap();
2145
2146        let path = unique_temp_db_path("readonly_backup");
2147        let writable = SqliteConnection::open_file(path.to_string_lossy().into_owned()).unwrap();
2148        writable
2149            .execute_raw("CREATE TABLE backup_destination (value INTEGER)")
2150            .unwrap();
2151        drop(writable);
2152        let destination = SqliteConnection::open(
2153            &SqliteConfig::file(path.to_string_lossy().into_owned()).flags(OpenFlags::read_only()),
2154        )
2155        .unwrap();
2156
2157        let error = source
2158            .backup_to_connection(&destination)
2159            .expect_err("read-only destination must reject backup writes");
2160        let code = sqlite_error_code(&error)
2161            .expect("native backup failure must retain its SQLite result code");
2162        assert_eq!(code.primary(), ffi::SQLITE_READONLY);
2163        assert!(error.to_string().to_ascii_lowercase().contains("readonly"));
2164
2165        drop(destination);
2166        let _ = std::fs::remove_file(path);
2167    }
2168
2169    #[test]
2170    fn test_backup_step_error_preserves_direct_extended_result() {
2171        let direct_extended = ffi::SQLITE_IOERR | (42 << 8);
2172        let error = backup_step_error(std::ptr::null_mut(), direct_extended);
2173        let code = sqlite_error_code(&error)
2174            .expect("a backup-step failure must retain its direct native result");
2175        assert_eq!(code.primary(), ffi::SQLITE_IOERR);
2176        assert_eq!(code.extended(), direct_extended);
2177    }
2178
2179    #[test]
2180    fn test_null_handling() {
2181        let conn = SqliteConnection::open_memory().unwrap();
2182        conn.execute_raw("CREATE TABLE test (id INTEGER PRIMARY KEY, name TEXT)")
2183            .unwrap();
2184
2185        conn.execute_sync("INSERT INTO test (name) VALUES (?)", &[Value::Null])
2186            .unwrap();
2187
2188        let rows = conn.query_sync("SELECT * FROM test", &[]).unwrap();
2189        assert_eq!(rows.len(), 1);
2190        assert_eq!(rows[0].get_named::<Option<String>>("name").unwrap(), None);
2191    }
2192
2193    #[test]
2194    fn test_transaction() {
2195        let conn = SqliteConnection::open_memory().unwrap();
2196        conn.execute_raw("CREATE TABLE test (id INTEGER PRIMARY KEY, name TEXT)")
2197            .unwrap();
2198
2199        // Start transaction, insert, rollback
2200        conn.begin_sync(IsolationLevel::default()).unwrap();
2201        conn.execute_sync(
2202            "INSERT INTO test (name) VALUES (?)",
2203            &[Value::Text("Alice".to_string())],
2204        )
2205        .unwrap();
2206        conn.rollback_sync().unwrap();
2207
2208        // Verify rollback worked
2209        let rows = conn.query_sync("SELECT * FROM test", &[]).unwrap();
2210        assert_eq!(rows.len(), 0);
2211
2212        // Start transaction, insert, commit
2213        conn.begin_sync(IsolationLevel::default()).unwrap();
2214        conn.execute_sync(
2215            "INSERT INTO test (name) VALUES (?)",
2216            &[Value::Text("Bob".to_string())],
2217        )
2218        .unwrap();
2219        conn.commit_sync().unwrap();
2220
2221        // Verify commit worked
2222        let rows = conn.query_sync("SELECT * FROM test", &[]).unwrap();
2223        assert_eq!(rows.len(), 1);
2224        assert_eq!(rows[0].get_named::<String>("name").unwrap(), "Bob");
2225    }
2226
2227    #[test]
2228    fn test_insert_rowid() {
2229        let conn = SqliteConnection::open_memory().unwrap();
2230        conn.execute_raw("CREATE TABLE test (id INTEGER PRIMARY KEY, name TEXT)")
2231            .unwrap();
2232
2233        let rowid = conn
2234            .insert_sync(
2235                "INSERT INTO test (name) VALUES (?)",
2236                &[Value::Text("Alice".to_string())],
2237            )
2238            .unwrap();
2239        assert_eq!(rowid, 1);
2240
2241        let rowid = conn
2242            .insert_sync(
2243                "INSERT INTO test (name) VALUES (?)",
2244                &[Value::Text("Bob".to_string())],
2245            )
2246            .unwrap();
2247        assert_eq!(rowid, 2);
2248    }
2249
2250    #[test]
2251    #[allow(clippy::approx_constant)]
2252    fn test_type_conversions() {
2253        let conn = SqliteConnection::open_memory().unwrap();
2254        conn.execute_raw(
2255            "CREATE TABLE types (
2256                b BOOLEAN,
2257                i INTEGER,
2258                f REAL,
2259                t TEXT,
2260                bl BLOB
2261            )",
2262        )
2263        .unwrap();
2264
2265        conn.execute_sync(
2266            "INSERT INTO types VALUES (?, ?, ?, ?, ?)",
2267            &[
2268                Value::Bool(true),
2269                Value::BigInt(42),
2270                Value::Double(3.14),
2271                Value::Text("hello".to_string()),
2272                Value::Bytes(vec![1, 2, 3]),
2273            ],
2274        )
2275        .unwrap();
2276
2277        let rows = conn.query_sync("SELECT * FROM types", &[]).unwrap();
2278        assert_eq!(rows.len(), 1);
2279
2280        // SQLite stores booleans as integers
2281        let b: i32 = rows[0].get_named("b").unwrap();
2282        assert_eq!(b, 1);
2283
2284        let i: i32 = rows[0].get_named("i").unwrap();
2285        assert_eq!(i, 42);
2286
2287        let f: f64 = rows[0].get_named("f").unwrap();
2288        assert!((f - 3.14).abs() < 0.001);
2289
2290        let t: String = rows[0].get_named("t").unwrap();
2291        assert_eq!(t, "hello");
2292
2293        let bl: Vec<u8> = rows[0].get_named("bl").unwrap();
2294        assert_eq!(bl, vec![1, 2, 3]);
2295    }
2296
2297    #[test]
2298    fn test_open_flags() {
2299        // Test creating a database with create flag
2300        let tmp = unique_temp_db_path("open_flags");
2301        let _ = std::fs::remove_file(&tmp); // Ensure it doesn't exist
2302
2303        let config = SqliteConfig::file(tmp.to_string_lossy().to_string())
2304            .flags(OpenFlags::create_read_write());
2305        let conn = SqliteConnection::open(&config).unwrap();
2306        conn.execute_raw("CREATE TABLE test (id INTEGER)").unwrap();
2307        drop(conn);
2308
2309        // Open as read-only
2310        let config =
2311            SqliteConfig::file(tmp.to_string_lossy().to_string()).flags(OpenFlags::read_only());
2312        let conn = SqliteConnection::open(&config).unwrap();
2313
2314        // Reading should work
2315        let rows = conn.query_sync("SELECT * FROM test", &[]).unwrap();
2316        assert_eq!(rows.len(), 0);
2317
2318        // Writing should fail
2319        let error = conn
2320            .execute_raw("INSERT INTO test VALUES (1)")
2321            .expect_err("read-only connection must reject writes");
2322        let error_code = sqlite_error_code(&error)
2323            .expect("native write rejection must retain its exact SQLite result code");
2324        assert_eq!(error_code.primary(), ffi::SQLITE_READONLY);
2325        assert_eq!(error_code.extended() & 0xff, ffi::SQLITE_READONLY);
2326        assert!(
2327            matches!(error, Error::Query(ref query) if query.kind == QueryErrorKind::Permission),
2328            "SQLITE_READONLY should map to the permission error family: {error}"
2329        );
2330
2331        let prepared_error = conn
2332            .execute_sync("INSERT INTO test VALUES (1)", &[])
2333            .expect_err("prepared writes must also retain SQLITE_READONLY");
2334        let prepared_code = sqlite_error_code(&prepared_error)
2335            .expect("prepared write rejection must retain its native result code");
2336        assert_eq!(prepared_code.primary(), ffi::SQLITE_READONLY);
2337        assert!(
2338            matches!(prepared_error, Error::Query(ref query) if query.kind == QueryErrorKind::Permission),
2339            "prepared SQLITE_READONLY should map to permission: {prepared_error}"
2340        );
2341
2342        drop(conn);
2343        let _ = std::fs::remove_file(&tmp);
2344    }
2345
2346    // ==================== Console Integration Tests ====================
2347
2348    #[cfg(feature = "console")]
2349    mod console_tests {
2350        use super::*;
2351
2352        /// Test that ConsoleAware trait is properly implemented.
2353        #[test]
2354        fn test_console_aware_trait_impl() {
2355            let mut conn = SqliteConnection::open_memory().unwrap();
2356
2357            // Initially no console
2358            assert!(!conn.has_console());
2359            assert!(conn.console().is_none());
2360
2361            // Attach console
2362            let console = Arc::new(SqlModelConsole::with_mode(
2363                sqlmodel_console::OutputMode::Plain,
2364            ));
2365            conn.set_console(Some(console.clone()));
2366
2367            // Verify console is attached
2368            assert!(conn.has_console());
2369            assert!(conn.console().is_some());
2370
2371            // Detach console
2372            conn.set_console(None);
2373            assert!(!conn.has_console());
2374        }
2375
2376        /// Test database open feedback is emitted when console is attached.
2377        #[test]
2378        fn test_database_open_feedback() {
2379            let mut conn = SqliteConnection::open_memory().unwrap();
2380
2381            // Attaching console should emit open status
2382            // (output goes to stderr, we just verify no panic)
2383            let console = Arc::new(SqlModelConsole::with_mode(
2384                sqlmodel_console::OutputMode::Plain,
2385            ));
2386            conn.set_console(Some(console));
2387
2388            // No panic means success
2389        }
2390
2391        /// Test PRAGMA query formatting.
2392        #[test]
2393        fn test_pragma_formatting() {
2394            let mut conn = SqliteConnection::open_memory().unwrap();
2395
2396            // Create a table to have something in pragma_table_info
2397            conn.execute_raw("CREATE TABLE test (id INTEGER PRIMARY KEY, name TEXT)")
2398                .unwrap();
2399
2400            // Attach console for formatted output
2401            let console = Arc::new(SqlModelConsole::with_mode(
2402                sqlmodel_console::OutputMode::Plain,
2403            ));
2404            conn.set_console(Some(console));
2405
2406            // Execute PRAGMA query - should format as table
2407            let rows = conn.query_sync("PRAGMA table_info(test)", &[]).unwrap();
2408
2409            // Verify we got the expected columns
2410            assert!(!rows.is_empty());
2411        }
2412
2413        /// Test transaction state display.
2414        #[test]
2415        fn test_transaction_state() {
2416            let mut conn = SqliteConnection::open_memory().unwrap();
2417            conn.execute_raw("CREATE TABLE test (id INTEGER PRIMARY KEY)")
2418                .unwrap();
2419
2420            // Attach console
2421            let console = Arc::new(SqlModelConsole::with_mode(
2422                sqlmodel_console::OutputMode::Plain,
2423            ));
2424            conn.set_console(Some(console));
2425
2426            // Transaction operations should emit state
2427            conn.begin_sync(IsolationLevel::default()).unwrap();
2428            conn.execute_sync("INSERT INTO test (id) VALUES (?)", &[Value::Int(1)])
2429                .unwrap();
2430            conn.commit_sync().unwrap();
2431
2432            // Verify the transaction worked
2433            let rows = conn.query_sync("SELECT * FROM test", &[]).unwrap();
2434            assert_eq!(rows.len(), 1);
2435        }
2436
2437        /// Test WAL checkpoint progress output.
2438        #[test]
2439        fn test_wal_checkpoint_progress() {
2440            let conn = SqliteConnection::open_memory().unwrap();
2441
2442            // emit_checkpoint_progress should not panic
2443            conn.emit_checkpoint_progress(50, 100);
2444            conn.emit_checkpoint_progress(100, 100);
2445            conn.emit_checkpoint_progress(0, 0);
2446        }
2447
2448        /// Test busy timeout feedback output.
2449        #[test]
2450        fn test_busy_timeout_feedback() {
2451            let conn = SqliteConnection::open_memory().unwrap();
2452
2453            // emit_busy_waiting should not panic
2454            conn.emit_busy_waiting(0.5);
2455            conn.emit_busy_waiting(2.1);
2456        }
2457
2458        /// Test that console disabled produces no output (no panic).
2459        #[test]
2460        fn test_console_disabled_no_output() {
2461            let conn = SqliteConnection::open_memory().unwrap();
2462
2463            // Without console, all emit methods should be no-ops
2464            conn.emit_busy_waiting(1.0);
2465            conn.emit_checkpoint_progress(10, 100);
2466
2467            // Query should work without console
2468            conn.execute_raw("CREATE TABLE test (id INTEGER PRIMARY KEY)")
2469                .unwrap();
2470            let rows = conn.query_sync("SELECT * FROM test", &[]).unwrap();
2471            assert_eq!(rows.len(), 0);
2472        }
2473
2474        /// Test plain mode output format (parseable by agents).
2475        #[test]
2476        fn test_plain_mode_output() {
2477            let mut conn = SqliteConnection::open_memory().unwrap();
2478
2479            // Attach plain mode console
2480            let console = Arc::new(SqlModelConsole::with_mode(
2481                sqlmodel_console::OutputMode::Plain,
2482            ));
2483            conn.set_console(Some(console.clone()));
2484
2485            // Verify plain mode is active
2486            assert!(conn.console().unwrap().is_plain());
2487
2488            // Execute operations (output should be plain text)
2489            conn.execute_raw("CREATE TABLE test (id INTEGER PRIMARY KEY, name TEXT)")
2490                .unwrap();
2491            conn.execute_sync(
2492                "INSERT INTO test (name) VALUES (?)",
2493                &[Value::Text("Alice".to_string())],
2494            )
2495            .unwrap();
2496
2497            let rows = conn.query_sync("PRAGMA table_info(test)", &[]).unwrap();
2498            assert!(!rows.is_empty());
2499        }
2500
2501        /// Test rich mode output format.
2502        #[test]
2503        fn test_rich_mode_output() {
2504            let mut conn = SqliteConnection::open_memory().unwrap();
2505
2506            // Attach rich mode console
2507            let console = Arc::new(SqlModelConsole::with_mode(
2508                sqlmodel_console::OutputMode::Rich,
2509            ));
2510            conn.set_console(Some(console.clone()));
2511
2512            // Verify rich mode is active
2513            assert!(conn.console().unwrap().is_rich());
2514
2515            // Execute operations (output should have formatting)
2516            conn.execute_raw("CREATE TABLE test (id INTEGER PRIMARY KEY)")
2517                .unwrap();
2518            conn.emit_checkpoint_progress(50, 100);
2519        }
2520    }
2521}