Skip to main content

turso_mappers/
lib.rs

1#![allow(clippy::uninlined_format_args)]
2
3extern crate self as turso_mappers;
4
5use std::collections::HashMap;
6use std::future::Future;
7use turso::{Column, Connection, IntoParams};
8pub use turso_mappers_derive::{TryFromRowByIndex, TryFromRowByName};
9
10#[doc = include_str!("../README.md")]
11#[cfg(doctest)]
12pub struct ReadmeDocTests;
13
14#[derive(Debug)]
15pub enum TursoMapperError {
16    ColumnNotFound(String),
17    InvalidType(String),
18    NullValue(String),
19    ConversionError(String),
20    IoError(std::io::Error),
21    TursoError(turso::Error),
22}
23
24impl From<turso::Error> for TursoMapperError {
25    fn from(err: turso::Error) -> Self {
26        TursoMapperError::TursoError(err)
27    }
28}
29
30impl From<std::io::Error> for TursoMapperError {
31    fn from(err: std::io::Error) -> Self {
32        TursoMapperError::IoError(err)
33    }
34}
35
36impl std::fmt::Display for TursoMapperError {
37    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
38        match self {
39            TursoMapperError::ColumnNotFound(msg) => write!(f, "Column not found: {}", msg),
40            TursoMapperError::InvalidType(msg) => write!(f, "Invalid type: {}", msg),
41            TursoMapperError::NullValue(msg) => write!(f, "Null value: {}", msg),
42            TursoMapperError::ConversionError(msg) => write!(f, "Conversion error: {}", msg),
43            TursoMapperError::IoError(err) => write!(f, "IO error: {}", err),
44            TursoMapperError::TursoError(err) => write!(f, "Turso error: {}", err),
45        }
46    }
47}
48
49impl std::error::Error for TursoMapperError {}
50
51pub type TursoMapperResult<T> = Result<T, TursoMapperError>;
52
53pub trait MapRows {
54    fn map_rows<F, T>(self, f: F) -> impl Future<Output = TursoMapperResult<Vec<T>>>
55    where
56        F: Fn(turso::Row) -> TursoMapperResult<T>,
57        T: Send;
58}
59
60impl MapRows for turso::Rows {
61    async fn map_rows<F, T>(mut self, f: F) -> TursoMapperResult<Vec<T>>
62    where
63        F: Fn(turso::Row) -> TursoMapperResult<T>,
64        T: Send,
65    {
66        let mut rows = vec![];
67
68        while let Some(row) = self.next().await? {
69            let t: T = f(row)?;
70            rows.push(t);
71        }
72
73        Ok(rows)
74    }
75}
76
77pub trait TryFromRow: Send {
78    fn try_from_row(row: turso::Row) -> TursoMapperResult<Self>
79    where
80        Self: Sized;
81}
82
83pub trait QueryAs {
84    fn query_as<T>(&self, sql: &str, params: impl IntoParams) -> impl Future<Output = TursoMapperResult<Vec<T>>>
85    where
86        T: TryFromRow + Send;
87}
88
89impl QueryAs for Connection {
90    async fn query_as<T>(&self, sql: &str, params: impl IntoParams) -> TursoMapperResult<Vec<T>>
91    where
92        T: TryFromRow + Send,
93    {
94        let rows = self.query(sql, params).await?;
95        rows.map_rows(T::try_from_row).await
96    }
97}
98
99pub trait TryFromRowByName: Send {
100    type Indices;
101    fn resolve_indices(column_indices: &ColumnIndices) -> TursoMapperResult<Self::Indices>;
102    fn try_from_row_by_name(row: turso::Row, indices: &Self::Indices) -> TursoMapperResult<Self>
103    where
104        Self: Sized;
105}
106
107pub trait QueryAsByName {
108    fn query_as_by_name<T>(&self, sql: &str, params: impl IntoParams) -> impl Future<Output = TursoMapperResult<Vec<T>>>
109    where
110        T: TryFromRowByName + Send;
111}
112
113impl QueryAsByName for Connection {
114    async fn query_as_by_name<T>(&self, sql: &str, params: impl IntoParams) -> TursoMapperResult<Vec<T>>
115    where
116        T: TryFromRowByName + Send,
117    {
118        let mut rows = self.query(sql, params).await?;
119        let column_indices = ColumnIndices::new(rows.columns());
120        let indices = T::resolve_indices(&column_indices)?;
121        let mut results = vec![];
122        while let Some(row) = rows.next().await? {
123            results.push(T::try_from_row_by_name(row, &indices)?);
124        }
125        Ok(results)
126    }
127}
128
129pub trait TryFromRowByIndex: Send {
130    fn try_from_row_by_index(row: turso::Row) -> TursoMapperResult<Self>
131    where
132        Self: Sized;
133}
134
135pub trait QueryAsByIndex {
136    fn query_as_by_index<T>(&self, sql: &str, params: impl IntoParams) -> impl Future<Output = TursoMapperResult<Vec<T>>>
137    where
138        T: TryFromRowByIndex + Send;
139}
140
141impl QueryAsByIndex for Connection {
142    async fn query_as_by_index<T>(&self, sql: &str, params: impl IntoParams) -> TursoMapperResult<Vec<T>>
143    where
144        T: TryFromRowByIndex + Send,
145    {
146        let rows = self.query(sql, params).await?;
147        rows.map_rows(T::try_from_row_by_index).await
148    }
149}
150
151pub struct ColumnIndices {
152    column_names: HashMap<String, usize>,
153}
154
155impl ColumnIndices {
156    pub fn new(columns: Vec<Column>) -> Self {
157        let column_names = columns
158            .iter()
159            .enumerate()
160            .map(|(i, column)| (column.name().to_string(), i))
161            .collect::<HashMap<String, usize>>();
162
163        ColumnIndices { column_names }
164    }
165
166    pub fn get_index(&self, column_name: &str) -> Result<usize, TursoMapperError> {
167        self.column_names
168            .get(column_name)
169            .cloned()
170            .ok_or_else(|| TursoMapperError::ColumnNotFound(column_name.to_string()))
171    }
172}
173
174#[cfg(test)]
175mod tests {
176    use super::{ColumnIndices, QueryAs, QueryAsByIndex, QueryAsByName, TryFromRow, TryFromRowByIndex, TryFromRowByName, TursoMapperResult};
177    use crate::{MapRows, TursoMapperError};
178    use turso::{Builder, Row};
179
180    struct CustomerWithManualTryFromRow {
181        id: i64,
182        name: String,
183        value: f64,
184        image: Vec<u8>,
185    }
186
187    impl TryFromRowByIndex for CustomerWithManualTryFromRow {
188        fn try_from_row_by_index(row: Row) -> TursoMapperResult<Self> {
189            Ok(CustomerWithManualTryFromRow {
190                id: *row
191                    .get_value(0)?
192                    .as_integer()
193                    .ok_or_else(|| TursoMapperError::ConversionError("id is not an integer".to_string()))?,
194                name: row
195                    .get_value(1)?
196                    .as_text()
197                    .ok_or_else(|| TursoMapperError::ConversionError("name is not a string".to_string()))?
198                    .clone(),
199                value: *row
200                    .get_value(2)?
201                    .as_real()
202                    .ok_or_else(|| TursoMapperError::ConversionError("value is not a real".to_string()))?,
203                image: row
204                    .get_value(3)?
205                    .as_blob()
206                    .ok_or_else(|| TursoMapperError::ConversionError("image is not a blob".to_string()))?
207                    .clone(),
208            })
209        }
210    }
211
212    #[derive(TryFromRowByIndex)]
213    struct Customer {
214        id: i64,
215        name: String,
216        value: f64,
217        image: Vec<u8>,
218    }
219
220    #[derive(TryFromRowByIndex)]
221    struct CustomerWithOptions {
222        id: i64,
223        name: String,
224        optional_value: Option<f64>,
225        optional_note: Option<String>,
226        optional_data: Option<Vec<u8>>,
227        optional_count: Option<i64>,
228    }
229
230    // Manual TryFromRow impl — positional access matching SELECT id, name, value, image
231    struct CustomerManual {
232        id: i64,
233        name: String,
234        value: f64,
235        image: Vec<u8>,
236    }
237
238    impl TryFromRow for CustomerManual {
239        fn try_from_row(row: Row) -> TursoMapperResult<Self> {
240            Ok(CustomerManual {
241                id: *row
242                    .get_value(0)?
243                    .as_integer()
244                    .ok_or_else(|| TursoMapperError::ConversionError("id is not an integer".to_string()))?,
245                name: row
246                    .get_value(1)?
247                    .as_text()
248                    .ok_or_else(|| TursoMapperError::ConversionError("name is not a string".to_string()))?
249                    .clone(),
250                value: *row
251                    .get_value(2)?
252                    .as_real()
253                    .ok_or_else(|| TursoMapperError::ConversionError("value is not a real".to_string()))?,
254                image: row
255                    .get_value(3)?
256                    .as_blob()
257                    .ok_or_else(|| TursoMapperError::ConversionError("image is not a blob".to_string()))?
258                    .clone(),
259            })
260        }
261    }
262
263    // Manual TryFromRow impl — positional access matching SELECT image, value, name, id
264    struct CustomerManualReordered {
265        id: i64,
266        name: String,
267        value: f64,
268        image: Vec<u8>,
269    }
270
271    impl TryFromRow for CustomerManualReordered {
272        fn try_from_row(row: Row) -> TursoMapperResult<Self> {
273            Ok(CustomerManualReordered {
274                image: row
275                    .get_value(0)?
276                    .as_blob()
277                    .ok_or_else(|| TursoMapperError::ConversionError("image is not a blob".to_string()))?
278                    .clone(),
279                value: *row
280                    .get_value(1)?
281                    .as_real()
282                    .ok_or_else(|| TursoMapperError::ConversionError("value is not a real".to_string()))?,
283                name: row
284                    .get_value(2)?
285                    .as_text()
286                    .ok_or_else(|| TursoMapperError::ConversionError("name is not a string".to_string()))?
287                    .clone(),
288                id: *row
289                    .get_value(3)?
290                    .as_integer()
291                    .ok_or_else(|| TursoMapperError::ConversionError("id is not an integer".to_string()))?,
292            })
293        }
294    }
295
296    #[tokio::test]
297    async fn can_get_values_using_map() -> TursoMapperResult<()> {
298        let db = Builder::new_local(":memory:").build().await?;
299        let conn = db.connect()?;
300
301        conn.execute(
302            "CREATE TABLE customer (id INTEGER PRIMARY KEY, name TEXT NOT NULL, value REAL NOT NULL, image BLOB NOT NULL);",
303            (),
304        )
305        .await?;
306        conn.execute("INSERT INTO customer (name, value, image) VALUES ('Charlie', 3.12, x'00010203');", ())
307            .await?;
308        conn.execute("INSERT INTO customer (name, value, image) VALUES ('Sarah', 0.99, x'09080706');", ())
309            .await?;
310
311        let rows = conn.query("SELECT id, name, value, image FROM customer;", ()).await?;
312
313        let customer_names = rows
314            .map_rows(|row| {
315
316
317
318                Ok(row
319                    .get_value(1)?
320                    .as_text()
321                    .ok_or_else(|| TursoMapperError::ConversionError("name is not a string".to_string()))?
322                    .clone())
323            })
324            .await?;
325
326        assert_eq!(customer_names.len(), 2);
327
328        assert_eq!(customer_names[0], "Charlie");
329        assert_eq!(customer_names[1], "Sarah");
330
331        Ok(())
332    }
333
334    #[tokio::test]
335    async fn can_get_values_using_map_with_names() -> TursoMapperResult<()> {
336        let db = Builder::new_local(":memory:").build().await?;
337        let conn = db.connect()?;
338
339        conn.execute(
340            "CREATE TABLE customer (id INTEGER PRIMARY KEY, name TEXT NOT NULL, value REAL NOT NULL, image BLOB NOT NULL);",
341            (),
342        )
343        .await?;
344
345        conn.execute("INSERT INTO customer (name, value, image) VALUES ('Charlie', 3.12, x'00010203');", ())
346            .await?;
347
348        conn.execute("INSERT INTO customer (name, value, image) VALUES ('Sarah', 0.99, x'09080706');", ())
349            .await?;
350
351        let mut statement = conn.prepare("SELECT id, name, value, image FROM customer;").await?;
352        let rows = statement.query(()).await?;
353
354        let column_indices = ColumnIndices::new(statement.columns());
355        let name_column_index = column_indices.get_index("name")?;
356
357        let customer_names = rows
358            .map_rows(|row| {
359                Ok(row
360                    .get_value(name_column_index)?
361                    .as_text()
362                    .ok_or_else(|| TursoMapperError::ConversionError("name is not a string".to_string()))?
363                    .clone())
364            })
365            .await?;
366
367        assert_eq!(customer_names.len(), 2);
368
369        assert_eq!(customer_names[0], "Charlie");
370        assert_eq!(customer_names[1], "Sarah");
371
372        Ok(())
373    }
374
375    #[tokio::test]
376    async fn manual_try_from_row_by_index_impl() -> TursoMapperResult<()> {
377        let db = Builder::new_local(":memory:").build().await?;
378        let conn = db.connect()?;
379        conn.execute("CREATE TABLE t (id INTEGER PRIMARY KEY, name TEXT NOT NULL, value REAL NOT NULL, image BLOB NOT NULL);", ()).await?;
380        conn.execute("INSERT INTO t (name, value, image) VALUES ('Charlie', 3.12, x'01020300');", ()).await?;
381
382        let mut rows = conn.query("SELECT id, name, value, image FROM t;", ()).await?;
383        let row = rows.next().await?.unwrap();
384        let customer = CustomerWithManualTryFromRow::try_from_row_by_index(row)?;
385
386        assert_eq!(customer.id, 1);
387        assert_eq!(customer.name, "Charlie");
388        assert_eq!(customer.value, 3.12);
389        assert_eq!(customer.image, vec![1, 2, 3, 0]);
390
391        Ok(())
392    }
393
394    #[tokio::test]
395    async fn derived_try_from_row_by_index_impl() -> TursoMapperResult<()> {
396        let db = Builder::new_local(":memory:").build().await?;
397        let conn = db.connect()?;
398        conn.execute("CREATE TABLE t (id INTEGER PRIMARY KEY, name TEXT NOT NULL, value REAL NOT NULL, image BLOB NOT NULL);", ()).await?;
399        conn.execute("INSERT INTO t (name, value, image) VALUES ('Charlie', 3.12, x'01020300');", ()).await?;
400
401        let mut rows = conn.query("SELECT id, name, value, image FROM t;", ()).await?;
402        let row = rows.next().await?.unwrap();
403        let customer = Customer::try_from_row_by_index(row)?;
404
405        assert_eq!(customer.id, 1);
406        assert_eq!(customer.name, "Charlie");
407        assert_eq!(customer.value, 3.12);
408        assert_eq!(customer.image, vec![1, 2, 3, 0]);
409
410        Ok(())
411    }
412
413    #[tokio::test]
414    async fn end_to_end_test_with_map_rows_and_try_from_row() -> TursoMapperResult<()> {
415        let db = Builder::new_local(":memory:").build().await?;
416        let conn = db.connect()?;
417
418        conn.execute(
419            "CREATE TABLE customer (id INTEGER PRIMARY KEY, name TEXT NOT NULL, value REAL NOT NULL, image BLOB NOT NULL);",
420            (),
421        )
422        .await?;
423
424        conn.execute("INSERT INTO customer (name, value, image) VALUES ('Charlie', 3.12, x'00010203');", ())
425            .await?;
426
427        conn.execute("INSERT INTO customer (name, value, image) VALUES ('Sarah', 0.99, x'09080706');", ())
428            .await?;
429
430        let customers = conn
431            .query("SELECT id, name, value, image FROM customer;", ())
432            .await?
433            .map_rows(Customer::try_from_row_by_index)
434            .await?;
435
436        assert_eq!(customers.len(), 2);
437
438        assert_eq!(customers[0].id, 1);
439        assert_eq!(customers[0].name, "Charlie");
440        assert_eq!(customers[0].value, 3.12);
441        assert_eq!(customers[0].image, vec![0, 1, 2, 3]);
442
443        assert_eq!(customers[1].id, 2);
444        assert_eq!(customers[1].name, "Sarah");
445        assert_eq!(customers[1].value, 0.99);
446        assert_eq!(customers[1].image, vec![9, 8, 7, 6]);
447
448        Ok(())
449    }
450
451    #[tokio::test]
452    async fn end_to_end_test_with_query_as_by_index() -> TursoMapperResult<()> {
453        let db = Builder::new_local(":memory:").build().await?;
454        let conn = db.connect()?;
455
456        conn.execute(
457            "CREATE TABLE customer (id INTEGER PRIMARY KEY, name TEXT NOT NULL, value REAL NOT NULL, image BLOB NOT NULL);",
458            (),
459        )
460        .await?;
461
462        conn.execute("INSERT INTO customer (name, value, image) VALUES ('Charlie', 3.12, x'00010203');", ())
463            .await?;
464
465        conn.execute("INSERT INTO customer (name, value, image) VALUES ('Sarah', 0.99, x'09080706');", ())
466            .await?;
467
468        let customers = conn.query_as_by_index::<Customer>("SELECT id, name, value, image FROM customer;", ()).await?;
469
470        assert_eq!(customers.len(), 2);
471
472        assert_eq!(customers[0].id, 1);
473        assert_eq!(customers[0].name, "Charlie");
474        assert_eq!(customers[0].value, 3.12);
475        assert_eq!(customers[0].image, vec![0, 1, 2, 3]);
476
477        assert_eq!(customers[1].id, 2);
478        assert_eq!(customers[1].name, "Sarah");
479        assert_eq!(customers[1].value, 0.99);
480        assert_eq!(customers[1].image, vec![9, 8, 7, 6]);
481
482        Ok(())
483    }
484
485    #[tokio::test]
486    async fn option_types_support_works() -> TursoMapperResult<()> {
487        let db = Builder::new_local(":memory:").build().await?;
488        let conn = db.connect()?;
489        conn.execute(
490            "CREATE TABLE t (id INTEGER PRIMARY KEY, name TEXT NOT NULL, optional_value REAL, optional_note TEXT, optional_data BLOB, optional_count INTEGER);",
491            (),
492        ).await?;
493
494        // Row with some NULLs
495        conn.execute("INSERT INTO t (name, optional_value, optional_data) VALUES ('Charlie', 3.12, x'010203');", ()).await?;
496        // Row with all non-NULL
497        conn.execute("INSERT INTO t (name, optional_value, optional_note, optional_data, optional_count) VALUES ('Sarah', 0.99, 'Some note', x'09080706', 42);", ()).await?;
498
499        let customers = conn.query_as_by_index::<CustomerWithOptions>("SELECT id, name, optional_value, optional_note, optional_data, optional_count FROM t;", ()).await?;
500
501        assert_eq!(customers[0].id, 1);
502        assert_eq!(customers[0].name, "Charlie");
503        assert_eq!(customers[0].optional_value, Some(3.12));
504        assert_eq!(customers[0].optional_note, None);
505        assert_eq!(customers[0].optional_data, Some(vec![1, 2, 3]));
506        assert_eq!(customers[0].optional_count, None);
507
508        assert_eq!(customers[1].id, 2);
509        assert_eq!(customers[1].name, "Sarah");
510        assert_eq!(customers[1].optional_value, Some(0.99));
511        assert_eq!(customers[1].optional_note, Some("Some note".to_string()));
512        assert_eq!(customers[1].optional_data, Some(vec![9, 8, 7, 6]));
513        assert_eq!(customers[1].optional_count, Some(42));
514
515        Ok(())
516    }
517
518    // --- TryFromRow (manual, simple) tests ---
519
520    #[tokio::test]
521    async fn try_from_row_manual_single_row() -> TursoMapperResult<()> {
522        let db = Builder::new_local(":memory:").build().await?;
523        let conn = db.connect()?;
524        conn.execute("CREATE TABLE t (id INTEGER PRIMARY KEY, name TEXT NOT NULL, value REAL NOT NULL, image BLOB NOT NULL);", ()).await?;
525        conn.execute("INSERT INTO t (name, value, image) VALUES ('Charlie', 3.12, x'01020300');", ()).await?;
526
527        let mut rows = conn.query("SELECT id, name, value, image FROM t;", ()).await?;
528        let row = rows.next().await?.unwrap();
529        let customer = CustomerManual::try_from_row(row)?;
530
531        assert_eq!(customer.id, 1);
532        assert_eq!(customer.name, "Charlie");
533        assert_eq!(customer.value, 3.12);
534        assert_eq!(customer.image, vec![1, 2, 3, 0]);
535
536        Ok(())
537    }
538
539    #[tokio::test]
540    async fn query_as_end_to_end() -> TursoMapperResult<()> {
541        let db = Builder::new_local(":memory:").build().await?;
542        let conn = db.connect()?;
543        conn.execute("CREATE TABLE customer (id INTEGER PRIMARY KEY, name TEXT NOT NULL, value REAL NOT NULL, image BLOB NOT NULL);", ()).await?;
544        conn.execute("INSERT INTO customer (name, value, image) VALUES ('Charlie', 3.12, x'00010203');", ()).await?;
545        conn.execute("INSERT INTO customer (name, value, image) VALUES ('Sarah', 0.99, x'09080706');", ()).await?;
546
547        let customers = conn.query_as::<CustomerManual>("SELECT id, name, value, image FROM customer;", ()).await?;
548
549        assert_eq!(customers.len(), 2);
550        assert_eq!(customers[0].id, 1);
551        assert_eq!(customers[0].name, "Charlie");
552        assert_eq!(customers[0].value, 3.12);
553        assert_eq!(customers[0].image, vec![0, 1, 2, 3]);
554        assert_eq!(customers[1].id, 2);
555        assert_eq!(customers[1].name, "Sarah");
556        assert_eq!(customers[1].value, 0.99);
557        assert_eq!(customers[1].image, vec![9, 8, 7, 6]);
558
559        Ok(())
560    }
561
562    #[tokio::test]
563    async fn query_as_column_reorder() -> TursoMapperResult<()> {
564        let db = Builder::new_local(":memory:").build().await?;
565        let conn = db.connect()?;
566        conn.execute("CREATE TABLE t (id INTEGER PRIMARY KEY, name TEXT NOT NULL, value REAL NOT NULL, image BLOB NOT NULL);", ()).await?;
567        conn.execute("INSERT INTO t (name, value, image) VALUES ('Charlie', 3.12, x'01020300');", ()).await?;
568
569        // Manual impl maps indices to match reordered SELECT
570        let customers = conn.query_as::<CustomerManualReordered>("SELECT image, value, name, id FROM t;", ()).await?;
571
572        assert_eq!(customers[0].id, 1);
573        assert_eq!(customers[0].name, "Charlie");
574        assert_eq!(customers[0].value, 3.12);
575        assert_eq!(customers[0].image, vec![1, 2, 3, 0]);
576
577        Ok(())
578    }
579
580    // --- By-name mapping tests ---
581
582    #[derive(TryFromRowByName)]
583    struct CustomerByName {
584        id: i64,
585        name: String,
586        value: f64,
587        image: Vec<u8>,
588    }
589
590    #[derive(TryFromRowByName)]
591    struct CustomerByNameWithOptions {
592        id: i64,
593        name: String,
594        optional_value: Option<f64>,
595        optional_note: Option<String>,
596        optional_data: Option<Vec<u8>>,
597        optional_count: Option<i64>,
598    }
599
600    #[tokio::test]
601    async fn derived_try_from_row_by_name_impl() -> TursoMapperResult<()> {
602        let db = Builder::new_local(":memory:").build().await?;
603        let conn = db.connect()?;
604        conn.execute("CREATE TABLE t (id INTEGER PRIMARY KEY, name TEXT NOT NULL, value REAL NOT NULL, image BLOB NOT NULL);", ()).await?;
605        conn.execute("INSERT INTO t (name, value, image) VALUES ('Charlie', 3.12, x'01020300');", ()).await?;
606
607        let mut rows = conn.query("SELECT id, name, value, image FROM t;", ()).await?;
608        let column_indices = ColumnIndices::new(rows.columns());
609        let indices = CustomerByName::resolve_indices(&column_indices)?;
610        let row = rows.next().await?.unwrap();
611        let customer = CustomerByName::try_from_row_by_name(row, &indices)?;
612
613        assert_eq!(customer.id, 1);
614        assert_eq!(customer.name, "Charlie");
615        assert_eq!(customer.value, 3.12);
616        assert_eq!(customer.image, vec![1, 2, 3, 0]);
617
618        Ok(())
619    }
620
621    #[tokio::test]
622    async fn end_to_end_test_with_query_as_by_name() -> TursoMapperResult<()> {
623        let db = Builder::new_local(":memory:").build().await?;
624        let conn = db.connect()?;
625        conn.execute("CREATE TABLE customer (id INTEGER PRIMARY KEY, name TEXT NOT NULL, value REAL NOT NULL, image BLOB NOT NULL);", ()).await?;
626        conn.execute("INSERT INTO customer (name, value, image) VALUES ('Charlie', 3.12, x'00010203');", ()).await?;
627        conn.execute("INSERT INTO customer (name, value, image) VALUES ('Sarah', 0.99, x'09080706');", ()).await?;
628
629        let customers = conn.query_as_by_name::<CustomerByName>("SELECT id, name, value, image FROM customer;", ()).await?;
630
631        assert_eq!(customers.len(), 2);
632        assert_eq!(customers[0].id, 1);
633        assert_eq!(customers[0].name, "Charlie");
634        assert_eq!(customers[0].value, 3.12);
635        assert_eq!(customers[0].image, vec![0, 1, 2, 3]);
636        assert_eq!(customers[1].id, 2);
637        assert_eq!(customers[1].name, "Sarah");
638        assert_eq!(customers[1].value, 0.99);
639        assert_eq!(customers[1].image, vec![9, 8, 7, 6]);
640
641        Ok(())
642    }
643
644    #[tokio::test]
645    async fn query_as_with_column_reorder() -> TursoMapperResult<()> {
646        let db = Builder::new_local(":memory:").build().await?;
647        let conn = db.connect()?;
648        conn.execute("CREATE TABLE t (id INTEGER PRIMARY KEY, name TEXT NOT NULL, value REAL NOT NULL, image BLOB NOT NULL);", ()).await?;
649        conn.execute("INSERT INTO t (name, value, image) VALUES ('Charlie', 3.12, x'01020300');", ()).await?;
650
651        // SELECT columns in different order than struct fields
652        let customers = conn.query_as_by_name::<CustomerByName>("SELECT image, value, name, id FROM t;", ()).await?;
653
654        assert_eq!(customers[0].id, 1);
655        assert_eq!(customers[0].name, "Charlie");
656        assert_eq!(customers[0].value, 3.12);
657        assert_eq!(customers[0].image, vec![1, 2, 3, 0]);
658
659        Ok(())
660    }
661
662    #[tokio::test]
663    async fn try_from_row_by_name_column_reorder() -> TursoMapperResult<()> {
664        let db = Builder::new_local(":memory:").build().await?;
665        let conn = db.connect()?;
666        conn.execute("CREATE TABLE t (id INTEGER PRIMARY KEY, name TEXT NOT NULL, value REAL NOT NULL, image BLOB NOT NULL);", ()).await?;
667        conn.execute("INSERT INTO t (name, value, image) VALUES ('Charlie', 3.12, x'01020300');", ()).await?;
668
669        // SELECT columns in different order — resolved automatically by name
670        let mut rows = conn.query("SELECT image, value, name, id FROM t;", ()).await?;
671        let column_indices = ColumnIndices::new(rows.columns());
672        let indices = CustomerByName::resolve_indices(&column_indices)?;
673        let row = rows.next().await?.unwrap();
674        let customer = CustomerByName::try_from_row_by_name(row, &indices)?;
675
676        assert_eq!(customer.id, 1);
677        assert_eq!(customer.name, "Charlie");
678        assert_eq!(customer.value, 3.12);
679        assert_eq!(customer.image, vec![1, 2, 3, 0]);
680
681        Ok(())
682    }
683
684    #[tokio::test]
685    async fn query_as_with_options() -> TursoMapperResult<()> {
686        let db = Builder::new_local(":memory:").build().await?;
687        let conn = db.connect()?;
688        conn.execute(
689            "CREATE TABLE t (id INTEGER PRIMARY KEY, name TEXT NOT NULL, optional_value REAL, optional_note TEXT, optional_data BLOB, optional_count INTEGER);",
690            (),
691        ).await?;
692        conn.execute("INSERT INTO t (name, optional_value, optional_data) VALUES ('Charlie', 3.12, x'010203');", ()).await?;
693        conn.execute("INSERT INTO t (name, optional_value, optional_note, optional_data, optional_count) VALUES ('Sarah', 0.99, 'Some note', x'09080706', 42);", ()).await?;
694
695        let customers = conn.query_as_by_name::<CustomerByNameWithOptions>("SELECT id, name, optional_value, optional_note, optional_data, optional_count FROM t;", ()).await?;
696
697        assert_eq!(customers[0].id, 1);
698        assert_eq!(customers[0].name, "Charlie");
699        assert_eq!(customers[0].optional_value, Some(3.12));
700        assert_eq!(customers[0].optional_note, None);
701        assert_eq!(customers[0].optional_data, Some(vec![1, 2, 3]));
702        assert_eq!(customers[0].optional_count, None);
703
704        assert_eq!(customers[1].id, 2);
705        assert_eq!(customers[1].name, "Sarah");
706        assert_eq!(customers[1].optional_value, Some(0.99));
707        assert_eq!(customers[1].optional_note, Some("Some note".to_string()));
708        assert_eq!(customers[1].optional_data, Some(vec![9, 8, 7, 6]));
709        assert_eq!(customers[1].optional_count, Some(42));
710
711        Ok(())
712    }
713
714    #[tokio::test]
715    async fn query_as_missing_column_error() -> TursoMapperResult<()> {
716        let db = Builder::new_local(":memory:").build().await?;
717        let conn = db.connect()?;
718        conn.execute("CREATE TABLE t (id INTEGER PRIMARY KEY, name TEXT NOT NULL, value REAL NOT NULL, image BLOB NOT NULL);", ()).await?;
719        conn.execute("INSERT INTO t (name, value, image) VALUES ('Charlie', 3.12, x'01020300');", ()).await?;
720
721        // Omit 'image' column -- should fail with ColumnNotFound
722        let result = conn.query_as_by_name::<CustomerByName>("SELECT id, name, value FROM t;", ()).await;
723        assert!(result.is_err());
724
725        Ok(())
726    }
727}