hyperdb_api/query_as.rs
1// Copyright (c) 2026, Salesforce, Inc. All rights reserved.
2// SPDX-License-Identifier: Apache-2.0 OR MIT
3
4//! Runtime builders for `query_as!` and `query_scalar!`.
5
6use std::marker::PhantomData;
7
8use crate::{Connection, FromRow, Result, RowValue};
9
10/// A compiled, type-safe query. Created by the `query_as!` macro.
11///
12/// Call `.fetch_all(&conn)`, `.fetch_one(&conn)`, or `.fetch_optional(&conn)`
13/// to execute it.
14#[derive(Debug)]
15pub struct QueryAs<T> {
16 sql: String,
17 // Bind parameters are stored as formatted strings for now — the macro
18 // accepts `$N` args and validates the SQL, but binding is not yet wired
19 // (the `fetch_*` methods below forward to the NON-param `fetch_*_as`).
20 //
21 // To finish this: change `params` to hold `ToSqlParam` values and route
22 // through `Connection::fetch_*_as_params` (added in issue #137 — the
23 // parameterized FromRow methods are exactly the primitive this needs).
24 #[allow(
25 dead_code,
26 reason = "typed parameter binding not yet wired — see issue #137"
27 )]
28 params: Vec<String>,
29 _phantom: PhantomData<fn() -> T>,
30}
31
32impl<T: FromRow> QueryAs<T> {
33 /// Construct a new `QueryAs`. Called by the `query_as!` macro; not intended
34 /// for direct use.
35 ///
36 /// `params` accepts `&dyn std::fmt::Debug` so the macro can pass any bind
37 /// arguments through — typed binding via `ToSqlParam` is not yet wired
38 /// (see the `TODO(#137)` on `fetch_all` below).
39 pub fn new(sql: &str, params: &[&dyn std::fmt::Debug]) -> Self {
40 Self {
41 sql: sql.to_owned(),
42 params: params.iter().map(|p| format!("{p:?}")).collect(),
43 _phantom: PhantomData,
44 }
45 }
46
47 /// Execute the query and collect all rows into a `Vec<T>`.
48 ///
49 /// # Errors
50 ///
51 /// Returns a `hyperdb_api::Error` on connection failure, SQL error, or
52 /// row-mapping failure.
53 pub fn fetch_all(self, conn: &Connection) -> Result<Vec<T>> {
54 // TODO(#137): forward to `conn.fetch_all_as_params(&self.sql, ¶ms)`
55 // once `params` holds `ToSqlParam` values, to actually bind `$N` args.
56 conn.fetch_all_as(&self.sql)
57 }
58
59 /// Execute the query and return exactly one row.
60 ///
61 /// # Errors
62 ///
63 /// Returns `Error::Conversion` if the query returns zero rows.
64 /// Returns a `hyperdb_api::Error` on connection or SQL failure.
65 pub fn fetch_one(self, conn: &Connection) -> Result<T> {
66 conn.fetch_one_as(&self.sql)
67 }
68
69 /// Execute the query and return `Some(row)` for the first row, or `None`
70 /// if the query returns zero rows.
71 ///
72 /// # Errors
73 ///
74 /// Returns a `hyperdb_api::Error` on connection or SQL failure.
75 pub fn fetch_optional(self, conn: &Connection) -> Result<Option<T>> {
76 let rows = conn.fetch_all_as::<T>(&self.sql)?;
77 Ok(rows.into_iter().next())
78 }
79}
80
81/// A compiled single-column query. Created by the `query_scalar!` macro.
82///
83/// Returns values of a single column (e.g. `COUNT(*)`, `MAX(score)`, etc.).
84/// The type `T` must implement [`RowValue`].
85///
86/// # Example
87///
88/// ```ignore
89/// let count: i64 = query_scalar!(i64, "SELECT COUNT(*) FROM users").fetch_one(&conn)?;
90/// let names: Vec<String> = query_scalar!(String, "SELECT name FROM users").fetch_all(&conn)?;
91/// ```
92#[derive(Debug)]
93pub struct QueryScalar<T> {
94 sql: String,
95 // Same gap as `QueryAs::params` — the macro validates the SQL and
96 // accepts args, but binding isn't wired yet. Route through
97 // `fetch_scalar_params` (or equivalent) once it exists.
98 #[allow(
99 dead_code,
100 reason = "typed parameter binding not yet wired — see issue #137"
101 )]
102 params: Vec<String>,
103 _phantom: PhantomData<fn() -> T>,
104}
105
106impl<T: RowValue> QueryScalar<T> {
107 /// Construct a new `QueryScalar`. Called by the `query_scalar!` macro.
108 pub fn new(sql: &str, params: &[&dyn std::fmt::Debug]) -> Self {
109 Self {
110 sql: sql.to_owned(),
111 params: params.iter().map(|p| format!("{p:?}")).collect(),
112 _phantom: PhantomData,
113 }
114 }
115
116 /// Execute and return all scalar values as a `Vec<T>`.
117 ///
118 /// # Errors
119 ///
120 /// Returns a `hyperdb_api::Error` on connection failure, SQL error, or
121 /// type conversion failure.
122 pub fn fetch_all(self, conn: &Connection) -> Result<Vec<T>> {
123 conn.fetch_all_as::<ScalarRow<T>>(&self.sql)
124 .map(|rows| rows.into_iter().map(|r| r.0).collect())
125 }
126
127 /// Execute and return exactly one scalar value.
128 ///
129 /// # Errors
130 ///
131 /// Returns `Error::Conversion` if the query returns zero rows.
132 pub fn fetch_one(self, conn: &Connection) -> Result<T> {
133 let rows = conn.fetch_all_as::<ScalarRow<T>>(&self.sql)?;
134 rows.into_iter()
135 .next()
136 .map(|r| r.0)
137 .ok_or_else(|| crate::Error::Conversion("query_scalar!: query returned no rows".into()))
138 }
139
140 /// Execute and return `Some(value)` for the first row, or `None`.
141 ///
142 /// # Errors
143 ///
144 /// Returns a `hyperdb_api::Error` on connection or SQL failure.
145 pub fn fetch_optional(self, conn: &Connection) -> Result<Option<T>> {
146 let rows = conn.fetch_all_as::<ScalarRow<T>>(&self.sql)?;
147 Ok(rows.into_iter().next().map(|r| r.0))
148 }
149}
150
151/// Internal single-column `FromRow` wrapper for `QueryScalar` methods.
152struct ScalarRow<T>(T);
153
154impl<T: RowValue> FromRow for ScalarRow<T> {
155 fn from_row(row: crate::RowAccessor<'_>) -> Result<Self> {
156 row.position::<T>(0).map(ScalarRow)
157 }
158}