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. Full typed
18 // parameter support (ToSqlParam) is wired in Milestone B.
19 #[allow(dead_code, reason = "full parameter binding wired in Milestone B (W3)")]
20 params: Vec<String>,
21 _phantom: PhantomData<fn() -> T>,
22}
23
24impl<T: FromRow> QueryAs<T> {
25 /// Construct a new `QueryAs`. Called by the `query_as!` macro; not intended
26 /// for direct use.
27 ///
28 /// `params` accepts `&dyn std::fmt::Debug` so the macro can pass any bind
29 /// arguments through — the actual typed binding will be tightened in W3.
30 pub fn new(sql: &str, params: &[&dyn std::fmt::Debug]) -> Self {
31 Self {
32 sql: sql.to_owned(),
33 params: params.iter().map(|p| format!("{p:?}")).collect(),
34 _phantom: PhantomData,
35 }
36 }
37
38 /// Execute the query and collect all rows into a `Vec<T>`.
39 ///
40 /// # Errors
41 ///
42 /// Returns a `hyperdb_api::Error` on connection failure, SQL error, or
43 /// row-mapping failure.
44 pub fn fetch_all(self, conn: &Connection) -> Result<Vec<T>> {
45 conn.fetch_all_as(&self.sql)
46 }
47
48 /// Execute the query and return exactly one row.
49 ///
50 /// # Errors
51 ///
52 /// Returns `Error::Conversion` if the query returns zero rows.
53 /// Returns a `hyperdb_api::Error` on connection or SQL failure.
54 pub fn fetch_one(self, conn: &Connection) -> Result<T> {
55 conn.fetch_one_as(&self.sql)
56 }
57
58 /// Execute the query and return `Some(row)` for the first row, or `None`
59 /// if the query returns zero rows.
60 ///
61 /// # Errors
62 ///
63 /// Returns a `hyperdb_api::Error` on connection or SQL failure.
64 pub fn fetch_optional(self, conn: &Connection) -> Result<Option<T>> {
65 let rows = conn.fetch_all_as::<T>(&self.sql)?;
66 Ok(rows.into_iter().next())
67 }
68}
69
70/// A compiled single-column query. Created by the `query_scalar!` macro.
71///
72/// Returns values of a single column (e.g. `COUNT(*)`, `MAX(score)`, etc.).
73/// The type `T` must implement [`RowValue`].
74///
75/// # Example
76///
77/// ```ignore
78/// let count: i64 = query_scalar!(i64, "SELECT COUNT(*) FROM users").fetch_one(&conn)?;
79/// let names: Vec<String> = query_scalar!(String, "SELECT name FROM users").fetch_all(&conn)?;
80/// ```
81#[derive(Debug)]
82pub struct QueryScalar<T> {
83 sql: String,
84 #[allow(
85 dead_code,
86 reason = "typed parameter binding wired in a future milestone"
87 )]
88 params: Vec<String>,
89 _phantom: PhantomData<fn() -> T>,
90}
91
92impl<T: RowValue> QueryScalar<T> {
93 /// Construct a new `QueryScalar`. Called by the `query_scalar!` macro.
94 pub fn new(sql: &str, params: &[&dyn std::fmt::Debug]) -> Self {
95 Self {
96 sql: sql.to_owned(),
97 params: params.iter().map(|p| format!("{p:?}")).collect(),
98 _phantom: PhantomData,
99 }
100 }
101
102 /// Execute and return all scalar values as a `Vec<T>`.
103 ///
104 /// # Errors
105 ///
106 /// Returns a `hyperdb_api::Error` on connection failure, SQL error, or
107 /// type conversion failure.
108 pub fn fetch_all(self, conn: &Connection) -> Result<Vec<T>> {
109 conn.fetch_all_as::<ScalarRow<T>>(&self.sql)
110 .map(|rows| rows.into_iter().map(|r| r.0).collect())
111 }
112
113 /// Execute and return exactly one scalar value.
114 ///
115 /// # Errors
116 ///
117 /// Returns `Error::Conversion` if the query returns zero rows.
118 pub fn fetch_one(self, conn: &Connection) -> Result<T> {
119 let rows = conn.fetch_all_as::<ScalarRow<T>>(&self.sql)?;
120 rows.into_iter()
121 .next()
122 .map(|r| r.0)
123 .ok_or_else(|| crate::Error::Conversion("query_scalar!: query returned no rows".into()))
124 }
125
126 /// Execute and return `Some(value)` for the first row, or `None`.
127 ///
128 /// # Errors
129 ///
130 /// Returns a `hyperdb_api::Error` on connection or SQL failure.
131 pub fn fetch_optional(self, conn: &Connection) -> Result<Option<T>> {
132 let rows = conn.fetch_all_as::<ScalarRow<T>>(&self.sql)?;
133 Ok(rows.into_iter().next().map(|r| r.0))
134 }
135}
136
137/// Internal single-column `FromRow` wrapper for `QueryScalar` methods.
138struct ScalarRow<T>(T);
139
140impl<T: RowValue> FromRow for ScalarRow<T> {
141 fn from_row(row: crate::RowAccessor<'_>) -> Result<Self> {
142 row.position::<T>(0).map(ScalarRow)
143 }
144}