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. 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}