Skip to main content

radixdb_storage/traits/
result.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//! Result trait for query results
16//!
17
18use rustc_hash::FxHashMap;
19
20use crate::traits::{Scanner, TypedBatchFallbackReason, TypedColumnBatch};
21use radixdb_core::value::NULL_VALUE;
22use radixdb_core::CompactArc;
23use radixdb_core::{Result, Row, Value};
24
25/// Source slot retained by an internal deferred row projection.
26///
27/// This is an executor/storage bridge, not part of the public SQL result
28/// contract. It lets a recursive JOIN hand its compact row representation to
29/// the next operator without first constructing a complete owned [`Row`].
30#[doc(hidden)]
31#[derive(Debug, Clone, Copy, PartialEq, Eq)]
32pub enum DeferredColumnSource {
33    Left(usize),
34    Right(usize),
35}
36
37/// Compact row representation carried across an internal [`QueryResult`]
38/// boundary.
39///
40/// Normal consumers still observe the established owned [`Row`] contract via
41/// [`QueryResult::row`] and [`QueryResult::take_row`]. Only executor adapters
42/// opt into [`QueryResult::take_deferred_row`].
43#[doc(hidden)]
44#[derive(Debug, Clone)]
45pub enum DeferredRow {
46    Owned(Row),
47    Shared {
48        rows: CompactArc<Vec<Row>>,
49        row_index: usize,
50    },
51    Projected {
52        left: Box<DeferredRow>,
53        right: Box<DeferredRow>,
54        columns: CompactArc<[DeferredColumnSource]>,
55    },
56    Remapped {
57        row: Box<DeferredRow>,
58        columns: CompactArc<[usize]>,
59    },
60}
61
62impl DeferredRow {
63    #[inline]
64    pub fn owned(row: Row) -> Self {
65        Self::Owned(row)
66    }
67
68    #[inline]
69    pub fn shared(rows: CompactArc<Vec<Row>>, row_index: usize) -> Self {
70        assert!(
71            row_index < rows.len(),
72            "deferred shared row index outside batch"
73        );
74        Self::Shared { rows, row_index }
75    }
76
77    #[inline]
78    pub fn projected(
79        left: DeferredRow,
80        right: DeferredRow,
81        columns: CompactArc<[DeferredColumnSource]>,
82    ) -> Self {
83        Self::Projected {
84            left: Box::new(left),
85            right: Box::new(right),
86            columns,
87        }
88    }
89
90    #[inline]
91    pub fn remapped(row: DeferredRow, columns: CompactArc<[usize]>) -> Self {
92        Self::Remapped {
93            row: Box::new(row),
94            columns,
95        }
96    }
97
98    #[inline]
99    pub fn len(&self) -> usize {
100        match self {
101            Self::Owned(row) => row.len(),
102            Self::Shared { rows, row_index } => rows[*row_index].len(),
103            Self::Projected { columns, .. } => columns.len(),
104            Self::Remapped { columns, .. } => columns.len(),
105        }
106    }
107
108    #[inline]
109    pub fn is_empty(&self) -> bool {
110        self.len() == 0
111    }
112
113    #[inline]
114    pub fn is_deferred(&self) -> bool {
115        !matches!(self, Self::Owned(_))
116    }
117
118    /// Conservative retained-size estimate for request-local backpressure.
119    ///
120    /// Shared batches are charged by their owning scan/hash state, so a shared
121    /// row charges only its handle. Owned rows charge their variable-width
122    /// payload, while projected/remapped rows charge the compact graph they
123    /// actually retain. This deliberately avoids materializing a deferred JOIN
124    /// row merely to decide whether another parallel probe batch may be pulled.
125    pub fn estimated_retained_bytes(&self) -> usize {
126        fn values_bytes(values: &[Value]) -> usize {
127            values.iter().fold(0_usize, |total, value| {
128                let payload = match value {
129                    Value::Text(text) => text.len(),
130                    Value::Extension(bytes) => bytes.len(),
131                    _ => 0,
132                };
133                total
134                    .saturating_add(std::mem::size_of::<Value>())
135                    .saturating_add(payload)
136            })
137        }
138
139        match self {
140            Self::Owned(row) => {
141                std::mem::size_of::<Self>().saturating_add(values_bytes(row.as_slice()))
142            }
143            Self::Shared { .. } => std::mem::size_of::<Self>(),
144            Self::Projected {
145                left,
146                right,
147                columns,
148            } => std::mem::size_of::<Self>()
149                .saturating_add(left.estimated_retained_bytes())
150                .saturating_add(right.estimated_retained_bytes())
151                .saturating_add(
152                    columns
153                        .len()
154                        .saturating_mul(std::mem::size_of::<DeferredColumnSource>()),
155                ),
156            Self::Remapped { row, columns } => std::mem::size_of::<Self>()
157                .saturating_add(row.estimated_retained_bytes())
158                .saturating_add(columns.len().saturating_mul(std::mem::size_of::<usize>())),
159        }
160    }
161
162    #[inline]
163    pub fn get(&self, index: usize) -> Option<&Value> {
164        match self {
165            Self::Owned(row) => row.get(index),
166            Self::Shared { rows, row_index } => rows[*row_index].get(index),
167            Self::Projected {
168                left,
169                right,
170                columns,
171            } => match columns.get(index)? {
172                DeferredColumnSource::Left(source_index) => left.get(*source_index),
173                DeferredColumnSource::Right(source_index) => right.get(*source_index),
174            },
175            Self::Remapped { row, columns } => row.get(*columns.get(index)?),
176        }
177    }
178
179    pub fn to_owned(&self) -> Row {
180        let row = match self {
181            Self::Owned(row) => row.clone(),
182            Self::Shared { rows, row_index } => rows[*row_index].clone(),
183            Self::Projected { columns, .. } => {
184                let mut row = Row::with_capacity(columns.len());
185                for index in 0..columns.len() {
186                    row.push(self.get(index).cloned().unwrap_or(NULL_VALUE));
187                }
188                row
189            }
190            Self::Remapped { columns, .. } => {
191                let mut row = Row::with_capacity(columns.len());
192                for index in 0..columns.len() {
193                    row.push(self.get(index).cloned().unwrap_or(NULL_VALUE));
194                }
195                row
196            }
197        };
198        crate::instrumentation::record_join_value_copies(row.as_slice());
199        row
200    }
201
202    #[inline]
203    pub fn into_owned(self) -> Row {
204        match self {
205            Self::Owned(row) => row,
206            Self::Shared { rows, row_index } => {
207                let row = rows[row_index].clone();
208                crate::instrumentation::record_join_value_copies(row.as_slice());
209                row
210            }
211            deferred @ (Self::Projected { .. } | Self::Remapped { .. }) => deferred.to_owned(),
212        }
213    }
214}
215
216/// Lower-level streaming result shared by storage and execution operators.
217///
218/// This is not the public embedded cursor or public row facade. The API layer
219/// adapts it once into its own cursor and typed row contracts. The trait
220/// provides internal iteration, direct row access, column metadata and
221/// aliasing without depending on the executor or public API.
222///
223/// # Example
224///
225/// ```ignore
226/// let result = transaction.select("users", &["id", "name"], None)?;
227/// println!("Columns: {:?}", result.columns());
228/// while result.next() {
229///     let row = result.row();
230///     // Process row...
231/// }
232/// result.close()?;
233/// ```
234pub trait QueryResult: Send {
235    /// Returns the column names in the result
236    ///
237    /// If aliases are set, this returns the aliased column names.
238    fn columns(&self) -> &[String];
239
240    /// Returns column names as Arc for zero-copy sharing
241    ///
242    /// Upper-layer adapters can use this to avoid cloning column names. The
243    /// default returns `None`, so an adapter falls back to `columns()`.
244    fn columns_arc(&self) -> Option<CompactArc<Vec<String>>> {
245        None
246    }
247
248    /// Moves the cursor to the next row
249    ///
250    /// Returns `true` if there is another row available, `false` otherwise.
251    fn next(&mut self) -> bool;
252
253    /// Scans the current row into the provided values
254    ///
255    /// The number of destination values must match the number of columns.
256    /// Values are converted to the destination types where possible.
257    fn scan(&self, dest: &mut [Value]) -> Result<()>;
258
259    /// Returns the current row directly without copying
260    ///
261    /// This is a high-performance method to access raw column values.
262    /// The returned row is valid until the next call to `next()` or `close()`.
263    fn row(&self) -> &Row;
264
265    /// Takes ownership of the current row (avoids clone)
266    ///
267    /// This is a high-performance method that moves the row data out of the result.
268    /// After calling this, `row()` will return an empty row until `next()` is called.
269    /// The default implementation clones the row for backward compatibility.
270    fn take_row(&mut self) -> Row {
271        self.row().clone()
272    }
273
274    /// Takes the current row while preserving an internal deferred JOIN shape
275    /// when the result supports it.
276    ///
277    /// The default keeps every existing result implementation source-compatible
278    /// and simply wraps its normal owned row. Executor-only results override it.
279    #[doc(hidden)]
280    fn take_deferred_row(&mut self) -> DeferredRow {
281        DeferredRow::owned(self.take_row())
282    }
283
284    /// Whether [`QueryResult::take_deferred_row`] can return an executor row
285    /// graph instead of merely wrapping the ordinary owned row.
286    ///
287    /// JOIN planning uses this capability to keep a deferred recursive side on
288    /// the streaming/probe path. The default is deliberately false so existing
289    /// public result implementations retain their established contract.
290    #[doc(hidden)]
291    fn preserves_deferred_rows(&self) -> bool {
292        false
293    }
294
295    /// Physical ascending, NULLS LAST ordering guaranteed by this result.
296    ///
297    /// The indices address [`QueryResult::columns`]. This is an executor
298    /// certificate, never an inference from the current row contents. Results
299    /// that filter or limit without reordering may forward it; projections must
300    /// remap it, while every other wrapper keeps the fail-closed default.
301    #[doc(hidden)]
302    fn ascending_nulls_last_ordering(&self) -> Option<Vec<usize>> {
303        None
304    }
305
306    /// Closes the result set and releases resources
307    ///
308    /// Default implementation does nothing. Override if cleanup is needed.
309    fn close(&mut self) -> Result<()> {
310        Ok(())
311    }
312
313    /// Returns the number of rows affected by an INSERT, UPDATE, or DELETE
314    ///
315    fn rows_affected(&self) -> i64;
316
317    /// Returns the last inserted ID for an INSERT operation
318    ///
319    fn last_insert_id(&self) -> i64;
320
321    /// Try to extract all rows as `CompactArc<Vec<Row>>` for zero-copy joins
322    ///
323    /// Returns None if the result cannot provide Arc-wrapped rows.
324    /// This consumes the result - after calling, iteration will yield no more rows.
325    /// Default implementation returns None.
326    fn try_into_arc_rows(&mut self) -> Option<CompactArc<Vec<Row>>> {
327        None
328    }
329
330    /// Returns an estimate of the total number of rows in the result.
331    ///
332    /// This is used for pre-allocating vectors to avoid reallocations.
333    /// Returns None if the count is unknown. Default implementation returns None.
334    fn estimated_count(&self) -> Option<usize> {
335        None
336    }
337
338    /// Whether the result can yield decoded typed column batches without
339    /// constructing a row/value object for every result record.
340    ///
341    /// This is deliberately opt-in. Any executor wrapper that changes row
342    /// order, filtering, projection, or value semantics inherits the safe
343    /// default (`false`) and continues through the established row contract.
344    fn supports_typed_batches(&self) -> bool {
345        false
346    }
347
348    /// If `supports_typed_batches()` is false, return the best semantic reason.
349    ///
350    /// This is a diagnostics contract. It must not alter cursor state.
351    fn typed_batch_fallback_reason(&self) -> Option<TypedBatchFallbackReason> {
352        Some(TypedBatchFallbackReason::UnsupportedResultShape)
353    }
354
355    /// Advance to the next typed column batch.
356    ///
357    /// Callers must first observe `supports_typed_batches() == true`. `None`
358    /// is EOF. A returned batch is in the same order and has the same
359    /// projected columns as normal row iteration.
360    fn next_typed_batch(&mut self) -> Result<Option<TypedColumnBatch>> {
361        Ok(None)
362    }
363
364    /// Returns a pending error from the last `next()` call, if any.
365    ///
366    /// When `next()` returns false due to a runtime error (e.g. invalid REGEXP
367    /// pattern), this method returns the error so callers can surface it.
368    /// Default returns None (no error).
369    fn last_error(&mut self) -> Option<radixdb_core::Error> {
370        None
371    }
372
373    /// Sets column aliases for this result
374    ///
375    /// The map keys are alias names, values are original column names.
376    /// Returns a new result with the aliases applied.
377    fn with_aliases(self: Box<Self>, aliases: FxHashMap<String, String>) -> Box<dyn QueryResult>;
378}
379
380/// Neutral result wrapper that changes column presentation only.
381pub struct AliasedResult {
382    inner: Box<dyn QueryResult>,
383    aliased_columns: Vec<String>,
384}
385
386impl AliasedResult {
387    pub fn new(inner: Box<dyn QueryResult>, aliases: FxHashMap<String, String>) -> Self {
388        let aliased_columns = inner
389            .columns()
390            .iter()
391            .map(|column| {
392                aliases
393                    .iter()
394                    .find(|(_, original)| *original == column)
395                    .map_or_else(|| column.clone(), |(alias, _)| alias.clone())
396            })
397            .collect();
398        Self {
399            inner,
400            aliased_columns,
401        }
402    }
403}
404
405impl QueryResult for AliasedResult {
406    fn columns(&self) -> &[String] {
407        &self.aliased_columns
408    }
409
410    fn next(&mut self) -> bool {
411        self.inner.next()
412    }
413
414    fn scan(&self, dest: &mut [Value]) -> Result<()> {
415        self.inner.scan(dest)
416    }
417
418    fn row(&self) -> &Row {
419        self.inner.row()
420    }
421
422    fn take_row(&mut self) -> Row {
423        self.inner.take_row()
424    }
425
426    fn take_deferred_row(&mut self) -> DeferredRow {
427        self.inner.take_deferred_row()
428    }
429
430    fn preserves_deferred_rows(&self) -> bool {
431        self.inner.preserves_deferred_rows()
432    }
433
434    fn ascending_nulls_last_ordering(&self) -> Option<Vec<usize>> {
435        self.inner.ascending_nulls_last_ordering()
436    }
437
438    fn close(&mut self) -> Result<()> {
439        self.inner.close()
440    }
441
442    fn rows_affected(&self) -> i64 {
443        self.inner.rows_affected()
444    }
445
446    fn last_insert_id(&self) -> i64 {
447        self.inner.last_insert_id()
448    }
449
450    fn last_error(&mut self) -> Option<radixdb_core::Error> {
451        self.inner.last_error()
452    }
453
454    fn with_aliases(self: Box<Self>, aliases: FxHashMap<String, String>) -> Box<dyn QueryResult> {
455        Box::new(Self::new(self, aliases))
456    }
457}
458
459/// Neutral query-result adapter backed directly by a storage scanner.
460///
461/// The adapter owns no SQL or executor policy. It only exposes the existing
462/// row/typed-batch scanner contract through [`QueryResult`].
463pub struct ScannerResult {
464    scanner: Box<dyn Scanner>,
465    columns: Vec<String>,
466    current_row: Row,
467    has_current: bool,
468}
469
470impl ScannerResult {
471    pub fn new(scanner: Box<dyn Scanner>, columns: Vec<String>) -> Self {
472        Self {
473            scanner,
474            columns,
475            current_row: Row::new(),
476            has_current: false,
477        }
478    }
479}
480
481impl QueryResult for ScannerResult {
482    fn columns(&self) -> &[String] {
483        &self.columns
484    }
485
486    fn next(&mut self) -> bool {
487        if self.scanner.next() {
488            self.current_row = self.scanner.take_row();
489            self.has_current = true;
490            true
491        } else {
492            self.has_current = false;
493            false
494        }
495    }
496
497    fn scan(&self, dest: &mut [Value]) -> Result<()> {
498        if !self.has_current {
499            return Err(radixdb_core::Error::internal(
500                "scan() called without successful next()",
501            ));
502        }
503        if dest.len() != self.current_row.len() {
504            return Err(radixdb_core::Error::internal(format!(
505                "scan destination has {} values but row has {} columns",
506                dest.len(),
507                self.current_row.len()
508            )));
509        }
510        for (dest, value) in dest.iter_mut().zip(self.current_row.iter()) {
511            *dest = value.clone();
512        }
513        Ok(())
514    }
515
516    fn row(&self) -> &Row {
517        assert!(self.has_current, "row() called without successful next()");
518        &self.current_row
519    }
520
521    fn take_row(&mut self) -> Row {
522        assert!(
523            self.has_current,
524            "take_row() called without successful next()"
525        );
526        std::mem::take(&mut self.current_row)
527    }
528
529    fn close(&mut self) -> Result<()> {
530        self.scanner.close()
531    }
532
533    fn last_error(&mut self) -> Option<radixdb_core::Error> {
534        self.scanner.err().cloned()
535    }
536
537    fn rows_affected(&self) -> i64 {
538        0
539    }
540
541    fn last_insert_id(&self) -> i64 {
542        0
543    }
544
545    fn estimated_count(&self) -> Option<usize> {
546        self.scanner.estimated_count()
547    }
548
549    fn supports_typed_batches(&self) -> bool {
550        !self.has_current && self.scanner.supports_typed_batches()
551    }
552
553    fn typed_batch_fallback_reason(&self) -> Option<TypedBatchFallbackReason> {
554        if self.supports_typed_batches() {
555            None
556        } else if self.has_current {
557            Some(TypedBatchFallbackReason::RowAlreadyFetched)
558        } else {
559            self.scanner.typed_batch_fallback_reason()
560        }
561    }
562
563    fn next_typed_batch(&mut self) -> Result<Option<TypedColumnBatch>> {
564        if self.has_current {
565            return Err(radixdb_core::Error::internal(
566                "typed batch requested after row-oriented scanner advance",
567            ));
568        }
569        self.scanner.next_typed_batch()
570    }
571
572    fn with_aliases(self: Box<Self>, aliases: FxHashMap<String, String>) -> Box<dyn QueryResult> {
573        Box::new(AliasedResult::new(self, aliases))
574    }
575}
576
577/// A simple in-memory query result (useful for testing and simple results)
578pub struct MemoryResult {
579    columns: Vec<String>,
580    rows: Vec<Row>,
581    current_index: Option<usize>,
582    rows_affected: i64,
583    last_insert_id: i64,
584    closed: bool,
585}
586
587impl MemoryResult {
588    /// Creates a new empty result with the given columns
589    pub fn new(columns: Vec<String>) -> Self {
590        Self {
591            columns,
592            rows: Vec::new(),
593            current_index: None,
594            rows_affected: 0,
595            last_insert_id: 0,
596            closed: false,
597        }
598    }
599
600    /// Creates a result with columns and rows
601    pub fn with_rows(columns: Vec<String>, rows: Vec<Row>) -> Self {
602        Self {
603            columns,
604            rows,
605            current_index: None,
606            rows_affected: 0,
607            last_insert_id: 0,
608            closed: false,
609        }
610    }
611
612    /// Creates a result for a modification operation (INSERT/UPDATE/DELETE)
613    pub fn for_modification(rows_affected: i64, last_insert_id: i64) -> Self {
614        Self {
615            columns: Vec::new(),
616            rows: Vec::new(),
617            current_index: None,
618            rows_affected,
619            last_insert_id,
620            closed: false,
621        }
622    }
623
624    /// Adds a row to the result
625    pub fn add_row(&mut self, row: Row) {
626        self.rows.push(row);
627    }
628
629    /// Sets the rows affected count
630    pub fn set_rows_affected(&mut self, count: i64) {
631        self.rows_affected = count;
632    }
633
634    /// Sets the last insert ID
635    pub fn set_last_insert_id(&mut self, id: i64) {
636        self.last_insert_id = id;
637    }
638}
639
640impl QueryResult for MemoryResult {
641    fn columns(&self) -> &[String] {
642        &self.columns
643    }
644
645    fn estimated_count(&self) -> Option<usize> {
646        Some(self.rows.len())
647    }
648
649    fn next(&mut self) -> bool {
650        if self.closed {
651            return false;
652        }
653
654        let next_index = match self.current_index {
655            None => 0,
656            Some(i) => i + 1,
657        };
658
659        if next_index < self.rows.len() {
660            self.current_index = Some(next_index);
661            true
662        } else {
663            false
664        }
665    }
666
667    fn scan(&self, dest: &mut [Value]) -> Result<()> {
668        let row = self.row();
669
670        if dest.len() != row.len() {
671            return Err(radixdb_core::Error::internal(format!(
672                "scan destination has {} values but row has {} columns",
673                dest.len(),
674                row.len()
675            )));
676        }
677
678        for (i, value) in row.iter().enumerate() {
679            dest[i] = value.clone();
680        }
681
682        Ok(())
683    }
684
685    fn row(&self) -> &Row {
686        match self.current_index {
687            Some(i) if i < self.rows.len() => &self.rows[i],
688            _ => panic!("row() called without successful next()"),
689        }
690    }
691
692    /// Optimized take_row that swaps out the row instead of cloning
693    fn take_row(&mut self) -> Row {
694        match self.current_index {
695            Some(i) if i < self.rows.len() => std::mem::take(&mut self.rows[i]),
696            _ => panic!("take_row() called without successful next()"),
697        }
698    }
699
700    fn close(&mut self) -> Result<()> {
701        self.closed = true;
702        Ok(())
703    }
704
705    fn rows_affected(&self) -> i64 {
706        self.rows_affected
707    }
708
709    fn last_insert_id(&self) -> i64 {
710        self.last_insert_id
711    }
712
713    fn with_aliases(
714        mut self: Box<Self>,
715        aliases: FxHashMap<String, String>,
716    ) -> Box<dyn QueryResult> {
717        // Apply aliases to column names
718        for col in &mut self.columns {
719            // Find if this column has an alias (reverse lookup)
720            for (alias, original) in &aliases {
721                if col == original {
722                    *col = alias.clone();
723                    break;
724                }
725            }
726        }
727        self
728    }
729}
730
731/// An empty result that returns no rows
732pub struct EmptyResult {
733    columns: Vec<String>,
734    rows_affected: i64,
735    last_insert_id: i64,
736}
737
738impl EmptyResult {
739    /// Creates a new empty result
740    pub fn new() -> Self {
741        Self {
742            columns: Vec::new(),
743            rows_affected: 0,
744            last_insert_id: 0,
745        }
746    }
747
748    /// Creates an empty result for a modification operation
749    pub fn for_modification(rows_affected: i64, last_insert_id: i64) -> Self {
750        Self {
751            columns: Vec::new(),
752            rows_affected,
753            last_insert_id,
754        }
755    }
756}
757
758impl Default for EmptyResult {
759    fn default() -> Self {
760        Self::new()
761    }
762}
763
764impl QueryResult for EmptyResult {
765    fn columns(&self) -> &[String] {
766        &self.columns
767    }
768
769    fn next(&mut self) -> bool {
770        false
771    }
772
773    fn scan(&self, _dest: &mut [Value]) -> Result<()> {
774        Err(radixdb_core::Error::internal(
775            "scan() called on empty result",
776        ))
777    }
778
779    fn row(&self) -> &Row {
780        panic!("row() called on empty result")
781    }
782
783    fn close(&mut self) -> Result<()> {
784        Ok(())
785    }
786
787    fn rows_affected(&self) -> i64 {
788        self.rows_affected
789    }
790
791    fn last_insert_id(&self) -> i64 {
792        self.last_insert_id
793    }
794
795    fn with_aliases(self: Box<Self>, _aliases: FxHashMap<String, String>) -> Box<dyn QueryResult> {
796        self
797    }
798}
799
800#[cfg(test)]
801mod tests {
802    use super::*;
803    use crate::traits::VecScanner;
804
805    #[test]
806    fn test_memory_result_empty() {
807        let mut result = MemoryResult::new(vec!["id".to_string(), "name".to_string()]);
808
809        assert_eq!(result.columns(), &["id", "name"]);
810        assert!(!result.next());
811        assert_eq!(result.rows_affected(), 0);
812        assert_eq!(result.last_insert_id(), 0);
813    }
814
815    #[test]
816    fn test_memory_result_with_rows() {
817        let rows = vec![
818            Row::from_values(vec![Value::Integer(1), Value::text("Alice")]),
819            Row::from_values(vec![Value::Integer(2), Value::text("Bob")]),
820        ];
821
822        let mut result = MemoryResult::with_rows(vec!["id".to_string(), "name".to_string()], rows);
823
824        assert!(result.next());
825        assert_eq!(result.row().get(0), Some(&Value::Integer(1)));
826
827        assert!(result.next());
828        assert_eq!(result.row().get(0), Some(&Value::Integer(2)));
829
830        assert!(!result.next());
831    }
832
833    #[test]
834    fn test_memory_result_scan() {
835        let rows = vec![Row::from_values(vec![
836            Value::Integer(42),
837            Value::text("test"),
838        ])];
839
840        let mut result = MemoryResult::with_rows(vec!["id".to_string(), "name".to_string()], rows);
841
842        assert!(result.next());
843
844        let mut dest = vec![Value::null_unknown(), Value::null_unknown()];
845        result.scan(&mut dest).unwrap();
846
847        assert_eq!(dest[0], Value::Integer(42));
848        assert_eq!(dest[1], Value::text("test"));
849    }
850
851    #[test]
852    fn test_memory_result_for_modification() {
853        let result = MemoryResult::for_modification(5, 100);
854
855        assert_eq!(result.rows_affected(), 5);
856        assert_eq!(result.last_insert_id(), 100);
857    }
858
859    #[test]
860    fn test_memory_result_close() {
861        let rows = vec![Row::from_values(vec![Value::Integer(1)])];
862        let mut result = MemoryResult::with_rows(vec!["id".to_string()], rows);
863
864        assert!(result.next());
865        assert!(result.close().is_ok());
866        assert!(!result.next()); // After close, next returns false
867    }
868
869    #[test]
870    fn test_memory_result_with_aliases() {
871        let rows = vec![Row::from_values(vec![Value::Integer(1)])];
872        let result = Box::new(MemoryResult::with_rows(vec!["user_id".to_string()], rows));
873
874        let mut aliases = FxHashMap::default();
875        aliases.insert("id".to_string(), "user_id".to_string());
876
877        let aliased = result.with_aliases(aliases);
878        assert_eq!(aliased.columns(), &["id"]);
879    }
880
881    #[test]
882    fn test_empty_result() {
883        let mut result = EmptyResult::new();
884
885        assert!(result.columns().is_empty());
886        assert!(!result.next());
887        assert_eq!(result.rows_affected(), 0);
888        assert!(result.close().is_ok());
889    }
890
891    #[test]
892    fn test_empty_result_for_modification() {
893        let result = EmptyResult::for_modification(10, 0);
894
895        assert_eq!(result.rows_affected(), 10);
896        assert_eq!(result.last_insert_id(), 0);
897    }
898
899    #[test]
900    fn scanner_result_streams_rows_and_preserves_alias_wrapper_contract() {
901        let scanner = VecScanner::new(vec![
902            Row::from_values(vec![Value::Integer(1), Value::text("one")]),
903            Row::from_values(vec![Value::Integer(2), Value::text("two")]),
904        ]);
905        let result: Box<dyn QueryResult> = Box::new(ScannerResult::new(
906            Box::new(scanner),
907            vec!["id".to_string(), "payload".to_string()],
908        ));
909        let mut aliases = FxHashMap::default();
910        aliases.insert("value".to_string(), "payload".to_string());
911        let mut result = result.with_aliases(aliases);
912
913        assert_eq!(result.columns(), &["id", "value"]);
914        assert!(result.next());
915        assert_eq!(result.take_row().get(0), Some(&Value::Integer(1)));
916        assert!(result.next());
917        assert_eq!(result.row().get(1), Some(&Value::text("two")));
918        assert!(!result.next());
919        assert!(result.last_error().is_none());
920        result.close().unwrap();
921    }
922
923    #[test]
924    fn scanner_result_surfaces_scanner_terminal_error() {
925        let scanner = VecScanner::with_error(radixdb_core::Error::internal("scanner terminal"));
926        let mut result = ScannerResult::new(Box::new(scanner), vec!["id".to_string()]);
927
928        assert!(!result.next());
929        assert!(result
930            .last_error()
931            .is_some_and(|error| error.to_string().contains("scanner terminal")));
932    }
933}