Skip to main content

clt_database/turso/src/
lib.rs

1//! # Turso bindings for Rust
2//!
3//! Turso is an in-process SQL database engine, compatible with SQLite.
4//!
5//! ## Getting Started
6//!
7//! To get started, you first need to create a [`Database`] object and then open a [`Connection`] to it, which you use to query:
8//!
9//! ```rust,no_run
10//! # async fn run() {
11//! use turso::Builder;
12//!
13//! let db = Builder::new_local(":memory:").build().await.unwrap();
14//! let conn = db.connect().unwrap();
15//! conn.execute("CREATE TABLE IF NOT EXISTS users (email TEXT)", ()).await.unwrap();
16//! conn.execute("INSERT INTO users (email) VALUES ('alice@example.org')", ()).await.unwrap();
17//! # }
18//! ```
19//!
20//! You can also prepare statements with the [`Connection`] object and then execute the [`Statement`] objects:
21//!
22//! ```rust,no_run
23//! # async fn run() {
24//! # use turso::Builder;
25//! # let db = Builder::new_local(":memory:").build().await.unwrap();
26//! # let conn = db.connect().unwrap();
27//! let mut stmt = conn.prepare("SELECT * FROM users WHERE email = ?1").await.unwrap();
28//! let mut rows = stmt.query(["foo@example.com"]).await.unwrap();
29//! let row = rows.next().await.unwrap().unwrap();
30//! let value = row.get_value(0).unwrap();
31//! println!("Row: {:?}", value);
32//! # }
33//! ```
34
35#[cfg(all(clt_turso_feature = "mimalloc", not(target_family = "wasm"), not(miri)))]
36#[global_allocator]
37static GLOBAL: mimalloc::MiMalloc = mimalloc::MiMalloc;
38
39pub mod connection;
40pub mod params;
41mod rows;
42pub mod transaction;
43pub mod value;
44
45pub use connection::Connection;
46use crate::turso_sdk_kit::rsapi::TursoError;
47pub use value::Value;
48
49pub use params::params_from_iter;
50pub use params::IntoParams;
51
52use std::fmt::Debug;
53use std::future::Future;
54use std::sync::Arc;
55use std::sync::Mutex;
56use std::task::Poll;
57
58// Re-exports rows
59pub use crate::turso::rows::{Row, Rows};
60
61// Re-export turso_core
62pub use turso_core as core;
63
64/// Assert that a type implements both Send and Sync at compile time.
65/// Usage: assert_send_sync!(MyType);
66/// Usage: assert_send_sync!(Type1, Type2, Type3);
67macro_rules! assert_send_sync {
68    ($($t:ty),+ $(,)?) => {
69        #[cfg(clt_turso_tests)]
70        $(const _: () = {
71            const fn _assert_send<T: ?Sized + Send>() {}
72            const fn _assert_sync<T: ?Sized + Sync>() {}
73            _assert_send::<$t>();
74            _assert_sync::<$t>();
75        };)+
76    };
77}
78
79pub(crate) use assert_send_sync;
80
81#[derive(Debug, thiserror::Error)]
82pub enum Error {
83    #[error("SQL conversion failure: `{0}`")]
84    ToSqlConversionFailure(BoxError),
85    #[error("Query returned no rows")]
86    QueryReturnedNoRows,
87    #[error("Conversion failure: `{0}`")]
88    ConversionFailure(String),
89    #[error("{0}")]
90    Busy(String),
91    #[error("{0}")]
92    BusySnapshot(String),
93    #[error("{0}")]
94    Interrupt(String),
95    #[error("{0}")]
96    Error(String),
97    #[error("{0}")]
98    Misuse(String),
99    #[error("{0}")]
100    Constraint(String),
101    #[error("{0}")]
102    Readonly(String),
103    #[error("{0}")]
104    DatabaseFull(String),
105    #[error("{0}")]
106    NotAdb(String),
107    #[error("{0}")]
108    Corrupt(String),
109    #[error("I/O error ({1}): {0}")]
110    IoError(std::io::ErrorKind, &'static str),
111}
112
113impl From<crate::turso_sdk_kit::rsapi::TursoError> for Error {
114    fn from(value: crate::turso_sdk_kit::rsapi::TursoError) -> Self {
115        match value {
116            crate::turso_sdk_kit::rsapi::TursoError::Busy(err) => Error::Busy(err),
117            crate::turso_sdk_kit::rsapi::TursoError::BusySnapshot(err) => Error::BusySnapshot(err),
118            crate::turso_sdk_kit::rsapi::TursoError::Interrupt(err) => Error::Interrupt(err),
119            crate::turso_sdk_kit::rsapi::TursoError::Error(err) => Error::Error(err),
120            crate::turso_sdk_kit::rsapi::TursoError::Misuse(err) => Error::Misuse(err),
121            crate::turso_sdk_kit::rsapi::TursoError::Constraint(err) => Error::Constraint(err),
122            crate::turso_sdk_kit::rsapi::TursoError::Readonly(err) => Error::Readonly(err),
123            crate::turso_sdk_kit::rsapi::TursoError::DatabaseFull(err) => Error::DatabaseFull(err),
124            crate::turso_sdk_kit::rsapi::TursoError::NotAdb(err) => Error::NotAdb(err),
125            crate::turso_sdk_kit::rsapi::TursoError::Corrupt(err) => Error::Corrupt(err),
126            crate::turso_sdk_kit::rsapi::TursoError::IoError(kind, op) => Error::IoError(kind, op),
127        }
128    }
129}
130
131pub(crate) type BoxError = Box<dyn std::error::Error + Send + Sync>;
132
133pub type Result<T> = std::result::Result<T, Error>;
134pub type EncryptionOpts = crate::turso_sdk_kit::rsapi::EncryptionOpts;
135
136/// A builder for `Database`.
137pub struct Builder {
138    path: String,
139    enable_encryption: bool,
140    enable_attach: bool,
141    enable_custom_types: bool,
142    enable_index_method: bool,
143    enable_materialized_views: bool,
144    enable_vacuum: bool,
145    enable_generated_columns: bool,
146    enable_multiprocess_wal: bool,
147    enable_without_rowid: bool,
148    enable_mvcc_passive_checkpoint: bool,
149    vfs: Option<String>,
150    encryption_opts: Option<crate::turso_sdk_kit::rsapi::EncryptionOpts>,
151    io: Option<Arc<dyn turso_core::IO>>,
152}
153
154impl Builder {
155    /// Create a new local database.
156    pub fn new_local(path: &str) -> Self {
157        Self {
158            path: path.to_string(),
159            enable_encryption: false,
160            enable_attach: false,
161            enable_custom_types: false,
162            enable_index_method: false,
163            enable_materialized_views: false,
164            enable_vacuum: false,
165            enable_generated_columns: false,
166            enable_multiprocess_wal: false,
167            enable_without_rowid: false,
168            enable_mvcc_passive_checkpoint: false,
169            vfs: None,
170            encryption_opts: None,
171            io: None,
172        }
173    }
174
175    pub fn experimental_encryption(mut self, encryption_enabled: bool) -> Self {
176        self.enable_encryption = encryption_enabled;
177        self
178    }
179
180    pub fn with_encryption(mut self, opts: crate::turso_sdk_kit::rsapi::EncryptionOpts) -> Self {
181        self.encryption_opts = Some(opts);
182        self
183    }
184
185    /// Kept for backwards compatibility. Triggers are now always enabled.
186    pub fn experimental_triggers(self, _triggers_enabled: bool) -> Self {
187        self
188    }
189
190    pub fn experimental_attach(mut self, attach_enabled: bool) -> Self {
191        self.enable_attach = attach_enabled;
192        self
193    }
194
195    /// Kept for backwards compatibility. Strict tables are now always enabled.
196    pub fn experimental_strict(self, _strict_enabled: bool) -> Self {
197        self
198    }
199
200    pub fn experimental_custom_types(mut self, custom_types_enabled: bool) -> Self {
201        self.enable_custom_types = custom_types_enabled;
202        self
203    }
204
205    pub fn experimental_generated_columns(mut self, gencols_enabled: bool) -> Self {
206        self.enable_generated_columns = gencols_enabled;
207        self
208    }
209
210    pub fn experimental_index_method(mut self, index_method_enabled: bool) -> Self {
211        self.enable_index_method = index_method_enabled;
212        self
213    }
214
215    pub fn experimental_materialized_views(mut self, enabled: bool) -> Self {
216        self.enable_materialized_views = enabled;
217        self
218    }
219
220    pub fn experimental_vacuum(mut self, enabled: bool) -> Self {
221        self.enable_vacuum = enabled;
222        self
223    }
224
225    pub fn experimental_multiprocess_wal(mut self, enabled: bool) -> Self {
226        self.enable_multiprocess_wal = enabled;
227        self
228    }
229
230    pub fn experimental_without_rowid(mut self, enabled: bool) -> Self {
231        self.enable_without_rowid = enabled;
232        self
233    }
234
235    pub fn experimental_mvcc_passive_checkpoint(mut self, enabled: bool) -> Self {
236        self.enable_mvcc_passive_checkpoint = enabled;
237        self
238    }
239
240    pub fn with_io(mut self, vfs: String) -> Self {
241        self.vfs = Some(vfs);
242        self
243    }
244
245    /// Can pass custom IO implementation
246    pub fn with_io_impl(mut self, io: Arc<dyn turso_core::IO>) -> Self {
247        self.io = Some(io);
248        self
249    }
250
251    fn build_features_string(&self) -> Option<String> {
252        let mut features = Vec::new();
253        if self.enable_encryption {
254            features.push("encryption");
255        }
256        if self.enable_attach {
257            features.push("attach");
258        }
259        if self.enable_custom_types {
260            features.push("custom_types");
261        }
262        if self.enable_index_method {
263            features.push("index_method");
264        }
265        if self.enable_materialized_views {
266            features.push("views");
267        }
268        if self.enable_vacuum {
269            features.push("vacuum");
270        }
271        if self.enable_generated_columns {
272            features.push("generated_columns");
273        }
274        if self.enable_multiprocess_wal {
275            features.push("multiprocess_wal");
276        }
277        if self.enable_without_rowid {
278            features.push("without_rowid");
279        }
280        if self.enable_mvcc_passive_checkpoint {
281            features.push("mvcc_passive_checkpoint");
282        }
283        if features.is_empty() {
284            return None;
285        }
286        Some(features.join(","))
287    }
288
289    /// Build the database.
290    #[allow(unused_variables, clippy::arc_with_non_send_sync)]
291    pub async fn build(self) -> Result<Database> {
292        let features = self.build_features_string();
293        let db =
294            crate::turso_sdk_kit::rsapi::TursoDatabase::new(crate::turso_sdk_kit::rsapi::TursoDatabaseConfig {
295                path: self.path,
296                experimental_features: features,
297                async_io: true,
298                encryption: self.encryption_opts,
299                vfs: self.vfs,
300                io: self.io,
301                db_file: None,
302            });
303        while let Some(io_c) = db.open()?.io() {
304            // At this point IO must already be created
305            let io = db
306                .io()
307                .expect("IO must have been set on the first call to db open");
308            io_c.wait_async(io.as_ref())
309                .await
310                .map_err(TursoError::from)?;
311        }
312        Ok(Database { inner: db })
313    }
314}
315
316/// A database.
317///
318/// The `Database` object points to a database and allows you to connect to it
319#[derive(Clone)]
320pub struct Database {
321    inner: Arc<crate::turso_sdk_kit::rsapi::TursoDatabase>,
322}
323
324impl Debug for Database {
325    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
326        f.debug_struct("Database").finish()
327    }
328}
329
330impl Database {
331    /// Connect to the database.
332    pub fn connect(&self) -> Result<Connection> {
333        let conn = self.inner.connect()?;
334        Ok(Connection::create(conn, None))
335    }
336}
337
338/// A prepared statement.
339#[derive(Clone)]
340pub struct Statement {
341    conn: Connection,
342    inner: Arc<Mutex<Box<crate::turso_sdk_kit::rsapi::TursoStatement>>>,
343}
344
345struct Execute {
346    stmt: Statement,
347}
348
349assert_send_sync!(Execute);
350
351impl Future for Execute {
352    type Output = Result<u64>;
353
354    fn poll(
355        self: std::pin::Pin<&mut Self>,
356        cx: &mut std::task::Context<'_>,
357    ) -> std::task::Poll<Self::Output> {
358        match self.stmt.step(None, cx)? {
359            Poll::Ready(_) => {
360                let n_change = self.stmt.inner.lock().unwrap().n_change();
361                Poll::Ready(Ok(n_change as u64))
362            }
363            Poll::Pending => Poll::Pending,
364        }
365    }
366}
367
368impl Statement {
369    fn step(
370        &self,
371        columns: Option<usize>,
372        cx: &mut std::task::Context<'_>,
373    ) -> Poll<Result<Option<Row>>> {
374        let mut stmt = self.inner.lock().unwrap();
375        match stmt.step(Some(cx.waker()))? {
376            crate::turso_sdk_kit::rsapi::TursoStatusCode::Row => {
377                if let Some(columns) = columns {
378                    let mut values = Vec::with_capacity(columns);
379                    for i in 0..columns {
380                        let value = stmt.row_value(i)?;
381                        values.push(value);
382                    }
383                    Poll::Ready(Ok(Some(Row { values })))
384                } else {
385                    Poll::Ready(Err(Error::Misuse(
386                        "unexpected row during execution".to_string(),
387                    )))
388                }
389            }
390            crate::turso_sdk_kit::rsapi::TursoStatusCode::Done => Poll::Ready(Ok(None)),
391            crate::turso_sdk_kit::rsapi::TursoStatusCode::Io => {
392                stmt.run_io()?;
393                if let Some(extra_io) = &self.conn.extra_io {
394                    extra_io(cx.waker().clone())?;
395                }
396                Poll::Pending
397            }
398        }
399    }
400    /// Query the database with this prepared statement.
401    pub async fn query(&mut self, params: impl IntoParams) -> Result<Rows> {
402        self.reset()?;
403
404        let mut stmt = self.inner.lock().unwrap();
405        let params = params.into_params()?;
406        match params {
407            params::Params::None => (),
408            params::Params::Positional(values) => {
409                for (i, value) in values.into_iter().enumerate() {
410                    stmt.bind_positional(i + 1, value.into())?;
411                }
412            }
413            params::Params::Named(values) => {
414                for (name, value) in values.into_iter() {
415                    let position = stmt.named_position(name)?;
416                    stmt.bind_positional(position, value.into())?;
417                }
418            }
419        }
420        let rows = Rows::new(self.clone());
421        Ok(rows)
422    }
423
424    /// Execute this prepared statement.
425    pub async fn execute(&mut self, params: impl IntoParams) -> Result<u64> {
426        {
427            // Reset the statement before executing
428            self.inner.lock().unwrap().reset()?;
429        }
430        let params = params.into_params()?;
431        match params {
432            params::Params::None => (),
433            params::Params::Positional(values) => {
434                for (i, value) in values.into_iter().enumerate() {
435                    let mut stmt = self.inner.lock().unwrap();
436                    stmt.bind_positional(i + 1, value.into())?;
437                }
438            }
439            params::Params::Named(values) => {
440                for (name, value) in values.into_iter() {
441                    let mut stmt = self.inner.lock().unwrap();
442                    let position = stmt.named_position(name)?;
443                    stmt.bind_positional(position, value.into())?;
444                }
445            }
446        }
447
448        let execute = Execute { stmt: self.clone() };
449        execute.await
450    }
451
452    /// Returns the number of columns in the result set.
453    pub fn column_count(&self) -> usize {
454        self.inner.lock().unwrap().column_count()
455    }
456
457    /// Returns the name of the column at the given index.
458    pub fn column_name(&self, idx: usize) -> Result<String> {
459        let stmt = self.inner.lock().unwrap();
460        if idx >= stmt.column_count() {
461            return Err(Error::Misuse(format!(
462                "column index {idx} out of bounds (statement has {} columns)",
463                stmt.column_count()
464            )));
465        }
466        Ok(stmt
467            .column_name(idx)
468            .expect("column index must be within valid range"))
469    }
470
471    /// Returns the names of all columns in the result set.
472    pub fn column_names(&self) -> Vec<String> {
473        let stmt = self.inner.lock().unwrap();
474        let n = stmt.column_count();
475        (0..n)
476            .map(|i| {
477                stmt.column_name(i)
478                    .expect("column index must be within valid range")
479            })
480            .collect()
481    }
482
483    /// Returns the index of the column with the given name.
484    pub fn column_index(&self, name: &str) -> Result<usize> {
485        let stmt = self.inner.lock().unwrap();
486        let n = stmt.column_count();
487        for i in 0..n {
488            let col_name = stmt
489                .column_name(i)
490                .expect("column index must be within valid range");
491            if col_name.as_str().eq_ignore_ascii_case(name) {
492                return Ok(i);
493            }
494        }
495        Err(Error::Misuse(format!(
496            "column '{name}' not found in result set"
497        )))
498    }
499
500    /// Returns columns of the result of this prepared statement.
501    pub fn columns(&self) -> Vec<Column> {
502        let stmt = self.inner.lock().unwrap();
503
504        let n = stmt.column_count();
505
506        let mut cols = Vec::with_capacity(n);
507
508        for i in 0..n {
509            let name = stmt
510                .column_name(i)
511                .expect("column index must be within valid range");
512            let decl_type = stmt.column_decltype(i);
513            cols.push(Column { name, decl_type });
514        }
515
516        cols
517    }
518
519    /// Reset internal statement state after previous execution so it can be reused again
520    pub fn reset(&self) -> Result<()> {
521        let mut stmt = self.inner.lock().unwrap();
522        stmt.reset()?;
523        Ok(())
524    }
525
526    /// Returns the number of rows modified (insert/delete operations) by the most recent executed statement.
527    pub fn n_change(&self) -> u64 {
528        self.inner.lock().unwrap().n_change() as u64
529    }
530
531    /// Execute a query that returns the first [`Row`].
532    ///
533    /// # Errors
534    ///
535    /// - Returns `QueryReturnedNoRows` if no rows were returned.
536    pub async fn query_row(&mut self, params: impl IntoParams) -> Result<Row> {
537        let mut rows = self.query(params).await?;
538
539        let first_row = rows.next().await?.ok_or(Error::QueryReturnedNoRows)?;
540        // Discard remaining rows so that the statement is executed to completion
541        // Otherwise Drop of the statement will cause transaction rollback
542        while rows.next().await?.is_some() {}
543        Ok(first_row)
544    }
545}
546
547/// Column information.
548pub struct Column {
549    name: String,
550    decl_type: Option<String>,
551}
552
553impl Column {
554    /// Return the name of the column.
555    pub fn name(&self) -> &str {
556        &self.name
557    }
558
559    /// Returns the type of the column.
560    pub fn decl_type(&self) -> Option<&str> {
561        self.decl_type.as_deref()
562    }
563}
564
565pub trait IntoValue {
566    fn into_value(self) -> Result<Value>;
567}
568
569#[derive(Debug, Clone)]
570pub enum Params {
571    None,
572    Positional(Vec<Value>),
573    Named(Vec<(String, Value)>),
574}
575
576pub struct Transaction {}
577
578#[cfg(clt_turso_tests)]
579mod tests {
580    use super::*;
581    use tempfile::NamedTempFile;
582
583    #[tokio::test]
584    async fn test_database_persistence() -> Result<()> {
585        let temp_file = NamedTempFile::new().unwrap();
586        let db_path = temp_file.path().to_str().unwrap();
587
588        // First, create the database, a table, and insert some data
589        {
590            let db = Builder::new_local(db_path).build().await?;
591            let conn = db.connect()?;
592            conn.execute(
593                "CREATE TABLE test_persistence (id INTEGER PRIMARY KEY, name TEXT NOT NULL);",
594                (),
595            )
596            .await?;
597            conn.execute("INSERT INTO test_persistence (name) VALUES ('Alice');", ())
598                .await?;
599            conn.execute("INSERT INTO test_persistence (name) VALUES ('Bob');", ())
600                .await?;
601        } // db and conn are dropped here, simulating closing
602
603        // Now, re-open the database and check if the data is still there
604        let db = Builder::new_local(db_path).build().await?;
605        let conn = db.connect()?;
606
607        let mut rows = conn
608            .query("SELECT name FROM test_persistence ORDER BY id;", ())
609            .await?;
610
611        let row1 = rows.next().await?.expect("Expected first row");
612        assert_eq!(row1.get_value(0)?, Value::Text("Alice".to_string()));
613
614        let row2 = rows.next().await?.expect("Expected second row");
615        assert_eq!(row2.get_value(0)?, Value::Text("Bob".to_string()));
616
617        assert!(rows.next().await?.is_none(), "Expected no more rows");
618
619        Ok(())
620    }
621
622    #[tokio::test]
623    async fn test_database_persistence_many_frames() -> Result<()> {
624        let temp_file = NamedTempFile::new().unwrap();
625        let db_path = temp_file.path().to_str().unwrap();
626
627        const NUM_INSERTS: usize = 100;
628        const TARGET_STRING_LEN: usize = 1024; // 1KB
629
630        let mut original_data = Vec::with_capacity(NUM_INSERTS);
631        for i in 0..NUM_INSERTS {
632            let prefix = format!("test_string_{i:04}_");
633            let padding_len = TARGET_STRING_LEN.saturating_sub(prefix.len());
634            let padding: String = "A".repeat(padding_len);
635            original_data.push(format!("{prefix}{padding}"));
636        }
637
638        // First, create the database, a table, and insert many large strings
639        {
640            let db = Builder::new_local(db_path).build().await?;
641            let conn = db.connect()?;
642            conn.execute(
643                "CREATE TABLE test_large_persistence (id INTEGER PRIMARY KEY AUTOINCREMENT, data TEXT NOT NULL);",
644                (),
645            )
646            .await?;
647
648            for data_val in &original_data {
649                conn.execute(
650                    "INSERT INTO test_large_persistence (data) VALUES (?);",
651                    params::Params::Positional(vec![Value::Text(data_val.clone())]),
652                )
653                .await?;
654            }
655        } // db and conn are dropped here, simulating closing
656
657        {
658            // Now, re-open the database and check if the data is still there
659            let db = Builder::new_local(db_path).build().await?;
660            let conn = db.connect()?;
661
662            let mut rows = conn
663                .query("SELECT data FROM test_large_persistence ORDER BY id;", ())
664                .await?;
665
666            for (i, value) in original_data.iter().enumerate().take(NUM_INSERTS) {
667                let row = rows
668                    .next()
669                    .await?
670                    .unwrap_or_else(|| panic!("Expected row {i} but found None"));
671                assert_eq!(
672                    row.get_value(0)?,
673                    Value::Text(value.clone()),
674                    "Mismatch in retrieved data for row {i}"
675                );
676            }
677
678            assert!(
679                rows.next().await?.is_none(),
680                "Expected no more rows after retrieving all inserted data"
681            );
682
683            // Delete the WAL file only and try to re-open and query
684            let wal_path = format!("{db_path}-wal");
685            std::fs::remove_file(&wal_path)
686                .map_err(|e| eprintln!("Warning: Failed to delete WAL file for test: {e}"))
687                .unwrap();
688        }
689
690        // Attempt to re-open the database after deleting WAL and assert that table is missing.
691        let db_after_wal_delete = Builder::new_local(db_path).build().await?;
692        let conn_after_wal_delete = db_after_wal_delete.connect()?;
693
694        let query_result_after_wal_delete = conn_after_wal_delete
695            .query("SELECT data FROM test_large_persistence ORDER BY id;", ())
696            .await;
697
698        match query_result_after_wal_delete {
699            Ok(_) => panic!("Query succeeded after WAL deletion and DB reopen, but was expected to fail because the table definition should have been in the WAL."),
700            Err(Error::Error(msg)) => {
701                assert!(
702                    msg.contains("no such table: test_large_persistence"),
703                    "Expected 'test_large_persistence not found' error, but got: {msg}"
704                );
705            }
706            Err(e) => panic!(
707                "Expected SqlExecutionFailure for 'no such table', but got a different error: {e:?}"
708            ),
709        }
710
711        Ok(())
712    }
713
714    #[tokio::test]
715    async fn test_rows_column_names() -> Result<()> {
716        let db = Builder::new_local(":memory:").build().await?;
717        let conn = db.connect()?;
718        conn.execute(
719            "CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT, email TEXT);",
720            (),
721        )
722        .await?;
723        conn.execute(
724            "INSERT INTO users (name, email) VALUES ('Alice', 'alice@example.org');",
725            (),
726        )
727        .await?;
728
729        let rows = conn.query("SELECT id, name, email FROM users;", ()).await?;
730
731        // columns()
732        let columns = rows.columns();
733        let names: Vec<&str> = columns.iter().map(|c| c.name()).collect();
734        assert_eq!(names, vec!["id", "name", "email"]);
735
736        // column_count()
737        assert_eq!(rows.column_count(), 3);
738
739        // column_name()
740        assert_eq!(rows.column_name(0)?, "id");
741        assert_eq!(rows.column_name(1)?, "name");
742        assert_eq!(rows.column_name(2)?, "email");
743        assert!(rows.column_name(3).is_err());
744
745        // column_names()
746        assert_eq!(rows.column_names(), vec!["id", "name", "email"]);
747
748        // column_index()
749        assert_eq!(rows.column_index("id")?, 0);
750        assert_eq!(rows.column_index("name")?, 1);
751        assert_eq!(rows.column_index("email")?, 2);
752        assert_eq!(rows.column_index("EMAIL")?, 2); // case-insensitive
753        assert!(rows.column_index("nonexistent").is_err());
754
755        Ok(())
756    }
757
758    #[tokio::test]
759    async fn test_database_persistence_write_one_frame_many_times() -> Result<()> {
760        let temp_file = NamedTempFile::new().unwrap();
761        let db_path = temp_file.path().to_str().unwrap();
762
763        for i in 0..100 {
764            {
765                let db = Builder::new_local(db_path).build().await?;
766                let conn = db.connect()?;
767
768                conn.execute("CREATE TABLE IF NOT EXISTS test_persistence (id INTEGER PRIMARY KEY, name TEXT NOT NULL);", ()).await?;
769                conn.execute("INSERT INTO test_persistence (name) VALUES ('Alice');", ())
770                    .await?;
771            }
772            {
773                let db = Builder::new_local(db_path).build().await?;
774                let conn = db.connect()?;
775
776                let mut rows_iter = conn
777                    .query("SELECT count(*) FROM test_persistence;", ())
778                    .await?;
779                let rows = rows_iter.next().await?.unwrap();
780                assert_eq!(rows.get_value(0)?, Value::Integer(i as i64 + 1));
781                assert!(rows_iter.next().await?.is_none());
782            }
783        }
784
785        Ok(())
786    }
787
788    #[tokio::test]
789    async fn test_parallel_writes_and_wal_size() -> Result<()> {
790        let temp_dir = tempfile::tempdir().unwrap();
791        let db_path = temp_dir.path().join("test.db");
792        let db_path_str = db_path.to_str().unwrap();
793
794        let db = Builder::new_local(db_path_str).build().await?;
795        let conn = db.connect()?;
796        conn.execute(
797            "CREATE TABLE test_data (id INTEGER PRIMARY KEY AUTOINCREMENT, payload TEXT NOT NULL);",
798            (),
799        )
800        .await?;
801
802        // Generate a ~200KB payload
803        let payload = "X".repeat(200 * 1024);
804
805        // Parallel writes: spawn 8 connections, each inserting 5 rows
806        let mut handles = Vec::new();
807        for conn_id in 0..8u32 {
808            let db = db.clone();
809            let payload = payload.clone();
810            handles.push(tokio::spawn(async move {
811                let conn = db.connect().unwrap();
812                for row_id in 0..5u32 {
813                    let tag = format!("conn{conn_id}_row{row_id}");
814                    let data = format!("{tag}_{payload}");
815                    loop {
816                        match conn
817                            .execute(
818                                "INSERT INTO test_data (payload) VALUES (?);",
819                                params::Params::Positional(vec![Value::Text(data.clone())]),
820                            )
821                            .await
822                        {
823                            Ok(_) => break,
824                            Err(Error::Busy(_)) => {
825                                tokio::time::sleep(std::time::Duration::from_millis(10)).await;
826                                continue;
827                            }
828                            Err(e) => panic!("Insert failed: {e:?}"),
829                        }
830                    }
831                }
832            }));
833        }
834        for h in handles {
835            h.await.unwrap();
836        }
837
838        // Sequential writes: 3 more large inserts
839        for i in 0..3 {
840            let data = format!("sequential_{i}_{payload}");
841            conn.execute(
842                "INSERT INTO test_data (payload) VALUES (?);",
843                params::Params::Positional(vec![Value::Text(data)]),
844            )
845            .await?;
846        }
847
848        // Verify row count: 8*5 + 3 = 43
849        let mut rows = conn.query("SELECT count(*) FROM test_data;", ()).await?;
850        let row = rows.next().await?.unwrap();
851        assert_eq!(row.get_value(0)?, Value::Integer(43));
852
853        // Report WAL size
854        let wal_path = format!("{db_path_str}-wal");
855        let wal_size = std::fs::metadata(&wal_path).map(|m| m.len()).unwrap_or(0);
856        eprintln!(
857            "WAL size after all writes: {} bytes ({:.2} KB)",
858            wal_size,
859            wal_size as f64 / 1024.0
860        );
861        assert!(wal_size > 0, "WAL file should exist and be non-empty");
862
863        Ok(())
864    }
865}
866
867pub use crate::{params, named_params};