Skip to content

Compile-time SQL verification (sqlx-style query!/query_as! macros) #63

@StefanSteiner

Description

@StefanSteiner

Summary

Add a SQLx-style query! / query_as! proc macro that verifies SQL at compile time against a real Hyper database, providing compile-time errors for typos, missing columns, type mismatches, and parameter arity bugs. Premium developer experience; no breaking changes — purely additive on top of the existing Connection / Rowset / FromRow API.

Motivation

The original RUST_API_GAP_ANALYSIS.md (predecessor repo) flagged the absence of compile-time query verification:

No #[derive(FromRow)] proc macro; no compile-time query verification (sqlx-style) — may be intentional scope limits.
docs/RUST_API_GAP_ANALYSIS.md:214-215 in hyper-api-rust-oldrepo

#[derive(FromRow)] is being tracked in #61. This issue covers the second half: compile-time SQL verification.

Today, a typo or schema drift in conn.fetch_one_as("SELECT name FROM userz WHERE id = $1") surfaces only at runtime. SQLx solved this for Postgres/MySQL/SQLite by talking to a live database during cargo build and generating column-typed bindings inline. The same approach is feasible for Hyper because Hyper supports PREPARE / DESCRIBE and we already speak the protocol — the macro only needs to wire those calls into a build-time query path.

Proposed work

A new hyperdb-api-macros proc-macro crate (or feature-gated in hyperdb-api) exposing:

let users: Vec<User> = query_as!(
    User,
    "SELECT id, name, score FROM users WHERE active = $1",
    true
).fetch_all(&conn)?;

At compile time the macro:

  1. Connects to a Hyper instance (path read from a build-time env var, e.g. HYPERDB_URL pointing at a .hyper file or running hyperd).
  2. Calls PREPARE on the literal SQL string.
  3. Reads the resulting parameter and result schemas from Hyper.
  4. Verifies the inferred parameter types match the Rust expressions passed.
  5. Verifies the result schema matches the target struct's fields (names + types). For query! (no struct), generates an anonymous record type.
  6. Emits a compile_error! with a precise span on mismatch.

Offline mode

SQLx's killer feature: cargo sqlx prepare snapshots the query metadata into a .sqlx/ directory checked into git, so consumers without a live database can still build. We replicate this:

  • cargo hyperdb prepare (CLI subcommand or workspace bin) connects once, runs every macro invocation, writes JSON metadata to .hyperdb-queries/.
  • Compilation in offline mode reads from that directory; no live connection needed.
  • CI flag (e.g. HYPERDB_OFFLINE=true) refuses to fall back to live mode, so PRs with stale metadata fail fast.

Scope (initial)

  • query! macro — anonymous record results.
  • query_as! macro — typed struct results, integrates with FromRow from Add #[derive(FromRow)] and named-column access for cleaner struct mapping #61.
  • Parameter type inference + arity checking.
  • Offline metadata cache (.hyperdb-queries/) + cargo hyperdb prepare subcommand.
  • Documentation, examples in hyperdb-api/examples/, integration tests that intentionally fail to compile (using trybuild).

Non-goals

  • Schema migrations (separate concern).
  • Query rewriting / planning hints.
  • Replacing the runtime Connection::fetch_* API. The macro is opt-in; runtime queries continue to work for dynamic SQL.

Backwards compatibility

No breaking changes. This is a brand-new crate / feature flag. Existing fetch_one, fetch_all, fetch_one_as, fetch_all_as, Rowset, and FromRow remain exactly as they are. Users opt in by reaching for the new macros.

Performance note

The macro runs at compile time, so runtime cost is identical to a hand-written prepare + execute pair. Build time grows with the number of macro invocations; the offline cache avoids repeated round-trips to Hyper across incremental builds.

Open questions

  • Crate layout: dedicated hyperdb-api-macros crate (mirroring sqlx-macros), or feature-gated inside hyperdb-api?
  • How to discover the Hyper endpoint at compile time — env var only, or a [package.metadata.hyperdb] block in Cargo.toml?
  • Should the macro support Arrow-result paths (execute_query_to_arrow), or restrict v1 to row-based results?

Related

Metadata

Metadata

Assignees

No one assigned

    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