Skip to content

Add #[derive(FromRow)] and named-column access for cleaner struct mapping #61

Description

@StefanSteiner

Summary

The current FromRow trait in hyperdb-api provides struct mapping for query results, but every consumer must hand-write a positional impl. This is idiomatic but has rough edges — proposing a #[derive(FromRow)] proc macro and named-column access to address them.

Current state

FromRow is defined in hyperdb-api/src/result.rs:744. Typical usage:

struct TestUser { id: i32, name: String, score: f64 }

impl FromRow for TestUser {
    fn from_row(row: &Row) -> hyperdb_api::Result<Self> {
        Ok(TestUser {
            id: row.get::<i32>(0).ok_or_else(|| hyperdb_api::Error::new("NULL id"))?,
            name: row.get::<String>(1).unwrap_or_default(),
            score: row.get::<f64>(2).unwrap_or(0.0),
        })
    }
}

Reference example: hyperdb-api/tests/remaining_features_tests.rs:244-309.

Pain points

  1. Positional indices couple the struct to column order in the SQL. Reordering a SELECT silently swaps fields. sqlx's row.try_get("name") is safer.
  2. Boilerplate scales linearly with column count. A 10-column struct is 10 nearly-identical lines of row.get::<T>(n).ok_or_else(...).
  3. NULL handling is ad-hoc. Each impl independently decides between .unwrap_or_default(), .unwrap_or(0.0), or error-on-NULL. Easy to be inconsistent; unwrap_or_default silently swallows real NULLs.
  4. No compile-time SQL ↔ struct check. Mismatches surface at runtime only.

Proposed work

  • Add a #[derive(FromRow)] proc macro that generates the impl from struct field names matched to column names by default, with #[hyperdb(rename = "...")] and #[hyperdb(index = N)] overrides for the positional case.
  • Add named-column access on Row (e.g. row.get_by_name::<T>("name")) so hand-written impls can opt out of positional brittleness.
  • Standardize NULL behavior inside the derive macro and get_by_name (no trait signature change): Option<T> fields → NULL becomes None; non-Option fields → NULL is an error with a clear message naming the column. Document this in the trait rustdoc. Existing hand-written FromRow impls keep their current behavior unchanged.
  • Update tests/remaining_features_tests.rs and examples/async_parity_smoke.rs to demonstrate the derive form alongside the manual impl.

Non-goals

  • Compile-time SQL verification (à la sqlx::query_as!). That requires a live database at compile time and is a much larger lift.
  • Breaking the existing hand-written FromRow API. The derive should be additive — FromRow::from_row(&Row) -> Result<Self> keeps its current signature, the tuple impls at result.rs:758-787 stay as-is, and existing impls (TestUser, Order) compile unchanged.

Performance note

ResultSchema::column_index at result.rs:893 is a linear scan over Vec<ResultColumn>. A naive row.get_by_name::<T>("name") called from from_row would re-scan per field per row — measurably worse for wide rows. The implementation must resolve column names once per query, not per row:

  • fetch_all_as / fetch_one_as build a name → index lookup table from the Rowset's ResultSchema once, then pass cached indices through to each from_row call.
  • The #[derive(FromRow)] macro emits code that uses those cached indices internally (positional access at the hot path) while presenting a name-based API to the user.

Net: name-based ergonomics, positional speed at the hot path.

Related

A breaking-change alternative covering trait-signature changes (typed RowAccessor, structured Error::Column, removing/feature-gating positional tuple impls) is tracked separately — see the follow-up issue.

Open questions

  • Crate layout: separate hyperdb-api-derive proc-macro crate, or feature-gate inside hyperdb-api?
  • Default field-to-column matching: exact name match, or apply a snake_case normalization rule?

Metadata

Metadata

Assignees

Labels

enhancementNew feature or request

Type

No type
No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions