Skip to main content

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, &params)`
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}