dynamodb-facade 0.1.1

A typed facade over aws-sdk-dynamodb with expression builders and batch/transaction support
Documentation
# AGENTS.md — dynamodb-facade

Rust library: typed facade over `aws-sdk-dynamodb` with expression builders,
typestate operation builders, and batch/transaction support. Single-table
(mono-table) friendly.

- **Edition:** 2024 — **MSRV:** 1.85.0 (`Cargo.toml` `rust-version`)
- **Dev toolchain:** 1.93 (`nix/rust-toolchain.toml`)
- Pure Cargo; no JS/TS tooling. No `rustfmt.toml` / `clippy.toml`.

## Build / Lint / Test

The canonical checks (same as CI and pre-commit) are:

```sh
cargo fmt --check \
&& cargo clippy --all-targets --all-features -- -D warnings \
&& RUSTDOCFLAGS="-D warnings" cargo doc --all-features --no-deps --document-private-items \
&& cargo test --all-features
```

All clippy + doc warnings are errors (`-D warnings`). Clippy's
`incompatible_msrv` lint catches APIs introduced after 1.85.0 — run clippy
on **stable**, not MSRV, or the lint is silent.

Single test / module:
```sh
cargo test --all-features <test_name>
cargo test --all-features expressions::utils::tests
```

### Feature flags — important

- `--all-features` enables `integration` **and** `test-fixtures`.
- `integration` — gates everything in `tests/operations.rs` behind
  `#![cfg(feature = "integration")]`. Requires **Docker** (testcontainers
  spins DynamoDB Local). Without Docker these tests don't compile into the
  binary.
- `test-fixtures` — exposes `dynamodb_facade::test_fixtures` (shared domain
  types: `PlatformTable`, `User`, `Enrollment`, `TypeIndex`, `EmailIndex`, ...)
  outside of `cfg(test)` / `cfg(doc)`. Integration tests and many doc examples
  depend on it.
- `.hooks/pre-commit` runs `cargo test --all-features` when Docker is up,
  else falls back to `cargo test --features test-fixtures` (NOT bare
  `cargo test` — bare would fail several doc examples).

### Test layout

Three integration-test binaries under `tests/`:
- `macros.rs` — macro expansion tests, no features needed.
- `try_build.rs` — trybuild compile-pass/compile-fail tests in `tests/try_build/`.
- `operations.rs` — end-to-end CRUD/query/batch/transactions against
  DynamoDB Local. Feature-gated on `integration`. All tests share one
  container via `LazyLock` + per-test random table names (see
  `tests/common/mod.rs`).

CI runs three parallel jobs: `lint`, `test` (both on stable, `--all-features`),
`msrv` (1.85.0, `cargo check` + `cargo test --all-features`). Release
publishes to crates.io on `v*` tags.

## Module Layout

`src/` has four files + six module directories:

- `lib.rs` — barrel (`mod x; pub use x::*;`) and crate-level `//!` docs.
  Re-exports `aws_sdk_dynamodb::{Client, Error as DynamoDBError, types::AttributeValue}`.
- `error.rs`, `utils.rs`, `macros.rs`, `test_fixtures.rs`
- `schema/``TableDefinition`, `IndexDefinition`, `KeySchema`, attribute
  marker types (`StringAttribute` / `NumberAttribute` / `BinaryAttribute`).
- `item/``DynamoDBItem`, `Item<TD>`, `Key<TD>`, `KeyId`, `NoId`.
- `values/``IntoAttributeValue`, `AsSet<T>`, `AsNumber<T>`, typed conversions.
- `expressions/``Condition`, `Update`, `KeyCondition`, `Projection`,
  builder traits (see below).
- `operations/` — per-verb request builders + pagination + batch + transactions
  + typestate markers.

### Trait hierarchy (blanket-impl chain)

```
DynamoDBItem<TD>
    └─ DynamoDBItemOp<TD>            (get/put/delete/update/query/scan)
           ├─ DynamoDBItemBatchOp<TD>    (batch_put / batch_delete)
           └─ DynamoDBItemTransactOp<TD> (transact_put / _delete / _update / _condition)
```

Implement **only** `DynamoDBItem` (typically via `dynamodb_item!` macro);
the rest follow automatically. `TD` is a generic parameter, not an associated
type — a single struct may implement `DynamoDBItem` for multiple tables
(useful for migrations between tables).

### Expression builder trait split (`expressions/builders.rs`)

- `ExpressionAttrNames` (sealed, base) — implemented for **all** SDK fluent
  builders, including `GetItemFluentBuilder` which has no
  `expression_attribute_values`.
- `ExpressionAttrBuilder: ExpressionAttrNames` — adds values. Implemented for
  every builder **except Get**.
- `ConditionableBuilder` / `FilterableBuilder` / `KeyConditionableBuilder` /
  `UpdatableBuilder` extend `ExpressionAttrBuilder`.
- `ProjectionableBuilder` extends `ExpressionAttrNames` only (projections
  never use value placeholders).

