1#![allow(clippy::cast_possible_truncation)]
23#![allow(clippy::cast_sign_loss)]
24#![allow(clippy::cast_lossless)]
25#![allow(clippy::result_large_err)] #![allow(clippy::borrow_as_ptr)] #![allow(clippy::if_not_else)] #![allow(clippy::implicit_clone)] #![allow(clippy::map_unwrap_or)] #![allow(clippy::redundant_closure)] use 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#[derive(Debug, Clone)]
50pub struct SqliteConfig {
51 pub path: String,
53 pub flags: OpenFlags,
55 pub busy_timeout_ms: u32,
57}
58
59#[derive(Debug, Clone, Copy, Default)]
61pub struct OpenFlags {
62 pub read_only: bool,
64 pub read_write: bool,
66 pub create: bool,
68 pub uri: bool,
70 pub no_mutex: bool,
72 pub full_mutex: bool,
74 pub shared_cache: bool,
76 pub private_cache: bool,
78}
79
80impl OpenFlags {
81 pub fn read_only() -> Self {
83 Self {
84 read_only: true,
85 ..Default::default()
86 }
87 }
88
89 pub fn read_write() -> Self {
91 Self {
92 read_write: true,
93 ..Default::default()
94 }
95 }
96
97 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 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 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 pub fn memory() -> Self {
165 Self::default()
166 }
167
168 pub fn flags(mut self, flags: OpenFlags) -> Self {
170 self.flags = flags;
171 self
172 }
173
174 pub fn busy_timeout(mut self, ms: u32) -> Self {
176 self.busy_timeout_ms = ms;
177 self
178 }
179}
180
181struct SqliteInner {
183 db: *mut ffi::sqlite3,
184 in_transaction: bool,
185}
186
187unsafe impl Send for SqliteInner {}
191
192pub struct SqliteConnection {
196 inner: Mutex<SqliteInner>,
197 path: String,
198 #[cfg(feature = "console")]
200 console: Option<Arc<SqlModelConsole>>,
201}
202
203unsafe impl Send for SqliteConnection {}
205unsafe impl Sync for SqliteConnection {}
206
207impl SqliteConnection {
208 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 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 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 if config.busy_timeout_ms > 0 {
246 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 pub fn open_memory() -> Result<Self, Error> {
265 Self::open(&SqliteConfig::memory())
266 }
267
268 pub fn open_file(path: impl Into<String>) -> Result<Self, Error> {
270 Self::open(&SqliteConfig::file(path))
271 }
272
273 pub fn path(&self) -> &str {
275 &self.path
276 }
277
278 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 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 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 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 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 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 pub fn last_insert_rowid(&self) -> i64 {
419 let inner = self.inner.lock().unwrap_or_else(|e| e.into_inner());
420 unsafe { ffi::sqlite3_last_insert_rowid(inner.db) }
422 }
423
424 pub fn changes(&self) -> i32 {
426 let inner = self.inner.lock().unwrap_or_else(|e| e.into_inner());
427 unsafe { ffi::sqlite3_changes(inner.db) }
429 }
430
431 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 for (i, param) in params.iter().enumerate() {
444 let rc = unsafe { types::bind_value(stmt, (i + 1) as c_int, param) };
446 if rc != ffi::SQLITE_OK {
447 unsafe { ffi::sqlite3_finalize(stmt) };
449 return Err(bind_error(inner.db, sql, i + 1));
450 }
451 }
452
453 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 let mut rows = Vec::new();
466 loop {
467 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 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 unsafe { ffi::sqlite3_finalize(stmt) };
483 return Err(step_error(inner.db, sql));
484 }
485 }
486 }
487
488 unsafe { ffi::sqlite3_finalize(stmt) };
490
491 #[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 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 for (i, param) in params.iter().enumerate() {
514 let rc = unsafe { types::bind_value(stmt, (i + 1) as c_int, param) };
516 if rc != ffi::SQLITE_OK {
517 unsafe { ffi::sqlite3_finalize(stmt) };
519 return Err(bind_error(inner.db, sql, i + 1));
520 }
521 }
522
523 let rc = unsafe { ffi::sqlite3_step(stmt) };
526
527 unsafe { ffi::sqlite3_finalize(stmt) };
529
530 match rc {
531 ffi::SQLITE_DONE | ffi::SQLITE_ROW => {
532 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 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 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 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); 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 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 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 unsafe {
643 ffi::sqlite3_close_v2(inner.db);
644 }
645 }
646 }
647 }
648}
649
650pub 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 let _ = self.conn.rollback_sync();
670 }
671 }
672}
673
674impl 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 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 unsafe { ffi::sqlite3_finalize(stmt) };
786
787 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 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 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 std::future::ready(Ok(()))
824 }
825}
826
827impl 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 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 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 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; std::future::ready(
895 self.conn
896 .rollback_sync()
897 .map_or_else(Outcome::Err, Outcome::Ok),
898 )
899 }
900}
901
902fn 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 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 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 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 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#[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!(
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#[cfg(feature = "console")]
1059impl ConsoleAware for SqliteConnection {
1060 fn set_console(&mut self, console: Option<Arc<SqlModelConsole>>) {
1061 self.console = console;
1062 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 #[cfg(feature = "console")]
1078 fn emit_open_status(&self) {
1079 if let Some(console) = &self.console {
1080 let mode = if self.path == ":memory:" {
1082 "in-memory"
1083 } else {
1084 "file"
1085 };
1086
1087 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 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 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 #[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 #[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 #[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 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 if console.mode().is_plain() {
1150 console.status(&format!("{}:", sql.trim()));
1152 console.status(&format!(" {}", col_names.join("|")));
1154 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 let mut table_output = String::new();
1172 table_output.push_str(&format!("PRAGMA Query Results ({:.1}ms)\n", elapsed_ms));
1173
1174 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); if val_len > *w {
1180 *w = val_len;
1181 }
1182 }
1183 }
1184
1185 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 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 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 self.emit_query_timing(elapsed_ms, rows.len());
1228 }
1229 }
1230 }
1231
1232 #[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 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 #[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 #[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 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 #[cfg(not(feature = "console"))]
1320 #[allow(dead_code)]
1321 fn emit_open_status(&self) {}
1322
1323 #[cfg(not(feature = "console"))]
1325 fn emit_transaction_state(&self, _state: &str) {}
1326
1327 #[cfg(not(feature = "console"))]
1329 #[allow(dead_code)]
1330 fn emit_query_timing(&self, _elapsed_ms: f64, _rows: usize) {}
1331
1332 #[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 #[cfg(not(feature = "console"))]
1346 #[allow(dead_code)]
1347 fn emit_execute_timing(&self, _sql: &str, _rows_affected: u64, _elapsed_ms: f64) {}
1348
1349 #[cfg(not(feature = "console"))]
1351 pub fn emit_busy_waiting(&self, _elapsed_secs: f64) {}
1352
1353 #[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 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 let rows = conn.query_sync("SELECT * FROM test", &[]).unwrap();
1453 assert_eq!(rows.len(), 0);
1454
1455 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 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 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 let tmp = std::env::temp_dir().join("sqlmodel_test.db");
1544 let _ = std::fs::remove_file(&tmp); 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 let config =
1554 SqliteConfig::file(tmp.to_string_lossy().to_string()).flags(OpenFlags::read_only());
1555 let conn = SqliteConnection::open(&config).unwrap();
1556
1557 let rows = conn.query_sync("SELECT * FROM test", &[]).unwrap();
1559 assert_eq!(rows.len(), 0);
1560
1561 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 #[cfg(feature = "console")]
1572 mod console_tests {
1573 use super::*;
1574
1575 #[test]
1577 fn test_console_aware_trait_impl() {
1578 let mut conn = SqliteConnection::open_memory().unwrap();
1579
1580 assert!(!conn.has_console());
1582 assert!(conn.console().is_none());
1583
1584 let console = Arc::new(SqlModelConsole::with_mode(
1586 sqlmodel_console::OutputMode::Plain,
1587 ));
1588 conn.set_console(Some(console.clone()));
1589
1590 assert!(conn.has_console());
1592 assert!(conn.console().is_some());
1593
1594 conn.set_console(None);
1596 assert!(!conn.has_console());
1597 }
1598
1599 #[test]
1601 fn test_database_open_feedback() {
1602 let mut conn = SqliteConnection::open_memory().unwrap();
1603
1604 let console = Arc::new(SqlModelConsole::with_mode(
1607 sqlmodel_console::OutputMode::Plain,
1608 ));
1609 conn.set_console(Some(console));
1610
1611 }
1613
1614 #[test]
1616 fn test_pragma_formatting() {
1617 let mut conn = SqliteConnection::open_memory().unwrap();
1618
1619 conn.execute_raw("CREATE TABLE test (id INTEGER PRIMARY KEY, name TEXT)")
1621 .unwrap();
1622
1623 let console = Arc::new(SqlModelConsole::with_mode(
1625 sqlmodel_console::OutputMode::Plain,
1626 ));
1627 conn.set_console(Some(console));
1628
1629 let rows = conn.query_sync("PRAGMA table_info(test)", &[]).unwrap();
1631
1632 assert!(!rows.is_empty());
1634 }
1635
1636 #[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 let console = Arc::new(SqlModelConsole::with_mode(
1645 sqlmodel_console::OutputMode::Plain,
1646 ));
1647 conn.set_console(Some(console));
1648
1649 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 let rows = conn.query_sync("SELECT * FROM test", &[]).unwrap();
1657 assert_eq!(rows.len(), 1);
1658 }
1659
1660 #[test]
1662 fn test_wal_checkpoint_progress() {
1663 let conn = SqliteConnection::open_memory().unwrap();
1664
1665 conn.emit_checkpoint_progress(50, 100);
1667 conn.emit_checkpoint_progress(100, 100);
1668 conn.emit_checkpoint_progress(0, 0);
1669 }
1670
1671 #[test]
1673 fn test_busy_timeout_feedback() {
1674 let conn = SqliteConnection::open_memory().unwrap();
1675
1676 conn.emit_busy_waiting(0.5);
1678 conn.emit_busy_waiting(2.1);
1679 }
1680
1681 #[test]
1683 fn test_console_disabled_no_output() {
1684 let conn = SqliteConnection::open_memory().unwrap();
1685
1686 conn.emit_busy_waiting(1.0);
1688 conn.emit_checkpoint_progress(10, 100);
1689
1690 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]
1699 fn test_plain_mode_output() {
1700 let mut conn = SqliteConnection::open_memory().unwrap();
1701
1702 let console = Arc::new(SqlModelConsole::with_mode(
1704 sqlmodel_console::OutputMode::Plain,
1705 ));
1706 conn.set_console(Some(console.clone()));
1707
1708 assert!(conn.console().unwrap().is_plain());
1710
1711 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]
1726 fn test_rich_mode_output() {
1727 let mut conn = SqliteConnection::open_memory().unwrap();
1728
1729 let console = Arc::new(SqlModelConsole::with_mode(
1731 sqlmodel_console::OutputMode::Rich,
1732 ));
1733 conn.set_console(Some(console.clone()));
1734
1735 assert!(conn.console().unwrap().is_rich());
1737
1738 conn.execute_raw("CREATE TABLE test (id INTEGER PRIMARY KEY)")
1740 .unwrap();
1741 conn.emit_checkpoint_progress(50, 100);
1742 }
1743 }
1744}