Skip to main content

radixdb_executor/
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//! Execution Result Types
16//!
17//! This module provides result types for SQL query execution.
18
19use crate::optimizer::workload::{global_workload_learner, QueryPattern};
20use radixdb_core::CompactArc;
21use radixdb_core::{Error, Result, Row, RowVec, Value};
22use radixdb_sql::ast::Expression;
23use radixdb_storage::traits::{
24    DeferredRow, QueryResult, TypedBatchFallbackReason, TypedColumnBatch,
25};
26use rustc_hash::{FxHashMap, FxHasher};
27use std::cell::OnceCell;
28use std::fs::{File, OpenOptions};
29use std::hash::{Hash, Hasher};
30use std::io::{BufReader, BufWriter, Read, Write};
31use std::path::PathBuf;
32use std::sync::atomic::{AtomicU64, Ordering as AtomicOrdering};
33use std::sync::Arc;
34
35use super::context::{CancellationHandle, TimeoutGuard};
36use super::expression::RowFilter;
37use super::operator::Operator;
38use crate::memory::RetainedRowsBudget;
39
40/// Internal streaming output of SQL execution.
41///
42/// The public embedded cursor adapts this value at the API boundary; API row
43/// and cursor types never implement the lower-level execution contract.
44pub type ExecutionResult = Box<dyn QueryResult>;
45
46// Compatibility path for existing executor consumers. The implementation is
47// storage-owned because it adapts only Scanner to QueryResult.
48pub use radixdb_storage::traits::{AliasedResult, ScannerResult};
49
50/// Keeps a statement timeout registered until the returned cursor is closed
51/// or dropped. Planning a streaming result is not query completion: rows may
52/// still execute storage and operator work while the caller consumes them.
53#[doc(hidden)]
54pub struct TimedQueryResult {
55    inner: Box<dyn QueryResult>,
56    timeout_guard: Option<TimeoutGuard>,
57    cancellation: CancellationHandle,
58    cancelled_error_returned: bool,
59    workload: Option<WorkloadObservation>,
60    workload_started_at: radixdb_core::time_compat::Instant,
61    workload_rows: u64,
62}
63
64#[derive(Clone, Copy)]
65struct WorkloadObservation {
66    fingerprint: u64,
67    pattern: QueryPattern,
68}
69
70impl TimedQueryResult {
71    #[cfg(test)]
72    pub fn wrap(
73        inner: ExecutionResult,
74        timeout_guard: Option<TimeoutGuard>,
75        cancellation: CancellationHandle,
76    ) -> ExecutionResult {
77        Box::new(Self {
78            inner,
79            timeout_guard,
80            cancellation,
81            cancelled_error_returned: false,
82            workload: None,
83            workload_started_at: radixdb_core::time_compat::Instant::now(),
84            workload_rows: 0,
85        })
86    }
87
88    pub fn wrap_with_workload(
89        inner: ExecutionResult,
90        timeout_guard: Option<TimeoutGuard>,
91        cancellation: CancellationHandle,
92        sql: &str,
93    ) -> ExecutionResult {
94        let upper = sql.to_ascii_uppercase();
95        let pattern = if upper.trim_start().starts_with("INSERT") {
96            QueryPattern::InsertHeavy
97        } else if upper.trim_start().starts_with("UPDATE")
98            || upper.trim_start().starts_with("DELETE")
99        {
100            QueryPattern::UpdateHeavy
101        } else if upper.contains(" JOIN ") {
102            QueryPattern::JoinHeavy
103        } else if ["COUNT(", "SUM(", "AVG(", "MIN(", "MAX(", "GROUP BY"]
104            .iter()
105            .any(|needle| upper.contains(needle))
106        {
107            QueryPattern::Aggregation
108        } else if upper.trim_start().starts_with("SELECT") {
109            QueryPattern::FullScan
110        } else {
111            QueryPattern::Unknown
112        };
113        let mut hasher = FxHasher::default();
114        sql.hash(&mut hasher);
115
116        Box::new(Self {
117            inner,
118            timeout_guard,
119            cancellation,
120            cancelled_error_returned: false,
121            workload: Some(WorkloadObservation {
122                fingerprint: hasher.finish(),
123                pattern,
124            }),
125            workload_started_at: radixdb_core::time_compat::Instant::now(),
126            workload_rows: 0,
127        })
128    }
129
130    fn finish_workload_observation(&mut self) {
131        let Some(observation) = self.workload.take() else {
132            return;
133        };
134        let affected = self.inner.rows_affected().max(0) as u64;
135        let rows = self.workload_rows.max(affected);
136        global_workload_learner().record_query(
137            observation.fingerprint,
138            observation.pattern,
139            self.workload_started_at.elapsed(),
140            0,
141            rows,
142            rows,
143            Vec::new(),
144            Vec::new(),
145            Vec::new(),
146        );
147    }
148}
149
150impl QueryResult for TimedQueryResult {
151    fn columns(&self) -> &[String] {
152        self.inner.columns()
153    }
154
155    fn columns_arc(&self) -> Option<CompactArc<Vec<String>>> {
156        self.inner.columns_arc()
157    }
158
159    fn next(&mut self) -> bool {
160        if self.cancellation.is_cancelled() {
161            return false;
162        }
163        let has_row = self.inner.next();
164        if has_row {
165            self.workload_rows = self.workload_rows.saturating_add(1);
166        } else {
167            self.finish_workload_observation();
168        }
169        has_row
170    }
171
172    fn scan(&self, dest: &mut [Value]) -> Result<()> {
173        self.inner.scan(dest)
174    }
175
176    fn row(&self) -> &Row {
177        self.inner.row()
178    }
179
180    fn take_row(&mut self) -> Row {
181        self.inner.take_row()
182    }
183
184    fn take_deferred_row(&mut self) -> DeferredRow {
185        self.inner.take_deferred_row()
186    }
187
188    fn preserves_deferred_rows(&self) -> bool {
189        self.inner.preserves_deferred_rows()
190    }
191
192    fn close(&mut self) -> Result<()> {
193        let result = self.inner.close();
194        self.finish_workload_observation();
195        self.timeout_guard.take();
196        result
197    }
198
199    fn rows_affected(&self) -> i64 {
200        self.inner.rows_affected()
201    }
202
203    fn last_insert_id(&self) -> i64 {
204        self.inner.last_insert_id()
205    }
206
207    fn try_into_arc_rows(&mut self) -> Option<CompactArc<Vec<Row>>> {
208        let rows = self.inner.try_into_arc_rows();
209        if let Some(rows) = rows.as_ref() {
210            self.workload_rows = self.workload_rows.saturating_add(rows.len() as u64);
211            self.finish_workload_observation();
212        }
213        rows
214    }
215
216    fn estimated_count(&self) -> Option<usize> {
217        self.inner.estimated_count()
218    }
219
220    fn supports_typed_batches(&self) -> bool {
221        self.inner.supports_typed_batches()
222    }
223
224    fn typed_batch_fallback_reason(&self) -> Option<TypedBatchFallbackReason> {
225        self.inner.typed_batch_fallback_reason()
226    }
227
228    fn next_typed_batch(&mut self) -> Result<Option<TypedColumnBatch>> {
229        if self.cancellation.is_cancelled() {
230            return Err(radixdb_core::Error::QueryCancelled);
231        }
232        let batch = self.inner.next_typed_batch()?;
233        if let Some(batch) = batch.as_ref() {
234            self.workload_rows = self.workload_rows.saturating_add(batch.row_count() as u64);
235        } else {
236            self.finish_workload_observation();
237        }
238        Ok(batch)
239    }
240
241    fn last_error(&mut self) -> Option<radixdb_core::Error> {
242        if self.cancellation.is_cancelled() && !self.cancelled_error_returned {
243            self.cancelled_error_returned = true;
244            Some(radixdb_core::Error::QueryCancelled)
245        } else {
246            self.inner.last_error()
247        }
248    }
249
250    fn with_aliases(self: Box<Self>, aliases: FxHashMap<String, String>) -> Box<dyn QueryResult> {
251        let Self {
252            inner,
253            timeout_guard,
254            cancellation,
255            cancelled_error_returned,
256            workload,
257            workload_started_at,
258            workload_rows,
259        } = *self;
260        Box::new(Self {
261            inner: inner.with_aliases(aliases),
262            timeout_guard,
263            cancellation,
264            cancelled_error_returned,
265            workload,
266            workload_started_at,
267            workload_rows,
268        })
269    }
270}
271
272/// Execution result for DML operations (INSERT, UPDATE, DELETE)
273///
274/// This result type tracks the number of rows affected and the last insert ID
275/// for auto-increment columns.
276///
277/// OPTIMIZATION: Uses static empty values for columns and empty_row to avoid
278/// allocations on every DML operation (was causing 40K+ allocations per benchmark).
279pub struct ExecResult {
280    /// Number of rows affected
281    affected: i64,
282    /// Last insert ID (for auto-increment)
283    insert_id: i64,
284}
285
286/// Static empty columns for ExecResult (avoids Vec allocation)
287static EMPTY_COLUMNS: &[String] = &[];
288
289/// Static empty row for ExecResult (avoids Row allocation)
290static EMPTY_ROW: std::sync::OnceLock<Row> = std::sync::OnceLock::new();
291
292#[inline]
293fn get_empty_row() -> &'static Row {
294    EMPTY_ROW.get_or_init(Row::new)
295}
296
297impl ExecResult {
298    /// Create a new execution result
299    #[inline]
300    pub fn new(rows_affected: i64, last_insert_id: i64) -> Self {
301        Self {
302            affected: rows_affected,
303            insert_id: last_insert_id,
304        }
305    }
306
307    /// Create an empty result (for DDL statements)
308    #[inline]
309    pub fn empty() -> Self {
310        Self::new(0, 0)
311    }
312
313    /// Create a result with just rows affected
314    #[inline]
315    pub fn with_rows_affected(rows_affected: i64) -> Self {
316        Self::new(rows_affected, 0)
317    }
318
319    /// Create a result with rows affected and last insert ID
320    #[inline]
321    pub fn with_last_insert_id(rows_affected: i64, last_insert_id: i64) -> Self {
322        Self::new(rows_affected, last_insert_id)
323    }
324}
325
326impl QueryResult for ExecResult {
327    fn columns(&self) -> &[String] {
328        EMPTY_COLUMNS
329    }
330
331    fn next(&mut self) -> bool {
332        // DML results have no rows
333        false
334    }
335
336    fn scan(&self, _dest: &mut [Value]) -> Result<()> {
337        Err(radixdb_core::Error::internal(
338            "scan() called on exec result",
339        ))
340    }
341
342    fn row(&self) -> &Row {
343        get_empty_row()
344    }
345
346    fn close(&mut self) -> Result<()> {
347        Ok(())
348    }
349
350    fn rows_affected(&self) -> i64 {
351        self.affected
352    }
353
354    fn last_insert_id(&self) -> i64 {
355        self.insert_id
356    }
357
358    fn with_aliases(self: Box<Self>, _aliases: FxHashMap<String, String>) -> Box<dyn QueryResult> {
359        self
360    }
361}
362
363/// Memory-based result for SELECT queries
364///
365/// This result type stores all rows in memory, suitable for
366/// small to medium result sets.
367/// Storage for result rows - either owned (pooled) or shared via Arc
368enum RowStorage {
369    /// Owned rows - pooled, returns to thread-local cache on drop
370    Owned(RowVec),
371    /// Shared rows from cache - read-only, clone on take
372    Shared(CompactArc<Vec<Row>>),
373}
374
375impl RowStorage {
376    #[inline]
377    fn len(&self) -> usize {
378        match self {
379            RowStorage::Owned(rv) => rv.len(),
380            RowStorage::Shared(rows) => rows.len(),
381        }
382    }
383
384    #[inline]
385    fn get(&self, index: usize) -> Option<&Row> {
386        match self {
387            RowStorage::Owned(rv) => rv.get(index).map(|(_, row)| row),
388            RowStorage::Shared(rows) => rows.get(index),
389        }
390    }
391
392    #[inline]
393    fn take(&mut self, index: usize) -> Row {
394        match self {
395            RowStorage::Owned(rv) => std::mem::take(&mut rv[index].1),
396            // For shared storage, we must clone since we can't take ownership
397            RowStorage::Shared(rows) => rows[index].clone(),
398        }
399    }
400}
401
402pub struct ExecutorResult {
403    /// Column names (Arc for zero-copy sharing with API layer)
404    columns: CompactArc<Vec<String>>,
405    /// Result rows - either owned or shared
406    rows: RowStorage,
407    /// Cached row count to avoid repeated match in next()
408    len: usize,
409    /// Current row index (None before first next())
410    current_index: Option<usize>,
411    /// Whether the result is closed
412    closed: bool,
413    /// Rows affected (0 for SELECT)
414    affected: i64,
415    /// Last insert ID (0 for SELECT)
416    insert_id: i64,
417}
418
419/// Internal result that carries compact JOIN rows to the next recursive edge.
420///
421/// Public consumers keep the ordinary QueryResult contract: `row()` and
422/// `take_row()` materialize at most once. QueryResultOperator instead consumes
423/// `take_deferred_row()` and preserves the row graph without cloning payload
424/// columns between binary JOIN nodes.
425#[doc(hidden)]
426pub struct DeferredExecutorResult {
427    columns: CompactArc<Vec<String>>,
428    rows: Vec<Option<DeferredRow>>,
429    current_index: Option<usize>,
430    current_materialized: OnceCell<Row>,
431    closed: bool,
432}
433
434/// Attach a physical ordering certificate to a result without changing rows.
435///
436/// Construction is restricted to executor paths that obtained rows from an
437/// ordered storage/index API. The wrapper deliberately performs no runtime
438/// sortedness check.
439#[doc(hidden)]
440pub struct CertifiedOrderedResult {
441    inner: Box<dyn QueryResult>,
442    ordering: Vec<usize>,
443}
444
445impl CertifiedOrderedResult {
446    pub fn ascending_nulls_last(inner: Box<dyn QueryResult>, ordering: Vec<usize>) -> Self {
447        Self { inner, ordering }
448    }
449}
450
451impl QueryResult for CertifiedOrderedResult {
452    fn columns(&self) -> &[String] {
453        self.inner.columns()
454    }
455
456    fn columns_arc(&self) -> Option<CompactArc<Vec<String>>> {
457        self.inner.columns_arc()
458    }
459
460    fn next(&mut self) -> bool {
461        self.inner.next()
462    }
463
464    fn scan(&self, dest: &mut [Value]) -> Result<()> {
465        self.inner.scan(dest)
466    }
467
468    fn row(&self) -> &Row {
469        self.inner.row()
470    }
471
472    fn take_row(&mut self) -> Row {
473        self.inner.take_row()
474    }
475
476    fn take_deferred_row(&mut self) -> DeferredRow {
477        self.inner.take_deferred_row()
478    }
479
480    fn preserves_deferred_rows(&self) -> bool {
481        self.inner.preserves_deferred_rows()
482    }
483
484    fn ascending_nulls_last_ordering(&self) -> Option<Vec<usize>> {
485        Some(self.ordering.clone())
486    }
487
488    fn close(&mut self) -> Result<()> {
489        self.inner.close()
490    }
491
492    fn rows_affected(&self) -> i64 {
493        self.inner.rows_affected()
494    }
495
496    fn last_insert_id(&self) -> i64 {
497        self.inner.last_insert_id()
498    }
499
500    fn last_error(&mut self) -> Option<radixdb_core::Error> {
501        self.inner.last_error()
502    }
503
504    fn estimated_count(&self) -> Option<usize> {
505        self.inner.estimated_count()
506    }
507
508    fn with_aliases(self: Box<Self>, aliases: FxHashMap<String, String>) -> Box<dyn QueryResult> {
509        let Self { inner, ordering } = *self;
510        Box::new(Self {
511            inner: inner.with_aliases(aliases),
512            ordering,
513        })
514    }
515}
516
517impl DeferredExecutorResult {
518    pub fn with_arc_columns(columns: CompactArc<Vec<String>>, rows: Vec<DeferredRow>) -> Self {
519        radixdb_storage::instrumentation::record_join_deferred_boundary_rows(rows.len() as u64);
520        Self {
521            columns,
522            rows: rows.into_iter().map(Some).collect(),
523            current_index: None,
524            current_materialized: OnceCell::new(),
525            closed: false,
526        }
527    }
528
529    fn current_deferred(&self) -> &DeferredRow {
530        let index = self
531            .current_index
532            .expect("row access without successful next()");
533        self.rows[index]
534            .as_ref()
535            .expect("row already consumed from deferred result")
536    }
537}
538
539impl QueryResult for DeferredExecutorResult {
540    fn columns(&self) -> &[String] {
541        &self.columns
542    }
543
544    fn columns_arc(&self) -> Option<CompactArc<Vec<String>>> {
545        Some(CompactArc::clone(&self.columns))
546    }
547
548    fn next(&mut self) -> bool {
549        if self.closed {
550            return false;
551        }
552        self.current_materialized = OnceCell::new();
553        let next_index = self.current_index.map_or(0, |index| index + 1);
554        if next_index < self.rows.len() {
555            self.current_index = Some(next_index);
556            true
557        } else {
558            false
559        }
560    }
561
562    fn scan(&self, dest: &mut [Value]) -> Result<()> {
563        let row = self.row();
564        if dest.len() != row.len() {
565            return Err(radixdb_core::Error::internal(format!(
566                "scan destination has {} values but row has {} columns",
567                dest.len(),
568                row.len()
569            )));
570        }
571        dest.clone_from_slice(row.as_slice());
572        Ok(())
573    }
574
575    fn row(&self) -> &Row {
576        self.current_materialized
577            .get_or_init(|| self.current_deferred().to_owned())
578    }
579
580    fn take_row(&mut self) -> Row {
581        let index = self
582            .current_index
583            .expect("take_row() called without successful next()");
584        let deferred = self.rows[index]
585            .take()
586            .expect("take_row() called after current row was consumed");
587        self.current_materialized
588            .take()
589            .unwrap_or_else(|| deferred.into_owned())
590    }
591
592    fn take_deferred_row(&mut self) -> DeferredRow {
593        let index = self
594            .current_index
595            .expect("take_deferred_row() called without successful next()");
596        let deferred = self.rows[index]
597            .take()
598            .expect("take_deferred_row() called after current row was consumed");
599        self.current_materialized
600            .take()
601            .map_or(deferred, DeferredRow::owned)
602    }
603
604    fn preserves_deferred_rows(&self) -> bool {
605        true
606    }
607
608    fn close(&mut self) -> Result<()> {
609        self.closed = true;
610        Ok(())
611    }
612
613    fn rows_affected(&self) -> i64 {
614        0
615    }
616
617    fn last_insert_id(&self) -> i64 {
618        0
619    }
620
621    fn estimated_count(&self) -> Option<usize> {
622        if self.closed {
623            return Some(0);
624        }
625        let consumed = self
626            .current_index
627            .map_or(0, |index| index.saturating_add(1));
628        Some(self.rows.len().saturating_sub(consumed))
629    }
630
631    fn with_aliases(
632        mut self: Box<Self>,
633        aliases: FxHashMap<String, String>,
634    ) -> Box<dyn QueryResult> {
635        let columns = CompactArc::make_mut(&mut self.columns);
636        for column in columns {
637            if let Some((alias, _)) = aliases.iter().find(|(_, original)| *original == column) {
638                *column = alias.clone();
639            }
640        }
641        self
642    }
643}
644
645/// Internal cursor backed directly by an opened Volcano operator.
646///
647/// It retains only the current deferred row. Recursive JOIN edges can wrap it
648/// in `QueryResultOperator`, so the downstream edge pulls upstream work with
649/// natural backpressure instead of crossing a `Vec<DeferredRow>` boundary.
650#[doc(hidden)]
651pub struct OperatorExecutorResult {
652    columns: CompactArc<Vec<String>>,
653    operator: Box<dyn Operator>,
654    cancellation: CancellationHandle,
655    current: Option<DeferredRow>,
656    current_materialized: OnceCell<Row>,
657    pending_error: Option<radixdb_core::Error>,
658    estimated_rows: Option<usize>,
659    emitted_rows: usize,
660    remaining_limit: Option<usize>,
661    ordering: Option<Vec<usize>>,
662    closed: bool,
663}
664
665impl OperatorExecutorResult {
666    pub fn open(
667        columns: CompactArc<Vec<String>>,
668        mut operator: Box<dyn Operator>,
669        cancellation: CancellationHandle,
670        limit: Option<u64>,
671    ) -> Result<Self> {
672        let estimated_rows = operator.estimated_rows().map(|rows| {
673            limit.map_or(rows, |limit| {
674                rows.min(usize::try_from(limit).unwrap_or(usize::MAX))
675            })
676        });
677        let ordering = match operator.ordering() {
678            super::operator::OrderingProperty::AscendingNullsLast(keys) => Some(keys),
679            super::operator::OrderingProperty::Unknown => None,
680        };
681        if let Err(error) = operator.open() {
682            let _ = operator.close();
683            return Err(error);
684        }
685        Ok(Self {
686            columns,
687            operator,
688            cancellation,
689            current: None,
690            current_materialized: OnceCell::new(),
691            pending_error: None,
692            estimated_rows,
693            emitted_rows: 0,
694            remaining_limit: limit.map(|limit| usize::try_from(limit).unwrap_or(usize::MAX)),
695            ordering,
696            closed: false,
697        })
698    }
699
700    fn finish(&mut self) -> Result<()> {
701        if self.closed {
702            return Ok(());
703        }
704        self.closed = true;
705        self.operator.close()
706    }
707
708    fn fail(&mut self, error: radixdb_core::Error) -> bool {
709        self.pending_error = Some(error);
710        if let Err(close_error) = self.finish() {
711            if self.pending_error.is_none() {
712                self.pending_error = Some(close_error);
713            }
714        }
715        false
716    }
717
718    fn current_deferred(&self) -> &DeferredRow {
719        self.current
720            .as_ref()
721            .expect("row access without successful next()")
722    }
723}
724
725impl Drop for OperatorExecutorResult {
726    fn drop(&mut self) {
727        let _ = self.finish();
728    }
729}
730
731impl QueryResult for OperatorExecutorResult {
732    fn columns(&self) -> &[String] {
733        &self.columns
734    }
735
736    fn columns_arc(&self) -> Option<CompactArc<Vec<String>>> {
737        Some(CompactArc::clone(&self.columns))
738    }
739
740    fn next(&mut self) -> bool {
741        if self.closed || self.pending_error.is_some() {
742            return false;
743        }
744        self.current = None;
745        self.current_materialized = OnceCell::new();
746        if self.remaining_limit == Some(0) {
747            return match self.finish() {
748                Ok(()) => false,
749                Err(error) => self.fail(error),
750            };
751        }
752        if self.cancellation.is_cancelled() {
753            return self.fail(radixdb_core::Error::QueryCancelled);
754        }
755
756        match self.operator.next() {
757            Ok(Some(row)) => {
758                self.current = Some(row.into_deferred());
759                self.emitted_rows = self.emitted_rows.saturating_add(1);
760                if let Some(remaining) = self.remaining_limit.as_mut() {
761                    *remaining = remaining.saturating_sub(1);
762                }
763                true
764            }
765            Ok(None) => match self.finish() {
766                Ok(()) => false,
767                Err(error) => self.fail(error),
768            },
769            Err(error) => self.fail(error),
770        }
771    }
772
773    fn scan(&self, dest: &mut [Value]) -> Result<()> {
774        let row = self.row();
775        if dest.len() != row.len() {
776            return Err(radixdb_core::Error::internal(format!(
777                "scan destination has {} values but row has {} columns",
778                dest.len(),
779                row.len()
780            )));
781        }
782        dest.clone_from_slice(row.as_slice());
783        Ok(())
784    }
785
786    fn row(&self) -> &Row {
787        self.current_materialized
788            .get_or_init(|| self.current_deferred().to_owned())
789    }
790
791    fn take_row(&mut self) -> Row {
792        let deferred = self
793            .current
794            .take()
795            .expect("take_row() called without successful next()");
796        self.current_materialized
797            .take()
798            .unwrap_or_else(|| deferred.into_owned())
799    }
800
801    fn take_deferred_row(&mut self) -> DeferredRow {
802        let deferred = self
803            .current
804            .take()
805            .expect("take_deferred_row() called without successful next()");
806        self.current_materialized
807            .take()
808            .map_or(deferred, DeferredRow::owned)
809    }
810
811    fn preserves_deferred_rows(&self) -> bool {
812        true
813    }
814
815    fn ascending_nulls_last_ordering(&self) -> Option<Vec<usize>> {
816        self.ordering.clone()
817    }
818
819    fn close(&mut self) -> Result<()> {
820        self.current = None;
821        self.current_materialized = OnceCell::new();
822        self.finish()
823    }
824
825    fn rows_affected(&self) -> i64 {
826        0
827    }
828
829    fn last_insert_id(&self) -> i64 {
830        0
831    }
832
833    fn estimated_count(&self) -> Option<usize> {
834        if self.closed {
835            return Some(0);
836        }
837        self.estimated_rows
838            .map(|rows| rows.saturating_sub(self.emitted_rows))
839    }
840
841    fn last_error(&mut self) -> Option<radixdb_core::Error> {
842        self.pending_error.take()
843    }
844
845    fn with_aliases(self: Box<Self>, aliases: FxHashMap<String, String>) -> Box<dyn QueryResult> {
846        Box::new(AliasedResult::new(self, aliases))
847    }
848}
849
850/// Query result backed by a fallible row iterator.
851///
852/// This is the common backpressure bridge for producers such as table-valued
853/// functions: construction retains no rows and each `next()` owns at most the
854/// current row.
855#[doc(hidden)]
856pub struct StreamingRowsResult {
857    columns: Vec<String>,
858    rows: Box<dyn Iterator<Item = Result<(i64, Row)>> + Send>,
859    current: Option<Row>,
860    pending_error: Option<radixdb_core::Error>,
861}
862
863impl StreamingRowsResult {
864    pub fn new(
865        columns: Vec<String>,
866        rows: Box<dyn Iterator<Item = Result<(i64, Row)>> + Send>,
867    ) -> Self {
868        Self {
869            columns,
870            rows,
871            current: None,
872            pending_error: None,
873        }
874    }
875}
876
877impl QueryResult for StreamingRowsResult {
878    fn columns(&self) -> &[String] {
879        &self.columns
880    }
881
882    fn next(&mut self) -> bool {
883        self.current = None;
884        match self.rows.next() {
885            Some(Ok((_, row))) => {
886                self.current = Some(row);
887                true
888            }
889            Some(Err(error)) => {
890                self.pending_error = Some(error);
891                false
892            }
893            None => false,
894        }
895    }
896
897    fn scan(&self, dest: &mut [Value]) -> Result<()> {
898        let row = self.row();
899        if dest.len() != row.len() {
900            return Err(radixdb_core::Error::internal(format!(
901                "scan destination has {} values but row has {} columns",
902                dest.len(),
903                row.len()
904            )));
905        }
906        dest.clone_from_slice(row.as_slice());
907        Ok(())
908    }
909
910    fn row(&self) -> &Row {
911        self.current
912            .as_ref()
913            .expect("row() called without successful next()")
914    }
915
916    fn rows_affected(&self) -> i64 {
917        0
918    }
919
920    fn last_insert_id(&self) -> i64 {
921        0
922    }
923
924    fn take_row(&mut self) -> Row {
925        self.current
926            .take()
927            .expect("take_row() called without successful next()")
928    }
929
930    fn last_error(&mut self) -> Option<radixdb_core::Error> {
931        self.pending_error.take()
932    }
933
934    fn with_aliases(self: Box<Self>, aliases: FxHashMap<String, String>) -> Box<dyn QueryResult> {
935        Box::new(AliasedResult::new(self, aliases))
936    }
937}
938
939impl ExecutorResult {
940    /// Create a new memory result with columns and pooled rows
941    pub fn new(columns: Vec<String>, rows: RowVec) -> Self {
942        let len = rows.len();
943        Self {
944            columns: CompactArc::new(columns),
945            rows: RowStorage::Owned(rows),
946            len,
947            current_index: None,
948            closed: false,
949            affected: 0,
950            insert_id: 0,
951        }
952    }
953
954    /// Create a new memory result with Arc columns (zero-copy)
955    pub fn with_arc_columns(columns: CompactArc<Vec<String>>, rows: RowVec) -> Self {
956        let len = rows.len();
957        Self {
958            columns,
959            rows: RowStorage::Owned(rows),
960            len,
961            current_index: None,
962            closed: false,
963            affected: 0,
964            insert_id: 0,
965        }
966    }
967
968    /// Create a new memory result with shared rows from cache (zero-copy for rows)
969    /// This avoids cloning the entire `Vec<Row>` when reading from semantic cache
970    pub fn with_shared_rows(columns: Vec<String>, rows: CompactArc<Vec<Row>>) -> Self {
971        let len = rows.len();
972        Self {
973            columns: CompactArc::new(columns),
974            rows: RowStorage::Shared(rows),
975            len,
976            current_index: None,
977            closed: false,
978            affected: 0,
979            insert_id: 0,
980        }
981    }
982
983    /// Create a new memory result with Arc columns and shared rows (zero-copy for both)
984    pub fn with_arc_columns_shared_rows(
985        columns: CompactArc<Vec<String>>,
986        rows: CompactArc<Vec<Row>>,
987    ) -> Self {
988        let len = rows.len();
989        Self {
990            columns,
991            rows: RowStorage::Shared(rows),
992            len,
993            current_index: None,
994            closed: false,
995            affected: 0,
996            insert_id: 0,
997        }
998    }
999
1000    /// Create an empty memory result
1001    pub fn empty() -> Self {
1002        Self::new(Vec::new(), RowVec::new())
1003    }
1004
1005    /// Create with columns only (no rows yet)
1006    pub fn with_columns(columns: Vec<String>) -> Self {
1007        Self::new(columns, RowVec::new())
1008    }
1009
1010    /// Add a row to the result
1011    pub fn add_row(&mut self, row: Row) {
1012        // Convert from shared to owned if needed
1013        let shared_rows = match &self.rows {
1014            RowStorage::Owned(_) => None,
1015            RowStorage::Shared(arc_rows) => {
1016                let mut rows = RowVec::with_capacity(arc_rows.len() + 1);
1017                for (index, row) in arc_rows.iter().enumerate() {
1018                    rows.push((index as i64, row.clone()));
1019                }
1020                Some(rows)
1021            }
1022        };
1023        if let Some(rows) = shared_rows {
1024            self.rows = RowStorage::Owned(rows);
1025        }
1026        if let RowStorage::Owned(rv) = &mut self.rows {
1027            rv.push((self.len as i64, row));
1028            self.len += 1;
1029        }
1030    }
1031
1032    /// Get the number of rows
1033    #[inline]
1034    pub fn row_count(&self) -> usize {
1035        self.len
1036    }
1037
1038    /// Get row by index
1039    #[inline]
1040    pub fn get_row(&self, index: usize) -> Option<&Row> {
1041        self.rows.get(index)
1042    }
1043
1044    /// Take ownership of all rows (extracts Row from RowVec)
1045    pub fn into_rows(self) -> Vec<Row> {
1046        match self.rows {
1047            RowStorage::Owned(mut rv) => rv.drain_rows().collect(),
1048            RowStorage::Shared(rows) => {
1049                // Must clone if shared
1050                CompactArc::try_unwrap(rows).unwrap_or_else(|arc| (*arc).clone())
1051            }
1052        }
1053    }
1054
1055    /// Take rows as CompactArc for zero-copy sharing with joins
1056    /// Returns `CompactArc<Vec<Row>>` - wraps owned rows or clones `CompactArc` for shared
1057    pub fn into_arc_rows(self) -> CompactArc<Vec<Row>> {
1058        match self.rows {
1059            RowStorage::Owned(mut rv) => CompactArc::new(rv.drain_rows().collect()),
1060            RowStorage::Shared(rows) => rows,
1061        }
1062    }
1063
1064    /// Reset the cursor to the beginning
1065    pub fn reset(&mut self) {
1066        self.current_index = None;
1067    }
1068
1069    /// Set rows affected (for modification results)
1070    pub fn set_rows_affected(&mut self, count: i64) {
1071        self.affected = count;
1072    }
1073
1074    /// Set last insert ID
1075    pub fn set_last_insert_id(&mut self, id: i64) {
1076        self.insert_id = id;
1077    }
1078}
1079
1080impl QueryResult for ExecutorResult {
1081    fn columns(&self) -> &[String] {
1082        &self.columns
1083    }
1084
1085    fn columns_arc(&self) -> Option<CompactArc<Vec<String>>> {
1086        Some(CompactArc::clone(&self.columns))
1087    }
1088
1089    #[inline]
1090    fn next(&mut self) -> bool {
1091        if self.closed {
1092            return false;
1093        }
1094
1095        let next_index = match self.current_index {
1096            None => 0,
1097            Some(i) => i + 1,
1098        };
1099
1100        if next_index < self.len {
1101            self.current_index = Some(next_index);
1102            true
1103        } else {
1104            false
1105        }
1106    }
1107
1108    fn scan(&self, dest: &mut [Value]) -> Result<()> {
1109        let row = self.row();
1110
1111        if dest.len() != row.len() {
1112            return Err(radixdb_core::Error::internal(format!(
1113                "scan destination has {} values but row has {} columns",
1114                dest.len(),
1115                row.len()
1116            )));
1117        }
1118
1119        for (i, value) in row.iter().enumerate() {
1120            dest[i] = value.clone();
1121        }
1122
1123        Ok(())
1124    }
1125
1126    fn row(&self) -> &Row {
1127        match self.current_index {
1128            Some(i) => self
1129                .rows
1130                .get(i)
1131                .expect("row() called without successful next()"),
1132            _ => panic!("row() called without successful next()"),
1133        }
1134    }
1135
1136    fn take_row(&mut self) -> Row {
1137        match self.current_index {
1138            Some(i) if i < self.rows.len() => self.rows.take(i),
1139            _ => panic!("take_row() called without successful next()"),
1140        }
1141    }
1142
1143    fn close(&mut self) -> Result<()> {
1144        self.closed = true;
1145        Ok(())
1146    }
1147
1148    fn rows_affected(&self) -> i64 {
1149        self.affected
1150    }
1151
1152    fn last_insert_id(&self) -> i64 {
1153        self.insert_id
1154    }
1155
1156    fn try_into_arc_rows(&mut self) -> Option<CompactArc<Vec<Row>>> {
1157        // Take ownership of rows and return as Arc
1158        let rows = std::mem::replace(&mut self.rows, RowStorage::Owned(RowVec::new()));
1159        self.closed = true; // Mark as consumed
1160        match rows {
1161            RowStorage::Owned(mut rv) => Some(CompactArc::new(rv.drain_rows().collect())),
1162            RowStorage::Shared(arc) => Some(arc),
1163        }
1164    }
1165
1166    fn estimated_count(&self) -> Option<usize> {
1167        if self.closed {
1168            return Some(0);
1169        }
1170        let consumed = self
1171            .current_index
1172            .map_or(0, |index| index.saturating_add(1));
1173        Some(self.len.saturating_sub(consumed))
1174    }
1175
1176    fn with_aliases(
1177        mut self: Box<Self>,
1178        aliases: FxHashMap<String, String>,
1179    ) -> Box<dyn QueryResult> {
1180        // Apply aliases to column names (use CompactArc::make_mut for copy-on-write)
1181        let columns = CompactArc::make_mut(&mut self.columns);
1182        for col in columns {
1183            // Find if this column has an alias (reverse lookup)
1184            for (alias, original) in &aliases {
1185                if col == original {
1186                    *col = alias.clone();
1187                    break;
1188                }
1189            }
1190        }
1191        self
1192    }
1193}
1194
1195/// Filtered result that applies a WHERE clause to an underlying result
1196///
1197/// This struct owns a pre-compiled RowFilter, avoiding per-row compilation.
1198/// The filter is compiled once during construction and reused for every row.
1199pub struct FilteredResult {
1200    /// Underlying result
1201    inner: Box<dyn QueryResult>,
1202    /// Pre-compiled row filter (thread-safe, reusable)
1203    filter: RowFilter,
1204    /// Current row (cached after filter passes)
1205    current_row: Option<Row>,
1206    /// Columns cached
1207    columns: Vec<String>,
1208    /// Pending error from filter evaluation (e.g. invalid REGEXP pattern)
1209    pending_error: Option<radixdb_core::Error>,
1210}
1211
1212/// Streaming post-operator filter that preserves deferred JOIN rows.
1213///
1214/// Unlike [`FilteredResult`], this wrapper evaluates the predicate directly
1215/// against `DeferredRow` and therefore does not publish an owned `Row` merely
1216/// to decide whether it passes a post-JOIN WHERE clause. This is the required
1217/// boundary for OUTER JOIN predicates that cannot be pushed below the join.
1218#[doc(hidden)]
1219pub struct DeferredFilteredResult {
1220    inner: Box<dyn QueryResult>,
1221    filter: RowFilter,
1222    current: Option<DeferredRow>,
1223    current_materialized: OnceCell<Row>,
1224    columns: CompactArc<Vec<String>>,
1225    pending_error: Option<radixdb_core::Error>,
1226}
1227
1228impl DeferredFilteredResult {
1229    pub fn from_filter(inner: Box<dyn QueryResult>, filter: RowFilter) -> Self {
1230        let columns = inner
1231            .columns_arc()
1232            .unwrap_or_else(|| CompactArc::new(inner.columns().to_vec()));
1233        Self {
1234            inner,
1235            filter,
1236            current: None,
1237            current_materialized: OnceCell::new(),
1238            columns,
1239            pending_error: None,
1240        }
1241    }
1242
1243    fn current_deferred(&self) -> &DeferredRow {
1244        self.current
1245            .as_ref()
1246            .expect("row access without successful next()")
1247    }
1248}
1249
1250impl QueryResult for DeferredFilteredResult {
1251    fn columns(&self) -> &[String] {
1252        &self.columns
1253    }
1254
1255    fn columns_arc(&self) -> Option<CompactArc<Vec<String>>> {
1256        Some(CompactArc::clone(&self.columns))
1257    }
1258
1259    fn next(&mut self) -> bool {
1260        self.current = None;
1261        self.current_materialized = OnceCell::new();
1262        while self.inner.next() {
1263            let row = self.inner.take_deferred_row();
1264            match self.filter.matches_deferred_checked(&row) {
1265                Ok(true) => {
1266                    self.current = Some(row);
1267                    return true;
1268                }
1269                Ok(false) => {}
1270                Err(error) => {
1271                    self.pending_error = Some(error);
1272                    return false;
1273                }
1274            }
1275        }
1276        false
1277    }
1278
1279    fn scan(&self, dest: &mut [Value]) -> Result<()> {
1280        let row = self.row();
1281        if dest.len() != row.len() {
1282            return Err(radixdb_core::Error::internal(format!(
1283                "scan destination has {} values but row has {} columns",
1284                dest.len(),
1285                row.len()
1286            )));
1287        }
1288        dest.clone_from_slice(row.as_slice());
1289        Ok(())
1290    }
1291
1292    fn row(&self) -> &Row {
1293        self.current_materialized
1294            .get_or_init(|| self.current_deferred().to_owned())
1295    }
1296
1297    fn take_row(&mut self) -> Row {
1298        let deferred = self
1299            .current
1300            .take()
1301            .expect("take_row() called without successful next()");
1302        self.current_materialized
1303            .take()
1304            .unwrap_or_else(|| deferred.into_owned())
1305    }
1306
1307    fn take_deferred_row(&mut self) -> DeferredRow {
1308        let deferred = self
1309            .current
1310            .take()
1311            .expect("take_deferred_row() called without successful next()");
1312        self.current_materialized
1313            .take()
1314            .map_or(deferred, DeferredRow::owned)
1315    }
1316
1317    fn preserves_deferred_rows(&self) -> bool {
1318        true
1319    }
1320
1321    fn ascending_nulls_last_ordering(&self) -> Option<Vec<usize>> {
1322        self.inner.ascending_nulls_last_ordering()
1323    }
1324
1325    fn close(&mut self) -> Result<()> {
1326        self.current = None;
1327        self.current_materialized = OnceCell::new();
1328        self.inner.close()
1329    }
1330
1331    fn rows_affected(&self) -> i64 {
1332        self.inner.rows_affected()
1333    }
1334
1335    fn last_insert_id(&self) -> i64 {
1336        self.inner.last_insert_id()
1337    }
1338
1339    fn last_error(&mut self) -> Option<radixdb_core::Error> {
1340        self.pending_error
1341            .take()
1342            .or_else(|| self.inner.last_error())
1343    }
1344
1345    fn estimated_count(&self) -> Option<usize> {
1346        self.inner.estimated_count()
1347    }
1348
1349    fn with_aliases(self: Box<Self>, aliases: FxHashMap<String, String>) -> Box<dyn QueryResult> {
1350        Box::new(AliasedResult::new(self, aliases))
1351    }
1352}
1353
1354impl FilteredResult {
1355    /// Create a new expression-filtered result
1356    ///
1357    /// # Arguments
1358    /// * `inner` - The source result to filter
1359    /// * `filter_expr` - The WHERE clause expression
1360    ///
1361    /// Returns an error if the filter expression cannot be compiled.
1362    pub fn new(inner: Box<dyn QueryResult>, filter_expr: &Expression) -> Result<Self> {
1363        let columns = inner.columns().to_vec();
1364        let filter = RowFilter::new(filter_expr, &columns)?;
1365
1366        Ok(Self {
1367            inner,
1368            filter,
1369            current_row: None,
1370            columns,
1371            pending_error: None,
1372        })
1373    }
1374
1375    /// Create from a pre-built RowFilter
1376    ///
1377    /// Use this when you have a RowFilter that was constructed with specific
1378    /// context (e.g., with_context for correlated subqueries).
1379    pub fn from_filter(inner: Box<dyn QueryResult>, filter: RowFilter) -> Self {
1380        let columns = inner.columns().to_vec();
1381        Self {
1382            inner,
1383            filter,
1384            current_row: None,
1385            columns,
1386            pending_error: None,
1387        }
1388    }
1389
1390    /// Create with default function registry (static lifetime)
1391    pub fn with_defaults(inner: Box<dyn QueryResult>, filter_expr: Expression) -> Result<Self> {
1392        let columns = inner.columns().to_vec();
1393        let filter = RowFilter::new(&filter_expr, &columns)?;
1394
1395        Ok(Self {
1396            inner,
1397            filter,
1398            current_row: None,
1399            columns,
1400            pending_error: None,
1401        })
1402    }
1403}
1404
1405impl QueryResult for FilteredResult {
1406    fn columns(&self) -> &[String] {
1407        &self.columns
1408    }
1409
1410    fn next(&mut self) -> bool {
1411        // Keep advancing until we find a row that passes the filter
1412        while self.inner.next() {
1413            let row = self.inner.row();
1414            // Use checked filter to propagate runtime errors (e.g. invalid REGEXP)
1415            match self.filter.matches_checked(row) {
1416                Ok(true) => {
1417                    self.current_row = Some(self.inner.take_row());
1418                    return true;
1419                }
1420                Ok(false) => continue,
1421                Err(e) => {
1422                    self.pending_error = Some(e);
1423                    self.current_row = None;
1424                    return false;
1425                }
1426            }
1427        }
1428        self.current_row = None;
1429        false
1430    }
1431
1432    fn scan(&self, dest: &mut [Value]) -> Result<()> {
1433        if let Some(ref row) = self.current_row {
1434            if dest.len() != row.len() {
1435                return Err(radixdb_core::Error::internal(format!(
1436                    "scan destination has {} values but row has {} columns",
1437                    dest.len(),
1438                    row.len()
1439                )));
1440            }
1441            for (i, value) in row.iter().enumerate() {
1442                dest[i] = value.clone();
1443            }
1444            Ok(())
1445        } else {
1446            Err(radixdb_core::Error::internal(
1447                "scan() called without successful next()",
1448            ))
1449        }
1450    }
1451
1452    fn row(&self) -> &Row {
1453        self.current_row
1454            .as_ref()
1455            .expect("row() called without successful next()")
1456    }
1457
1458    fn take_row(&mut self) -> Row {
1459        self.current_row
1460            .take()
1461            .expect("take_row() called without successful next()")
1462    }
1463
1464    fn close(&mut self) -> Result<()> {
1465        self.inner.close()
1466    }
1467
1468    fn rows_affected(&self) -> i64 {
1469        self.inner.rows_affected()
1470    }
1471
1472    fn last_insert_id(&self) -> i64 {
1473        self.inner.last_insert_id()
1474    }
1475
1476    fn last_error(&mut self) -> Option<radixdb_core::Error> {
1477        // Return own error first, then check inner for nested FilteredResult chains
1478        self.pending_error
1479            .take()
1480            .or_else(|| self.inner.last_error())
1481    }
1482
1483    fn with_aliases(self: Box<Self>, aliases: FxHashMap<String, String>) -> Box<dyn QueryResult> {
1484        Box::new(AliasedResult::new(self, aliases))
1485    }
1486}
1487
1488/// A result wrapper that returns one row already consumed from `inner` before
1489/// continuing with the remaining source rows.
1490///
1491/// Optimizers sometimes need to inspect the first row to select a physical
1492/// path. If they reject that path after inspection, this wrapper preserves the
1493/// consumed row so the caller can continue with the same source instead of
1494/// executing the source query again.
1495#[doc(hidden)]
1496pub struct PrefetchedResult {
1497    inner: Box<dyn QueryResult>,
1498    columns: Vec<String>,
1499    prefetched: Option<Row>,
1500    current_row: Option<Row>,
1501}
1502
1503impl PrefetchedResult {
1504    pub fn new(prefetched: Row, inner: Box<dyn QueryResult>) -> Self {
1505        let columns = inner.columns().to_vec();
1506        Self {
1507            inner,
1508            columns,
1509            prefetched: Some(prefetched),
1510            current_row: None,
1511        }
1512    }
1513}
1514
1515impl QueryResult for PrefetchedResult {
1516    fn columns(&self) -> &[String] {
1517        &self.columns
1518    }
1519
1520    fn next(&mut self) -> bool {
1521        self.current_row = None;
1522        if let Some(row) = self.prefetched.take() {
1523            self.current_row = Some(row);
1524            return true;
1525        }
1526        if self.inner.next() {
1527            self.current_row = Some(self.inner.take_row());
1528            return true;
1529        }
1530        false
1531    }
1532
1533    fn scan(&self, dest: &mut [Value]) -> Result<()> {
1534        let row = self.current_row.as_ref().ok_or_else(|| {
1535            radixdb_core::Error::internal("scan() called without successful next()")
1536        })?;
1537        if dest.len() != row.len() {
1538            return Err(radixdb_core::Error::internal(format!(
1539                "scan destination has {} values but row has {} columns",
1540                dest.len(),
1541                row.len()
1542            )));
1543        }
1544        for (index, value) in row.iter().enumerate() {
1545            dest[index] = value.clone();
1546        }
1547        Ok(())
1548    }
1549
1550    fn row(&self) -> &Row {
1551        self.current_row
1552            .as_ref()
1553            .expect("row() called without successful next()")
1554    }
1555
1556    fn take_row(&mut self) -> Row {
1557        self.current_row
1558            .take()
1559            .expect("take_row() called without successful next()")
1560    }
1561
1562    fn close(&mut self) -> Result<()> {
1563        self.prefetched = None;
1564        self.current_row = None;
1565        self.inner.close()
1566    }
1567
1568    fn rows_affected(&self) -> i64 {
1569        self.inner.rows_affected()
1570    }
1571
1572    fn last_insert_id(&self) -> i64 {
1573        self.inner.last_insert_id()
1574    }
1575
1576    fn estimated_count(&self) -> Option<usize> {
1577        self.inner
1578            .estimated_count()
1579            .map(|count| count.saturating_add(usize::from(self.prefetched.is_some())))
1580    }
1581
1582    fn last_error(&mut self) -> Option<radixdb_core::Error> {
1583        self.inner.last_error()
1584    }
1585
1586    fn with_aliases(self: Box<Self>, aliases: FxHashMap<String, String>) -> Box<dyn QueryResult> {
1587        Box::new(AliasedResult::new(self, aliases))
1588    }
1589}
1590
1591/// Pre-compiled projection that can be either a Star expansion or a compiled expression.
1592enum CompiledProjection {
1593    /// Expand all columns from source (SELECT *)
1594    Star,
1595    /// Expand columns for specific table/alias (SELECT t.*)
1596    QualifiedStar {
1597        /// Lowercase qualifier for matching (e.g., "t") - no format! allocation
1598        qualifier_lower: String,
1599    },
1600    /// A pre-compiled expression program
1601    Compiled(super::expression::SharedProgram),
1602}
1603
1604/// Expression-based mapped result with pre-compiled projections
1605///
1606/// This struct pre-compiles all expressions during construction, providing
1607/// efficient per-row evaluation through the Expression VM.
1608pub struct ExprMappedResult {
1609    /// Underlying result
1610    inner: Box<dyn QueryResult>,
1611    /// Pre-compiled projections (one per output column)
1612    projections: Vec<CompiledProjection>,
1613    /// VM instance for expression execution (reused)
1614    vm: super::expression::ExprVM,
1615    /// Current mapped row
1616    current_row: Row,
1617    /// Output column names
1618    output_columns: Vec<String>,
1619    /// Pre-computed lowercase source columns (avoids per-row to_lowercase())
1620    source_columns_lower: Vec<String>,
1621    /// Pending error from projection evaluation.
1622    pending_error: Option<radixdb_core::Error>,
1623    params: Vec<Value>,
1624    named_params: FxHashMap<String, Value>,
1625    transaction_id: Option<u64>,
1626    stored_function_invoker: Option<Arc<dyn super::context::StoredFunctionInvoker>>,
1627    ordering: Option<Vec<usize>>,
1628}
1629
1630impl ExprMappedResult {
1631    fn direct_source_index(expression: &Expression, source_columns: &[String]) -> Option<usize> {
1632        let expression = match expression {
1633            Expression::Aliased(aliased) => aliased.expression.as_ref(),
1634            other => other,
1635        };
1636        match expression {
1637            Expression::QualifiedIdentifier(identifier) => {
1638                let qualified = identifier.to_string();
1639                source_columns
1640                    .iter()
1641                    .position(|column| column.eq_ignore_ascii_case(&qualified))
1642            }
1643            Expression::Identifier(identifier) => {
1644                let mut matches = source_columns.iter().enumerate().filter(|(_, column)| {
1645                    column.eq_ignore_ascii_case(identifier.value.as_str())
1646                        || column.rsplit_once('.').is_some_and(|(_, base)| {
1647                            base.eq_ignore_ascii_case(identifier.value.as_str())
1648                        })
1649                });
1650                let (index, _) = matches.next()?;
1651                matches.next().is_none().then_some(index)
1652            }
1653            _ => None,
1654        }
1655    }
1656
1657    /// Create a new expression-mapped result
1658    ///
1659    /// # Arguments
1660    /// * `inner` - The source result to project
1661    /// * `expressions` - The projection expressions
1662    /// * `output_columns` - Names for the output columns
1663    ///
1664    /// Returns an error if any expression cannot be compiled.
1665    pub fn new(
1666        inner: Box<dyn QueryResult>,
1667        expressions: Vec<Expression>,
1668        output_columns: Vec<String>,
1669    ) -> Result<Self> {
1670        Self::new_with_optional_context(inner, expressions, output_columns, None)
1671    }
1672
1673    pub fn with_context(
1674        inner: Box<dyn QueryResult>,
1675        expressions: Vec<Expression>,
1676        output_columns: Vec<String>,
1677        ctx: &super::context::ExecutionContext,
1678    ) -> Result<Self> {
1679        Self::new_with_optional_context(inner, expressions, output_columns, Some(ctx))
1680    }
1681
1682    fn new_with_optional_context(
1683        inner: Box<dyn QueryResult>,
1684        expressions: Vec<Expression>,
1685        output_columns: Vec<String>,
1686        ctx: Option<&super::context::ExecutionContext>,
1687    ) -> Result<Self> {
1688        use super::expression::compile_expression;
1689
1690        let source_columns = inner.columns().to_vec();
1691        let source_ordering = inner.ascending_nulls_last_ordering();
1692
1693        // Pre-compile all expressions
1694        let mut projections = Vec::with_capacity(expressions.len());
1695        for expr in &expressions {
1696            let projection = match expr {
1697                Expression::Star(_) => CompiledProjection::Star,
1698                Expression::QualifiedStar(qs) => CompiledProjection::QualifiedStar {
1699                    qualifier_lower: qs.qualifier.to_lowercase().to_string(),
1700                },
1701                _ => {
1702                    let program = compile_expression(expr, &source_columns)?;
1703                    CompiledProjection::Compiled(program)
1704                }
1705            };
1706            projections.push(projection);
1707        }
1708
1709        // Pre-compute lowercase source columns to avoid per-row to_lowercase() calls
1710        let source_columns_lower: Vec<String> =
1711            source_columns.iter().map(|c| c.to_lowercase()).collect();
1712        let ordering = source_ordering.and_then(|keys| {
1713            let mut remapped = Vec::with_capacity(keys.len());
1714            for key in keys {
1715                let output = expressions.iter().position(|expression| {
1716                    Self::direct_source_index(expression, &source_columns) == Some(key)
1717                })?;
1718                remapped.push(output);
1719            }
1720            (!remapped.is_empty()).then_some(remapped)
1721        });
1722
1723        // Pre-allocate buffer with capacity for reuse
1724        let capacity = projections.len();
1725        Ok(Self {
1726            inner,
1727            projections,
1728            vm: super::expression::ExprVM::new(),
1729            current_row: Row::with_capacity(capacity),
1730            output_columns,
1731            source_columns_lower,
1732            pending_error: None,
1733            params: ctx.map_or_else(Vec::new, |ctx| ctx.params().to_vec()),
1734            named_params: ctx.map_or_else(FxHashMap::default, |ctx| ctx.named_params().clone()),
1735            transaction_id: ctx.and_then(super::context::ExecutionContext::transaction_id),
1736            stored_function_invoker: ctx.and_then(|ctx| ctx.stored_function_invoker().cloned()),
1737            ordering,
1738        })
1739    }
1740
1741    /// Create with default function registry (static lifetime)
1742    pub fn with_defaults(
1743        inner: Box<dyn QueryResult>,
1744        expressions: Vec<Expression>,
1745        output_columns: Vec<String>,
1746    ) -> Result<Self> {
1747        Self::new(inner, expressions, output_columns)
1748    }
1749}
1750
1751impl QueryResult for ExprMappedResult {
1752    fn columns(&self) -> &[String] {
1753        &self.output_columns
1754    }
1755
1756    fn next(&mut self) -> bool {
1757        use super::expression::ExecuteContext;
1758
1759        if self.inner.next() {
1760            let source_row = self.inner.row();
1761
1762            // OPTIMIZATION: Lazy capacity reservation - only allocate when Row was taken
1763            // This avoids allocation in take_row() when caller drops the Row
1764            self.current_row.reserve_inline(self.projections.len());
1765            // Reuse buffer with inline storage - clear_inline preserves capacity
1766            self.current_row.clear_inline();
1767            for projection in &self.projections {
1768                match projection {
1769                    CompiledProjection::Star => {
1770                        // Expand all columns from source
1771                        for value in source_row.iter() {
1772                            self.current_row.push_inline(value.clone());
1773                        }
1774                    }
1775                    CompiledProjection::QualifiedStar { qualifier_lower } => {
1776                        // Expand columns for specific table/alias
1777                        // Use pre-computed lowercase columns to avoid per-row to_lowercase()
1778                        let qualifier_len = qualifier_lower.len();
1779                        for (idx, col_lower) in self.source_columns_lower.iter().enumerate() {
1780                            // Inline prefix check: "qualifier." without format! allocation
1781                            if col_lower.len() > qualifier_len
1782                                && col_lower.starts_with(qualifier_lower.as_str())
1783                                && col_lower.as_bytes()[qualifier_len] == b'.'
1784                                && idx < source_row.len()
1785                            {
1786                                self.current_row.push_inline(source_row[idx].clone());
1787                            }
1788                        }
1789                    }
1790                    CompiledProjection::Compiled(program) => {
1791                        let ctx = ExecuteContext::with_common_params(
1792                            source_row,
1793                            &self.params,
1794                            (!self.named_params.is_empty()).then_some(&self.named_params),
1795                            self.transaction_id,
1796                        )
1797                        .with_stored_function_invoker(self.stored_function_invoker.as_ref());
1798                        let value = self.vm.execute_cow(program, &ctx);
1799                        match value {
1800                            Ok(value) => self.current_row.push_inline(value),
1801                            Err(error) => {
1802                                self.pending_error = Some(error);
1803                                self.current_row.clear_inline();
1804                                return false;
1805                            }
1806                        }
1807                    }
1808                }
1809            }
1810            true
1811        } else {
1812            false
1813        }
1814    }
1815
1816    fn scan(&self, dest: &mut [Value]) -> Result<()> {
1817        if dest.len() != self.current_row.len() {
1818            return Err(radixdb_core::Error::internal(format!(
1819                "scan destination has {} values but row has {} columns",
1820                dest.len(),
1821                self.current_row.len()
1822            )));
1823        }
1824        for (i, value) in self.current_row.iter().enumerate() {
1825            dest[i] = value.clone();
1826        }
1827        Ok(())
1828    }
1829
1830    fn row(&self) -> &Row {
1831        &self.current_row
1832    }
1833
1834    fn take_row(&mut self) -> Row {
1835        // Don't pre-allocate here - let next() do lazy initialization
1836        // This eliminates one allocation per row when the caller drops the row
1837        std::mem::take(&mut self.current_row)
1838    }
1839
1840    fn ascending_nulls_last_ordering(&self) -> Option<Vec<usize>> {
1841        self.ordering.clone()
1842    }
1843
1844    fn close(&mut self) -> Result<()> {
1845        self.inner.close()
1846    }
1847
1848    fn rows_affected(&self) -> i64 {
1849        0
1850    }
1851
1852    fn last_insert_id(&self) -> i64 {
1853        0
1854    }
1855
1856    fn last_error(&mut self) -> Option<radixdb_core::Error> {
1857        self.pending_error
1858            .take()
1859            .or_else(|| self.inner.last_error())
1860    }
1861
1862    fn with_aliases(self: Box<Self>, aliases: FxHashMap<String, String>) -> Box<dyn QueryResult> {
1863        Box::new(AliasedResult::new(self, aliases))
1864    }
1865}
1866
1867mod ordering;
1868pub use ordering::{LimitedResult, OrderedResult, RadixOrderSpec, TopNResult};
1869#[cfg(test)]
1870use ordering::{ORDERED_RUN_MAX_BYTES, ORDERED_RUN_MAX_ROWS};
1871
1872/// Streaming distinct result that removes duplicate rows on-the-fly
1873///
1874/// This streams rows and only stores seen row values for deduplication. This enables:
1875/// - Early termination with LIMIT (no need to scan all rows)
1876/// - Lower latency to first row
1877/// - Streaming output
1878pub struct DistinctResult {
1879    /// Underlying result source
1880    inner: Box<dyn QueryResult>,
1881    /// Columns from inner result
1882    columns: Vec<String>,
1883    /// Number of columns to consider for distinctness
1884    /// (may be less than total columns when ORDER BY adds extra columns)
1885    distinct_column_count: usize,
1886    /// Seen rows for deduplication: hash -> list of row values (for collision handling)
1887    /// We only store the distinct columns, not the full row
1888    seen: FxHashMap<u64, Vec<Vec<Value>>>,
1889    /// Current row (stored for row() method)
1890    current_row: Row,
1891    /// Whether we have a valid current row
1892    has_current: bool,
1893    budget: RetainedRowsBudget,
1894    terminal_error: Option<radixdb_core::Error>,
1895}
1896
1897impl DistinctResult {
1898    /// Create a new streaming distinct result
1899    pub fn new(inner: Box<dyn QueryResult>) -> Self {
1900        Self::with_column_count(inner, None)
1901    }
1902
1903    /// Create a distinct result that only considers the first `distinct_columns` columns
1904    /// for uniqueness comparison. This is used when ORDER BY references columns not in SELECT.
1905    ///
1906    /// For example: SELECT DISTINCT a FROM t ORDER BY b
1907    /// - The result has columns [a, b] for sorting
1908    /// - But distinctness should only compare column `a`
1909    pub fn with_column_count(inner: Box<dyn QueryResult>, distinct_columns: Option<usize>) -> Self {
1910        let columns = inner.columns().to_vec();
1911        let distinct_column_count = distinct_columns.unwrap_or(columns.len());
1912
1913        Self {
1914            inner,
1915            columns,
1916            distinct_column_count,
1917            seen: FxHashMap::default(),
1918            current_row: Row::new(),
1919            has_current: false,
1920            budget: RetainedRowsBudget::new("DISTINCT"),
1921            terminal_error: None,
1922        }
1923    }
1924
1925    /// Compute hash for the distinct columns of a row
1926    fn hash_row(&self, row: &Row) -> u64 {
1927        use std::hash::{Hash, Hasher};
1928        let mut hasher = FxHasher::default();
1929        for value in row.iter().take(self.distinct_column_count) {
1930            value.hash(&mut hasher);
1931        }
1932        hasher.finish()
1933    }
1934
1935    /// Extract distinct column values from a row
1936    fn extract_distinct_values(&self, row: &Row) -> Vec<Value> {
1937        row.iter()
1938            .take(self.distinct_column_count)
1939            .cloned()
1940            .collect()
1941    }
1942}
1943
1944impl QueryResult for DistinctResult {
1945    fn columns(&self) -> &[String] {
1946        &self.columns
1947    }
1948
1949    fn next(&mut self) -> bool {
1950        // Keep getting rows until we find a non-duplicate
1951        while self.inner.next() {
1952            let row = self.inner.row();
1953            let hash = self.hash_row(row);
1954
1955            // Extract distinct values first for duplicate check
1956            let values = self.extract_distinct_values(row);
1957
1958            // Check if we've seen this combination before
1959            let is_dup = if let Some(seen_rows) = self.seen.get(&hash) {
1960                seen_rows.contains(&values)
1961            } else {
1962                false
1963            };
1964
1965            if !is_dup {
1966                // New unique row found - take ownership and mark as seen
1967                if let Err(error) = self.budget.admit_values(&values) {
1968                    self.terminal_error = Some(error);
1969                    self.has_current = false;
1970                    return false;
1971                }
1972                self.current_row = self.inner.take_row();
1973                self.seen.entry(hash).or_default().push(values);
1974                self.has_current = true;
1975                return true;
1976            }
1977            // Duplicate - continue to next row
1978        }
1979
1980        // No more rows
1981        self.has_current = false;
1982        false
1983    }
1984
1985    fn scan(&self, dest: &mut [Value]) -> Result<()> {
1986        if !self.has_current {
1987            return Err(radixdb_core::Error::internal(
1988                "scan() called without successful next()",
1989            ));
1990        }
1991        if dest.len() != self.current_row.len() {
1992            return Err(radixdb_core::Error::internal(format!(
1993                "scan destination has {} values but row has {} columns",
1994                dest.len(),
1995                self.current_row.len()
1996            )));
1997        }
1998        for (i, v) in self.current_row.iter().enumerate() {
1999            dest[i] = v.clone();
2000        }
2001        Ok(())
2002    }
2003
2004    fn row(&self) -> &Row {
2005        &self.current_row
2006    }
2007
2008    fn take_row(&mut self) -> Row {
2009        self.has_current = false;
2010        std::mem::take(&mut self.current_row)
2011    }
2012
2013    fn close(&mut self) -> Result<()> {
2014        self.inner.close()
2015    }
2016
2017    fn rows_affected(&self) -> i64 {
2018        0
2019    }
2020
2021    fn last_insert_id(&self) -> i64 {
2022        0
2023    }
2024
2025    fn last_error(&mut self) -> Option<radixdb_core::Error> {
2026        self.terminal_error
2027            .take()
2028            .or_else(|| self.inner.last_error())
2029    }
2030
2031    fn with_aliases(self: Box<Self>, aliases: FxHashMap<String, String>) -> Box<dyn QueryResult> {
2032        Box::new(AliasedResult::new(self, aliases))
2033    }
2034}
2035
2036/// DISTINCT ON result — keeps only the first row per unique combination
2037/// of the specified key columns. Uses hash-based deduplication so the input
2038/// does not need to be sorted by the key columns (ORDER BY controls which
2039/// row is "first" because the input stream preserves ORDER BY order).
2040/// Memory usage is O(groups) where groups is the number of unique key
2041/// combinations, not total rows.
2042pub struct DistinctOnResult {
2043    inner: Box<dyn QueryResult>,
2044    columns: Vec<String>,
2045    /// Column indices that form the DISTINCT ON key
2046    key_indices: Vec<usize>,
2047    /// Seen keys: hash -> list of key values (for collision handling)
2048    seen: FxHashMap<u64, Vec<Vec<Value>>>,
2049    current_row: Row,
2050    has_current: bool,
2051    budget: RetainedRowsBudget,
2052    terminal_error: Option<radixdb_core::Error>,
2053}
2054
2055impl DistinctOnResult {
2056    pub fn new(inner: Box<dyn QueryResult>, key_indices: Vec<usize>) -> Self {
2057        let columns = inner.columns().to_vec();
2058        Self {
2059            inner,
2060            columns,
2061            key_indices,
2062            seen: FxHashMap::default(),
2063            current_row: Row::new(),
2064            has_current: false,
2065            budget: RetainedRowsBudget::new("DISTINCT ON"),
2066            terminal_error: None,
2067        }
2068    }
2069
2070    fn extract_key(&self, row: &Row) -> Vec<Value> {
2071        self.key_indices
2072            .iter()
2073            .map(|&i| row.get(i).cloned().unwrap_or_else(Value::null_unknown))
2074            .collect()
2075    }
2076
2077    fn hash_key(&self, key: &[Value]) -> u64 {
2078        use std::hash::{Hash, Hasher};
2079        let mut hasher = FxHasher::default();
2080        for value in key {
2081            value.hash(&mut hasher);
2082        }
2083        hasher.finish()
2084    }
2085}
2086
2087impl QueryResult for DistinctOnResult {
2088    fn columns(&self) -> &[String] {
2089        &self.columns
2090    }
2091
2092    fn next(&mut self) -> bool {
2093        while self.inner.next() {
2094            let row = self.inner.row();
2095            let key = self.extract_key(row);
2096            let hash = self.hash_key(&key);
2097
2098            // Check if we've seen this key before
2099            let is_dup = if let Some(seen_keys) = self.seen.get(&hash) {
2100                seen_keys.contains(&key)
2101            } else {
2102                false
2103            };
2104
2105            if is_dup {
2106                continue; // already emitted a row for this group
2107            }
2108
2109            if let Err(error) = self.budget.admit_values(&key) {
2110                self.terminal_error = Some(error);
2111                self.has_current = false;
2112                return false;
2113            }
2114            self.seen.entry(hash).or_default().push(key);
2115            self.current_row = self.inner.take_row();
2116            self.has_current = true;
2117            return true;
2118        }
2119        self.has_current = false;
2120        false
2121    }
2122
2123    fn scan(&self, dest: &mut [Value]) -> Result<()> {
2124        if !self.has_current {
2125            return Err(radixdb_core::Error::internal(
2126                "scan() called without successful next()",
2127            ));
2128        }
2129        if dest.len() != self.current_row.len() {
2130            return Err(radixdb_core::Error::internal(format!(
2131                "scan destination has {} values but row has {} columns",
2132                dest.len(),
2133                self.current_row.len()
2134            )));
2135        }
2136        for (i, v) in self.current_row.iter().enumerate() {
2137            dest[i] = v.clone();
2138        }
2139        Ok(())
2140    }
2141
2142    fn row(&self) -> &Row {
2143        &self.current_row
2144    }
2145
2146    fn take_row(&mut self) -> Row {
2147        self.has_current = false;
2148        std::mem::take(&mut self.current_row)
2149    }
2150
2151    fn close(&mut self) -> Result<()> {
2152        self.seen.clear();
2153        self.inner.close()
2154    }
2155
2156    fn rows_affected(&self) -> i64 {
2157        0
2158    }
2159
2160    fn last_insert_id(&self) -> i64 {
2161        0
2162    }
2163
2164    fn last_error(&mut self) -> Option<radixdb_core::Error> {
2165        self.terminal_error
2166            .take()
2167            .or_else(|| self.inner.last_error())
2168    }
2169
2170    fn with_aliases(self: Box<Self>, aliases: FxHashMap<String, String>) -> Box<dyn QueryResult> {
2171        Box::new(AliasedResult::new(self, aliases))
2172    }
2173}
2174
2175/// Projected result that removes extra columns (e.g., ORDER BY columns not in SELECT)
2176///
2177/// This result type projects rows to only include the first N columns,
2178/// removing any extra columns that were added for sorting purposes.
2179pub struct ProjectedResult {
2180    inner: Box<dyn QueryResult>,
2181    /// Number of columns to keep
2182    keep_columns: usize,
2183    /// Cached projected columns
2184    projected_columns: Vec<String>,
2185    /// Cached projected row
2186    current_row: Row,
2187}
2188
2189impl ProjectedResult {
2190    /// Create a new projected result
2191    pub fn new(inner: Box<dyn QueryResult>, keep_columns: usize) -> Self {
2192        let projected_columns: Vec<String> =
2193            inner.columns().iter().take(keep_columns).cloned().collect();
2194
2195        // Pre-allocate Inline storage - no Arc overhead for intermediate results
2196        Self {
2197            inner,
2198            keep_columns,
2199            projected_columns,
2200            current_row: Row::with_capacity(keep_columns),
2201        }
2202    }
2203}
2204
2205impl QueryResult for ProjectedResult {
2206    fn columns(&self) -> &[String] {
2207        &self.projected_columns
2208    }
2209
2210    fn next(&mut self) -> bool {
2211        if self.inner.next() {
2212            // Project the row to keep only the first N columns
2213            // OPTIMIZATION: Use Inline storage - no Arc overhead for intermediate results
2214            self.current_row.reserve_inline(self.keep_columns);
2215            self.current_row.clear_inline();
2216            let full_row = self.inner.row();
2217            for i in 0..self.keep_columns {
2218                self.current_row
2219                    .push_inline(full_row.get(i).cloned().unwrap_or(Value::null_unknown()));
2220            }
2221            true
2222        } else {
2223            false
2224        }
2225    }
2226
2227    fn scan(&self, dest: &mut [Value]) -> Result<()> {
2228        for (i, val) in dest.iter_mut().enumerate().take(self.keep_columns) {
2229            *val = self
2230                .current_row
2231                .get(i)
2232                .cloned()
2233                .unwrap_or(Value::null_unknown());
2234        }
2235        Ok(())
2236    }
2237
2238    fn row(&self) -> &Row {
2239        &self.current_row
2240    }
2241
2242    fn take_row(&mut self) -> Row {
2243        std::mem::take(&mut self.current_row)
2244    }
2245
2246    fn close(&mut self) -> Result<()> {
2247        self.inner.close()
2248    }
2249
2250    fn rows_affected(&self) -> i64 {
2251        0
2252    }
2253
2254    fn last_insert_id(&self) -> i64 {
2255        0
2256    }
2257
2258    fn last_error(&mut self) -> Option<radixdb_core::Error> {
2259        self.inner.last_error()
2260    }
2261
2262    fn with_aliases(self: Box<Self>, aliases: FxHashMap<String, String>) -> Box<dyn QueryResult> {
2263        Box::new(AliasedResult::new(self, aliases))
2264    }
2265}
2266
2267/// Streaming projection result that projects columns row-by-row
2268///
2269/// This allows streaming projection without materializing all rows into a Vec.
2270/// Uses simple column index-based projection for performance.
2271pub struct StreamingProjectionResult {
2272    /// Underlying result
2273    inner: Box<dyn QueryResult>,
2274    /// Column indices to project (from source to output)
2275    column_indices: Vec<usize>,
2276    /// Output column names
2277    output_columns: Vec<String>,
2278    /// Current projected row
2279    current_row: Row,
2280}
2281
2282impl StreamingProjectionResult {
2283    /// Create a new streaming projection result
2284    ///
2285    /// # Arguments
2286    /// * `inner` - The source result
2287    /// * `column_indices` - Indices of columns to keep from the source
2288    /// * `output_columns` - Names for the output columns
2289    pub fn new(
2290        inner: Box<dyn QueryResult>,
2291        column_indices: Vec<usize>,
2292        output_columns: Vec<String>,
2293    ) -> Self {
2294        // Pre-allocate Inline storage - no Arc overhead for intermediate results
2295        let capacity = column_indices.len();
2296        Self {
2297            inner,
2298            column_indices,
2299            output_columns,
2300            current_row: Row::with_capacity(capacity),
2301        }
2302    }
2303}
2304
2305impl QueryResult for StreamingProjectionResult {
2306    fn columns(&self) -> &[String] {
2307        &self.output_columns
2308    }
2309
2310    fn next(&mut self) -> bool {
2311        if self.inner.next() {
2312            // OPTIMIZATION: Use Inline storage - no Arc overhead for intermediate results
2313            self.current_row.reserve_inline(self.column_indices.len());
2314            self.current_row.clear_inline();
2315            let source_row = self.inner.row();
2316            for &idx in &self.column_indices {
2317                self.current_row.push_inline(
2318                    source_row
2319                        .get(idx)
2320                        .cloned()
2321                        .unwrap_or(Value::null_unknown()),
2322                );
2323            }
2324            true
2325        } else {
2326            false
2327        }
2328    }
2329
2330    fn scan(&self, dest: &mut [Value]) -> Result<()> {
2331        if dest.len() != self.current_row.len() {
2332            return Err(radixdb_core::Error::internal(format!(
2333                "scan destination has {} values but row has {} columns",
2334                dest.len(),
2335                self.current_row.len()
2336            )));
2337        }
2338        for (i, value) in self.current_row.iter().enumerate() {
2339            dest[i] = value.clone();
2340        }
2341        Ok(())
2342    }
2343
2344    fn row(&self) -> &Row {
2345        &self.current_row
2346    }
2347
2348    fn take_row(&mut self) -> Row {
2349        std::mem::take(&mut self.current_row)
2350    }
2351
2352    fn close(&mut self) -> Result<()> {
2353        self.inner.close()
2354    }
2355
2356    fn rows_affected(&self) -> i64 {
2357        0
2358    }
2359
2360    fn last_insert_id(&self) -> i64 {
2361        0
2362    }
2363
2364    fn last_error(&mut self) -> Option<radixdb_core::Error> {
2365        self.inner.last_error()
2366    }
2367
2368    fn with_aliases(self: Box<Self>, aliases: FxHashMap<String, String>) -> Box<dyn QueryResult> {
2369        Box::new(AliasedResult::new(self, aliases))
2370    }
2371}
2372
2373/// Columnar result that stores data column-major and materializes rows lazily.
2374///
2375/// This is optimized for window functions and other operations that naturally
2376/// produce column-major output. Instead of allocating millions of Row objects
2377/// upfront, it stores data as `Vec<Value>` per column and materializes rows
2378/// on-demand during iteration.
2379///
2380/// Key benefits:
2381/// - Reduces allocations from O(num_rows) to O(num_columns)
2382/// - Reuses a single Row buffer during iteration (zero per-row allocation)
2383/// - Natural fit for window function results
2384///
2385/// The returned row from `row()` is valid only until the next `next()` call.
2386pub struct ColumnarResult {
2387    /// Column names
2388    columns: CompactArc<Vec<String>>,
2389    /// Column-major storage: data[col_idx][row_idx]
2390    data: Vec<Vec<Value>>,
2391    /// Number of rows (cached for fast access)
2392    num_rows: usize,
2393    /// Current row index (None before first next())
2394    current_index: Option<usize>,
2395    /// Reusable row buffer - avoids allocation per row
2396    current_row: Row,
2397    /// Whether the result is closed
2398    closed: bool,
2399}
2400
2401impl ColumnarResult {
2402    /// Create a new columnar result from column-major data
2403    ///
2404    /// # Arguments
2405    /// * `columns` - Column names (must match data.len())
2406    /// * `data` - Column-major data where `data[col_idx]` contains all values for that column
2407    ///
2408    /// # Panics
2409    /// Panics if columns.len() != data.len() or if columns have different lengths
2410    pub fn new(columns: Vec<String>, data: Vec<Vec<Value>>) -> Self {
2411        debug_assert!(
2412            columns.len() == data.len(),
2413            "columns.len() ({}) != data.len() ({})",
2414            columns.len(),
2415            data.len()
2416        );
2417
2418        let num_rows = data.first().map(|c| c.len()).unwrap_or(0);
2419
2420        // Verify all columns have the same length
2421        #[cfg(debug_assertions)]
2422        for (i, col) in data.iter().enumerate() {
2423            debug_assert!(
2424                col.len() == num_rows,
2425                "column {} has {} rows but expected {}",
2426                i,
2427                col.len(),
2428                num_rows
2429            );
2430        }
2431
2432        // Pre-allocate the row buffer with capacity for all columns
2433        let num_cols = columns.len();
2434
2435        Self {
2436            columns: CompactArc::new(columns),
2437            data,
2438            num_rows,
2439            current_index: None,
2440            current_row: Row::with_capacity(num_cols),
2441            closed: false,
2442        }
2443    }
2444
2445    /// Create with CompactArc columns (zero-copy)
2446    pub fn with_arc_columns(columns: CompactArc<Vec<String>>, data: Vec<Vec<Value>>) -> Self {
2447        let num_rows = data.first().map(|c| c.len()).unwrap_or(0);
2448        let num_cols = columns.len();
2449
2450        Self {
2451            columns,
2452            data,
2453            num_rows,
2454            current_index: None,
2455            current_row: Row::with_capacity(num_cols),
2456            closed: false,
2457        }
2458    }
2459
2460    /// Get the number of rows
2461    #[inline]
2462    pub fn row_count(&self) -> usize {
2463        self.num_rows
2464    }
2465
2466    /// Get the number of columns
2467    #[inline]
2468    pub fn column_count(&self) -> usize {
2469        self.data.len()
2470    }
2471}
2472
2473impl QueryResult for ColumnarResult {
2474    fn columns(&self) -> &[String] {
2475        &self.columns
2476    }
2477
2478    fn columns_arc(&self) -> Option<CompactArc<Vec<String>>> {
2479        Some(CompactArc::clone(&self.columns))
2480    }
2481
2482    #[inline]
2483    fn next(&mut self) -> bool {
2484        if self.closed {
2485            return false;
2486        }
2487
2488        let next_idx = match self.current_index {
2489            None => 0,
2490            Some(i) => i + 1,
2491        };
2492
2493        if next_idx >= self.num_rows {
2494            return false;
2495        }
2496
2497        self.current_index = Some(next_idx);
2498
2499        // OPTIMIZATION: Lazy capacity reservation - only allocate when Row was taken
2500        self.current_row.reserve_inline(self.data.len());
2501        // Materialize row from column data - clear_inline preserves capacity
2502        self.current_row.clear_inline();
2503        for col_data in &self.data {
2504            // Safety: we verified all columns have num_rows elements
2505            self.current_row.push_inline(col_data[next_idx].clone());
2506        }
2507
2508        true
2509    }
2510
2511    fn scan(&self, dest: &mut [Value]) -> Result<()> {
2512        if dest.len() != self.current_row.len() {
2513            return Err(radixdb_core::Error::internal(format!(
2514                "scan destination has {} values but row has {} columns",
2515                dest.len(),
2516                self.current_row.len()
2517            )));
2518        }
2519        for (i, value) in self.current_row.iter().enumerate() {
2520            dest[i] = value.clone();
2521        }
2522        Ok(())
2523    }
2524
2525    #[inline]
2526    fn row(&self) -> &Row {
2527        &self.current_row
2528    }
2529
2530    fn take_row(&mut self) -> Row {
2531        // Don't pre-allocate here - let next() do lazy initialization
2532        // This eliminates one allocation per row when the caller drops the Row
2533        std::mem::take(&mut self.current_row)
2534    }
2535
2536    fn close(&mut self) -> Result<()> {
2537        self.closed = true;
2538        // Clear data to free memory
2539        self.data.clear();
2540        Ok(())
2541    }
2542
2543    fn rows_affected(&self) -> i64 {
2544        0
2545    }
2546
2547    fn last_insert_id(&self) -> i64 {
2548        0
2549    }
2550
2551    fn estimated_count(&self) -> Option<usize> {
2552        Some(self.num_rows)
2553    }
2554
2555    fn with_aliases(self: Box<Self>, aliases: FxHashMap<String, String>) -> Box<dyn QueryResult> {
2556        Box::new(AliasedResult::new(self, aliases))
2557    }
2558}
2559
2560#[cfg(test)]
2561mod tests;