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