Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .decapod/managed/specs/INTERFACES.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ pub enum ApiError {
- No legacy `DACTYL_*` variables are honored.
- Parameters are always adapter-bound, never interpolated into SQL. `Parameter` enumerates `Null` / `Bool` / `Integer` / `Real` / `Text`.
- `execute` is the caller-owned schema surface: dactyl never silently creates tables.
- `Row` provides strict `get<T: DeserializeOwned>` plus lenient `get_bool` / `get_int` / `get_real` / `get_str` / `get_json` with explicit `ColumnNotFound` / `Conversion` error semantics.
- `Row` provides strict `get` / `try_get<T: DeserializeOwned>`, lenient `get_bool` / `get_int` / `get_real` / `get_str` / `get_json`, borrowed `get_str_ref` / `get_json_ref`, and `is_null`, with explicit `ColumnNotFound` / `Conversion` errors. Named lookup is left-to-right first-match for duplicate aliases. SQL NULL maps to `Option<T>` or a `Conversion` that mentions NULL for non-Option targets. Rows own their cells; borrowed accessors are tied to `&Row` only (dactyl #25 / #2; DecapodLabs/decapod#1111).
- `transaction` is atomic: any per-statement failure rolls back the whole unit on SQLite and is rejected by the Neon `/batch` endpoint.
- `query!("sql")` lexically analyzes the literal at compile time and returns the rewritten SQL as a `String` for the caller to pass to `query`.

Expand Down
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added

- *(dactyl)* complete typed/NULL-safe named row projections for [#25](https://github.com/DecapodLabs/dactyl/issues/25): `try_get`, `is_null`, borrowed `get_str_ref` / `get_json_ref`, explicit first-match duplicate-alias semantics, NULL conversion messages, unit + SQLite/Neon matrix conformance (also DecapodLabs/decapod#1111, dactyl #2).

### Documentation

- *(dactyl)* document the full `Row` projection contract (scalars, NULL, missing columns, aliases, ownership/lifetime) in the README and crate docs.

## [0.2.3](https://github.com/DecapodLabs/dactyl/compare/dactyl-db-macros-v0.2.1...dactyl-db-macros-v0.2.3) - 2026-08-01

### Fixed
Expand Down
25 changes: 22 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,10 +51,12 @@ fn main() -> Result<(), dactyl_db::DactylError> {

let sql = query!("select id, title, status from todos");
for row in dactyl_db::query(&sql, &[])?.iter() {
let id: i64 = row.get("id")?;
// Strict typed projection (owned). Prefer try_get if you like Result style.
let id: i64 = row.try_get("id")?;
let title: String = row.get("title")?;
let status: String = row.get("status")?;
println!("todo {id}: {title} [{status}]");
// Nullable columns use Option<T>; missing columns are ColumnNotFound.
let status: Option<String> = row.get("status")?;
println!("todo {id}: {title} [{status:?}]");
}
Ok(())
}
Expand All @@ -67,6 +69,21 @@ DATASTORE=sqlite DATASTORE_ROUTE=/tmp/dactyl-example.db \
cargo run --features sqlite --example readme_example
```

## Named-column projections (`Row`)

This is the stable contract for typed and NULL-safe extraction (dactyl [#25](https://github.com/DecapodLabs/dactyl/issues/25), conformance [#2](https://github.com/DecapodLabs/dactyl/issues/2); also DecapodLabs/decapod#1111):

| Concern | Semantics |
|---|---|
| Integer / real / bool / text | `get_int`, `get_real`, `get_bool`, `get_str` (owned) or strict `get::<T>` / `try_get::<T>` via serde |
| Portable bool | `get_bool` accepts JSON `true`/`false` **or** integer `0`/`1` (SQLite stores bools as integers) |
| JSON / text | `get_json` / `get_json_ref` return the raw cell. Text payloads stay strings until the caller parses them; Neon may surface structured JSON objects. |
| SQL NULL | `get::<Option<T>>` → `None`; non-`Option` getters → `Conversion` mentioning NULL; `is_null` / `get_json` surface null without converting |
| Missing column | `DactylError::ColumnNotFound` |
| Duplicate aliases | **First match** left-to-right. `select a as x, b as x` → `get("x")` is `a`. Use a positional index for later duplicates. |
| Owned vs borrowed | `get` / `get_*` return owned values that outlive the row. `get_str_ref` / `get_json_ref` borrow from `&Row` for the row lifetime. A `Row` outlives the adapter connection. |
| Conversion failure | `DactylError::Conversion` with the column key and a reason |

## How dactyl selects the backend

The active backend is chosen by ambient environment variables — no `init()` call, no per-call datastore argument, no process-wide connection cache. Each `query` / `execute` / `transaction` call constructs a fresh short-lived adapter and drops it on return, so workspace and session isolation is automatic and the public surface is `Send + Sync` without any lock.
Expand All @@ -88,6 +105,8 @@ The active backend is chosen by ambient environment variables — no `init()` ca

`Parameter` enumerates the typed binding set: `Null`, `Bool`, `Integer`, `Real`, `Text`. The adapter forwards the values verbatim — never as interpolated SQL.

`Row` provides `get` / `try_get`, lenient scalar getters, `is_null`, and borrowed `get_str_ref` / `get_json_ref` under the projection contract above.

## License

This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
8 changes: 5 additions & 3 deletions examples/readme_example.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,12 @@ fn main() -> Result<(), dactyl_db::DactylError> {

let sql = dactyl_db::query!("select id, title, status from todos");
for row in dactyl_db::query(&sql, &[])?.iter() {
let id: i64 = row.get("id")?;
let id: i64 = row.try_get("id")?;
let title: String = row.get("title")?;
let status: String = row.get("status")?;
println!("todo {id}: {title} [{status}]");
let status: String = row.get_str("status")?;
// Borrowed accessor is valid for the row lifetime.
let title_ref: &str = row.get_str_ref("title")?;
println!("todo {id}: {title} ({title_ref}) [{status}]");
}
Ok(())
}
Loading
Loading