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`.
81pub trait Executor {
82    /// Execute a query and return all rows.
83    fn query_raw(
84        &self,
85        sql: &str,
86        sql_hash: u64,
87        params: &[&(dyn Encode + Sync)],
88    ) -> BsqlResult<OwnedResult>;
89
90    /// Execute a read-only query. May route to replicas in the future.
91    fn query_raw_readonly(
92        &self,
93        sql: &str,
94        sql_hash: u64,
95        params: &[&(dyn Encode + Sync)],
96    ) -> BsqlResult<OwnedResult>;
97
98    /// Execute a query and return the number of affected rows.
99    fn execute_raw(
100        &self,
101        sql: &str,
102        sql_hash: u64,
103        params: &[&(dyn Encode + Sync)],
104    ) -> BsqlResult<u64>;
105}
106
107impl Executor for Pool {
108    #[inline]
109    fn query_raw(
110        &self,
111        sql: &str,
112        sql_hash: u64,
113        params: &[&(dyn Encode + Sync)],
114    ) -> BsqlResult<OwnedResult> {
115        let mut guard = self.inner.acquire().map_err(BsqlError::from)?;
116        let result = guard
117            .query(sql, sql_hash, params)
118            .map_err(BsqlError::from_driver_query)?;
119        Ok(OwnedResult::without_arena(result))
120    }
121
122    #[inline]
123    fn query_raw_readonly(
124        &self,
125        sql: &str,
126        sql_hash: u64,
127        params: &[&(dyn Encode + Sync)],
128    ) -> BsqlResult<OwnedResult> {
129        let pool = self.read_pool.as_ref().unwrap_or(&self.inner);
130        let mut guard = pool.acquire().map_err(BsqlError::from)?;
131        let result = guard
132            .query(sql, sql_hash, params)
133            .map_err(BsqlError::from_driver_query)?;
134        Ok(OwnedResult::without_arena(result))
135    }
136
137    #[inline]
138    fn execute_raw(
139        &self,
140        sql: &str,
141        sql_hash: u64,
142        params: &[&(dyn Encode + Sync)],
143    ) -> BsqlResult<u64> {
144        let mut guard = self.inner.acquire().map_err(BsqlError::from)?;
145        guard
146            .execute(sql, sql_hash, params)
147            .map_err(BsqlError::from_driver_query)
148    }
149}
150
151impl Executor for PoolConnection {
152    #[inline]
153    fn query_raw(
154        &self,
155        sql: &str,
156        sql_hash: u64,
157        params: &[&(dyn Encode + Sync)],
158    ) -> BsqlResult<OwnedResult> {
159        let mut guard = self.inner.lock().unwrap_or_else(|e| e.into_inner());
160        let result = guard
161            .query(sql, sql_hash, params)
162            .map_err(BsqlError::from_driver_query)?;
163        Ok(OwnedResult::without_arena(result))
164    }
165
166    #[inline]
167    fn query_raw_readonly(
168        &self,
169        sql: &str,
170        sql_hash: u64,
171        params: &[&(dyn Encode + Sync)],
172    ) -> BsqlResult<OwnedResult> {
173        self.query_raw(sql, sql_hash, params)
174    }
175
176    #[inline]
177    fn execute_raw(
178        &self,
179        sql: &str,
180        sql_hash: u64,
181        params: &[&(dyn Encode + Sync)],
182    ) -> BsqlResult<u64> {
183        let mut guard = self.inner.lock().unwrap_or_else(|e| e.into_inner());
184        guard
185            .execute(sql, sql_hash, params)
186            .map_err(BsqlError::from_driver_query)
187    }
188}
189
190impl Executor for Transaction {
191    fn query_raw(
192        &self,
193        sql: &str,
194        sql_hash: u64,
195        params: &[&(dyn Encode + Sync)],
196    ) -> BsqlResult<OwnedResult> {
197        self.query_inner(sql, sql_hash, params)
198    }
199
200    #[inline]
201    fn query_raw_readonly(
202        &self,
203        sql: &str,
204        sql_hash: u64,
205        params: &[&(dyn Encode + Sync)],
206    ) -> BsqlResult<OwnedResult> {
207        self.query_raw(sql, sql_hash, params)
208    }
209
210    #[inline]
211    fn execute_raw(
212        &self,
213        sql: &str,
214        sql_hash: u64,
215        params: &[&(dyn Encode + Sync)],
216    ) -> BsqlResult<u64> {
217        self.execute_inner(sql, sql_hash, params)
218    }
219}
220
221#[cfg(test)]
222mod tests {
223    use super::*;
224    use bsql_driver_postgres::arena::{acquire_arena, release_arena};
225    use bsql_driver_postgres::{ColumnDesc, QueryResult};
226    use std::sync::Arc;
227
228    /// Helper: build an OwnedResult with `n` rows and `num_cols` columns.
229    /// Each column offset entry is a dummy (0, 0) pair — sufficient for
230    /// testing len/is_empty/row-count without decoding real data.
231    fn make_owned_result(num_rows: usize, num_cols: usize) -> OwnedResult {
232        let arena = acquire_arena();
233        let cols: Arc<[ColumnDesc]> = (0..num_cols)
234            .map(|i| ColumnDesc {
235                name: format!("c{i}").into(),
236                type_oid: 23, // int4
237                type_size: 4,
238                table_oid: 0,
239                column_id: 0,
240            })
241            .collect::<Vec<_>>()
242            .into();
243
244        let col_offsets: Vec<(usize, i32)> = vec![(0, -1); num_rows * num_cols]; // NULL columns
245        let result = QueryResult::from_parts(col_offsets, num_cols, cols, 0);
246        OwnedResult { result, arena }
247    }
248
249    // --- OwnedResult ---
250
251    #[test]
252    fn owned_result_new_zero_rows() {
253        let owned = make_owned_result(0, 2);
254        assert_eq!(owned.len(), 0);
255        assert!(owned.is_empty());
256    }
257
258    #[test]
259    fn owned_result_new_single_row() {
260        let owned = make_owned_result(1, 3);
261        assert_eq!(owned.len(), 1);
262        assert!(!owned.is_empty());
263    }
264
265    #[test]
266    fn owned_result_new_multiple_rows() {
267        let owned = make_owned_result(5, 2);
268        assert_eq!(owned.len(), 5);
269        assert!(!owned.is_empty());
270    }
271
272    // --- OwnedResult::row ---
273
274    #[test]
275    fn owned_result_row_access() {
276        let owned = make_owned_result(3, 2);
277        // Should not panic for valid indices
278        let _r0 = owned.row(0);
279        let _r1 = owned.row(1);
280        let _r2 = owned.row(2);
281    }
282
283    #[test]
284    #[should_panic]
285    fn owned_result_row_out_of_bounds_panics() {
286        let owned = make_owned_result(2, 1);
287        let _r = owned.row(2); // out of bounds
288    }
289
290    // --- OwnedResult::iter ---
291
292    #[test]
293    fn owned_result_iter_count() {
294        let owned = make_owned_result(4, 2);
295        let count = owned.iter().count();
296        assert_eq!(count, 4);
297    }
298
299    #[test]
300    fn owned_result_iter_empty() {
301        let owned = make_owned_result(0, 2);
302        let count = owned.iter().count();
303        assert_eq!(count, 0);
304    }
305
306    // --- OwnedResult::Drop releases arena back to pool ---
307
308    #[test]
309    fn owned_result_drop_releases_arena() {
310        // Acquire an arena, wrap it in OwnedResult, drop it.
311        // After drop, acquiring should succeed (arena was returned to pool).
312        let owned = make_owned_result(1, 1);
313        drop(owned);
314        // If the arena was released, we can acquire again without issue.
315        let arena = acquire_arena();
316        release_arena(arena);
317    }
318
319    // --- OwnedResult with zero columns ---
320
321    #[test]
322    fn owned_result_zero_columns() {
323        // Commands like INSERT without RETURNING have 0 columns
324        let arena = acquire_arena();
325        let cols: Arc<[ColumnDesc]> = Arc::from(Vec::new());
326        let result = QueryResult::from_parts(vec![], 0, cols, 42);
327        let owned = OwnedResult { result, arena };
328        assert_eq!(owned.len(), 0);
329        assert!(owned.is_empty());
330        assert_eq!(owned.result.affected_rows(), 42);
331    }
332
333    // --- OwnedResult::without_arena ---
334
335    #[test]
336    fn owned_result_without_arena_len_zero() {
337        let cols: Arc<[ColumnDesc]> = Arc::from(Vec::new());
338        let result = QueryResult::from_parts(vec![], 0, cols, 0);
339        let owned = OwnedResult::without_arena(result);
340        assert_eq!(owned.len(), 0);
341    }
342
343    #[test]
344    fn owned_result_without_arena_is_empty() {
345        let cols: Arc<[ColumnDesc]> = Arc::from(Vec::new());
346        let result = QueryResult::from_parts(vec![], 0, cols, 0);
347        let owned = OwnedResult::without_arena(result);
348        assert!(owned.is_empty());
349    }
350
351    #[test]
352    fn owned_result_without_arena_with_rows() {
353        let cols: Arc<[ColumnDesc]> = vec![ColumnDesc {
354            name: "c0".into(),
355            type_oid: 23,
356            type_size: 4,
357            table_oid: 0,
358            column_id: 0,
359        }]
360        .into();
361        let col_offsets = vec![(0, -1); 3]; // 3 rows, 1 col each (all NULL)
362        let result = QueryResult::from_parts(col_offsets, 1, cols, 0);
363        let owned = OwnedResult::without_arena(result);
364        assert_eq!(owned.len(), 3);
365        assert!(!owned.is_empty());
366    }
367
368    // --- OwnedResult Debug ---
369
370    #[test]
371    fn owned_result_debug_format() {
372        let owned = make_owned_result(5, 2);
373        let dbg = format!("{owned:?}");
374        assert!(
375            dbg.contains("OwnedResult"),
376            "Debug should contain struct name: {dbg}"
377        );
378        assert!(dbg.contains("5"), "Debug should contain row count: {dbg}");
379    }
380
381    // --- OwnedResult drop without_arena variant ---
382
383    #[test]
384    fn owned_result_without_arena_drop_does_not_panic() {
385        let cols: Arc<[ColumnDesc]> = Arc::from(Vec::new());
386        let result = QueryResult::from_parts(vec![], 0, cols, 0);
387        let owned = OwnedResult::without_arena(result);
388        drop(owned); // Must not panic — arena is Arena::empty()
389    }
390}