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::{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;
44
45#[cfg(feature = "console")]
46use sqlmodel_console::{ConsoleAware, SqlModelConsole};
47
48/// Configuration for opening SQLite connections.
49#[derive(Debug, Clone)]
50pub struct SqliteConfig {
51    /// Path to the database file, or ":memory:" for in-memory database.
52    pub path: String,
53    /// Open flags (read-only, read-write, create, etc.)
54    pub flags: OpenFlags,
55    /// Busy timeout in milliseconds.
56    pub busy_timeout_ms: u32,
57}
58
59/// Flags controlling how the database is opened.
60#[derive(Debug, Clone, Copy, Default)]
61pub struct OpenFlags {
62    /// Open for reading only.
63    pub read_only: bool,
64    /// Open for reading and writing.
65    pub read_write: bool,
66    /// Create the database if it doesn't exist.
67    pub create: bool,
68    /// Enable URI filename interpretation.
69    pub uri: bool,
70    /// Open in multi-thread mode (connections not shared between threads).
71    pub no_mutex: bool,
72    /// Open in serialized mode (connections can be shared).
73    pub full_mutex: bool,
74    /// Enable shared cache mode.
75    pub shared_cache: bool,
76    /// Disable shared cache mode.
77    pub private_cache: bool,
78}
79
80impl OpenFlags {
81    /// Create flags for read-only access.
82    pub fn read_only() -> Self {
83        Self {
84            read_only: true,
85            ..Default::default()
86        }
87    }
88
89    /// Create flags for read-write access (database must exist).
90    pub fn read_write() -> Self {
91        Self {
92            read_write: true,
93            ..Default::default()
94        }
95    }
96
97    /// Create flags for read-write access with creation if needed.
98    pub fn create_read_write() -> Self {
99        Self {
100            read_write: true,
101            create: true,
102            ..Default::default()
103        }
104    }
105
106    fn to_sqlite_flags(self) -> c_int {
107        let mut flags = 0;
108
109        if self.read_only {
110            flags |= ffi::SQLITE_OPEN_READONLY;
111        }
112        if self.read_write {
113            flags |= ffi::SQLITE_OPEN_READWRITE;
114        }
115        if self.create {
116            flags |= ffi::SQLITE_OPEN_CREATE;
117        }
118        if self.uri {
119            flags |= ffi::SQLITE_OPEN_URI;
120        }
121        if self.no_mutex {
122            flags |= ffi::SQLITE_OPEN_NOMUTEX;
123        }
124        if self.full_mutex {
125            flags |= ffi::SQLITE_OPEN_FULLMUTEX;
126        }
127        if self.shared_cache {
128            flags |= ffi::SQLITE_OPEN_SHAREDCACHE;
129        }
130        if self.private_cache {
131            flags |= ffi::SQLITE_OPEN_PRIVATECACHE;
132        }
133
134        // Default to read-write if no mode specified
135        if flags & (ffi::SQLITE_OPEN_READONLY | ffi::SQLITE_OPEN_READWRITE) == 0 {
136            flags |= ffi::SQLITE_OPEN_READWRITE | ffi::SQLITE_OPEN_CREATE;
137        }
138
139        flags
140    }
141}
142
143impl Default for SqliteConfig {
144    fn default() -> Self {
145        Self {
146            path: ":memory:".to_string(),
147            flags: OpenFlags::create_read_write(),
148            busy_timeout_ms: 5000,
149        }
150    }
151}
152
153impl SqliteConfig {
154    /// Create a new config for a file-based database.
155    pub fn file(path: impl Into<String>) -> Self {
156        Self {
157            path: path.into(),
158            flags: OpenFlags::create_read_write(),
159            busy_timeout_ms: 5000,
160        }
161    }
162
163    /// Create a new config for an in-memory database.
164    pub fn memory() -> Self {
165        Self::default()
166    }
167
168    /// Set open flags.
169    pub fn flags(mut self, flags: OpenFlags) -> Self {
170        self.flags = flags;
171        self
172    }
173
174    /// Set busy timeout.
175    pub fn busy_timeout(mut self, ms: u32) -> Self {
176        self.busy_timeout_ms = ms;
177        self
178    }
179}
180
181/// Inner state of the SQLite connection, protected by a mutex for thread safety.
182struct SqliteInner {
183    db: *mut ffi::sqlite3,
184    in_transaction: bool,
185}
186
187// SAFETY: SQLite handles can be safely sent between threads when using
188// SQLITE_OPEN_FULLMUTEX (serialized mode) or when properly synchronized.
189// We use a Mutex to ensure synchronization.
190unsafe impl Send for SqliteInner {}
191
192/// A connection to a SQLite database.
193///
194/// This is a thread-safe wrapper around a SQLite database handle.
195pub struct SqliteConnection {
196    inner: Mutex<SqliteInner>,
197    path: String,
198    /// Optional console for rich output
199    #[cfg(feature = "console")]
200    console: Option<Arc<SqlModelConsole>>,
201}
202
203// SqliteConnection is Send + Sync because all access goes through the Mutex
204unsafe impl Send for SqliteConnection {}
205unsafe impl Sync for SqliteConnection {}
206
207impl SqliteConnection {
208    /// Open a new SQLite connection with the given configuration.
209    pub fn open(config: &SqliteConfig) -> Result<Self, Error> {
210        let c_path = CString::new(config.path.as_str()).map_err(|_| {
211            Error::Connection(ConnectionError {
212                kind: ConnectionErrorKind::Connect,
213                message: "Invalid path: contains null byte".to_string(),
214                source: None,
215            })
216        })?;
217
218        let mut db: *mut ffi::sqlite3 = ptr::null_mut();
219        let flags = config.flags.to_sqlite_flags();
220
221        // SAFETY: We pass valid pointers and check the return value
222        let rc = unsafe { ffi::sqlite3_open_v2(c_path.as_ptr(), &mut db, flags, ptr::null()) };
223
224        if rc != ffi::SQLITE_OK {
225            let msg = if !db.is_null() {
226                // SAFETY: db is valid, errmsg returns a valid C string
227                unsafe {
228                    let err_ptr = ffi::sqlite3_errmsg(db);
229                    let msg = CStr::from_ptr(err_ptr).to_string_lossy().into_owned();
230                    ffi::sqlite3_close(db);
231                    msg
232                }
233            } else {
234                ffi::error_string(rc).to_string()
235            };
236
237            return Err(Error::Connection(ConnectionError {
238                kind: ConnectionErrorKind::Connect,
239                message: format!("Failed to open database: {}", msg),
240                source: None,
241            }));
242        }
243
244        // Set busy timeout
245        if config.busy_timeout_ms > 0 {
246            // SAFETY: db is valid
247            unsafe {
248                ffi::sqlite3_busy_timeout(db, config.busy_timeout_ms as c_int);
249            }
250        }
251
252        Ok(Self {
253            inner: Mutex::new(SqliteInner {
254                db,
255                in_transaction: false,
256            }),
257            path: config.path.clone(),
258            #[cfg(feature = "console")]
259            console: None,
260        })
261    }
262
263    /// Open an in-memory database.
264    pub fn open_memory() -> Result<Self, Error> {
265        Self::open(&SqliteConfig::memory())
266    }
267
268    /// Open a file-based database.
269    pub fn open_file(path: impl Into<String>) -> Result<Self, Error> {
270        Self::open(&SqliteConfig::file(path))
271    }
272
273    /// Get the database path.
274    pub fn path(&self) -> &str {
275        &self.path
276    }
277
278    /// Execute SQL directly without preparing (for DDL, etc.)
279    pub fn execute_raw(&self, sql: &str) -> Result<(), Error> {
280        let inner = self.inner.lock().unwrap_or_else(|e| e.into_inner());
281        let c_sql = CString::new(sql).map_err(|_| {
282            Error::Query(QueryError {
283                kind: QueryErrorKind::Syntax,
284                sql: Some(sql.to_string()),
285                sqlstate: None,
286                message: "SQL contains null byte".to_string(),
287                detail: None,
288                hint: None,
289                position: None,
290                source: None,
291            })
292        })?;
293
294        let mut errmsg: *mut std::ffi::c_char = ptr::null_mut();
295
296        // SAFETY: All pointers are valid
297        let rc = unsafe {
298            ffi::sqlite3_exec(inner.db, c_sql.as_ptr(), None, ptr::null_mut(), &mut errmsg)
299        };
300
301        if rc != ffi::SQLITE_OK {
302            let msg = if !errmsg.is_null() {
303                // SAFETY: errmsg is valid
304                let msg = unsafe { CStr::from_ptr(errmsg).to_string_lossy().into_owned() };
305                unsafe { ffi::sqlite3_free(errmsg.cast()) };
306                msg
307            } else {
308                ffi::error_string(rc).to_string()
309            };
310
311            return Err(Error::Query(QueryError {
312                kind: error_code_to_kind(rc),
313                sql: Some(sql.to_string()),
314                sqlstate: None,
315                message: msg,
316                detail: None,
317                hint: None,
318                position: None,
319                source: None,
320            }));
321        }
322
323        Ok(())
324    }
325
326    /// Backup the current database to a destination path using the SQLite backup API.
327    ///
328    /// This opens (or creates) the destination database and performs an online backup
329    /// from this connection's `main` database into the destination's `main` database.
330    pub fn backup_to_path(&self, dest_path: impl AsRef<str>) -> Result<(), Error> {
331        let dest = SqliteConnection::open(
332            &SqliteConfig::file(dest_path.as_ref()).flags(OpenFlags::create_read_write()),
333        )?;
334        self.backup_to_connection(&dest)
335    }
336
337    /// Backup the current database to another open SQLite connection.
338    pub fn backup_to_connection(&self, dest: &SqliteConnection) -> Result<(), Error> {
339        let self_first = (std::ptr::from_ref(self) as usize) <= (std::ptr::from_ref(dest) as usize);
340        let (source_guard, dest_guard) = if self_first {
341            let source_guard = self.inner.lock().unwrap_or_else(|e| e.into_inner());
342            let dest_guard = dest.inner.lock().unwrap_or_else(|e| e.into_inner());
343            (source_guard, dest_guard)
344        } else {
345            let dest_guard = dest.inner.lock().unwrap_or_else(|e| e.into_inner());
346            let source_guard = self.inner.lock().unwrap_or_else(|e| e.into_inner());
347            (source_guard, dest_guard)
348        };
349
350        let source_db = source_guard.db;
351        let dest_db = dest_guard.db;
352
353        let main = CString::new("main").expect("static sqlite db name");
354
355        // SAFETY: We hold locks on both connections; db pointers are valid.
356        let backup =
357            unsafe { ffi::sqlite3_backup_init(dest_db, main.as_ptr(), source_db, main.as_ptr()) };
358        if backup.is_null() {
359            let msg = unsafe { CStr::from_ptr(ffi::sqlite3_errmsg(dest_db)) }
360                .to_string_lossy()
361                .into_owned();
362            return Err(Error::Connection(ConnectionError {
363                kind: ConnectionErrorKind::Connect,
364                message: format!("SQLite backup init failed: {msg}"),
365                source: None,
366            }));
367        }
368
369        let mut rc = unsafe { ffi::sqlite3_backup_step(backup, 100) };
370        loop {
371            if rc == ffi::SQLITE_DONE {
372                break;
373            }
374            if rc == ffi::SQLITE_OK {
375                rc = unsafe { ffi::sqlite3_backup_step(backup, 100) };
376                continue;
377            }
378            if rc == ffi::SQLITE_BUSY || rc == ffi::SQLITE_LOCKED {
379                std::thread::sleep(Duration::from_millis(50));
380                rc = unsafe { ffi::sqlite3_backup_step(backup, 100) };
381                continue;
382            }
383            break;
384        }
385
386        let finish_rc = unsafe { ffi::sqlite3_backup_finish(backup) };
387
388        if rc != ffi::SQLITE_DONE && rc != ffi::SQLITE_OK {
389            let msg = unsafe { CStr::from_ptr(ffi::sqlite3_errmsg(dest_db)) }
390                .to_string_lossy()
391                .into_owned();
392            return Err(Error::Connection(ConnectionError {
393                kind: ConnectionErrorKind::Connect,
394                message: format!("SQLite backup failed: {} ({})", msg, ffi::error_string(rc)),
395                source: None,
396            }));
397        }
398
399        if finish_rc != ffi::SQLITE_OK {
400            let msg = unsafe { CStr::from_ptr(ffi::sqlite3_errmsg(dest_db)) }
401                .to_string_lossy()
402                .into_owned();
403            return Err(Error::Connection(ConnectionError {
404                kind: ConnectionErrorKind::Connect,
405                message: format!(
406                    "SQLite backup finish failed: {} ({})",
407                    msg,
408                    ffi::error_string(finish_rc)
409                ),
410                source: None,
411            }));
412        }
413
414        Ok(())
415    }
416
417    /// Get the last insert rowid.
418    pub fn last_insert_rowid(&self) -> i64 {
419        let inner = self.inner.lock().unwrap_or_else(|e| e.into_inner());
420        // SAFETY: db is valid
421        unsafe { ffi::sqlite3_last_insert_rowid(inner.db) }
422    }
423
424    /// Get the number of rows changed by the last statement.
425    pub fn changes(&self) -> i32 {
426        let inner = self.inner.lock().unwrap_or_else(|e| e.into_inner());
427        // SAFETY: db is valid
428        unsafe { ffi::sqlite3_changes(inner.db) }
429    }
430
431    /// Prepare and execute a query synchronously, returning all rows.
432    ///
433    /// This is a blocking operation suitable for simple use cases.
434    /// For async usage, use the `Connection` trait methods instead.
435    pub fn query_sync(&self, sql: &str, params: &[Value]) -> Result<Vec<Row>, Error> {
436        #[cfg(feature = "console")]
437        let start = std::time::Instant::now();
438
439        let inner = self.inner.lock().unwrap_or_else(|e| e.into_inner());
440        let stmt = prepare_stmt(inner.db, sql)?;
441
442        // Bind parameters
443        for (i, param) in params.iter().enumerate() {
444            // SAFETY: stmt is valid, index is 1-based
445            let rc = unsafe { types::bind_value(stmt, (i + 1) as c_int, param) };
446            if rc != ffi::SQLITE_OK {
447                // SAFETY: stmt is valid
448                unsafe { ffi::sqlite3_finalize(stmt) };
449                return Err(bind_error(inner.db, sql, i + 1));
450            }
451        }
452
453        // Fetch column names
454        // SAFETY: stmt is valid
455        let col_count = unsafe { ffi::sqlite3_column_count(stmt) };
456        let mut col_names = Vec::with_capacity(col_count as usize);
457        for i in 0..col_count {
458            let name =
459                unsafe { types::column_name(stmt, i) }.unwrap_or_else(|| format!("col{}", i));
460            col_names.push(name);
461        }
462        let columns = Arc::new(ColumnInfo::new(col_names.clone()));
463
464        // Fetch rows
465        let mut rows = Vec::new();
466        loop {
467            // SAFETY: stmt is valid
468            let rc = unsafe { ffi::sqlite3_step(stmt) };
469            match rc {
470                ffi::SQLITE_ROW => {
471                    let mut values = Vec::with_capacity(col_count as usize);
472                    for i in 0..col_count {
473                        // SAFETY: stmt is valid, we just got SQLITE_ROW
474                        let value = unsafe { types::read_column(stmt, i) };
475                        values.push(value);
476                    }
477                    rows.push(Row::with_columns(Arc::clone(&columns), values));
478                }
479                ffi::SQLITE_DONE => break,
480                _ => {
481                    // SAFETY: stmt is valid
482                    unsafe { ffi::sqlite3_finalize(stmt) };
483                    return Err(step_error(inner.db, sql));
484                }
485            }
486        }
487
488        // SAFETY: stmt is valid
489        unsafe { ffi::sqlite3_finalize(stmt) };
490
491        // Emit console output for PRAGMA queries and timing
492        #[cfg(feature = "console")]
493        {
494            let elapsed_ms = start.elapsed().as_secs_f64() * 1000.0;
495            self.emit_query_result(sql, &col_names, &rows, elapsed_ms);
496        }
497
498        Ok(rows)
499    }
500
501    /// Prepare and execute a statement synchronously, returning rows affected.
502    ///
503    /// This is a blocking operation suitable for simple use cases.
504    /// For async usage, use the `Connection` trait methods instead.
505    pub fn execute_sync(&self, sql: &str, params: &[Value]) -> Result<u64, Error> {
506        #[cfg(feature = "console")]
507        let start = std::time::Instant::now();
508
509        let inner = self.inner.lock().unwrap_or_else(|e| e.into_inner());
510        let stmt = prepare_stmt(inner.db, sql)?;
511
512        // Bind parameters
513        for (i, param) in params.iter().enumerate() {
514            // SAFETY: stmt is valid
515            let rc = unsafe { types::bind_value(stmt, (i + 1) as c_int, param) };
516            if rc != ffi::SQLITE_OK {
517                // SAFETY: stmt is valid
518                unsafe { ffi::sqlite3_finalize(stmt) };
519                return Err(bind_error(inner.db, sql, i + 1));
520            }
521        }
522
523        // Execute
524        // SAFETY: stmt is valid
525        let rc = unsafe { ffi::sqlite3_step(stmt) };
526
527        // SAFETY: stmt is valid
528        unsafe { ffi::sqlite3_finalize(stmt) };
529
530        match rc {
531            ffi::SQLITE_DONE | ffi::SQLITE_ROW => {
532                // SAFETY: db is valid
533                let changes = unsafe { ffi::sqlite3_changes(inner.db) };
534
535                #[cfg(feature = "console")]
536                {
537                    let elapsed_ms = start.elapsed().as_secs_f64() * 1000.0;
538                    self.emit_execute_timing(sql, changes as u64, elapsed_ms);
539                }
540
541                Ok(changes as u64)
542            }
543            _ => Err(step_error(inner.db, sql)),
544        }
545    }
546
547    /// Execute an INSERT and return the last inserted rowid.
548    fn insert_sync(&self, sql: &str, params: &[Value]) -> Result<i64, Error> {
549        self.execute_sync(sql, params)?;
550        Ok(self.last_insert_rowid())
551    }
552
553    /// Begin a transaction.
554    fn begin_sync(&self, isolation: IsolationLevel) -> Result<(), Error> {
555        let inner = self.inner.lock().unwrap_or_else(|e| e.into_inner());
556        if inner.in_transaction {
557            return Err(Error::Query(QueryError {
558                kind: QueryErrorKind::Database,
559                sql: None,
560                sqlstate: None,
561                message: "Already in a transaction".to_string(),
562                detail: None,
563                hint: None,
564                position: None,
565                source: None,
566            }));
567        }
568
569        // SQLite doesn't support isolation levels in the same way as PostgreSQL,
570        // but we can approximate with different transaction types
571        let begin_sql = match isolation {
572            IsolationLevel::Serializable => "BEGIN EXCLUSIVE",
573            IsolationLevel::RepeatableRead | IsolationLevel::ReadCommitted => "BEGIN IMMEDIATE",
574            IsolationLevel::ReadUncommitted => "BEGIN DEFERRED",
575        };
576
577        drop(inner); // Release lock before calling execute_raw
578        self.execute_raw(begin_sql)?;
579
580        let mut inner = self.inner.lock().unwrap_or_else(|e| e.into_inner());
581        inner.in_transaction = true;
582        self.emit_transaction_state("BEGIN");
583        Ok(())
584    }
585
586    /// Commit the current transaction.
587    fn commit_sync(&self) -> Result<(), Error> {
588        let inner = self.inner.lock().unwrap_or_else(|e| e.into_inner());
589        if !inner.in_transaction {
590            return Err(Error::Query(QueryError {
591                kind: QueryErrorKind::Database,
592                sql: None,
593                sqlstate: None,
594                message: "Not in a transaction".to_string(),
595                detail: None,
596                hint: None,
597                position: None,
598                source: None,
599            }));
600        }
601
602        drop(inner);
603        self.execute_raw("COMMIT")?;
604
605        let mut inner = self.inner.lock().unwrap_or_else(|e| e.into_inner());
606        inner.in_transaction = false;
607        self.emit_transaction_state("COMMIT");
608        Ok(())
609    }
610
611    /// Rollback the current transaction.
612    fn rollback_sync(&self) -> Result<(), Error> {
613        let inner = self.inner.lock().unwrap_or_else(|e| e.into_inner());
614        if !inner.in_transaction {
615            return Err(Error::Query(QueryError {
616                kind: QueryErrorKind::Database,
617                sql: None,
618                sqlstate: None,
619                message: "Not in a transaction".to_string(),
620                detail: None,
621                hint: None,
622                position: None,
623                source: None,
624            }));
625        }
626
627        drop(inner);
628        self.execute_raw("ROLLBACK")?;
629
630        let mut inner = self.inner.lock().unwrap_or_else(|e| e.into_inner());
631        inner.in_transaction = false;
632        self.emit_transaction_state("ROLLBACK");
633        Ok(())
634    }
635}
636
637impl Drop for SqliteConnection {
638    fn drop(&mut self) {
639        if let Ok(inner) = self.inner.lock() {
640            if !inner.db.is_null() {
641                // SAFETY: db is valid
642                unsafe {
643                    ffi::sqlite3_close_v2(inner.db);
644                }
645            }
646        }
647    }
648}
649
650/// A SQLite transaction.
651pub struct SqliteTransaction<'conn> {
652    conn: &'conn SqliteConnection,
653    committed: bool,
654}
655
656impl<'conn> SqliteTransaction<'conn> {
657    fn new(conn: &'conn SqliteConnection) -> Self {
658        Self {
659            conn,
660            committed: false,
661        }
662    }
663}
664
665impl Drop for SqliteTransaction<'_> {
666    fn drop(&mut self) {
667        if !self.committed {
668            // Auto-rollback on drop if not committed
669            let _ = self.conn.rollback_sync();
670        }
671    }
672}
673
674// Implement Connection trait for SqliteConnection
675impl Connection for SqliteConnection {
676    type Tx<'conn>
677        = SqliteTransaction<'conn>
678    where
679        Self: 'conn;
680
681    fn dialect(&self) -> sqlmodel_core::Dialect {
682        sqlmodel_core::Dialect::Sqlite
683    }
684
685    fn query(
686        &self,
687        _cx: &Cx,
688        sql: &str,
689        params: &[Value],
690    ) -> impl Future<Output = Outcome<Vec<Row>, Error>> + Send {
691        let result = self.query_sync(sql, params);
692        async move { result.map_or_else(Outcome::Err, Outcome::Ok) }
693    }
694
695    fn query_one(
696        &self,
697        _cx: &Cx,
698        sql: &str,
699        params: &[Value],
700    ) -> impl Future<Output = Outcome<Option<Row>, Error>> + Send {
701        let result = self.query_sync(sql, params).map(|mut rows| rows.pop());
702        async move { result.map_or_else(Outcome::Err, Outcome::Ok) }
703    }
704
705    fn execute(
706        &self,
707        _cx: &Cx,
708        sql: &str,
709        params: &[Value],
710    ) -> impl Future<Output = Outcome<u64, Error>> + Send {
711        let result = self.execute_sync(sql, params);
712        async move { result.map_or_else(Outcome::Err, Outcome::Ok) }
713    }
714
715    fn insert(
716        &self,
717        _cx: &Cx,
718        sql: &str,
719        params: &[Value],
720    ) -> impl Future<Output = Outcome<i64, Error>> + Send {
721        let result = self.insert_sync(sql, params);
722        async move { result.map_or_else(Outcome::Err, Outcome::Ok) }
723    }
724
725    fn batch(
726        &self,
727        _cx: &Cx,
728        statements: &[(String, Vec<Value>)],
729    ) -> impl Future<Output = Outcome<Vec<u64>, Error>> + Send {
730        let mut results = Vec::with_capacity(statements.len());
731        let mut error = None;
732
733        for (sql, params) in statements {
734            match self.execute_sync(sql, params) {
735                Ok(n) => results.push(n),
736                Err(e) => {
737                    error = Some(e);
738                    break;
739                }
740            }
741        }
742
743        async move {
744            match error {
745                Some(e) => Outcome::Err(e),
746                None => Outcome::Ok(results),
747            }
748        }
749    }
750
751    fn begin(&self, cx: &Cx) -> impl Future<Output = Outcome<Self::Tx<'_>, Error>> + Send {
752        self.begin_with(cx, IsolationLevel::default())
753    }
754
755    fn begin_with(
756        &self,
757        _cx: &Cx,
758        isolation: IsolationLevel,
759    ) -> impl Future<Output = Outcome<Self::Tx<'_>, Error>> + Send {
760        let result = self
761            .begin_sync(isolation)
762            .map(|()| SqliteTransaction::new(self));
763        async move { result.map_or_else(Outcome::Err, Outcome::Ok) }
764    }
765
766    fn prepare(
767        &self,
768        _cx: &Cx,
769        sql: &str,
770    ) -> impl Future<Output = Outcome<PreparedStatement, Error>> + Send {
771        let inner = self.inner.lock().unwrap_or_else(|e| e.into_inner());
772        let result = prepare_stmt(inner.db, sql).map(|stmt| {
773            // SAFETY: stmt is valid
774            let param_count = unsafe { ffi::sqlite3_bind_parameter_count(stmt) } as usize;
775            let col_count = unsafe { ffi::sqlite3_column_count(stmt) } as c_int;
776
777            let mut columns = Vec::with_capacity(col_count as usize);
778            for i in 0..col_count {
779                if let Some(name) = unsafe { types::column_name(stmt, i) } {
780                    columns.push(name);
781                }
782            }
783
784            // SAFETY: stmt is valid
785            unsafe { ffi::sqlite3_finalize(stmt) };
786
787            // Use address as pseudo-ID since we don't cache statements yet
788            let id = sql.as_ptr() as u64;
789            PreparedStatement::with_columns(id, sql.to_string(), param_count, columns)
790        });
791
792        async move { result.map_or_else(Outcome::Err, Outcome::Ok) }
793    }
794
795    fn query_prepared(
796        &self,
797        cx: &Cx,
798        stmt: &PreparedStatement,
799        params: &[Value],
800    ) -> impl Future<Output = Outcome<Vec<Row>, Error>> + Send {
801        // For now, just re-execute the SQL
802        // Future optimization: cache prepared statements
803        self.query(cx, stmt.sql(), params)
804    }
805
806    fn execute_prepared(
807        &self,
808        cx: &Cx,
809        stmt: &PreparedStatement,
810        params: &[Value],
811    ) -> impl Future<Output = Outcome<u64, Error>> + Send {
812        self.execute(cx, stmt.sql(), params)
813    }
814
815    fn ping(&self, _cx: &Cx) -> impl Future<Output = Outcome<(), Error>> + Send {
816        // Simple ping: execute a trivial query
817        let result = self.query_sync("SELECT 1", &[]).map(|_| ());
818        async move { result.map_or_else(Outcome::Err, Outcome::Ok) }
819    }
820
821    fn close(self, _cx: &Cx) -> impl Future<Output = sqlmodel_core::Result<()>> + Send {
822        // Connection is closed on drop
823        std::future::ready(Ok(()))
824    }
825}
826
827// Implement TransactionOps for SqliteTransaction
828impl TransactionOps for SqliteTransaction<'_> {
829    fn query(
830        &self,
831        _cx: &Cx,
832        sql: &str,
833        params: &[Value],
834    ) -> impl Future<Output = Outcome<Vec<Row>, Error>> + Send {
835        let result = self.conn.query_sync(sql, params);
836        async move { result.map_or_else(Outcome::Err, Outcome::Ok) }
837    }
838
839    fn query_one(
840        &self,
841        _cx: &Cx,
842        sql: &str,
843        params: &[Value],
844    ) -> impl Future<Output = Outcome<Option<Row>, Error>> + Send {
845        let result = self.conn.query_sync(sql, params).map(|mut rows| rows.pop());
846        async move { result.map_or_else(Outcome::Err, Outcome::Ok) }
847    }
848
849    fn execute(
850        &self,
851        _cx: &Cx,
852        sql: &str,
853        params: &[Value],
854    ) -> impl Future<Output = Outcome<u64, Error>> + Send {
855        let result = self.conn.execute_sync(sql, params);
856        async move { result.map_or_else(Outcome::Err, Outcome::Ok) }
857    }
858
859    fn savepoint(&self, _cx: &Cx, name: &str) -> impl Future<Output = Outcome<(), Error>> + Send {
860        // Quote identifier to prevent SQL injection
861        let quoted_name = format!("\"{}\"", name.replace('"', "\"\""));
862        let sql = format!("SAVEPOINT {}", quoted_name);
863        let result = self.conn.execute_raw(&sql);
864        async move { result.map_or_else(Outcome::Err, Outcome::Ok) }
865    }
866
867    fn rollback_to(&self, _cx: &Cx, name: &str) -> impl Future<Output = Outcome<(), Error>> + Send {
868        // Quote identifier to prevent SQL injection
869        let quoted_name = format!("\"{}\"", name.replace('"', "\"\""));
870        let sql = format!("ROLLBACK TO {}", quoted_name);
871        let result = self.conn.execute_raw(&sql);
872        async move { result.map_or_else(Outcome::Err, Outcome::Ok) }
873    }
874
875    fn release(&self, _cx: &Cx, name: &str) -> impl Future<Output = Outcome<(), Error>> + Send {
876        // Quote identifier to prevent SQL injection
877        let quoted_name = format!("\"{}\"", name.replace('"', "\"\""));
878        let sql = format!("RELEASE {}", quoted_name);
879        let result = self.conn.execute_raw(&sql);
880        async move { result.map_or_else(Outcome::Err, Outcome::Ok) }
881    }
882
883    fn commit(mut self, _cx: &Cx) -> impl Future<Output = Outcome<(), Error>> + Send {
884        self.committed = true;
885        std::future::ready(
886            self.conn
887                .commit_sync()
888                .map_or_else(Outcome::Err, Outcome::Ok),
889        )
890    }
891
892    fn rollback(mut self, _cx: &Cx) -> impl Future<Output = Outcome<(), Error>> + Send {
893        self.committed = true; // Prevent double rollback in drop
894        std::future::ready(
895            self.conn
896                .rollback_sync()
897                .map_or_else(Outcome::Err, Outcome::Ok),
898        )
899    }
900}
901
902// Helper functions
903
904fn prepare_stmt(db: *mut ffi::sqlite3, sql: &str) -> Result<*mut ffi::sqlite3_stmt, Error> {
905    let c_sql = CString::new(sql).map_err(|_| {
906        Error::Query(QueryError {
907            kind: QueryErrorKind::Syntax,
908            sql: Some(sql.to_string()),
909            sqlstate: None,
910            message: "SQL contains null byte".to_string(),
911            detail: None,
912            hint: None,
913            position: None,
914            source: None,
915        })
916    })?;
917
918    let mut stmt: *mut ffi::sqlite3_stmt = ptr::null_mut();
919
920    // SAFETY: All pointers are valid
921    let rc = unsafe {
922        ffi::sqlite3_prepare_v2(
923            db,
924            c_sql.as_ptr(),
925            c_sql.as_bytes().len() as c_int,
926            &mut stmt,
927            ptr::null_mut(),
928        )
929    };
930
931    if rc != ffi::SQLITE_OK {
932        return Err(prepare_error(db, sql));
933    }
934
935    Ok(stmt)
936}
937
938fn prepare_error(db: *mut ffi::sqlite3, sql: &str) -> Error {
939    // SAFETY: db is valid
940    let msg = unsafe {
941        let ptr = ffi::sqlite3_errmsg(db);
942        CStr::from_ptr(ptr).to_string_lossy().into_owned()
943    };
944    let code = unsafe { ffi::sqlite3_errcode(db) };
945
946    Error::Query(QueryError {
947        kind: error_code_to_kind(code),
948        sql: Some(sql.to_string()),
949        sqlstate: None,
950        message: msg,
951        detail: None,
952        hint: None,
953        position: None,
954        source: None,
955    })
956}
957
958fn bind_error(db: *mut ffi::sqlite3, sql: &str, param_index: usize) -> Error {
959    // SAFETY: db is valid
960    let msg = unsafe {
961        let ptr = ffi::sqlite3_errmsg(db);
962        CStr::from_ptr(ptr).to_string_lossy().into_owned()
963    };
964
965    Error::Query(QueryError {
966        kind: QueryErrorKind::Database,
967        sql: Some(sql.to_string()),
968        sqlstate: None,
969        message: format!("Failed to bind parameter {}: {}", param_index, msg),
970        detail: None,
971        hint: None,
972        position: None,
973        source: None,
974    })
975}
976
977fn step_error(db: *mut ffi::sqlite3, sql: &str) -> Error {
978    // SAFETY: db is valid
979    let msg = unsafe {
980        let ptr = ffi::sqlite3_errmsg(db);
981        CStr::from_ptr(ptr).to_string_lossy().into_owned()
982    };
983    let code = unsafe { ffi::sqlite3_errcode(db) };
984
985    Error::Query(QueryError {
986        kind: error_code_to_kind(code),
987        sql: Some(sql.to_string()),
988        sqlstate: None,
989        message: msg,
990        detail: None,
991        hint: None,
992        position: None,
993        source: None,
994    })
995}
996
997fn error_code_to_kind(code: c_int) -> QueryErrorKind {
998    match code {
999        ffi::SQLITE_CONSTRAINT => QueryErrorKind::Constraint,
1000        ffi::SQLITE_BUSY | ffi::SQLITE_LOCKED => QueryErrorKind::Deadlock,
1001        ffi::SQLITE_PERM | ffi::SQLITE_AUTH => QueryErrorKind::Permission,
1002        ffi::SQLITE_NOTFOUND => QueryErrorKind::NotFound,
1003        ffi::SQLITE_TOOBIG => QueryErrorKind::DataTruncation,
1004        ffi::SQLITE_INTERRUPT => QueryErrorKind::Cancelled,
1005        _ => QueryErrorKind::Database,
1006    }
1007}
1008
1009/// Format a Value for display in console output.
1010#[allow(dead_code)]
1011fn format_value(value: &Value) -> String {
1012    match value {
1013        Value::Null => "NULL".to_string(),
1014        Value::Bool(b) => if *b { "true" } else { "false" }.to_string(),
1015        Value::TinyInt(n) => n.to_string(),
1016        Value::SmallInt(n) => n.to_string(),
1017        Value::Int(n) => n.to_string(),
1018        Value::BigInt(n) => n.to_string(),
1019        Value::Float(n) => format!("{:.6}", n),
1020        Value::Double(n) => format!("{:.6}", n),
1021        Value::Text(s) => s.clone(),
1022        Value::Bytes(b) => format!("[BLOB: {} bytes]", b.len()),
1023        Value::Date(d) => d.to_string(),
1024        Value::Time(t) => t.to_string(),
1025        Value::Timestamp(ts) => ts.to_string(),
1026        Value::TimestampTz(ts) => ts.to_string(),
1027        Value::Json(j) => j.to_string(),
1028        Value::Uuid(u) => {
1029            // Format UUID as hex string: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
1030            format!(
1031                "{:02x}{:02x}{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}",
1032                u[0],
1033                u[1],
1034                u[2],
1035                u[3],
1036                u[4],
1037                u[5],
1038                u[6],
1039                u[7],
1040                u[8],
1041                u[9],
1042                u[10],
1043                u[11],
1044                u[12],
1045                u[13],
1046                u[14],
1047                u[15]
1048            )
1049        }
1050        Value::Decimal(d) => d.to_string(),
1051        Value::Array(arr) => format!("[{} items]", arr.len()),
1052        Value::Default => "DEFAULT".to_string(),
1053    }
1054}
1055
1056// ==================== Console Support ====================
1057
1058#[cfg(feature = "console")]
1059impl ConsoleAware for SqliteConnection {
1060    fn set_console(&mut self, console: Option<Arc<SqlModelConsole>>) {
1061        self.console = console;
1062        // Emit database status when console is attached
1063        self.emit_open_status();
1064    }
1065
1066    fn console(&self) -> Option<&Arc<SqlModelConsole>> {
1067        self.console.as_ref()
1068    }
1069
1070    fn has_console(&self) -> bool {
1071        self.console.is_some()
1072    }
1073}
1074
1075impl SqliteConnection {
1076    /// Emit database open status to console if available.
1077    #[cfg(feature = "console")]
1078    fn emit_open_status(&self) {
1079        if let Some(console) = &self.console {
1080            // Get database info
1081            let mode = if self.path == ":memory:" {
1082                "in-memory"
1083            } else {
1084                "file"
1085            };
1086
1087            // Query journal mode if we can
1088            let journal_mode = self
1089                .query_sync("PRAGMA journal_mode", &[])
1090                .ok()
1091                .and_then(|rows| rows.first().and_then(|r| r.get_as::<String>(0).ok()));
1092
1093            let page_size = self
1094                .query_sync("PRAGMA page_size", &[])
1095                .ok()
1096                .and_then(|rows| rows.first().and_then(|r| r.get_as::<i64>(0).ok()));
1097
1098            if console.mode().is_plain() {
1099                // Plain text output for agents
1100                let journal = journal_mode.as_deref().unwrap_or("unknown");
1101                console.status(&format!(
1102                    "Opened SQLite database: {} ({} mode, journal: {})",
1103                    self.path, mode, journal
1104                ));
1105            } else {
1106                // Rich output
1107                console.status(&format!("SQLite database: {}", self.path));
1108                console.status(&format!("  Mode: {}", mode));
1109                if let Some(journal) = journal_mode {
1110                    console.status(&format!("  Journal: {}", journal.to_uppercase()));
1111                }
1112                if let Some(size) = page_size {
1113                    console.status(&format!("  Page size: {} bytes", size));
1114                }
1115            }
1116        }
1117    }
1118
1119    /// Emit transaction state to console if available.
1120    #[cfg(feature = "console")]
1121    fn emit_transaction_state(&self, state: &str) {
1122        if let Some(console) = &self.console {
1123            if console.mode().is_plain() {
1124                console.status(&format!("Transaction: {}", state));
1125            } else {
1126                console.status(&format!("[{}] Transaction {}", state, state.to_lowercase()));
1127            }
1128        }
1129    }
1130
1131    /// Emit query timing to console if available.
1132    #[cfg(feature = "console")]
1133    fn emit_query_timing(&self, elapsed_ms: f64, rows: usize) {
1134        if let Some(console) = &self.console {
1135            console.status(&format!("Query: {:.1}ms, {} rows", elapsed_ms, rows));
1136        }
1137    }
1138
1139    /// Emit query results with PRAGMA-aware formatting.
1140    #[cfg(feature = "console")]
1141    fn emit_query_result(&self, sql: &str, col_names: &[String], rows: &[Row], elapsed_ms: f64) {
1142        if let Some(console) = &self.console {
1143            // Check if this is a PRAGMA query for special formatting
1144            let sql_upper = sql.trim().to_uppercase();
1145            let is_pragma = sql_upper.starts_with("PRAGMA");
1146
1147            if is_pragma && !rows.is_empty() {
1148                // Format PRAGMA results as a table
1149                if console.mode().is_plain() {
1150                    // Plain text format for agents
1151                    console.status(&format!("{}:", sql.trim()));
1152                    // Header
1153                    console.status(&format!("  {}", col_names.join("|")));
1154                    // Rows
1155                    for row in rows.iter().take(20) {
1156                        let values: Vec<String> = (0..col_names.len())
1157                            .map(|i| {
1158                                row.get(i)
1159                                    .map(|v| format_value(v))
1160                                    .unwrap_or_else(|| "NULL".to_string())
1161                            })
1162                            .collect();
1163                        console.status(&format!("  {}", values.join("|")));
1164                    }
1165                    if rows.len() > 20 {
1166                        console.status(&format!("  ... and {} more rows", rows.len() - 20));
1167                    }
1168                    console.status(&format!("  ({:.1}ms)", elapsed_ms));
1169                } else {
1170                    // Rich format with table rendering
1171                    let mut table_output = String::new();
1172                    table_output.push_str(&format!("PRAGMA Query Results ({:.1}ms)\n", elapsed_ms));
1173
1174                    // Calculate column widths
1175                    let mut widths: Vec<usize> = col_names.iter().map(|c| c.len()).collect();
1176                    for row in rows.iter().take(20) {
1177                        for (i, w) in widths.iter_mut().enumerate() {
1178                            let val_len = row.get(i).map(|v| format_value(v).len()).unwrap_or(4); // "NULL".len()
1179                            if val_len > *w {
1180                                *w = val_len;
1181                            }
1182                        }
1183                    }
1184
1185                    // Build header separator
1186                    let sep: String = widths
1187                        .iter()
1188                        .map(|w| "-".repeat(*w + 2))
1189                        .collect::<Vec<_>>()
1190                        .join("+");
1191                    table_output.push_str(&format!("+{}+\n", sep));
1192
1193                    // Header row
1194                    let header: String = col_names
1195                        .iter()
1196                        .enumerate()
1197                        .map(|(i, name)| format!(" {:width$} ", name, width = widths[i]))
1198                        .collect::<Vec<_>>()
1199                        .join("|");
1200                    table_output.push_str(&format!("|{}|\n", header));
1201                    table_output.push_str(&format!("+{}+\n", sep));
1202
1203                    // Data rows
1204                    for row in rows.iter().take(20) {
1205                        let data: String = (0..col_names.len())
1206                            .map(|i| {
1207                                let val = row
1208                                    .get(i)
1209                                    .map(|v| format_value(v))
1210                                    .unwrap_or_else(|| "NULL".to_string());
1211                                format!(" {:width$} ", val, width = widths[i])
1212                            })
1213                            .collect::<Vec<_>>()
1214                            .join("|");
1215                        table_output.push_str(&format!("|{}|\n", data));
1216                    }
1217                    table_output.push_str(&format!("+{}+", sep));
1218
1219                    if rows.len() > 20 {
1220                        table_output.push_str(&format!("\n... and {} more rows", rows.len() - 20));
1221                    }
1222
1223                    console.status(&table_output);
1224                }
1225            } else {
1226                // Regular query timing
1227                self.emit_query_timing(elapsed_ms, rows.len());
1228            }
1229        }
1230    }
1231
1232    /// Emit execute operation timing to console.
1233    #[cfg(feature = "console")]
1234    fn emit_execute_timing(&self, sql: &str, rows_affected: u64, elapsed_ms: f64) {
1235        if let Some(console) = &self.console {
1236            let sql_upper = sql.trim().to_uppercase();
1237
1238            // Provide contextual message based on operation type
1239            let op_type = if sql_upper.starts_with("INSERT") {
1240                "Insert"
1241            } else if sql_upper.starts_with("UPDATE") {
1242                "Update"
1243            } else if sql_upper.starts_with("DELETE") {
1244                "Delete"
1245            } else if sql_upper.starts_with("CREATE") {
1246                "Create"
1247            } else if sql_upper.starts_with("DROP") {
1248                "Drop"
1249            } else if sql_upper.starts_with("ALTER") {
1250                "Alter"
1251            } else {
1252                "Execute"
1253            };
1254
1255            if console.mode().is_plain() {
1256                console.status(&format!(
1257                    "{}: {} rows affected ({:.1}ms)",
1258                    op_type, rows_affected, elapsed_ms
1259                ));
1260            } else {
1261                console.status(&format!(
1262                    "[{}] {} rows affected ({:.1}ms)",
1263                    op_type.to_uppercase(),
1264                    rows_affected,
1265                    elapsed_ms
1266                ));
1267            }
1268        }
1269    }
1270
1271    /// Emit busy waiting status to console.
1272    #[cfg(feature = "console")]
1273    pub fn emit_busy_waiting(&self, elapsed_secs: f64) {
1274        if let Some(console) = &self.console {
1275            if console.mode().is_plain() {
1276                console.status(&format!(
1277                    "Waiting for database lock... ({:.1}s)",
1278                    elapsed_secs
1279                ));
1280            } else {
1281                console.status(&format!(
1282                    "[..] Waiting for database lock... ({:.1}s)",
1283                    elapsed_secs
1284                ));
1285            }
1286        }
1287    }
1288
1289    /// Emit WAL checkpoint progress to console.
1290    #[cfg(feature = "console")]
1291    pub fn emit_checkpoint_progress(&self, pages_done: u32, pages_total: u32) {
1292        if let Some(console) = &self.console {
1293            let pct = if pages_total > 0 {
1294                (pages_done as f64 / pages_total as f64) * 100.0
1295            } else {
1296                100.0
1297            };
1298
1299            if console.mode().is_plain() {
1300                console.status(&format!(
1301                    "WAL checkpoint: {:.0}% ({}/{} pages)",
1302                    pct, pages_done, pages_total
1303                ));
1304            } else {
1305                // ASCII progress bar for rich mode
1306                let bar_width: usize = 20;
1307                let filled = ((pct / 100.0) * bar_width as f64).round() as usize;
1308                let empty = bar_width.saturating_sub(filled);
1309                let bar = format!("[{}{}]", "=".repeat(filled), " ".repeat(empty));
1310                console.status(&format!(
1311                    "WAL checkpoint: {} {:.0}% ({}/{} pages)",
1312                    bar, pct, pages_done, pages_total
1313                ));
1314            }
1315        }
1316    }
1317
1318    /// No-op when console feature is disabled.
1319    #[cfg(not(feature = "console"))]
1320    #[allow(dead_code)]
1321    fn emit_open_status(&self) {}
1322
1323    /// No-op when console feature is disabled.
1324    #[cfg(not(feature = "console"))]
1325    fn emit_transaction_state(&self, _state: &str) {}
1326
1327    /// No-op when console feature is disabled.
1328    #[cfg(not(feature = "console"))]
1329    #[allow(dead_code)]
1330    fn emit_query_timing(&self, _elapsed_ms: f64, _rows: usize) {}
1331
1332    /// No-op when console feature is disabled.
1333    #[cfg(not(feature = "console"))]
1334    #[allow(dead_code)]
1335    fn emit_query_result(
1336        &self,
1337        _sql: &str,
1338        _col_names: &[String],
1339        _rows: &[Row],
1340        _elapsed_ms: f64,
1341    ) {
1342    }
1343
1344    /// No-op when console feature is disabled.
1345    #[cfg(not(feature = "console"))]
1346    #[allow(dead_code)]
1347    fn emit_execute_timing(&self, _sql: &str, _rows_affected: u64, _elapsed_ms: f64) {}
1348
1349    /// No-op when console feature is disabled.
1350    #[cfg(not(feature = "console"))]
1351    pub fn emit_busy_waiting(&self, _elapsed_secs: f64) {}
1352
1353    /// No-op when console feature is disabled.
1354    #[cfg(not(feature = "console"))]
1355    pub fn emit_checkpoint_progress(&self, _pages_done: u32, _pages_total: u32) {}
1356}
1357
1358#[cfg(test)]
1359mod tests {
1360    use super::*;
1361
1362    #[test]
1363    fn test_open_memory() {
1364        let conn = SqliteConnection::open_memory().unwrap();
1365        assert_eq!(conn.path(), ":memory:");
1366    }
1367
1368    #[test]
1369    fn test_execute_raw() {
1370        let conn = SqliteConnection::open_memory().unwrap();
1371        conn.execute_raw("CREATE TABLE test (id INTEGER PRIMARY KEY, name TEXT)")
1372            .unwrap();
1373        conn.execute_raw("INSERT INTO test (name) VALUES ('Alice')")
1374            .unwrap();
1375        assert_eq!(conn.changes(), 1);
1376        assert_eq!(conn.last_insert_rowid(), 1);
1377    }
1378
1379    #[test]
1380    fn test_query_sync() {
1381        let conn = SqliteConnection::open_memory().unwrap();
1382        conn.execute_raw("CREATE TABLE test (id INTEGER PRIMARY KEY, name TEXT)")
1383            .unwrap();
1384        conn.execute_raw("INSERT INTO test (name) VALUES ('Alice'), ('Bob')")
1385            .unwrap();
1386
1387        let rows = conn
1388            .query_sync("SELECT * FROM test ORDER BY id", &[])
1389            .unwrap();
1390        assert_eq!(rows.len(), 2);
1391
1392        assert_eq!(rows[0].get_named::<i32>("id").unwrap(), 1);
1393        assert_eq!(rows[0].get_named::<String>("name").unwrap(), "Alice");
1394        assert_eq!(rows[1].get_named::<i32>("id").unwrap(), 2);
1395        assert_eq!(rows[1].get_named::<String>("name").unwrap(), "Bob");
1396    }
1397
1398    #[test]
1399    fn test_parameterized_query() {
1400        let conn = SqliteConnection::open_memory().unwrap();
1401        conn.execute_raw("CREATE TABLE test (id INTEGER PRIMARY KEY, name TEXT, age INTEGER)")
1402            .unwrap();
1403
1404        conn.execute_sync(
1405            "INSERT INTO test (name, age) VALUES (?, ?)",
1406            &[Value::Text("Alice".to_string()), Value::Int(30)],
1407        )
1408        .unwrap();
1409
1410        let rows = conn
1411            .query_sync(
1412                "SELECT * FROM test WHERE name = ?",
1413                &[Value::Text("Alice".to_string())],
1414            )
1415            .unwrap();
1416
1417        assert_eq!(rows.len(), 1);
1418        assert_eq!(rows[0].get_named::<String>("name").unwrap(), "Alice");
1419        assert_eq!(rows[0].get_named::<i32>("age").unwrap(), 30);
1420    }
1421
1422    #[test]
1423    fn test_null_handling() {
1424        let conn = SqliteConnection::open_memory().unwrap();
1425        conn.execute_raw("CREATE TABLE test (id INTEGER PRIMARY KEY, name TEXT)")
1426            .unwrap();
1427
1428        conn.execute_sync("INSERT INTO test (name) VALUES (?)", &[Value::Null])
1429            .unwrap();
1430
1431        let rows = conn.query_sync("SELECT * FROM test", &[]).unwrap();
1432        assert_eq!(rows.len(), 1);
1433        assert_eq!(rows[0].get_named::<Option<String>>("name").unwrap(), None);
1434    }
1435
1436    #[test]
1437    fn test_transaction() {
1438        let conn = SqliteConnection::open_memory().unwrap();
1439        conn.execute_raw("CREATE TABLE test (id INTEGER PRIMARY KEY, name TEXT)")
1440            .unwrap();
1441
1442        // Start transaction, insert, rollback
1443        conn.begin_sync(IsolationLevel::default()).unwrap();
1444        conn.execute_sync(
1445            "INSERT INTO test (name) VALUES (?)",
1446            &[Value::Text("Alice".to_string())],
1447        )
1448        .unwrap();
1449        conn.rollback_sync().unwrap();
1450
1451        // Verify rollback worked
1452        let rows = conn.query_sync("SELECT * FROM test", &[]).unwrap();
1453        assert_eq!(rows.len(), 0);
1454
1455        // Start transaction, insert, commit
1456        conn.begin_sync(IsolationLevel::default()).unwrap();
1457        conn.execute_sync(
1458            "INSERT INTO test (name) VALUES (?)",
1459            &[Value::Text("Bob".to_string())],
1460        )
1461        .unwrap();
1462        conn.commit_sync().unwrap();
1463
1464        // Verify commit worked
1465        let rows = conn.query_sync("SELECT * FROM test", &[]).unwrap();
1466        assert_eq!(rows.len(), 1);
1467        assert_eq!(rows[0].get_named::<String>("name").unwrap(), "Bob");
1468    }
1469
1470    #[test]
1471    fn test_insert_rowid() {
1472        let conn = SqliteConnection::open_memory().unwrap();
1473        conn.execute_raw("CREATE TABLE test (id INTEGER PRIMARY KEY, name TEXT)")
1474            .unwrap();
1475
1476        let rowid = conn
1477            .insert_sync(
1478                "INSERT INTO test (name) VALUES (?)",
1479                &[Value::Text("Alice".to_string())],
1480            )
1481            .unwrap();
1482        assert_eq!(rowid, 1);
1483
1484        let rowid = conn
1485            .insert_sync(
1486                "INSERT INTO test (name) VALUES (?)",
1487                &[Value::Text("Bob".to_string())],
1488            )
1489            .unwrap();
1490        assert_eq!(rowid, 2);
1491    }
1492
1493    #[test]
1494    #[allow(clippy::approx_constant)]
1495    fn test_type_conversions() {
1496        let conn = SqliteConnection::open_memory().unwrap();
1497        conn.execute_raw(
1498            "CREATE TABLE types (
1499                b BOOLEAN,
1500                i INTEGER,
1501                f REAL,
1502                t TEXT,
1503                bl BLOB
1504            )",
1505        )
1506        .unwrap();
1507
1508        conn.execute_sync(
1509            "INSERT INTO types VALUES (?, ?, ?, ?, ?)",
1510            &[
1511                Value::Bool(true),
1512                Value::BigInt(42),
1513                Value::Double(3.14),
1514                Value::Text("hello".to_string()),
1515                Value::Bytes(vec![1, 2, 3]),
1516            ],
1517        )
1518        .unwrap();
1519
1520        let rows = conn.query_sync("SELECT * FROM types", &[]).unwrap();
1521        assert_eq!(rows.len(), 1);
1522
1523        // SQLite stores booleans as integers
1524        let b: i32 = rows[0].get_named("b").unwrap();
1525        assert_eq!(b, 1);
1526
1527        let i: i32 = rows[0].get_named("i").unwrap();
1528        assert_eq!(i, 42);
1529
1530        let f: f64 = rows[0].get_named("f").unwrap();
1531        assert!((f - 3.14).abs() < 0.001);
1532
1533        let t: String = rows[0].get_named("t").unwrap();
1534        assert_eq!(t, "hello");
1535
1536        let bl: Vec<u8> = rows[0].get_named("bl").unwrap();
1537        assert_eq!(bl, vec![1, 2, 3]);
1538    }
1539
1540    #[test]
1541    fn test_open_flags() {
1542        // Test creating a database with create flag
1543        let tmp = std::env::temp_dir().join("sqlmodel_test.db");
1544        let _ = std::fs::remove_file(&tmp); // Ensure it doesn't exist
1545
1546        let config = SqliteConfig::file(tmp.to_string_lossy().to_string())
1547            .flags(OpenFlags::create_read_write());
1548        let conn = SqliteConnection::open(&config).unwrap();
1549        conn.execute_raw("CREATE TABLE test (id INTEGER)").unwrap();
1550        drop(conn);
1551
1552        // Open as read-only
1553        let config =
1554            SqliteConfig::file(tmp.to_string_lossy().to_string()).flags(OpenFlags::read_only());
1555        let conn = SqliteConnection::open(&config).unwrap();
1556
1557        // Reading should work
1558        let rows = conn.query_sync("SELECT * FROM test", &[]).unwrap();
1559        assert_eq!(rows.len(), 0);
1560
1561        // Writing should fail
1562        let result = conn.execute_raw("INSERT INTO test VALUES (1)");
1563        assert!(result.is_err());
1564
1565        drop(conn);
1566        let _ = std::fs::remove_file(&tmp);
1567    }
1568
1569    // ==================== Console Integration Tests ====================
1570
1571    #[cfg(feature = "console")]
1572    mod console_tests {
1573        use super::*;
1574
1575        /// Test that ConsoleAware trait is properly implemented.
1576        #[test]
1577        fn test_console_aware_trait_impl() {
1578            let mut conn = SqliteConnection::open_memory().unwrap();
1579
1580            // Initially no console
1581            assert!(!conn.has_console());
1582            assert!(conn.console().is_none());
1583
1584            // Attach console
1585            let console = Arc::new(SqlModelConsole::with_mode(
1586                sqlmodel_console::OutputMode::Plain,
1587            ));
1588            conn.set_console(Some(console.clone()));
1589
1590            // Verify console is attached
1591            assert!(conn.has_console());
1592            assert!(conn.console().is_some());
1593
1594            // Detach console
1595            conn.set_console(None);
1596            assert!(!conn.has_console());
1597        }
1598
1599        /// Test database open feedback is emitted when console is attached.
1600        #[test]
1601        fn test_database_open_feedback() {
1602            let mut conn = SqliteConnection::open_memory().unwrap();
1603
1604            // Attaching console should emit open status
1605            // (output goes to stderr, we just verify no panic)
1606            let console = Arc::new(SqlModelConsole::with_mode(
1607                sqlmodel_console::OutputMode::Plain,
1608            ));
1609            conn.set_console(Some(console));
1610
1611            // No panic means success
1612        }
1613
1614        /// Test PRAGMA query formatting.
1615        #[test]
1616        fn test_pragma_formatting() {
1617            let mut conn = SqliteConnection::open_memory().unwrap();
1618
1619            // Create a table to have something in pragma_table_info
1620            conn.execute_raw("CREATE TABLE test (id INTEGER PRIMARY KEY, name TEXT)")
1621                .unwrap();
1622
1623            // Attach console for formatted output
1624            let console = Arc::new(SqlModelConsole::with_mode(
1625                sqlmodel_console::OutputMode::Plain,
1626            ));
1627            conn.set_console(Some(console));
1628
1629            // Execute PRAGMA query - should format as table
1630            let rows = conn.query_sync("PRAGMA table_info(test)", &[]).unwrap();
1631
1632            // Verify we got the expected columns
1633            assert!(!rows.is_empty());
1634        }
1635
1636        /// Test transaction state display.
1637        #[test]
1638        fn test_transaction_state() {
1639            let mut conn = SqliteConnection::open_memory().unwrap();
1640            conn.execute_raw("CREATE TABLE test (id INTEGER PRIMARY KEY)")
1641                .unwrap();
1642
1643            // Attach console
1644            let console = Arc::new(SqlModelConsole::with_mode(
1645                sqlmodel_console::OutputMode::Plain,
1646            ));
1647            conn.set_console(Some(console));
1648
1649            // Transaction operations should emit state
1650            conn.begin_sync(IsolationLevel::default()).unwrap();
1651            conn.execute_sync("INSERT INTO test (id) VALUES (?)", &[Value::Int(1)])
1652                .unwrap();
1653            conn.commit_sync().unwrap();
1654
1655            // Verify the transaction worked
1656            let rows = conn.query_sync("SELECT * FROM test", &[]).unwrap();
1657            assert_eq!(rows.len(), 1);
1658        }
1659
1660        /// Test WAL checkpoint progress output.
1661        #[test]
1662        fn test_wal_checkpoint_progress() {
1663            let conn = SqliteConnection::open_memory().unwrap();
1664
1665            // emit_checkpoint_progress should not panic
1666            conn.emit_checkpoint_progress(50, 100);
1667            conn.emit_checkpoint_progress(100, 100);
1668            conn.emit_checkpoint_progress(0, 0);
1669        }
1670
1671        /// Test busy timeout feedback output.
1672        #[test]
1673        fn test_busy_timeout_feedback() {
1674            let conn = SqliteConnection::open_memory().unwrap();
1675
1676            // emit_busy_waiting should not panic
1677            conn.emit_busy_waiting(0.5);
1678            conn.emit_busy_waiting(2.1);
1679        }
1680
1681        /// Test that console disabled produces no output (no panic).
1682        #[test]
1683        fn test_console_disabled_no_output() {
1684            let conn = SqliteConnection::open_memory().unwrap();
1685
1686            // Without console, all emit methods should be no-ops
1687            conn.emit_busy_waiting(1.0);
1688            conn.emit_checkpoint_progress(10, 100);
1689
1690            // Query should work without console
1691            conn.execute_raw("CREATE TABLE test (id INTEGER PRIMARY KEY)")
1692                .unwrap();
1693            let rows = conn.query_sync("SELECT * FROM test", &[]).unwrap();
1694            assert_eq!(rows.len(), 0);
1695        }
1696
1697        /// Test plain mode output format (parseable by agents).
1698        #[test]
1699        fn test_plain_mode_output() {
1700            let mut conn = SqliteConnection::open_memory().unwrap();
1701
1702            // Attach plain mode console
1703            let console = Arc::new(SqlModelConsole::with_mode(
1704                sqlmodel_console::OutputMode::Plain,
1705            ));
1706            conn.set_console(Some(console.clone()));
1707
1708            // Verify plain mode is active
1709            assert!(conn.console().unwrap().is_plain());
1710
1711            // Execute operations (output should be plain text)
1712            conn.execute_raw("CREATE TABLE test (id INTEGER PRIMARY KEY, name TEXT)")
1713                .unwrap();
1714            conn.execute_sync(
1715                "INSERT INTO test (name) VALUES (?)",
1716                &[Value::Text("Alice".to_string())],
1717            )
1718            .unwrap();
1719
1720            let rows = conn.query_sync("PRAGMA table_info(test)", &[]).unwrap();
1721            assert!(!rows.is_empty());
1722        }
1723
1724        /// Test rich mode output format.
1725        #[test]
1726        fn test_rich_mode_output() {
1727            let mut conn = SqliteConnection::open_memory().unwrap();
1728
1729            // Attach rich mode console
1730            let console = Arc::new(SqlModelConsole::with_mode(
1731                sqlmodel_console::OutputMode::Rich,
1732            ));
1733            conn.set_console(Some(console.clone()));
1734
1735            // Verify rich mode is active
1736            assert!(conn.console().unwrap().is_rich());
1737
1738            // Execute operations (output should have formatting)
1739            conn.execute_raw("CREATE TABLE test (id INTEGER PRIMARY KEY)")
1740                .unwrap();
1741            conn.emit_checkpoint_progress(50, 100);
1742        }
1743    }
1744}