## Patterns and Conventions

### Imports — three groups, blank-line separated

1. `std` / `core`
2. External crates (`aws_sdk_dynamodb`, `serde`, `serde_dynamo`, `thiserror`, `tracing`)
3. Intra-crate — **always** `use super::...`, never `crate::...`

**Only** accepted use of `crate::` path: macro invocations like
`crate::utils::impl_sealed_marker_types!(...)` (used in `schema/mod.rs`,
`schema/attributes.rs`, `expressions/key_conditions.rs`) — macro paths
require absolute crate-rooted resolution.

### Parameter type preferences

- String-like → `impl Into<Cow<'a, str>>` (zero-copy)
- DynamoDB values → `impl IntoAttributeValue`
- Table / index names → `impl Into<String>`

### Newtype + sealed-inner

Public expression types wrap a **private** inner enum:
`Condition<'a>(ConditionInner<'a>)`, `Update<'a>(UpdateInner<'a>)`, etc.
Inner types stay `pub(super)` / private — never `pub`.

### Sealed trait naming

- Standard: `mod sealed_traits { pub trait FooSeal {} }` — used in
  `builders.rs`, `key_conditions.rs`, `schema/mod.rs`,
  `schema/attributes.rs`, `schema/attribute_list.rs`, `values/typed.rs`.
- **Exception:** `operations/type_state.rs` uses `mod state_traits` with
  **no `*Seal` suffix** (hand-written marker structs + trait impls per
  typestate dimension, not the macro).

### Visibility

- `pub(crate)` — internal traits (`ApplyCondition`, `ApplyUpdate`,
  `ApplyFilter`, `ApplyKeyCondition`, `ExpressionAttrBuilder`, ...).
- `pub(super)` — helpers scoped to a module subtree.
- **Never** leak `*Inner` enums or `Built*` types to `pub`.

### Error handling (`error.rs`)

`Error` variants: `DynamoDB(Box<aws_sdk_dynamodb::Error>)`, `Serde`,
`FailedBatchWrite(Vec<WriteRequest>)`, `Other(Box<dyn Error + Send>)`,
`Custom(String)`. `Result<T>` alias provided.

- `From<SdkError<T,R>>` and `From<aws_sdk_dynamodb::Error>``Error::DynamoDB`.
- `Error::as_dynamodb_error()` downcasts for matching specific SDK errors
  (e.g. `ConditionalCheckFailedException`).
- `Error::custom(msg)` / `Error::other(err)` constructors.
- **Never** use bare `.unwrap()` — always `.expect("why this cannot fail")`.
- `panic!()` for violated structural invariants only (e.g. missing PK/SK keys).

### Typestate operation builders — three orthogonal dimensions

All marker types live in `operations/type_state.rs`:

1. **`OutputFormat`**`Typed``Raw`. `.raw()` (one-way) and
   `.project()` (any → Raw) transition. Raw terminals return `Item<TD>`
   instead of deserializing into `T`.
2. **`ReturnValue`**`ReturnNothing``Return<Old>``Return<New>`.
   `.return_old()` / `.return_new()` / `.return_none()` transition.
3. **Expression-set state**`NoCondition`/`AlreadyHasCondition`,
   `NoFilter`/`AlreadyHasFilter`, `NoProjection`/`AlreadyHasProjection`.
   Calling `.condition()` / `.filter()` / `.project()` twice is a
   **compile error**.

All three dimensions are fully orthogonal: every transition preserves the
other typestate parameters.

Type-parameter order (matters for `Type::<TD>` turbofish):
- put / delete / update: `<TD, T, O, R, C>`
- get: `<TD, T, O, P>`
- query / scan: `<TD, T, O, F, P>`

Each builder structure:
1. `pub fn new(...)` — stand-alone constructor (`T = ()`, `O = Raw`).
2. `impl<...> Builder<...>` — shared methods (`into_inner()`).
3. Per-state `impl` blocks — transitioning methods consume `self`.
4. `.execute()` for single-item; `.all()` / `.stream()` for query/scan.
5. `.into_inner()` — escape hatch returning the raw SDK fluent builder.

`IntoFuture` impls extract the SDK builder **before** the `async move` so
`PhantomData<(TD, T, ...)>` is never captured in the returned future.

### Documentation

- `///` on every public item. `//!` module docs only in `lib.rs` and
  `test_fixtures.rs`.
- Section-separator comments: `// -- Section Name ---...`
- Include `# Errors` / `# Panics` on doc comments where they apply.

### Tests

- Unit tests: inline `#[cfg(test)] mod tests { use super::*; ... }`.
- Naming: `test_<function_name>_<scenario>`.
- `assert_eq!` / `assert!` only — no external test frameworks, no mocking.
- Integration tests (Docker) live in `tests/operations/`, share one
  DynamoDB Local container via `LazyLock` in `tests/common/mod.rs`.
- Compile-fail tests in `tests/try_build/fail/*.rs`, compile-pass in
  `tests/try_build/pass/*.rs` (trybuild).