Skip to main content

radixdb_api/
rows.rs

1// Copyright 2026 RadixDB Contributors
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! Rows iterator for query results - Rust idiomatic API
16//!
17//! # Example
18//!
19//! ```no_run
20//! use radixdb_api::{Database, FromRow, ResultRow};
21//! use radixdb_core::Result;
22//! # fn main() -> Result<()> {
23//! # let db = Database::open_in_memory()?;
24//! # db.execute("CREATE TABLE users (id INTEGER, name TEXT)", ())?;
25//! // Idiomatic Rust iteration
26//! for row in db.query("SELECT * FROM users", ())? {
27//!     let row = row?;
28//!     let id: i64 = row.get(0)?;
29//!     let name: String = row.get(1)?;
30//!     println!("{}: {}", id, name);
31//! }
32//!
33//! // With combinators
34//! let names: Vec<String> = db.query("SELECT name FROM users", ())?
35//!     .map(|r| r.and_then(|row| row.get(0)))
36//!     .collect::<Result<_>>()?;
37//!
38//! // With FromRow for struct mapping
39//! struct User { id: i64, name: String }
40//!
41//! impl FromRow for User {
42//!     fn from_row(row: &ResultRow) -> Result<Self> {
43//!         Ok(User {
44//!             id: row.get(0)?,
45//!             name: row.get(1)?,
46//!         })
47//!     }
48//! }
49//!
50//! let users: Vec<User> = db.query_as("SELECT id, name FROM users", ())?;
51//! # assert!(users.is_empty());
52//! # Ok(())
53//! # }
54//! ```
55
56use radixdb_core::CompactArc;
57use radixdb_core::{Error, Result, Row, Value};
58use radixdb_executor::result::ExecutionResult;
59use rustc_hash::FxHashMap;
60
61use super::database::FromValue;
62use super::result_adapter::ApiResultCursor;
63
64/// Trait for converting a database row into a Rust struct
65///
66/// Implement this trait for your structs to enable automatic mapping
67/// from query results using `query_as`.
68///
69/// # Example
70///
71/// ```ignore
72/// use radixdb::{Database, FromRow, ResultRow, Result};
73///
74/// struct User {
75///     id: i64,
76///     name: String,
77///     email: Option<String>,
78/// }
79///
80/// impl FromRow for User {
81///     fn from_row(row: &ResultRow) -> Result<Self> {
82///         Ok(User {
83///             id: row.get(0)?,
84///             name: row.get(1)?,
85///             email: row.get(2)?,  // Option<T> handles NULL
86///         })
87///     }
88/// }
89///
90/// // Now you can use query_as
91/// let db = Database::open("memory://")?;
92/// let users: Vec<User> = db.query_as("SELECT id, name, email FROM users", ())?;
93/// ```
94///
95/// # Using column names
96///
97/// You can also use column names for more robust mapping:
98///
99/// ```ignore
100/// impl FromRow for User {
101///     fn from_row(row: &ResultRow) -> Result<Self> {
102///         Ok(User {
103///             id: row.get_by_name("id")?,
104///             name: row.get_by_name("name")?,
105///             email: row.get_by_name("email")?,
106///         })
107///     }
108/// }
109/// ```
110pub trait FromRow: Sized {
111    /// Convert a result row into Self
112    fn from_row(row: &ResultRow) -> Result<Self>;
113}
114
115/// A single row from a query result with typed accessors
116#[derive(Debug, Clone)]
117pub struct ResultRow {
118    row: Row,
119    /// Shared column names (Arc avoids per-row allocation)
120    columns: CompactArc<Vec<String>>,
121    /// Shared case-folded name map built once per result cursor.
122    column_lookup: CompactArc<FxHashMap<String, Vec<usize>>>,
123}
124
125impl ResultRow {
126    /// Create a new ResultRow
127    pub(crate) fn new(
128        row: Row,
129        columns: CompactArc<Vec<String>>,
130        column_lookup: CompactArc<FxHashMap<String, Vec<usize>>>,
131    ) -> Self {
132        Self {
133            row,
134            columns,
135            column_lookup,
136        }
137    }
138
139    /// Get a column value by index with type conversion
140    ///
141    /// # Example
142    ///
143    /// ```ignore
144    /// let id: i64 = row.get(0)?;
145    /// let name: String = row.get(1)?;
146    /// let score: Option<f64> = row.get(2)?;
147    /// ```
148    pub fn get<T: FromValue>(&self, index: usize) -> Result<T> {
149        let value = self
150            .row
151            .get(index)
152            .ok_or(Error::ColumnIndexOutOfBounds { index })?;
153        T::from_value(value)
154    }
155
156    /// Get a column value by name with type conversion
157    ///
158    /// # Example
159    ///
160    /// ```ignore
161    /// let name: String = row.get_by_name("name")?;
162    /// let age: i64 = row.get_by_name("age")?;
163    /// ```
164    pub fn get_by_name<T: FromValue>(&self, name: &str) -> Result<T> {
165        let index = self.column_index(name)?;
166        self.get(index)
167    }
168
169    /// Get the raw Value at an index
170    pub fn get_value(&self, index: usize) -> Option<&Value> {
171        self.row.get(index)
172    }
173
174    /// Get the underlying Row
175    pub fn into_inner(self) -> Row {
176        self.row
177    }
178
179    /// Get a reference to the underlying Row
180    pub fn as_row(&self) -> &Row {
181        &self.row
182    }
183
184    /// Get the column names
185    pub fn columns(&self) -> &[String] {
186        &self.columns
187    }
188
189    /// Get the number of columns
190    pub fn len(&self) -> usize {
191        self.row.len()
192    }
193
194    /// Check if row is empty
195    pub fn is_empty(&self) -> bool {
196        self.row.len() == 0
197    }
198
199    /// Check if a column value is NULL
200    pub fn is_null(&self, index: usize) -> Result<bool> {
201        self.row
202            .get(index)
203            .map(Value::is_null)
204            .ok_or(Error::ColumnIndexOutOfBounds { index })
205    }
206
207    /// Get the index of a column by name (case-insensitive)
208    fn column_index(&self, name: &str) -> Result<usize> {
209        let name_lower = name.to_lowercase();
210        let matches = self
211            .column_lookup
212            .get(&name_lower)
213            .ok_or_else(|| Error::ColumnNotFound(name.to_string()))?;
214        if matches.len() > 1 {
215            Err(Error::AmbiguousColumn(name.to_string()))
216        } else {
217            Ok(matches[0])
218        }
219    }
220}
221
222/// Iterator over query result rows
223///
224/// Implements `Iterator<Item = Result<ResultRow>>` for idiomatic Rust usage.
225///
226/// # Example
227///
228/// ```ignore
229/// // Standard for loop
230/// for row in db.query("SELECT * FROM users")? {
231///     let row = row?;
232///     println!("{:?}", row.get::<String>(0)?);
233/// }
234///
235/// // Collect into Vec
236/// let rows: Vec<ResultRow> = db.query("SELECT * FROM users")?
237///     .collect::<Result<Vec<_>, _>>()?;
238///
239/// // Filter and map
240/// let adults: Vec<String> = db.query("SELECT name, age FROM users")?
241///     .filter_map(|r| {
242///         let row = r.ok()?;
243///         let age: i64 = row.get(1).ok()?;
244///         if age >= 18 {
245///             row.get::<String>(0).ok()
246///         } else {
247///             None
248///         }
249///     })
250///     .collect();
251/// ```
252pub struct Rows {
253    result: ApiResultCursor,
254    /// Shared column names (Arc to avoid cloning per row)
255    columns: CompactArc<Vec<String>>,
256    column_lookup: CompactArc<FxHashMap<String, Vec<usize>>>,
257    closed: bool,
258    positioned: bool,
259    /// Pending error from a filter runtime failure (e.g., invalid REGEXP)
260    pending_error: Option<radixdb_core::Error>,
261    close_error: Option<radixdb_core::Error>,
262    iterator_close_error_yielded: bool,
263}
264
265impl Rows {
266    /// Adapt an internal execution result into the public row cursor.
267    pub(crate) fn new(result: ExecutionResult) -> Self {
268        let result = ApiResultCursor::new(result);
269        // Use columns_arc() if available (zero-copy), otherwise clone
270        let columns = result
271            .columns_arc()
272            .unwrap_or_else(|| CompactArc::new(result.columns().to_vec()));
273        let mut column_lookup: FxHashMap<String, Vec<usize>> = FxHashMap::default();
274        for (index, column) in columns.iter().enumerate() {
275            column_lookup
276                .entry(column.to_lowercase())
277                .or_default()
278                .push(index);
279        }
280        Self {
281            result,
282            columns,
283            column_lookup: CompactArc::new(column_lookup),
284            closed: false,
285            positioned: false,
286            pending_error: None,
287            close_error: None,
288            iterator_close_error_yielded: false,
289        }
290    }
291
292    /// Get the column names
293    pub fn columns(&self) -> &[String] {
294        &self.columns
295    }
296
297    /// Get the number of columns
298    pub fn column_count(&self) -> usize {
299        self.columns.len()
300    }
301
302    /// Get the number of rows affected (for DML statements)
303    pub fn rows_affected(&self) -> i64 {
304        self.result.rows_affected()
305    }
306
307    /// Get the last generated id from an AUTO_INCREMENT insert.
308    pub fn last_insert_id(&self) -> i64 {
309        self.result.last_insert_id()
310    }
311
312    /// Whether this result can be fetched as decoded typed column batches.
313    ///
314    /// The method is intended for the server transport boundary. Ordinary API
315    /// callers keep using `advance()` / `Iterator` and therefore retain the
316    /// historic row-oriented contract.
317    #[inline]
318    pub fn supports_server_column_batches(&self) -> bool {
319        !self.closed && self.result.supports_typed_batches()
320    }
321
322    /// If typed batches are unavailable, return a stable diagnostics reason.
323    #[inline]
324    pub fn server_column_batch_fallback(&self) -> Option<super::ServerBatchFallback> {
325        if self.supports_server_column_batches() {
326            None
327        } else if self.closed {
328            Some(super::ServerBatchFallback::RowState)
329        } else {
330            self.result
331                .typed_batch_fallback_reason()
332                .map(super::ServerBatchFallback::from_storage)
333        }
334    }
335
336    /// Return the next decoded typed batch, closing the result at EOF.
337    pub fn next_server_column_batch(&mut self) -> Result<Option<super::ServerColumnBatch>> {
338        if self.closed {
339            return self.close_error.clone().map_or(Ok(None), Err);
340        }
341        match self.result.next_typed_batch() {
342            Ok(Some(batch)) => super::ServerColumnBatch::from_storage(batch).map(Some),
343            Ok(None) => {
344                self.close_internal()?;
345                Ok(None)
346            }
347            Err(error) => {
348                let _ = self.close_internal();
349                Err(error)
350            }
351        }
352    }
353
354    /// Advance the cursor to the next row.
355    ///
356    /// Returns `true` if a row is available, `false` when exhausted.
357    /// Use `current_row()` to access the row by reference (no clone).
358    ///
359    /// This is faster than the Iterator interface for bulk serialization
360    /// because it avoids `take_row()` which clones the row.
361    #[inline]
362    pub fn advance(&mut self) -> bool {
363        if self.closed {
364            return false;
365        }
366        if self.result.next() {
367            self.positioned = true;
368            return true;
369        }
370        self.positioned = false;
371        // Check for runtime filter errors (e.g., invalid REGEXP)
372        if let Some(err) = self.result.last_error() {
373            self.pending_error = Some(err);
374        }
375        // Clean up resources (scanner close, etc.) now that iteration is done
376        let _ = self.close_internal();
377        false
378    }
379
380    /// Return any runtime error that caused `advance()` to return false.
381    ///
382    /// After `advance()` returns `false`, call this to distinguish between
383    /// normal end-of-stream (returns `None`) and a runtime filter error
384    /// like an invalid parameterized REGEXP (returns `Some(error)`).
385    #[inline]
386    pub fn error(&mut self) -> Option<radixdb_core::Error> {
387        if let Some(error) = self.pending_error.take() {
388            return Some(error);
389        }
390        let error = self.close_error.clone();
391        if error.is_some() {
392            self.iterator_close_error_yielded = true;
393        }
394        error
395    }
396
397    /// Get a reference to the current row (after a successful `advance()`).
398    ///
399    /// Returns `&Row` directly — no clone, no ResultRow wrapper.
400    #[inline]
401    pub fn current_row(&self) -> Result<&Row> {
402        if self.closed || !self.positioned {
403            Err(Error::CursorNotPositioned)
404        } else {
405            Ok(self.result.row())
406        }
407    }
408
409    /// Collect all rows into a Vec
410    ///
411    /// # Example
412    ///
413    /// ```ignore
414    /// let rows = db.query("SELECT * FROM users")?.collect_vec()?;
415    /// for row in rows {
416    ///     println!("{:?}", row);
417    /// }
418    /// ```
419    pub fn collect_vec(self) -> Result<Vec<ResultRow>> {
420        self.collect()
421    }
422
423    /// Close the result set explicitly
424    ///
425    /// This is called automatically when the Rows is dropped.
426    pub fn close(&mut self) -> Result<()> {
427        let result = self.close_internal();
428        if result.is_err() {
429            self.iterator_close_error_yielded = true;
430        }
431        result
432    }
433
434    fn close_internal(&mut self) -> Result<()> {
435        if !self.closed {
436            let result = self.result.close();
437            self.closed = true;
438            self.positioned = false;
439            if let Err(error) = result {
440                self.close_error = Some(error);
441            }
442        }
443        self.close_error.clone().map_or(Ok(()), Err)
444    }
445}
446
447impl Iterator for Rows {
448    type Item = Result<ResultRow>;
449
450    fn next(&mut self) -> Option<Self::Item> {
451        if self.closed {
452            if !self.iterator_close_error_yielded {
453                if let Some(error) = self.close_error.clone() {
454                    self.iterator_close_error_yielded = true;
455                    return Some(Err(error));
456                }
457            }
458            return None;
459        }
460
461        self.positioned = false;
462
463        if self.result.next() {
464            // Use take_row() to avoid cloning - moves the row out of the result
465            let row = self.result.take_row();
466            // Arc clone is O(1) - just increments reference count
467            Some(Ok(ResultRow::new(
468                row,
469                CompactArc::clone(&self.columns),
470                CompactArc::clone(&self.column_lookup),
471            )))
472        } else if let Some(err) = self.result.last_error() {
473            // Surface runtime errors (e.g. invalid REGEXP pattern).
474            // close() forwards to result.close() for proper scanner cleanup
475            let _ = self.close_internal();
476            Some(Err(err))
477        } else {
478            match self.close_internal() {
479                Ok(()) => None,
480                Err(error) => {
481                    self.iterator_close_error_yielded = true;
482                    Some(Err(error))
483                }
484            }
485        }
486    }
487}
488
489impl Drop for Rows {
490    fn drop(&mut self) {
491        let _ = self.close_internal();
492    }
493}
494
495#[cfg(test)]
496mod tests {
497    use super::*;
498    use crate::result_adapter::close_failing_result;
499    use radixdb_storage::traits::MemoryResult;
500
501    fn create_test_rows() -> Rows {
502        let columns = vec!["id".to_string(), "name".to_string(), "value".to_string()];
503        let rows = vec![
504            Row::from_values(vec![
505                Value::Integer(1),
506                Value::text("Alice"),
507                Value::Float(10.5),
508            ]),
509            Row::from_values(vec![
510                Value::Integer(2),
511                Value::text("Bob"),
512                Value::Float(20.0),
513            ]),
514        ];
515
516        let result = MemoryResult::with_rows(columns, rows);
517        Rows::new(Box::new(result))
518    }
519
520    #[test]
521    fn test_iterator_for_loop() {
522        let rows = create_test_rows();
523        let mut count = 0;
524
525        for row in rows {
526            let row = row.unwrap();
527            assert!(row.get::<i64>(0).is_ok());
528            count += 1;
529        }
530
531        assert_eq!(count, 2);
532    }
533
534    #[test]
535    fn r6_l01_b_rows_close_surfaces_cleanup_failure() {
536        let mut rows = Rows::new(close_failing_result());
537        let error = rows.close().expect_err("close failure must be observable");
538        assert!(error.to_string().contains("injected close failure"));
539        assert!(rows.close().is_err(), "close failure remains observable");
540    }
541
542    #[test]
543    fn r8_iterator_surfaces_cleanup_failure_at_eof_once() {
544        let mut rows = Rows::new(close_failing_result());
545        let error = rows
546            .next()
547            .expect("terminal cleanup error")
548            .expect_err("cleanup must fail");
549        assert!(error.to_string().contains("injected close failure"));
550        assert!(rows.next().is_none());
551    }
552
553    #[test]
554    fn r8_advance_exposes_terminal_cleanup_failure() {
555        let mut rows = Rows::new(close_failing_result());
556        assert!(!rows.advance());
557        let error = rows.error().expect("cleanup error must be recoverable");
558        assert!(error.to_string().contains("injected close failure"));
559    }
560
561    #[test]
562    fn test_iterator_collect() {
563        let rows = create_test_rows();
564        let collected: Vec<ResultRow> = rows.collect::<std::result::Result<Vec<_>, _>>().unwrap();
565
566        assert_eq!(collected.len(), 2);
567        assert_eq!(collected[0].get::<i64>(0).unwrap(), 1);
568        assert_eq!(collected[1].get::<i64>(0).unwrap(), 2);
569    }
570
571    #[test]
572    fn test_iterator_map() {
573        let rows = create_test_rows();
574        let names: Vec<String> = rows
575            .map(|r| r.and_then(|row| row.get(1)))
576            .collect::<std::result::Result<Vec<_>, _>>()
577            .unwrap();
578
579        assert_eq!(names, vec!["Alice", "Bob"]);
580    }
581
582    #[test]
583    fn test_iterator_filter() {
584        let rows = create_test_rows();
585        let filtered: Vec<ResultRow> = rows
586            .filter_map(|r| {
587                let row = r.ok()?;
588                let id: i64 = row.get(0).ok()?;
589                if id > 1 {
590                    Some(row)
591                } else {
592                    None
593                }
594            })
595            .collect();
596
597        assert_eq!(filtered.len(), 1);
598        assert_eq!(filtered[0].get::<String>(1).unwrap(), "Bob");
599    }
600
601    #[test]
602    fn test_result_row_get() {
603        let rows = create_test_rows();
604        let row = rows.into_iter().next().unwrap().unwrap();
605
606        assert_eq!(row.get::<i64>(0).unwrap(), 1);
607        assert_eq!(row.get::<String>(1).unwrap(), "Alice");
608        assert_eq!(row.get::<f64>(2).unwrap(), 10.5);
609    }
610
611    #[test]
612    fn test_result_row_get_by_name() {
613        let rows = create_test_rows();
614        let row = rows.into_iter().next().unwrap().unwrap();
615
616        assert_eq!(row.get_by_name::<i64>("id").unwrap(), 1);
617        assert_eq!(row.get_by_name::<String>("name").unwrap(), "Alice");
618        assert_eq!(row.get_by_name::<f64>("value").unwrap(), 10.5);
619
620        // Case insensitive
621        assert_eq!(row.get_by_name::<i64>("ID").unwrap(), 1);
622        assert_eq!(row.get_by_name::<String>("NAME").unwrap(), "Alice");
623    }
624
625    #[test]
626    fn r8_l01_batch_j_result_rows_share_case_folded_column_lookup() {
627        let result = MemoryResult::with_rows(
628            vec!["Name".to_string(), "NAME".to_string(), "id".to_string()],
629            vec![
630                Row::from_values(vec![Value::text("a"), Value::text("b"), Value::Integer(1)]),
631                Row::from_values(vec![Value::text("c"), Value::text("d"), Value::Integer(2)]),
632            ],
633        );
634        let mut rows = Rows::new(Box::new(result));
635        let first = rows.next().unwrap().unwrap();
636        let second = rows.next().unwrap().unwrap();
637
638        assert_eq!(first.get_by_name::<i64>("ID").unwrap(), 1);
639        assert_eq!(second.get_by_name::<i64>("id").unwrap(), 2);
640        assert!(matches!(
641            first.get_by_name::<String>("name"),
642            Err(Error::AmbiguousColumn(column)) if column == "name"
643        ));
644    }
645
646    #[test]
647    fn test_result_row_columns() {
648        let rows = create_test_rows();
649        let row = rows.into_iter().next().unwrap().unwrap();
650
651        assert_eq!(row.columns(), &["id", "name", "value"]);
652        assert_eq!(row.len(), 3);
653        assert!(!row.is_empty());
654    }
655
656    #[test]
657    fn test_rows_columns() {
658        let rows = create_test_rows();
659        assert_eq!(rows.columns(), &["id", "name", "value"]);
660        assert_eq!(rows.column_count(), 3);
661    }
662
663    #[test]
664    fn test_collect_vec() {
665        let rows = create_test_rows();
666        let collected = rows.collect_vec().unwrap();
667
668        assert_eq!(collected.len(), 2);
669    }
670
671    #[test]
672    fn test_out_of_bounds() {
673        let rows = create_test_rows();
674        let row = rows.into_iter().next().unwrap().unwrap();
675
676        assert!(row.get::<i64>(10).is_err());
677    }
678
679    #[test]
680    fn test_column_not_found() {
681        let rows = create_test_rows();
682        let row = rows.into_iter().next().unwrap().unwrap();
683
684        assert!(row.get_by_name::<String>("nonexistent").is_err());
685    }
686
687    #[test]
688    fn test_advance_and_current_row() {
689        let mut rows = create_test_rows();
690
691        // First row
692        assert!(rows.advance());
693        let row = rows.current_row().unwrap();
694        assert_eq!(row.get(0), Some(&Value::Integer(1)));
695        assert_eq!(row.get(1), Some(&Value::text("Alice")));
696
697        // Second row
698        assert!(rows.advance());
699        let row = rows.current_row().unwrap();
700        assert_eq!(row.get(0), Some(&Value::Integer(2)));
701        assert_eq!(row.get(1), Some(&Value::text("Bob")));
702
703        // Exhausted
704        assert!(!rows.advance());
705    }
706
707    #[test]
708    fn test_advance_on_closed_rows() {
709        let mut rows = create_test_rows();
710        rows.close().unwrap();
711        assert!(!rows.advance());
712    }
713
714    #[test]
715    fn test_advance_full_scan() {
716        let mut rows = create_test_rows();
717        let mut count = 0;
718        while rows.advance() {
719            let _row = rows.current_row();
720            count += 1;
721        }
722        assert_eq!(count, 2);
723    }
724}