turso-mappers 0.4.0

Row mappers for turso
Documentation
# turso-mappers

![logo-export.svg](assets/logo.svg)

Map [turso](https://github.com/tursodatabase/turso) database rows to Rust structs. Provides traits and derive macros that eliminate boilerplate when converting query results into typed data structures.

Three approaches to row mapping:
- **Manual** (`TryFromRow`) — you control exactly how each column maps to each field
- **Positional** (`TryFromRowByIndex`) — derive macro, columns matched by position in the SELECT
- **By name** (`TryFromRowByName`) — derive macro, columns matched by name regardless of SELECT order

Each has a corresponding query extension trait (`QueryAs`, `QueryAsByIndex`, `QueryAsByName`) on `turso::Connection` for one-line query-and-map.

See the [published crate](https://crates.io/crates/turso-mappers) and
the [documentation](https://docs.rs/crate/turso-mappers/latest) for more information.

## Features

- `MapRows` trait for iterating over `turso::Rows` with a mapping closure
- Three mapping traits with derive macros and query helpers:
  - **`TryFromRow`** — manual impl, full control over row-to-struct mapping
  - **`TryFromRowByIndex`** — derive macro, maps columns by position
  - **`TryFromRowByName`** — derive macro, maps columns by name (order-independent)
- Supported column types: `i64`, `f64`, `String`, `Vec<u8>`, and `Option<T>` wrappers
- Query extension traits on `turso::Connection`: `QueryAs`, `QueryAsByIndex`, `QueryAsByName`

## MapRows — low-level row mapping

`MapRows::map_rows` applies a closure to each row, useful for ad-hoc queries or when you don't need a full struct mapping.

```rust
use turso_mappers::{MapRows, TursoMapperResult, TursoMapperError};
use turso::Builder;

#[tokio::main]
async fn main() -> TursoMapperResult<()> {
    let db = Builder::new_local(":memory:").build().await?;
    let conn = db.connect()?;
    conn.execute("CREATE TABLE customer (id INTEGER PRIMARY KEY, name TEXT NOT NULL);", ()).await?;
    conn.execute("INSERT INTO customer (name) VALUES ('Charlie');", ()).await?;
    conn.execute("INSERT INTO customer (name) VALUES ('Sarah');", ()).await?;

    let names = conn
        .query("SELECT id, name FROM customer;", ())
        .await?
        .map_rows(|row| {
            Ok(row.get_value(1)?.as_text()
                .ok_or_else(|| TursoMapperError::ConversionError("name is not text".to_string()))?
                .clone())
        })
        .await?;

    assert_eq!(names, vec!["Charlie", "Sarah"]);
    Ok(())
}
```

## TryFromRow — manual mapping

Implement `TryFromRow` for full control. Use `QueryAs::query_as` or `MapRows::map_rows` to execute.

```rust
use turso_mappers::{TryFromRow, TursoMapperResult, TursoMapperError, QueryAs};
use turso::{Builder, Row};

struct Customer {
    id: i64,
    name: String,
}

impl TryFromRow for Customer {
    fn try_from_row(row: Row) -> TursoMapperResult<Self> {
        Ok(Customer {
            id: *row.get_value(0)?.as_integer()
                .ok_or_else(|| TursoMapperError::ConversionError("id is not an integer".to_string()))?,
            name: row.get_value(1)?.as_text()
                .ok_or_else(|| TursoMapperError::ConversionError("name is not a string".to_string()))?.clone(),
        })
    }
}

#[tokio::main]
async fn main() -> TursoMapperResult<()> {
    let db = Builder::new_local(":memory:").build().await?;
    let conn = db.connect()?;
    conn.execute("CREATE TABLE customer (id INTEGER PRIMARY KEY, name TEXT NOT NULL);", ()).await?;
    conn.execute("INSERT INTO customer (name) VALUES ('Charlie');", ()).await?;

    let customers = conn.query_as::<Customer>("SELECT id, name FROM customer;", ()).await?;
    assert_eq!(customers[0].id, 1);
    assert_eq!(customers[0].name, "Charlie");
    Ok(())
}
```

## TryFromRowByIndex — derive, positional

Derive `TryFromRowByIndex` to map columns by position. Struct field order must match the SELECT column order. Use `QueryAsByIndex::query_as_by_index` to execute.

```rust
use turso_mappers::{TryFromRowByIndex, QueryAsByIndex, TursoMapperResult, TursoMapperError};
use turso::Builder;

#[derive(TryFromRowByIndex)]
struct Customer {
    id: i64,
    name: String,
    value: f64,
    description: Option<String>,
}

#[tokio::main]
async fn main() -> TursoMapperResult<()> {
    let db = Builder::new_local(":memory:").build().await?;
    let conn = db.connect()?;
    conn.execute("CREATE TABLE customer (id INTEGER PRIMARY KEY, name TEXT NOT NULL, value REAL NOT NULL, description TEXT);", ()).await?;
    conn.execute("INSERT INTO customer (name, value) VALUES ('Charlie', 3.12);", ()).await?;

    let customers = conn.query_as_by_index::<Customer>("SELECT id, name, value, description FROM customer;", ()).await?;
    assert_eq!(customers[0].id, 1);
    assert_eq!(customers[0].name, "Charlie");
    assert_eq!(customers[0].value, 3.12);
    assert_eq!(customers[0].description, None);
    Ok(())
}
```

## TryFromRowByName — derive, by name

Derive `TryFromRowByName` to map columns by name. Column order in the SELECT doesn't matter — fields are resolved by matching struct field names to column names. Use `QueryAsByName::query_as_by_name` to execute.

```rust
use turso_mappers::{TryFromRowByName, QueryAsByName, TursoMapperResult, TursoMapperError, ColumnIndices};
use turso::Builder;

#[derive(TryFromRowByName)]
struct Customer {
    id: i64,
    name: String,
    value: f64,
    description: Option<String>,
}

#[tokio::main]
async fn main() -> TursoMapperResult<()> {
    let db = Builder::new_local(":memory:").build().await?;
    let conn = db.connect()?;
    conn.execute("CREATE TABLE customer (id INTEGER PRIMARY KEY, name TEXT NOT NULL, value REAL NOT NULL, description TEXT);", ()).await?;
    conn.execute("INSERT INTO customer (name, value) VALUES ('Charlie', 3.12);", ()).await?;

    // Column order doesn't need to match struct field order
    let customers = conn.query_as_by_name::<Customer>("SELECT description, value, name, id FROM customer;", ()).await?;
    assert_eq!(customers[0].id, 1);
    assert_eq!(customers[0].name, "Charlie");
    assert_eq!(customers[0].value, 3.12);
    assert_eq!(customers[0].description, None);
    Ok(())
}
```

## TODO
- Improve error messages and error handling
- Reduce duplicated code in derive macro implementation