Skip to content

fix(api): parameterized FromRow methods (fetch_*_as_params / stream_as_params)#152

Merged
StefanSteiner merged 4 commits into
tableau:mainfrom
StefanSteiner:ssteiner/issue-137-fromrow-params
Jun 16, 2026
Merged

fix(api): parameterized FromRow methods (fetch_*_as_params / stream_as_params)#152
StefanSteiner merged 4 commits into
tableau:mainfrom
StefanSteiner:ssteiner/issue-137-fromrow-params

Conversation

@StefanSteiner

Copy link
Copy Markdown
Contributor

Summary

Adds the missing intersection of FromRow struct-mapping and ToSqlParam
parameter binding
(issue #137). Before this, you could map rows into a struct
(fetch_*_as / stream_as, plain &str queries) or bind $N parameters
(query_params, raw Rows) — but not both in one call. A parameterized
SELECT … WHERE org_id = $1 you wanted as a Vec<User> forced a manual
RowAccessor/row.get() loop.

New _as_params trio on both Connection and AsyncConnection:

  • fetch_one_as_params::<T>(query, params) -> Result<T>
  • fetch_all_as_params::<T>(query, params) -> Result<Vec<T>>
  • stream_as_params::<T>(query, params) -> Result<impl Iterator<Item = Result<T>>>
    (async: impl Stream<Item = Result<T>>, no outer Result)
#[derive(hyperdb_api_derive::FromRow)]
struct User { id: i32, name: String }

let users: Vec<User> = conn.fetch_all_as_params(
    "SELECT id, name FROM users WHERE org_id = $1",
    &[&42i32],
)?;

Design

The two halves already existed — this plumbs them together.

  • Sync (all 3) + async fetch_one_as_params / fetch_all_as_params delegate to
    query_params, which already returns a Rowset/AsyncRowset carrying the
    prepared-statement guard. TypedRowIterator::new takes the whole rowset, so
    Drop ordering (close_statement after the rowset releases its connection
    lock) is preserved with no extra code. Rows are mapped exactly as the existing
    *_as methods (build_indices once + FromRow::from_row per row).
  • Async stream_as_params is the exception: &[&dyn ToSqlParam] can't cross the
    try_stream! await points, so it encodes params up front (encoding needs no
    connection) and rebuilds query_params' Parse/Bind/Execute sequence inline,
    carrying the statement guard. The Prepared path exposes its schema at prepare
    time, so the column-index map is built once up front. A "keep in sync with
    query_params" comment marks the duplication.

Naming: _as_params composes the two existing axes (_as = map to struct,
_params = bind params) in their established reading order. Both plan reviewers
endorsed it over _params_as.

Error semantics

  • fetch_one_as_params returns "Query returned no rows" on empty results
    (same as fetch_one_as).
  • Sync stream_as_params surfaces stream-open errors — including
    FeatureNotSupported on gRPC (prepared statements are TCP-only) — on the
    outer Result; per-row mapping errors come back as each item's Result.
  • Async stream_as_params has no outer Result, so submission failures
    (incl. the gRPC FeatureNotSupported) surface as the first yielded Err item.

Docs & examples

  • FromRow and ToSqlParam rustdoc cross-reference the new methods.
  • row_mapping_forms example gains Form 6 (verified end-to-end).
  • docs/ROW_MAPPING.md: new "Form 6 — Parameterized struct mapping" section +
    Choosing-a-form row; retitled "Six Forms".
  • README: note + snippet under Parameterized Queries.
  • Bonus: fixed stale async query_params rustdoc that described "text-mode
    escaping" — it has done binary Parse/Bind/Execute since the async-parity work.

Out of scope (deliberate)

  • Node bindings — additive Rust-only convenience methods; N-API surface is
    opt-in per binding, so adding Rust methods doesn't auto-expose them.
  • compile-check crate — unaffected (plain inherent methods, no derive/version-gate interaction).
  • gRPC error-path tests — pre-existing gap shared with query_params; not widened here.

Test plan

  • cargo clippy --workspace --all-targets -- -D warnings — clean
  • cargo test -p hyperdb-api — full suite green (8 new tests: 5 sync + 3 async)
  • cargo test -p hyperdb-api --doc — 171 passed
  • cargo run -p hyperdb-api --example row_mapping_forms — Form 6 filters price < $15 → 3 rows
  • cargo build --workspace — green

Acceptance criteria (#137)

  • Sync + async variants for fetch_one / fetch_all / stream.
  • Round-trip test: $1-bound ToSqlParam#[derive(FromRow)] struct,
    asserting values (plus multi-param and empty-result edge cases).
  • Docs/examples updated; FromRow and ToSqlParam module docs
    cross-reference the new methods.

Closes #137.

Add fetch_one_as_params, fetch_all_as_params, and stream_as_params on
Connection — the intersection of FromRow struct-mapping and ToSqlParam
parameter binding. Each delegates to query_params (binary Parse/Bind/
Execute) then maps rows exactly as the existing *_as methods do; the
prepared-statement guard rides along on the returned Rowset, so Drop
ordering is preserved.

Round-trip tests cover single/all/stream, multi-param binding, and the
empty-result error path.
Mirror the sync fetch_one_as_params / fetch_all_as_params /
stream_as_params on AsyncConnection. fetch_one/fetch_all delegate to
query_params; stream_as_params encodes params before entering the
try_stream! generator (it can't borrow &[&dyn ToSqlParam] across await
points) and rebuilds query_params' prepare/execute sequence inline,
carrying the statement guard. The Prepared path exposes its schema at
prepare time, so the column-index map is built once up front.

gRPC FeatureNotSupported surfaces as the stream's first yielded item
(documented), not eagerly. Async round-trip tests cover all three.
- FromRow and ToSqlParam rustdoc cross-reference the new _as_params trio.
- row_mapping_forms example gains Form 6 (fetch_all_as_params +
  stream_as_params), wired into main and verified end-to-end.
- docs/ROW_MAPPING.md: new 'Form 6 — Parameterized struct mapping'
  section + Choosing-a-form row; retitled 'Six Forms'.
- README: note + snippet under Parameterized Queries.
- Fix stale async query_params rustdoc that described text-mode escaping;
  it has done binary Parse/Bind/Execute since the sync parity work.
Final-sweep reviewer caught line 9 still saying 'All five forms' after
the doc was retitled to Six Forms.
@StefanSteiner StefanSteiner merged commit c576945 into tableau:main Jun 16, 2026
12 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add parameterized FromRow methods (fetch_one_as/fetch_all_as/stream_as + ToSqlParam params)

1 participant