Skip to main content

bsql_core/
executor.rs

1//! The `Executor` trait — the runtime contract between generated code and the pool.
2//!
3//! Code generated by `bsql::query!` calls methods on this trait. `Pool`,
4//! `PoolConnection`, and `Transaction` all implement it.
5//!
6//! The `query_raw` / `query_raw_readonly` methods use the bsql-driver's arena-based
7//! row storage. Generated code decodes columns from `Row` via typed getters.
8
9use bsql_driver_postgres::arena::release_arena;
10use bsql_driver_postgres::codec::Encode;
11use bsql_driver_postgres::{Arena, QueryResult};
12
13use crate::error::{BsqlError, BsqlResult};
14use crate::pool::{Pool, PoolConnection};
15use crate::transaction::Transaction;
16
17/// Owned query result that carries its arena alongside the result metadata.
18///
19/// Generated code calls `.row(i)` to access individual rows. This struct
20/// bundles the arena with the result so callsites don't manage arenas manually.
21pub struct OwnedResult {
22    pub result: QueryResult,
23    arena: Arena,
24}
25
26impl std::fmt::Debug for OwnedResult {
27    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
28        f.debug_struct("OwnedResult")
29            .field("rows", &self.result.len())
30            .finish()
31    }
32}
33
34impl OwnedResult {
35    /// Create without arena — for queries that use data_buf instead of arena.
36    /// Zero allocation: Arena::empty() allocates nothing.
37    pub(crate) fn without_arena(result: QueryResult) -> Self {
38        Self {
39            result,
40            arena: Arena::empty(),
41        }
42    }
43
44    /// Number of rows.
45    pub fn len(&self) -> usize {
46        self.result.len()
47    }
48
49    /// Whether the result set is empty.
50    pub fn is_empty(&self) -> bool {
51        self.result.is_empty()
52    }
53
54    /// Get a row by index.
55    pub fn row(&self, idx: usize) -> bsql_driver_postgres::Row<'_> {
56        self.result.row(idx, &self.arena)
57    }
58
59    /// Iterate over rows.
60    pub fn iter(&self) -> impl Iterator<Item = bsql_driver_postgres::Row<'_>> {
61        self.result.rows(&self.arena)
62    }
63}
64
65impl Drop for OwnedResult {
66    fn drop(&mut self) {
67        // Return arena to thread-local pool.
68        let arena = std::mem::take(&mut self.arena);
69        release_arena(arena);
70        // Return data buffer to thread-local pool for reuse by next query.
71        if let Some(buf) = self.result.take_data_buf() {
72            bsql_driver_postgres::release_resp_buf(buf);
73        }
74    }
75}
76
77/// Execute a prepared query and return rows.
78///
79/// The generated code calls `query_raw`, `query_raw_readonly`, and
80/// `execute_raw` on `&Pool`, `&PoolConnection`, or `&Transaction`.
81///
82/// All methods are `async fn` returning futures that complete immediately
83/// (sync under the hood). The internal connection I/O is microsecond-level
84/// on UDS and fast on TCP; the `async` signature exists for ergonomic
85/// compatibility with async runtimes (tokio, etc.).
86pub trait Executor {
87    /// Execute a query and return all rows.
88    fn query_raw(
89        &self,
90        sql: &str,
91        sql_hash: u64,
92        params: &[&(dyn Encode + Sync)],
93    ) -> BsqlResult<OwnedResult>;
94
95    /// Execute a read-only query. May route to replicas in the future.
96    fn query_raw_readonly(
97        &self,
98        sql: &str,
99        sql_hash: u64,
100        params: &[&(dyn Encode + Sync)],
101    ) -> BsqlResult<OwnedResult>;
102
103    /// Execute a query and return the number of affected rows.
104    fn execute_raw(
105        &self,
106        sql: &str,
107        sql_hash: u64,
108        params: &[&(dyn Encode + Sync)],
109    ) -> BsqlResult<u64>;
110}
111
112impl Executor for Pool {
113    #[inline]
114    fn query_raw(
115        &self,
116        sql: &str,
117        sql_hash: u64,
118        params: &[&(dyn Encode + Sync)],
119    ) -> BsqlResult<OwnedResult> {
120        let mut guard = self.inner.acquire().map_err(BsqlError::from)?;
121        let result = guard
122            .query(sql, sql_hash, params)
123            .map_err(BsqlError::from_driver_query)?;
124        Ok(OwnedResult::without_arena(result))
125    }
126
127    #[inline]
128    fn query_raw_readonly(
129        &self,
130        sql: &str,
131        sql_hash: u64,
132        params: &[&(dyn Encode + Sync)],
133    ) -> BsqlResult<OwnedResult> {
134        let pool = self.read_pool.as_ref().unwrap_or(&self.inner);
135        let mut guard = pool.acquire().map_err(BsqlError::from)?;
136        let result = guard
137            .query(sql, sql_hash, params)
138            .map_err(BsqlError::from_driver_query)?;
139        Ok(OwnedResult::without_arena(result))
140    }
141
142    #[inline]
143    fn execute_raw(
144        &self,
145        sql: &str,
146        sql_hash: u64,
147        params: &[&(dyn Encode + Sync)],
148    ) -> BsqlResult<u64> {
149        let mut guard = self.inner.acquire().map_err(BsqlError::from)?;
150        guard
151            .execute(sql, sql_hash, params)
152            .map_err(BsqlError::from_driver_query)
153    }
154}
155
156impl Executor for PoolConnection {
157    #[inline]
158    fn query_raw(
159        &self,
160        sql: &str,
161        sql_hash: u64,
162        params: &[&(dyn Encode + Sync)],
163    ) -> BsqlResult<OwnedResult> {
164        let mut guard = self.inner.lock().unwrap_or_else(|e| e.into_inner());
165        let result = guard
166            .query(sql, sql_hash, params)
167            .map_err(BsqlError::from_driver_query)?;
168        Ok(OwnedResult::without_arena(result))
169    }
170
171    #[inline]
172    fn query_raw_readonly(
173        &self,
174        sql: &str,
175        sql_hash: u64,
176        params: &[&(dyn Encode + Sync)],
177    ) -> BsqlResult<OwnedResult> {
178        self.query_raw(sql, sql_hash, params)
179    }
180
181    #[inline]
182    fn execute_raw(
183        &self,
184        sql: &str,
185        sql_hash: u64,
186        params: &[&(dyn Encode + Sync)],
187    ) -> BsqlResult<u64> {
188        let mut guard = self.inner.lock().unwrap_or_else(|e| e.into_inner());
189        guard
190            .execute(sql, sql_hash, params)
191            .map_err(BsqlError::from_driver_query)
192    }
193}
194
195impl Executor for Transaction {
196    fn query_raw(
197        &self,
198        sql: &str,
199        sql_hash: u64,
200        params: &[&(dyn Encode + Sync)],
201    ) -> BsqlResult<OwnedResult> {
202        self.query_inner(sql, sql_hash, params)
203    }
204
205    #[inline]
206    fn query_raw_readonly(
207        &self,
208        sql: &str,
209        sql_hash: u64,
210        params: &[&(dyn Encode + Sync)],
211    ) -> BsqlResult<OwnedResult> {
212        self.query_raw(sql, sql_hash, params)
213    }
214
215    #[inline]
216    fn execute_raw(
217        &self,
218        sql: &str,
219        sql_hash: u64,
220        params: &[&(dyn Encode + Sync)],
221    ) -> BsqlResult<u64> {
222        self.execute_inner(sql, sql_hash, params)
223    }
224}
225
226#[cfg(test)]
227mod tests {
228    use super::*;
229    use bsql_driver_postgres::arena::{acquire_arena, release_arena};
230    use bsql_driver_postgres::{ColumnDesc, QueryResult};
231    use std::sync::Arc;
232
233    /// Helper: build an OwnedResult with `n` rows and `num_cols` columns.
234    /// Each column offset entry is a dummy (0, 0) pair — sufficient for
235    /// testing len/is_empty/row-count without decoding real data.
236    fn make_owned_result(num_rows: usize, num_cols: usize) -> OwnedResult {
237        let arena = acquire_arena();
238        let cols: Arc<[ColumnDesc]> = (0..num_cols)
239            .map(|i| ColumnDesc {
240                name: format!("c{i}").into(),
241                type_oid: 23, // int4
242                type_size: 4,
243                table_oid: 0,
244                column_id: 0,
245            })
246            .collect::<Vec<_>>()
247            .into();
248
249        let col_offsets: Vec<(usize, i32)> = vec![(0, -1); num_rows * num_cols]; // NULL columns
250        let result = QueryResult::from_parts(col_offsets, num_cols, cols, 0);
251        OwnedResult { result, arena }
252    }
253
254    // --- OwnedResult ---
255
256    #[test]
257    fn owned_result_new_zero_rows() {
258        let owned = make_owned_result(0, 2);
259        assert_eq!(owned.len(), 0);
260        assert!(owned.is_empty());
261    }
262
263    #[test]
264    fn owned_result_new_single_row() {
265        let owned = make_owned_result(1, 3);
266        assert_eq!(owned.len(), 1);
267        assert!(!owned.is_empty());
268    }
269
270    #[test]
271    fn owned_result_new_multiple_rows() {
272        let owned = make_owned_result(5, 2);
273        assert_eq!(owned.len(), 5);
274        assert!(!owned.is_empty());
275    }
276
277    // --- OwnedResult::row ---
278
279    #[test]
280    fn owned_result_row_access() {
281        let owned = make_owned_result(3, 2);
282        // Should not panic for valid indices
283        let _r0 = owned.row(0);
284        let _r1 = owned.row(1);
285        let _r2 = owned.row(2);
286    }
287
288    #[test]
289    #[should_panic]
290    fn owned_result_row_out_of_bounds_panics() {
291        let owned = make_owned_result(2, 1);
292        let _r = owned.row(2); // out of bounds
293    }
294
295    // --- OwnedResult::iter ---
296
297    #[test]
298    fn owned_result_iter_count() {
299        let owned = make_owned_result(4, 2);
300        let count = owned.iter().count();
301        assert_eq!(count, 4);
302    }
303
304    #[test]
305    fn owned_result_iter_empty() {
306        let owned = make_owned_result(0, 2);
307        let count = owned.iter().count();
308        assert_eq!(count, 0);
309    }
310
311    // --- OwnedResult::Drop releases arena back to pool ---
312
313    #[test]
314    fn owned_result_drop_releases_arena() {
315        // Acquire an arena, wrap it in OwnedResult, drop it.
316        // After drop, acquiring should succeed (arena was returned to pool).
317        let owned = make_owned_result(1, 1);
318        drop(owned);
319        // If the arena was released, we can acquire again without issue.
320        let arena = acquire_arena();
321        release_arena(arena);
322    }
323
324    // --- OwnedResult with zero columns ---
325
326    #[test]
327    fn owned_result_zero_columns() {
328        // Commands like INSERT without RETURNING have 0 columns
329        let arena = acquire_arena();
330        let cols: Arc<[ColumnDesc]> = Arc::from(Vec::new());
331        let result = QueryResult::from_parts(vec![], 0, cols, 42);
332        let owned = OwnedResult { result, arena };
333        assert_eq!(owned.len(), 0);
334        assert!(owned.is_empty());
335        assert_eq!(owned.result.affected_rows(), 42);
336    }
337
338    // --- OwnedResult::without_arena ---
339
340    #[test]
341    fn owned_result_without_arena_len_zero() {
342        let cols: Arc<[ColumnDesc]> = Arc::from(Vec::new());
343        let result = QueryResult::from_parts(vec![], 0, cols, 0);
344        let owned = OwnedResult::without_arena(result);
345        assert_eq!(owned.len(), 0);
346    }
347
348    #[test]
349    fn owned_result_without_arena_is_empty() {
350        let cols: Arc<[ColumnDesc]> = Arc::from(Vec::new());
351        let result = QueryResult::from_parts(vec![], 0, cols, 0);
352        let owned = OwnedResult::without_arena(result);
353        assert!(owned.is_empty());
354    }
355
356    #[test]
357    fn owned_result_without_arena_with_rows() {
358        let cols: Arc<[ColumnDesc]> = vec![ColumnDesc {
359            name: "c0".into(),
360            type_oid: 23,
361            type_size: 4,
362            table_oid: 0,
363            column_id: 0,
364        }]
365        .into();
366        let col_offsets = vec![(0, -1); 3]; // 3 rows, 1 col each (all NULL)
367        let result = QueryResult::from_parts(col_offsets, 1, cols, 0);
368        let owned = OwnedResult::without_arena(result);
369        assert_eq!(owned.len(), 3);
370        assert!(!owned.is_empty());
371    }
372
373    // --- OwnedResult Debug ---
374
375    #[test]
376    fn owned_result_debug_format() {
377        let owned = make_owned_result(5, 2);
378        let dbg = format!("{owned:?}");
379        assert!(
380            dbg.contains("OwnedResult"),
381            "Debug should contain struct name: {dbg}"
382        );
383        assert!(dbg.contains("5"), "Debug should contain row count: {dbg}");
384    }
385
386    // --- OwnedResult drop without_arena variant ---
387
388    #[test]
389    fn owned_result_without_arena_drop_does_not_panic() {
390        let cols: Arc<[ColumnDesc]> = Arc::from(Vec::new());
391        let result = QueryResult::from_parts(vec![], 0, cols, 0);
392        let owned = OwnedResult::without_arena(result);
393        drop(owned); // Must not panic — arena is Arena::empty()
394    }
395}