chore(api): modernize FromRow with #[derive] + RowAccessor (#61, #62)#74
Merged
StefanSteiner merged 6 commits intoMay 28, 2026
Conversation
…ableau#61, tableau#62 part 1) Foundation commit for the FromRow modernization PR. Introduces RowAccessor and the Row::get_by_name accessor; changes the FromRow trait signature from `&Row` to `RowAccessor<'_>`; deletes the 1/2/3/4- tuple FromRow impls; migrates the 3 hand-written FromRow impls; and wires the cached column-name -> index lookup through fetch_one_as / fetch_all_as (sync + async). This is a breaking change to the FromRow trait. Lands as `chore:` to defer release-please version bump until the v0.3.0 rollup commit. What changed: - New `hyperdb-api/src/row_accessor.rs` with `RowAccessor<'a>`. Borrows &Row + a pre-built HashMap<&str, usize>. Methods: `get<T>(name)`, `get_opt<T>(name)`, `position<T>(idx)`, `row()`. Errors map to Error::Column { kind: Missing | Null | TypeMismatch } and Error::ColumnIndexOutOfBounds. Includes 7 unit tests covering each error path and the happy paths. - New `Row::get_by_name<T>(name)` for hand-coded paths that don't go through FromRow. Uses ResultSchema::column_index (linear scan). Doc recommends #[derive(FromRow)] or fetch_*_as for hot paths (both build the cache once per query). - FromRow trait signature changed: `fn from_row(row: &Row)` -> `fn from_row(row: RowAccessor<'_>)`. Trait rustdoc updated with the recommended derive form (preview for the upcoming proc-macro crate) and a hand-written impl example using `row.get(...)` / `row.get_opt(...)`. - Deleted the 4 tuple FromRow impls (1/2/3/4-tuple). Migration: callers define a struct with #[derive(FromRow)] (added in next commit) or use Row::get(idx) directly. - Migrated the 3 hand-written FromRow impls to the new shape: TestUser (tests/remaining_features_tests.rs), User (tests/async_connection_tests.rs), Order (examples/async_parity_smoke.rs). - fetch_one_as / fetch_all_as (4 sites: sync + async) now build the HashMap lookup once per query from the result schema, then construct a RowAccessor per row. All rows in a result set share the same Arc<ResultSchema>, so this is safe. - Doctest examples in connection.rs and result.rs updated to the new trait shape; the example_raw_transaction direct call to Order::from_row in async_parity_smoke.rs replaced with a fetch_all_as call (since the trait now takes a RowAccessor that needs pre-built indices). Verification: - cargo build --workspace --all-targets -- clean - cargo clippy --workspace --all-targets -- -D warnings -- clean - cargo test --workspace -- all targets pass (including doctests) - cargo fmt --check -- clean
…leau#61, tableau#62 part 2) Adds the proc-macro crate `hyperdb-api-derive` that provides `#[derive(FromRow)]`. Re-exported from `hyperdb-api` so callers don't need a direct dependency — same pattern as serde / thiserror. What changed: - New workspace member `hyperdb-api-derive/` with `proc-macro = true`, deps on syn v2 (with the `full` feature), quote, proc-macro2. - `#[derive(FromRow)]` proc-macro generates an `impl FromRow` that uses the `RowAccessor` API from Commit 1: `row.get("col")?` for required fields, `row.get_opt("col")?` for `Option<T>` fields. Field name → column name match is exact by default; the `#[hyperdb(rename = "...")]` attribute overrides on a per-field basis. - Helpful compile errors for unsupported shapes: - Tuple structs → "tuple-struct fields are not supported" - Enums → "FromRow cannot be derived on enums" - Unions → "FromRow cannot be derived on unions" - Unknown attribute → "unrecognized hyperdb attribute `{x}`; expected `rename = \"...\"`" - `hyperdb-api`'s lib.rs adds `pub use hyperdb_api_derive::FromRow;` alongside the trait re-export. The derive macro and the trait share the name "FromRow" (Rust treats them as different namespaces). - 3 integration tests in `hyperdb-api/tests/remaining_features_tests.rs`: - `test_derive_from_row_parity_with_handwritten`: derived `TestUserDerived` produces identical values to the hand-written `TestUser` impl for the same query. - `test_derive_from_row_with_rename`: `#[hyperdb(rename = "score")]` redirects field-name lookup. - `test_derive_from_row_missing_column_errors`: a derived struct with a column not in the SELECT list surfaces as `Error::Column { kind: Missing }`. Verification: - cargo build --workspace --all-targets — clean - cargo clippy --workspace --all-targets -- -D warnings — clean - cargo test --workspace --lib + --doc — all pass - cargo fmt --check — clean
….3.md Adds a "FromRow modernization" section to the consolidated migration guide covering: - Trait signature change (&Row -> RowAccessor<'_>) - Tuple impl deletion + recipes for migrating (define a struct with #[derive(FromRow)] or use Row::get(idx) directly) - New #[derive(FromRow)] proc-macro with #[hyperdb(rename = "...")] - New Row::get_by_name accessor - Error::Column / ColumnErrorKind error shape (already in tableau#70) - Performance note about cached-index lookup vs. linear scan - Note that hyperdb-api-derive doesn't need to be a direct dep Also fixes a broken intra-doc link in row_accessor.rs (FromRow -> crate::FromRow); doc-warning count back to baseline 6. Verification: - cargo build --workspace --all-targets — clean - cargo doc --workspace --no-deps — 6 warnings (= post-tableau#70 baseline) - cargo fmt --check — clean
…ableau#61, tableau#62 part 4) Three findings from the architectural pre-PR review: - M1 (consistency): RowAccessor::position now produces Error::Column with ColumnErrorKind::Null / TypeMismatch (synthesized name "col[{idx}]") instead of Error::Conversion. This aligns positional errors with the named-access error shape, so callers can match on Error::Column { kind, .. } uniformly across get/get_opt/position rather than special-casing positional access. Out-of-bounds still uses Error::ColumnIndexOutOfBounds for index integrity. Added a dedicated unit test (position_null_errors_with_kind_null). - m1 (API surface): Removed RowAccessor::row(). It was a leak vector that let FromRow impls drop down to bare Row methods, bypassing the cached-index lookup the accessor exists to provide — exactly the anti-pattern this PR set out to eliminate. With no production caller needing it (verified by build), removing rather than gating to pub(crate) is the cleanest move. Easy to add back if a real consumer surfaces. - m2 (forward-compat): Derive macro's "unrecognized hyperdb attribute" error now reads "supported attributes: rename" instead of pinning a specific syntax. When new attributes (skip, default, with) ship in v0.3.x, the message stays accurate without a macro patch. Verification: - cargo build --workspace --all-targets — clean - cargo clippy --workspace --all-targets -- -D warnings — clean - cargo test --workspace --lib — 8 row_accessor tests pass (incl. new position_null_errors_with_kind_null) - cargo fmt --check — clean
…:position_opt Extends #[derive(FromRow)] with a positional access mode for queries where columns have no stable name (e.g. SELECT id, COUNT(*) FROM ... GROUP BY id). Mutually exclusive with #[hyperdb(rename = "...")]. Adds RowAccessor::position_opt as the symmetric Option<T> counterpart to position, mirroring the get/get_opt naming pair. NULL becomes None; out-of-bounds and type-mismatch still error. The macro emits position(N)? for non-Option fields and position_opt(N)? for Option<T> fields, matching the existing get/get_opt dispatch on name-based access. Updates MIGRATING-0.3.md and the derive crate README. Doc-warning count back at baseline 6. Verification: - cargo build/clippy/test/fmt/doc all clean on workspace - new RowAccessor::position_opt unit tests (NULL, happy path, OOB) - new integration test test_derive_from_row_with_index runs against a real query with COUNT(*) (unnamed column)
StefanSteiner
added a commit
to StefanSteiner/hyper-api-rust
that referenced
this pull request
May 28, 2026
Pre-release adversarial review of the v0.3.0 rollup CI/CD config caught that hyperdb-api-derive (added in PR tableau#74) was missing from release.yml's publish-in-dependency-order step. hyperdb-api/Cargo.toml strictly pins hyperdb-api-derive = "=0.X.Y", so cargo publish -p hyperdb-api would fail at release time when crates.io can't resolve the strict version of derive (because release.yml never published it). Verified topologically: - hyperdb-api-derive has zero workspace deps (only syn/quote/proc-macro2 from the registry), so it can publish before any workspace crate. - It's a runtime dep of hyperdb-api only. - Inserted right after hyperdb-api-salesforce; existing order otherwise unchanged. Added a dependency-order comment to the publish step explaining the topology so future contributors don't break it. Also adds hyperdb-api-derive to ci.yml's publish dry-run job. The dry-run job exists exactly to catch this class of bug before release time. Without this addition, the same blocker could re-emerge after a future major-version refactor of derive. Updates the stale "7 workspace-member version rows" comment in release-please.yml to reflect the current 8-member workspace (hyperdb-api-derive added in tableau#74). The lockfile-sync sentinel enumerates members at runtime via cargo metadata, so the count is informational; correctness is unchanged. Verified locally: - cargo publish -p hyperdb-api-derive --dry-run: succeeds - cargo publish -p sea-query-hyperdb --dry-run: succeeds - cargo publish -p hyperdb-bootstrap --dry-run: succeeds - cargo metadata workspace topology check: order in release.yml is consistent with non-dev deps across all 7 publishable crates.
6 tasks
StefanSteiner
added a commit
that referenced
this pull request
May 28, 2026
* feat: stabilize v0.3.0 public API bundle This commit aggregates the breaking and additive API changes that ship together as v0.3.0. The individual PRs landed under chore: prefixes to defer release-please from cutting an early version; this single feat: commit with a BREAKING CHANGE: footer is the trigger for the v0.3.0 release PR. Bundle contents (all merged to main): - #70 (PR #71) — Flat public Error enum + ergonomic constructors workspace-wide - #69 (PR #73) — Transaction API consolidation (RAII guard recommended; raw trio deprecated and #[doc(hidden)]) - #61 + #62 (PR #74) — FromRow modernization: #[derive(FromRow)] in new hyperdb-api-derive crate, RowAccessor with cached name->index lookup, breaking trait signature change, blanket tuple impls deleted, #[hyperdb(rename)] and #[hyperdb(index)] attributes - #76 — Follow-ups A/B/C: typed io::Error sources in process.rs, Error::InvalidOperation variant for caller misuse, structured SQLSTATE on Cancelled/Closed/Connection Follow-up D (flatten internal client::Error) deferred to v0.3.x as issue #75 — internal-only, zero external consumers, larger than originally scoped. The code change in this commit is a small documentation refresh on the crate-level rustdoc to (a) include hyperdb-api-derive in the companion crates list and (b) fix a stale crate name (sea-query-hyper -> sea-query-hyperdb). The body of the commit is the BREAKING CHANGE: footer below; release-please uses it to generate the v0.3.0 entry in CHANGELOG.md. See MIGRATING-0.3.md for full migration recipes covering every breaking change in the bundle. BREAKING CHANGE: v0.3.0 reshapes the public hyperdb_api::Error enum into a flat canonical structure (no Box<dyn StdError> cause channel, no kind() method, no Other catch-all variant), and its constructor surface (Error::new and Error::with_cause are deleted in favor of domain-specific snake_case constructors). It also changes the FromRow trait signature from fn from_row(row: &Row) to fn from_row(row: RowAccessor<'_>), deletes the blanket 1/2/3/4-tuple FromRow impls, deprecates Connection::begin_transaction/commit/rollback (use the RAII guard at Connection::transaction() instead), introduces a new Error::InvalidOperation variant, and changes Error::Cancelled and Error::Closed from tuple to struct variants carrying structured sqlstate. Every variant has a snake_case constructor; the FromRow derive lives in a re-exported hyperdb-api-derive crate. See MIGRATING-0.3.md for migration recipes. * chore(ci): publish hyperdb-api-derive in release.yml + dry-run in ci.yml Pre-release adversarial review of the v0.3.0 rollup CI/CD config caught that hyperdb-api-derive (added in PR #74) was missing from release.yml's publish-in-dependency-order step. hyperdb-api/Cargo.toml strictly pins hyperdb-api-derive = "=0.X.Y", so cargo publish -p hyperdb-api would fail at release time when crates.io can't resolve the strict version of derive (because release.yml never published it). Verified topologically: - hyperdb-api-derive has zero workspace deps (only syn/quote/proc-macro2 from the registry), so it can publish before any workspace crate. - It's a runtime dep of hyperdb-api only. - Inserted right after hyperdb-api-salesforce; existing order otherwise unchanged. Added a dependency-order comment to the publish step explaining the topology so future contributors don't break it. Also adds hyperdb-api-derive to ci.yml's publish dry-run job. The dry-run job exists exactly to catch this class of bug before release time. Without this addition, the same blocker could re-emerge after a future major-version refactor of derive. Updates the stale "7 workspace-member version rows" comment in release-please.yml to reflect the current 8-member workspace (hyperdb-api-derive added in #74). The lockfile-sync sentinel enumerates members at runtime via cargo metadata, so the count is informational; correctness is unchanged. Verified locally: - cargo publish -p hyperdb-api-derive --dry-run: succeeds - cargo publish -p sea-query-hyperdb --dry-run: succeeds - cargo publish -p hyperdb-bootstrap --dry-run: succeeds - cargo metadata workspace topology check: order in release.yml is consistent with non-dev deps across all 7 publishable crates.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Modernizes
FromRowfor the v0.3.0 bundle (closes #61, #62):RowAccessor<'a>— name-based column access with a per-query cachedHashMap<&str, usize>lookup. Replaces the&Rowparameter onFromRow::from_row(breaking).#[derive(FromRow)]in a new workspace cratehyperdb-api-derive, re-exported fromhyperdb-api(same pattern asserde/thiserror/tokio). Supports#[hyperdb(rename = "...")]for column-name overrides and#[hyperdb(index = N)]for positional access (useful with computed/unnamed columns likeSELECT id, COUNT(*) FROM ... GROUP BY id).Row::get_by_name<T>(name)for one-off named access outsidefetch_*_as(linear scan; rustdoc steers hot paths to the derive).FromRowimpls — define a struct with#[derive(FromRow)]instead.FromRowimpls in tests/examples; documents migration inMIGRATING-0.3.md.Bundle policy:
chore:prefix to defer release-please version bump until the v0.3.0 rollupfeat!:commit. PR is breaking — landed under the bundle umbrella.Commits
e705aa5— Foundation:RowAccessor, trait signature change,Row::get_by_name,fetch_*_ascache, hand-written impl migration, 8 unit tests.70356c4—hyperdb-api-derivecrate;#[derive(FromRow)]with#[hyperdb(rename = "...")]; integration tests (parity, NULL, rename, missing-column).ac3166e—MIGRATING-0.3.md#61+#62section.676a4c2— Final-review fixes:RowAccessor::positionreturns structuredError::Column { kind: Null | TypeMismatch }; removes leak-vectorRowAccessor::row(); clearer attribute error message.378c0a6— Adds#[hyperdb(index = N)]andRowAccessor::position_opt. Mutually exclusive withrename. Integration test against aCOUNT(*)query.a50b97a— Derive crate README: 2×2 accessor cheat sheet, zero-based indexing note, type-system rules for theposition/position_optpair.Why a separate
hyperdb-api-derivecrateRust requires
#[proc_macro_derive]to live in a crate withproc-macro = true.hyperdb-api-coreis not a proc-macro crate and cannot host the derive. Re-export fromhyperdb-apimeans downstream callers douse hyperdb_api::FromRow;and never depend onhyperdb-api-derivedirectly — same ergonomic shape asserde/thiserror.Performance
Ran
cargo run --release --example benchmark_suite -- 200000against this branch and againstmain(origin/main =5c7e78b). N=1 at 200K rows; deltas are within typical run-to-run noise on this M3 Max. No regression observed.The
fetch_*_ascached-index path is hit by the FromRow tests rather than the bulk-insert/query benchmarks — the lookup is O(N) once per query plus O(1) per field per row, strictly better than the previous "no cache, hand-coded positions" pattern.Migration
MIGRATING-0.3.mdcovers the recipes; the short version:Tuple
FromRowimpls are gone; define a struct.Verification
cargo build --workspace --all-targets✅cargo clippy --workspace --all-targets -- -D warnings✅cargo test --workspace(full, including doctests) ✅cargo fmt --check✅cargo doc --workspace --no-deps✅ — 6 warnings (= post-Flatten Error type to canonical M-ERRORS shape (drop Client wrapper, Box<dyn> source, Option<ErrorKind>) #70 baseline)grep -rn 'fn from_row(row: &Row)' --include='*.rs' .→ zero hitscargo run --example async_parity_smokeagainst local hyperd ✅Test plan
RowAccessorunit tests cover Missing, Null, TypeMismatch, position-OOB, position-NullOption<T>,#[hyperdb(rename)],#[hyperdb(index)](againstSELECT id, COUNT(*) FROM ... GROUP BY id), missing column →Error::Column { kind: Missing }async_parity_smokeexample exercises the newfetch_*_ascached-index path end-to-endCloses #61
Closes #62