1#[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
58pub use crate::turso::rows::{Row, Rows};
60
61pub use turso_core as core;
63
64macro_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
136pub 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 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 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 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 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 #[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 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#[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 pub fn connect(&self) -> Result<Connection> {
333 let conn = self.inner.connect()?;
334 Ok(Connection::create(conn, None))
335 }
336}
337
338#[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 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 pub async fn execute(&mut self, params: impl IntoParams) -> Result<u64> {
426 {
427 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 pub fn column_count(&self) -> usize {
454 self.inner.lock().unwrap().column_count()
455 }
456
457 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 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 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 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 pub fn reset(&self) -> Result<()> {
521 let mut stmt = self.inner.lock().unwrap();
522 stmt.reset()?;
523 Ok(())
524 }
525
526 pub fn n_change(&self) -> u64 {
528 self.inner.lock().unwrap().n_change() as u64
529 }
530
531 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 while rows.next().await?.is_some() {}
543 Ok(first_row)
544 }
545}
546
547pub struct Column {
549 name: String,
550 decl_type: Option<String>,
551}
552
553impl Column {
554 pub fn name(&self) -> &str {
556 &self.name
557 }
558
559 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 {
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 } 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; 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 {
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 } {
658 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 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 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 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 assert_eq!(rows.column_count(), 3);
738
739 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 assert_eq!(rows.column_names(), vec!["id", "name", "email"]);
747
748 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); 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 let payload = "X".repeat(200 * 1024);
804
805 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 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 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 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};