Skip to main content

radixdb_executor/
operator.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//! Volcano-style operator interface for streaming query execution.
16//!
17//! This module provides the foundation for a streaming execution model where
18//! operators pull rows on-demand rather than materializing everything upfront.
19//!
20//! # Architecture
21//!
22//! ```text
23//! ┌──────────────┐
24//! │ Consumer     │ ← Pulls rows via next()
25//! └──────┬───────┘
26//!        │
27//! ┌──────▼───────┐
28//! │ Join Op      │ ← Build side materialized, probe side streamed
29//! └──────┬───────┘
30//!        │
31//! ┌──────┴──────┐
32//! │             │
33//! ▼             ▼
34//! ┌─────┐   ┌─────┐
35//! │Scan │   │Scan │ ← Stream rows from storage
36//! └─────┘   └─────┘
37//! ```
38//!
39//! # Key Benefits
40//!
41//! 1. **Reduced Memory**: Only materialize what's needed (e.g., hash join build side)
42//! 2. **Early Termination**: LIMIT can stop execution without processing all rows
43//! 3. **Pipelining**: Multiple operators can work on the same row in sequence
44//! 4. **Zero-Copy**: RowRef allows referencing rows without cloning
45
46use std::fmt;
47
48use radixdb_core::value::NULL_VALUE;
49use radixdb_core::{CompactArc, CompactVec};
50use radixdb_core::{Error, Result, Row, Value};
51use radixdb_storage::{DeferredColumnSource, DeferredRow};
52
53/// Column information for operator schema.
54#[derive(Debug, Clone)]
55pub struct ColumnInfo {
56    /// Column name
57    pub name: String,
58    /// Original table alias (if from a table)
59    pub table_alias: Option<String>,
60}
61
62/// Physical ordering guaranteed by an operator.
63///
64/// This is a certificate produced by the physical plan, not a runtime guess
65/// from inspecting materialized rows.  Merge consumers may use only the
66/// leading key prefix and NULL ordering they explicitly require.
67#[derive(Debug, Clone, PartialEq, Eq, Default)]
68pub enum OrderingProperty {
69    /// The operator makes no ordering guarantee.
70    #[default]
71    Unknown,
72    /// Rows are ascending on the listed key prefix, with NULL values last.
73    AscendingNullsLast(Vec<usize>),
74}
75
76impl OrderingProperty {
77    /// Create an ascending, NULLS LAST ordering certificate.
78    pub fn ascending_nulls_last(key_indices: Vec<usize>) -> Self {
79        if key_indices.is_empty() {
80            Self::Unknown
81        } else {
82            Self::AscendingNullsLast(key_indices)
83        }
84    }
85
86    /// Return whether this certificate proves the ordering required by merge.
87    pub fn proves_ascending_nulls_last(&self, required_keys: &[usize]) -> bool {
88        if required_keys.is_empty() {
89            return false;
90        }
91        match self {
92            Self::AscendingNullsLast(keys) => keys.starts_with(required_keys),
93            Self::Unknown => false,
94        }
95    }
96
97    /// Preserve this certificate through a fused JOIN projection.
98    ///
99    /// Every certified outer key must remain in the projected row. Missing,
100    /// reordered, or inner-sourced keys fail closed to Unknown.
101    pub fn remap_outer_projection(&self, columns: &[ColumnSource]) -> Self {
102        let Self::AscendingNullsLast(keys) = self else {
103            return Self::Unknown;
104        };
105        let mut remapped = Vec::with_capacity(keys.len());
106        for key in keys {
107            let Some(output) = columns
108                .iter()
109                .position(|source| matches!(source, ColumnSource::Outer(index) if index == key))
110            else {
111                return Self::Unknown;
112            };
113            remapped.push(output);
114        }
115        Self::ascending_nulls_last(remapped)
116    }
117}
118
119impl ColumnInfo {
120    /// Create a new column info with just a name.
121    pub fn new(name: impl Into<String>) -> Self {
122        Self {
123            name: name.into(),
124            table_alias: None,
125        }
126    }
127}
128
129/// Specifies which side of a binary join a projected column comes from.
130#[derive(Debug, Clone, Copy, PartialEq, Eq)]
131pub enum ColumnSource {
132    /// Column from the left/outer row at the given index.
133    Outer(usize),
134    /// Column from the right/inner row at the given index.
135    Inner(usize),
136}
137
138/// Projection configuration for fused projection during join row creation.
139#[derive(Debug, Clone)]
140pub struct JoinProjection {
141    /// Columns to extract, in SELECT order.
142    pub columns: Vec<ColumnSource>,
143}
144
145impl JoinProjection {
146    /// Validate a public projection against both logical input schemas before
147    /// any row is read. Execution may then use indexed access without trusting
148    /// an external caller to have run the internal planner.
149    pub fn validate(
150        &self,
151        left_columns: usize,
152        right_columns: usize,
153        output_columns: usize,
154    ) -> Result<()> {
155        if self.columns.len() != output_columns {
156            return Err(Error::invalid_argument(format!(
157                "join projection has {} values but output schema has {} columns",
158                self.columns.len(),
159                output_columns
160            )));
161        }
162        for source in &self.columns {
163            let (side, index, width) = match source {
164                ColumnSource::Outer(index) => ("left", *index, left_columns),
165                ColumnSource::Inner(index) => ("right", *index, right_columns),
166            };
167            if index >= width {
168                return Err(Error::invalid_argument(format!(
169                    "join projection {side} column index {index} is outside width {width}"
170                )));
171            }
172        }
173        Ok(())
174    }
175}
176
177/// Volcano-style iterator interface for query operators.
178///
179/// Each operator implements this trait to participate in the streaming
180/// execution pipeline. The execution follows the open-next-close pattern:
181///
182/// 1. `open()` - Initialize the operator (called once)
183/// 2. `next()` - Get the next row (called repeatedly until None)
184/// 3. `close()` - Release resources (called once at end)
185///
186/// # Thread Safety
187///
188/// Operators are `Send` to allow execution on different threads,
189/// but individual operators are not `Sync` - they maintain mutable state.
190pub trait Operator: Send {
191    /// Initialize the operator.
192    ///
193    /// Called once before the first `next()` call.
194    /// This is where child operators should be opened and
195    /// any one-time initialization should occur.
196    fn open(&mut self) -> Result<()>;
197
198    /// Get the next row from this operator.
199    ///
200    /// Returns:
201    /// - `Ok(Some(row))` - A row is available
202    /// - `Ok(None)` - No more rows (exhausted)
203    /// - `Err(e)` - An error occurred
204    ///
205    /// After returning `None`, subsequent calls should continue to return `None`.
206    fn next(&mut self) -> Result<Option<RowRef>>;
207
208    /// Close the operator and release resources.
209    ///
210    /// Called once after all rows have been consumed or when
211    /// execution is terminated early. Child operators should
212    /// also be closed.
213    fn close(&mut self) -> Result<()>;
214
215    /// Get the schema (column information) for this operator's output.
216    fn schema(&self) -> &[ColumnInfo];
217
218    /// Get an estimate of the number of rows this operator will produce.
219    ///
220    /// Returns `None` if the estimate is not available.
221    /// Used by the query planner for cost estimation.
222    fn estimated_rows(&self) -> Option<usize> {
223        None
224    }
225
226    /// Physical ordering guaranteed by this operator's output.
227    ///
228    /// Unknown is deliberately fail-closed: an executor must never discover
229    /// ordering by rescanning the complete output merely to choose an
230    /// algorithm.
231    fn ordering(&self) -> OrderingProperty {
232        OrderingProperty::Unknown
233    }
234
235    /// Get a descriptive name for this operator (for EXPLAIN).
236    fn name(&self) -> &str;
237}
238
239/// A row reference that can be borrowed, owned, or composite.
240///
241/// This enum allows operators to return rows without always cloning:
242/// - `Borrowed`: Reference to an existing row (zero-copy)
243/// - `Owned`: An owned row (when materialization is needed)
244/// - `Composite`: Virtual row combining left and right join sides
245///
246/// # Performance
247///
248/// The key optimization is that `Composite` allows hash joins to
249/// return combined rows without actually copying values from both sides.
250/// Values are only copied when the final result is materialized.
251#[derive(Debug, Clone)]
252pub enum RowRef {
253    /// Owned row - the row data is owned by this RowRef.
254    Owned(Row),
255
256    /// Composite row - combines two rows without copying.
257    /// Used by join operators to avoid materializing combined rows.
258    Composite(CompositeRow),
259
260    /// Direct build composite - combines owned probe row with Arc-referenced build rows.
261    /// OPTIMIZATION: Avoids Arc allocation for probe row (saves 1 allocation per match).
262    /// Used for hash joins where build rows are shared via Arc.
263    DirectBuildComposite(DirectBuildCompositeRow),
264
265    /// One row in an immutable shared build batch.
266    ///
267    /// This lets a projected JOIN output retain the build row by Arc + index
268    /// instead of cloning every value from that row.
269    Shared(SharedRow),
270
271    /// Projected virtual row over an existing outer row and one joined row.
272    ///
273    /// Unlike an owned projected `Row`, this keeps the selected slots virtual
274    /// across subsequent JOIN edges. Values are copied only when the final
275    /// result consumer requests an owned row.
276    Projected(ProjectedRow),
277
278    /// Deferred row carried through an internal QueryResult boundary.
279    Deferred(DeferredRow),
280}
281
282impl RowRef {
283    /// Create an owned RowRef from a Row.
284    #[inline]
285    pub fn owned(row: Row) -> Self {
286        RowRef::Owned(row)
287    }
288
289    /// Create a composite RowRef from left and right rows.
290    #[inline]
291    pub fn composite(left: Row, right: Row) -> Self {
292        RowRef::Composite(CompositeRow::new(left, right))
293    }
294
295    /// Get the number of columns in this row.
296    #[inline]
297    pub fn len(&self) -> usize {
298        match self {
299            RowRef::Owned(row) => row.len(),
300            RowRef::Composite(comp) => comp.len(),
301            RowRef::DirectBuildComposite(direct) => direct.len(),
302            RowRef::Shared(shared) => shared.len(),
303            RowRef::Projected(projected) => projected.len(),
304            RowRef::Deferred(deferred) => deferred.len(),
305        }
306    }
307
308    /// Check if this row is empty.
309    #[inline]
310    pub fn is_empty(&self) -> bool {
311        self.len() == 0
312    }
313
314    /// Get a value by index without cloning.
315    #[inline]
316    pub fn get(&self, idx: usize) -> Option<&Value> {
317        match self {
318            RowRef::Owned(row) => row.get(idx),
319            RowRef::Composite(comp) => comp.get(idx),
320            RowRef::DirectBuildComposite(direct) => direct.get(idx),
321            RowRef::Shared(shared) => shared.get(idx),
322            RowRef::Projected(projected) => projected.get(idx),
323            RowRef::Deferred(deferred) => deferred.get(idx),
324        }
325    }
326
327    /// Convert to an owned Row.
328    ///
329    /// For `Owned`, this is a no-op move.
330    /// For `Composite` and `DirectBuildComposite`, this materializes the combined row.
331    #[inline]
332    pub fn into_owned(self) -> Row {
333        match self {
334            RowRef::Owned(row) => row,
335            // Use materialize_owned to move values instead of cloning
336            RowRef::Composite(comp) => comp.materialize_owned(),
337            RowRef::DirectBuildComposite(direct) => direct.materialize_owned(),
338            RowRef::Shared(shared) => shared.materialize_owned(),
339            RowRef::Projected(projected) => projected.materialize_owned(),
340            RowRef::Deferred(deferred) => deferred.into_owned(),
341        }
342    }
343
344    /// Clone to an owned Row.
345    ///
346    /// Use `into_owned()` when possible to avoid cloning.
347    pub fn to_owned(&self) -> Row {
348        match self {
349            RowRef::Owned(row) => row.clone(),
350            RowRef::Composite(comp) => comp.materialize(),
351            RowRef::DirectBuildComposite(direct) => direct.materialize(),
352            RowRef::Shared(shared) => shared.materialize(),
353            RowRef::Projected(projected) => projected.materialize(),
354            RowRef::Deferred(deferred) => deferred.to_owned(),
355        }
356    }
357
358    /// Get a reference to the underlying Row if this is Owned.
359    #[inline]
360    pub fn as_row(&self) -> Option<&Row> {
361        match self {
362            RowRef::Owned(row) => Some(row),
363            RowRef::Shared(shared) => Some(shared.row()),
364            RowRef::Composite(_)
365            | RowRef::DirectBuildComposite(_)
366            | RowRef::Projected(_)
367            | RowRef::Deferred(_) => None,
368        }
369    }
370
371    /// Create a direct-build composite RowRef.
372    ///
373    /// OPTIMIZATION: Avoids Arc allocation for probe row.
374    /// Use this for 1:1 joins where probe row is owned and not shared.
375    #[inline]
376    pub fn direct_build_composite(
377        probe: Row,
378        build_rows: CompactArc<Vec<Row>>,
379        build_idx: usize,
380        probe_is_left: bool,
381    ) -> Self {
382        RowRef::DirectBuildComposite(DirectBuildCompositeRow::new(
383            probe,
384            build_rows,
385            build_idx,
386            probe_is_left,
387        ))
388    }
389
390    /// Reference one row in an immutable shared batch.
391    #[inline]
392    pub fn shared(rows: CompactArc<Vec<Row>>, row_idx: usize) -> Self {
393        RowRef::Shared(SharedRow::new(rows, row_idx))
394    }
395
396    /// Create a deferred projected row for a JOIN output.
397    #[inline]
398    pub fn projected(left: RowRef, right: RowRef, columns: CompactArc<[ColumnSource]>) -> Self {
399        RowRef::Projected(ProjectedRow::new(left, right, columns))
400    }
401
402    /// Restore a compact row received from an internal QueryResult boundary.
403    #[inline]
404    pub fn deferred(row: DeferredRow) -> Self {
405        RowRef::Deferred(row)
406    }
407
408    /// Convert into the portable representation used between recursive JOINs.
409    pub fn into_deferred(self) -> DeferredRow {
410        match self {
411            RowRef::Owned(row) => DeferredRow::owned(row),
412            RowRef::Shared(SharedRow { rows, row_idx }) => DeferredRow::shared(rows, row_idx),
413            RowRef::Projected(ProjectedRow {
414                left,
415                right,
416                columns,
417            }) => {
418                let columns = columns
419                    .iter()
420                    .map(|source| match source {
421                        ColumnSource::Outer(index) => DeferredColumnSource::Left(*index),
422                        ColumnSource::Inner(index) => DeferredColumnSource::Right(*index),
423                    })
424                    .collect::<Vec<_>>();
425                DeferredRow::projected(
426                    left.into_deferred(),
427                    right.into_deferred(),
428                    CompactArc::from(columns),
429                )
430            }
431            // These legacy unprojected shapes move complete rows already. Keep
432            // their established materialization behavior until JR-09 replaces
433            // the binary JoinResult<RowVec> boundary altogether.
434            RowRef::Composite(composite) => DeferredRow::owned(composite.materialize_owned()),
435            RowRef::DirectBuildComposite(composite) => {
436                DeferredRow::owned(composite.materialize_owned())
437            }
438            RowRef::Deferred(row) => row,
439        }
440    }
441
442    /// Whether this row still carries a deferred JOIN representation.
443    #[inline]
444    pub fn is_deferred(&self) -> bool {
445        match self {
446            RowRef::Owned(_) => false,
447            RowRef::Deferred(row) => row.is_deferred(),
448            _ => true,
449        }
450    }
451
452    /// Conservative size of the request-local row graph retained by this
453    /// handle. Arc-backed immutable batches are owned and charged elsewhere;
454    /// this method accounts only for the handle/graph added by a pull batch.
455    pub fn estimated_retained_bytes(&self) -> usize {
456        fn row_bytes(row: &Row) -> usize {
457            row.iter().fold(std::mem::size_of::<Row>(), |total, value| {
458                let payload = match value {
459                    Value::Text(text) => text.len(),
460                    Value::Extension(bytes) => bytes.len(),
461                    _ => 0,
462                };
463                total
464                    .saturating_add(std::mem::size_of::<Value>())
465                    .saturating_add(payload)
466            })
467        }
468
469        match self {
470            Self::Owned(row) => row_bytes(row),
471            Self::Shared(_) => std::mem::size_of::<Self>(),
472            Self::Composite(row) => std::mem::size_of::<Self>()
473                .saturating_add(row_bytes(&row.left))
474                .saturating_add(row_bytes(&row.right)),
475            Self::DirectBuildComposite(row) => {
476                std::mem::size_of::<Self>().saturating_add(row_bytes(&row.probe))
477            }
478            Self::Projected(row) => std::mem::size_of::<Self>()
479                .saturating_add(row.left.estimated_retained_bytes())
480                .saturating_add(row.right.estimated_retained_bytes())
481                .saturating_add(
482                    row.columns
483                        .len()
484                        .saturating_mul(std::mem::size_of::<ColumnSource>()),
485                ),
486            Self::Deferred(row) => row.estimated_retained_bytes(),
487        }
488    }
489}
490
491/// Arc-backed reference to one row in a materialized batch.
492#[derive(Debug, Clone)]
493pub struct SharedRow {
494    rows: CompactArc<Vec<Row>>,
495    row_idx: usize,
496}
497
498impl SharedRow {
499    #[inline]
500    pub fn new(rows: CompactArc<Vec<Row>>, row_idx: usize) -> Self {
501        assert!(
502            row_idx < rows.len(),
503            "shared row index outside immutable batch"
504        );
505        Self { rows, row_idx }
506    }
507
508    #[inline]
509    fn row(&self) -> &Row {
510        &self.rows[self.row_idx]
511    }
512
513    #[inline]
514    pub fn len(&self) -> usize {
515        self.row().len()
516    }
517
518    #[inline]
519    pub fn is_empty(&self) -> bool {
520        self.row().is_empty()
521    }
522
523    #[inline]
524    pub fn get(&self, idx: usize) -> Option<&Value> {
525        self.row().get(idx)
526    }
527
528    #[inline]
529    pub fn materialize(&self) -> Row {
530        self.row().clone()
531    }
532
533    #[inline]
534    pub fn materialize_owned(self) -> Row {
535        self.row().clone()
536    }
537}
538
539/// A projected row whose slots still reference the preceding JOIN edge.
540///
541/// The outer side may itself be projected, so a long selective JOIN chain is
542/// represented as a shallow sequence of slot maps instead of repeatedly
543/// copying every retained payload value into a new `Row` at each edge.
544#[derive(Debug, Clone)]
545pub struct ProjectedRow {
546    left: Box<RowRef>,
547    right: Box<RowRef>,
548    columns: CompactArc<[ColumnSource]>,
549}
550
551impl ProjectedRow {
552    #[inline]
553    pub fn new(left: RowRef, right: RowRef, columns: CompactArc<[ColumnSource]>) -> Self {
554        Self {
555            left: Box::new(left),
556            right: Box::new(right),
557            columns,
558        }
559    }
560
561    #[inline]
562    pub fn len(&self) -> usize {
563        self.columns.len()
564    }
565
566    #[inline]
567    pub fn is_empty(&self) -> bool {
568        self.columns.is_empty()
569    }
570
571    #[inline]
572    pub fn get(&self, idx: usize) -> Option<&Value> {
573        match self.columns.get(idx)? {
574            ColumnSource::Outer(index) => self.left.get(*index),
575            ColumnSource::Inner(index) => self.right.get(*index),
576        }
577    }
578
579    pub fn materialize(&self) -> Row {
580        let mut values = CompactVec::with_capacity(self.columns.len());
581        for index in 0..self.columns.len() {
582            values.push(self.get(index).cloned().unwrap_or(NULL_VALUE));
583        }
584        Row::from_compact_vec(values)
585    }
586
587    #[inline]
588    pub fn materialize_owned(self) -> Row {
589        // Projection may reorder or repeat slots, so consuming the source rows
590        // cannot generally move their values. Materialize once at the final
591        // ownership boundary instead of once per JOIN edge.
592        self.materialize()
593    }
594}
595
596impl fmt::Display for ProjectedRow {
597    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
598        write!(f, "(")?;
599        for index in 0..self.len() {
600            if index > 0 {
601                write!(f, ", ")?;
602            }
603            match self.get(index) {
604                Some(value) => write!(f, "{value}")?,
605                None => write!(f, "NULL")?,
606            }
607        }
608        write!(f, ")")
609    }
610}
611
612/// A composite row that references values from two source rows.
613///
614/// This is the key optimization for joins - instead of cloning all values
615/// from both the left and right rows into a new row, we keep references
616/// to both and provide a unified view.
617///
618/// # Memory Layout
619///
620/// ```text
621/// CompositeRow
622/// ├── left: Row (owned)
623/// ├── right: Row (owned)
624/// └── left_cols: usize
625///
626/// Logical columns: [left_col_0, left_col_1, ..., right_col_0, right_col_1, ...]
627///                  |<--- left_cols --->|<--- right cols --->|
628/// ```
629#[derive(Debug, Clone)]
630pub struct CompositeRow {
631    /// Left side of the join (probe row in hash join)
632    left: Row,
633    /// Right side of the join (build row in hash join)
634    right: Row,
635    /// Number of columns from the left side
636    left_cols: usize,
637}
638
639impl CompositeRow {
640    /// Create a new composite row from left and right parts.
641    #[inline]
642    pub fn new(left: Row, right: Row) -> Self {
643        let left_cols = left.len();
644        Self {
645            left,
646            right,
647            left_cols,
648        }
649    }
650
651    /// Get the total number of columns.
652    #[inline]
653    pub fn len(&self) -> usize {
654        self.left_cols + self.right.len()
655    }
656
657    /// Check if this composite row is empty.
658    #[inline]
659    pub fn is_empty(&self) -> bool {
660        self.left.is_empty() && self.right.is_empty()
661    }
662
663    /// Get a value by index without cloning.
664    ///
665    /// Indexes 0..left_cols return from the left row.
666    /// Indexes left_cols..total return from the right row.
667    #[inline]
668    pub fn get(&self, idx: usize) -> Option<&Value> {
669        if idx < self.left_cols {
670            self.left.get(idx)
671        } else {
672            self.right.get(idx - self.left_cols)
673        }
674    }
675
676    /// Get a reference to the left row.
677    #[inline]
678    pub fn left(&self) -> &Row {
679        &self.left
680    }
681
682    /// Get a reference to the right row.
683    #[inline]
684    pub fn right(&self) -> &Row {
685        &self.right
686    }
687
688    /// Materialize into an owned Row (cloning version).
689    ///
690    /// This creates a single Row by copying all values from both sides.
691    /// Only call this when the final result needs to be stored.
692    /// Prefer `materialize_owned()` when you can consume the CompositeRow.
693    pub fn materialize(&self) -> Row {
694        let total = self.len();
695        let mut values: CompactVec<Value> = CompactVec::with_capacity(total);
696
697        // Copy left values using extend_clone for efficiency
698        values.extend_clone(self.left.as_slice());
699
700        // Copy right values using extend_clone for efficiency
701        values.extend_clone(self.right.as_slice());
702
703        Row::from_compact_vec(values)
704    }
705
706    /// Materialize into an owned Row by moving values (zero-copy).
707    ///
708    /// This consumes the CompositeRow and moves all values without cloning.
709    /// Use this instead of `materialize()` when you no longer need the CompositeRow.
710    #[inline]
711    pub fn materialize_owned(self) -> Row {
712        Row::from_combined_owned(self.left, self.right)
713    }
714
715    /// Decompose into the left and right rows.
716    #[inline]
717    pub fn into_parts(self) -> (Row, Row) {
718        (self.left, self.right)
719    }
720}
721
722impl fmt::Display for CompositeRow {
723    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
724        write!(f, "(")?;
725        for i in 0..self.len() {
726            if i > 0 {
727                write!(f, ", ")?;
728            }
729            if let Some(v) = self.get(i) {
730                write!(f, "{}", v)?;
731            } else {
732                write!(f, "NULL")?;
733            }
734        }
735        write!(f, ")")
736    }
737}
738
739/// A direct-build composite row that owns the probe row directly.
740///
741/// This stores the probe Row directly without Arc wrapping, saving one Arc
742/// allocation per matched row. The build rows are shared via Arc for
743/// efficient access.
744#[derive(Debug)]
745pub struct DirectBuildCompositeRow {
746    /// Probe row (owned directly, no Arc overhead)
747    probe: Row,
748    /// Shared reference to build rows
749    build_rows: CompactArc<Vec<Row>>,
750    /// Index into build_rows
751    build_idx: usize,
752    /// Number of columns from the probe side
753    probe_cols: usize,
754    /// Whether probe is left side (true) or right side (false)
755    probe_is_left: bool,
756}
757
758impl DirectBuildCompositeRow {
759    /// Create a new direct-build composite row.
760    #[inline]
761    pub fn new(
762        probe: Row,
763        build_rows: CompactArc<Vec<Row>>,
764        build_idx: usize,
765        probe_is_left: bool,
766    ) -> Self {
767        debug_assert!(
768            build_idx < build_rows.len(),
769            "build_idx {} out of bounds (len={})",
770            build_idx,
771            build_rows.len()
772        );
773        let probe_cols = probe.len();
774        Self {
775            probe,
776            build_rows,
777            build_idx,
778            probe_cols,
779            probe_is_left,
780        }
781    }
782
783    /// Get the total number of columns.
784    #[inline]
785    pub fn len(&self) -> usize {
786        self.probe_cols + self.build_rows[self.build_idx].len()
787    }
788
789    /// Check if this row is empty.
790    #[inline]
791    pub fn is_empty(&self) -> bool {
792        self.probe.is_empty() && self.build_rows[self.build_idx].is_empty()
793    }
794
795    /// Get a value by index without cloning.
796    #[inline]
797    pub fn get(&self, idx: usize) -> Option<&Value> {
798        let build_row = &self.build_rows[self.build_idx];
799        if self.probe_is_left {
800            // Output: [probe, build]
801            if idx < self.probe_cols {
802                self.probe.get(idx)
803            } else {
804                build_row.get(idx - self.probe_cols)
805            }
806        } else {
807            // Output: [build, probe]
808            let build_cols = build_row.len();
809            if idx < build_cols {
810                build_row.get(idx)
811            } else {
812                self.probe.get(idx - build_cols)
813            }
814        }
815    }
816
817    /// Materialize into an owned Row (cloning version).
818    pub fn materialize(&self) -> Row {
819        let build_row = &self.build_rows[self.build_idx];
820        let total = self.probe_cols + build_row.len();
821        let mut values: CompactVec<Value> = CompactVec::with_capacity(total);
822
823        if self.probe_is_left {
824            values.extend_clone(self.probe.as_slice());
825            values.extend_clone(build_row.as_slice());
826        } else {
827            values.extend_clone(build_row.as_slice());
828            values.extend_clone(self.probe.as_slice());
829        }
830
831        Row::from_compact_vec(values)
832    }
833
834    /// Materialize into an owned Row by moving probe values.
835    ///
836    /// Combines probe and build rows efficiently:
837    /// - Moves probe values (owned) - uses extend_into_compact_vec to avoid Vec allocation
838    /// - Clones build values (shared reference)
839    #[inline]
840    pub fn materialize_owned(self) -> Row {
841        let build_row = &self.build_rows[self.build_idx];
842        let total = self.probe_cols + build_row.len();
843
844        let mut values: CompactVec<Value> = CompactVec::with_capacity(total);
845        if self.probe_is_left {
846            // Use extend_into_compact_vec to avoid intermediate Vec allocation
847            self.probe.extend_into_compact_vec(&mut values);
848            values.extend_clone(build_row.as_slice());
849        } else {
850            values.extend_clone(build_row.as_slice());
851            // Use extend_into_compact_vec to avoid intermediate Vec allocation
852            self.probe.extend_into_compact_vec(&mut values);
853        }
854        Row::from_compact_vec(values)
855    }
856}
857
858impl Clone for DirectBuildCompositeRow {
859    fn clone(&self) -> Self {
860        Self {
861            probe: self.probe.clone(),
862            build_rows: CompactArc::clone(&self.build_rows),
863            build_idx: self.build_idx,
864            probe_cols: self.probe_cols,
865            probe_is_left: self.probe_is_left,
866        }
867    }
868}
869
870impl fmt::Display for DirectBuildCompositeRow {
871    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
872        write!(f, "(")?;
873        for i in 0..self.len() {
874            if i > 0 {
875                write!(f, ", ")?;
876            }
877            if let Some(v) = self.get(i) {
878                write!(f, "{}", v)?;
879            } else {
880                write!(f, "NULL")?;
881            }
882        }
883        write!(f, ")")
884    }
885}
886
887// ============================================================================
888// Helper Operators
889// ============================================================================
890
891/// An empty operator that produces no rows.
892///
893/// Useful as a placeholder or for empty result sets.
894pub struct EmptyOperator {
895    schema: Vec<ColumnInfo>,
896    opened: bool,
897}
898
899impl EmptyOperator {
900    /// Create an empty operator with no schema.
901    pub fn new() -> Self {
902        Self {
903            schema: Vec::new(),
904            opened: false,
905        }
906    }
907}
908
909impl Default for EmptyOperator {
910    fn default() -> Self {
911        Self::new()
912    }
913}
914
915impl Operator for EmptyOperator {
916    fn open(&mut self) -> Result<()> {
917        self.opened = true;
918        Ok(())
919    }
920
921    fn next(&mut self) -> Result<Option<RowRef>> {
922        Ok(None)
923    }
924
925    fn close(&mut self) -> Result<()> {
926        Ok(())
927    }
928
929    fn schema(&self) -> &[ColumnInfo] {
930        &self.schema
931    }
932
933    fn name(&self) -> &str {
934        "Empty"
935    }
936}
937
938/// An operator that yields rows from a pre-materialized vector.
939///
940/// This is useful for:
941/// - Converting existing `Vec<Row>` results to the operator model
942/// - CTEs that have been pre-computed
943/// - Subquery results
944pub struct MaterializedOperator {
945    rows: Vec<Row>,
946    schema: Vec<ColumnInfo>,
947    ordering: OrderingProperty,
948    current_idx: usize,
949    opened: bool,
950}
951
952impl MaterializedOperator {
953    /// Create an operator from a vector of rows.
954    pub fn new(rows: Vec<Row>, schema: Vec<ColumnInfo>) -> Self {
955        Self {
956            rows,
957            schema,
958            ordering: OrderingProperty::Unknown,
959            current_idx: 0,
960            opened: false,
961        }
962    }
963
964    /// Attach ordering already proven by the producing physical operator.
965    ///
966    /// This method does not inspect rows.  Callers must propagate a genuine
967    /// physical certificate rather than infer one from current contents.
968    pub fn with_ordering(mut self, ordering: OrderingProperty) -> Self {
969        self.ordering = ordering;
970        self
971    }
972
973    /// Create from a `CompactArc<Vec<Row>>`, unwrapping if sole owner or cloning if shared.
974    /// This is optimal for CTE results which may have multiple references.
975    pub fn from_arc(arc_rows: CompactArc<Vec<Row>>, schema: Vec<ColumnInfo>) -> Self {
976        let rows = CompactArc::try_unwrap(arc_rows).unwrap_or_else(|arc| (*arc).clone());
977        Self::new(rows, schema)
978    }
979}
980
981impl Operator for MaterializedOperator {
982    fn open(&mut self) -> Result<()> {
983        self.current_idx = 0;
984        self.opened = true;
985        Ok(())
986    }
987
988    fn next(&mut self) -> Result<Option<RowRef>> {
989        if self.current_idx >= self.rows.len() {
990            return Ok(None);
991        }
992
993        // Take ownership of the row, leaving an empty Row in its place.
994        // This is O(1) instead of clone() which is O(n) for row width.
995        // Safe because we only iterate forward and never revisit rows.
996        let row = std::mem::take(&mut self.rows[self.current_idx]);
997        self.current_idx += 1;
998        Ok(Some(RowRef::Owned(row)))
999    }
1000
1001    fn close(&mut self) -> Result<()> {
1002        Ok(())
1003    }
1004
1005    fn schema(&self) -> &[ColumnInfo] {
1006        &self.schema
1007    }
1008
1009    fn estimated_rows(&self) -> Option<usize> {
1010        Some(self.rows.len())
1011    }
1012
1013    fn ordering(&self) -> OrderingProperty {
1014        self.ordering.clone()
1015    }
1016
1017    fn name(&self) -> &str {
1018        "Materialized"
1019    }
1020}
1021
1022// ============================================================================
1023// QueryResult to Operator Adapter
1024// ============================================================================
1025
1026use radixdb_storage::QueryResult as StorageQueryResult;
1027
1028/// Operator that streams rows from a QueryResult.
1029///
1030/// This adapter allows existing QueryResult (from table scans, etc.)
1031/// to be used in the streaming operator pipeline. Unlike MaterializedOperator,
1032/// this does NOT load all rows upfront - it streams them on demand.
1033///
1034/// # Benefits
1035///
1036/// - **Memory efficient**: Only one row in memory at a time
1037/// - **Early termination**: LIMIT stops reading immediately
1038/// - **Streaming pipeline**: Fits into Volcano execution model
1039pub struct QueryResultOperator {
1040    result: Box<dyn StorageQueryResult>,
1041    schema: Vec<ColumnInfo>,
1042    ordering: OrderingProperty,
1043    opened: bool,
1044}
1045
1046impl QueryResultOperator {
1047    /// Create a new streaming operator from a QueryResult.
1048    pub fn new(result: Box<dyn StorageQueryResult>, columns: Vec<String>) -> Self {
1049        let ordering = result.ascending_nulls_last_ordering().map_or(
1050            OrderingProperty::Unknown,
1051            OrderingProperty::ascending_nulls_last,
1052        );
1053        let schema = columns.into_iter().map(ColumnInfo::new).collect();
1054        Self {
1055            result,
1056            schema,
1057            ordering,
1058            opened: false,
1059        }
1060    }
1061}
1062
1063impl Operator for QueryResultOperator {
1064    fn open(&mut self) -> Result<()> {
1065        radixdb_storage::instrumentation::record_join_source_open(self.opened);
1066        self.opened = true;
1067        Ok(())
1068    }
1069
1070    fn next(&mut self) -> Result<Option<RowRef>> {
1071        if !self.opened {
1072            return Ok(None);
1073        }
1074
1075        if self.result.next() {
1076            // Preserve an internal deferred JOIN row when available. Ordinary
1077            // QueryResult implementations return an owned row through the
1078            // trait's compatibility default.
1079            Ok(Some(RowRef::deferred(self.result.take_deferred_row())))
1080        } else if let Some(error) = self.result.last_error() {
1081            // A scanner result signals I/O/filter failures after `next()` has
1082            // returned false. Operators must not translate that into a clean
1083            // end-of-stream (especially count-only paths, where it would yield
1084            // a plausible but wrong scalar result).
1085            Err(error)
1086        } else {
1087            Ok(None)
1088        }
1089    }
1090
1091    fn close(&mut self) -> Result<()> {
1092        self.result.close()
1093    }
1094
1095    fn schema(&self) -> &[ColumnInfo] {
1096        &self.schema
1097    }
1098
1099    fn estimated_rows(&self) -> Option<usize> {
1100        // QueryResult doesn't expose count, return None
1101        None
1102    }
1103
1104    fn ordering(&self) -> OrderingProperty {
1105        self.ordering.clone()
1106    }
1107
1108    fn name(&self) -> &str {
1109        "QueryResultScan"
1110    }
1111}
1112
1113#[cfg(test)]
1114#[allow(clippy::approx_constant)]
1115mod tests {
1116    use super::*;
1117
1118    #[test]
1119    fn join_projection_rejects_shape_and_side_indices_before_execution() {
1120        let wrong_width = JoinProjection {
1121            columns: vec![ColumnSource::Outer(0)],
1122        };
1123        assert!(wrong_width.validate(1, 1, 2).is_err());
1124
1125        let bad_left = JoinProjection {
1126            columns: vec![ColumnSource::Outer(1)],
1127        };
1128        assert!(bad_left.validate(1, 1, 1).is_err());
1129
1130        let bad_right = JoinProjection {
1131            columns: vec![ColumnSource::Inner(1)],
1132        };
1133        assert!(bad_right.validate(1, 1, 1).is_err());
1134
1135        let valid = JoinProjection {
1136            columns: vec![ColumnSource::Inner(0), ColumnSource::Outer(0)],
1137        };
1138        valid.validate(1, 1, 2).unwrap();
1139    }
1140
1141    #[test]
1142    fn test_composite_row_basic() {
1143        let left = Row::from_values(vec![Value::integer(1), Value::text("hello")]);
1144        let right = Row::from_values(vec![Value::float(3.14), Value::boolean(true)]);
1145
1146        let comp = CompositeRow::new(left, right);
1147
1148        assert_eq!(comp.len(), 4);
1149        assert_eq!(comp.get(0), Some(&Value::integer(1)));
1150        assert_eq!(comp.get(1), Some(&Value::text("hello")));
1151        assert_eq!(comp.get(2), Some(&Value::float(3.14)));
1152        assert_eq!(comp.get(3), Some(&Value::boolean(true)));
1153        assert_eq!(comp.get(4), None);
1154    }
1155
1156    #[test]
1157    fn test_composite_row_materialize() {
1158        let left = Row::from_values(vec![Value::integer(1)]);
1159        let right = Row::from_values(vec![Value::integer(2)]);
1160
1161        let comp = CompositeRow::new(left, right);
1162        let materialized = comp.materialize();
1163
1164        assert_eq!(materialized.len(), 2);
1165        assert_eq!(materialized.get(0), Some(&Value::integer(1)));
1166        assert_eq!(materialized.get(1), Some(&Value::integer(2)));
1167    }
1168
1169    #[test]
1170    fn test_row_ref_owned() {
1171        let row = Row::from_values(vec![Value::integer(42)]);
1172        let row_ref = RowRef::owned(row);
1173
1174        assert_eq!(row_ref.len(), 1);
1175        assert_eq!(row_ref.get(0), Some(&Value::integer(42)));
1176
1177        let owned = row_ref.into_owned();
1178        assert_eq!(owned.get(0), Some(&Value::integer(42)));
1179    }
1180
1181    #[test]
1182    fn test_row_ref_composite() {
1183        let left = Row::from_values(vec![Value::integer(1)]);
1184        let right = Row::from_values(vec![Value::integer(2)]);
1185        let row_ref = RowRef::composite(left, right);
1186
1187        assert_eq!(row_ref.len(), 2);
1188        assert_eq!(row_ref.get(0), Some(&Value::integer(1)));
1189        assert_eq!(row_ref.get(1), Some(&Value::integer(2)));
1190    }
1191
1192    #[test]
1193    fn projected_row_ref_keeps_transitive_join_slots_deferred() {
1194        let first = RowRef::projected(
1195            RowRef::owned(Row::from_values(vec![
1196                Value::integer(1),
1197                Value::text("payload"),
1198            ])),
1199            RowRef::owned(Row::from_values(vec![
1200                Value::integer(10),
1201                Value::text("dictionary"),
1202            ])),
1203            CompactArc::from(vec![
1204                ColumnSource::Outer(0),
1205                ColumnSource::Outer(1),
1206                ColumnSource::Inner(1),
1207            ]),
1208        );
1209        let second = RowRef::projected(
1210            first,
1211            RowRef::owned(Row::from_values(vec![
1212                Value::integer(20),
1213                Value::text("leaf"),
1214            ])),
1215            CompactArc::from(vec![
1216                ColumnSource::Outer(1),
1217                ColumnSource::Inner(1),
1218                ColumnSource::Outer(2),
1219            ]),
1220        );
1221
1222        assert!(second.is_deferred());
1223        assert_eq!(second.len(), 3);
1224        assert_eq!(second.get(0), Some(&Value::text("payload")));
1225        assert_eq!(second.get(1), Some(&Value::text("leaf")));
1226        assert_eq!(second.get(2), Some(&Value::text("dictionary")));
1227
1228        let materialized = second.into_owned();
1229        assert_eq!(
1230            materialized,
1231            Row::from_values(vec![
1232                Value::text("payload"),
1233                Value::text("leaf"),
1234                Value::text("dictionary"),
1235            ])
1236        );
1237    }
1238
1239    #[test]
1240    fn test_empty_operator() {
1241        let mut op = EmptyOperator::new();
1242        op.open().unwrap();
1243
1244        assert!(op.next().unwrap().is_none());
1245        assert!(op.next().unwrap().is_none());
1246
1247        op.close().unwrap();
1248    }
1249
1250    #[test]
1251    fn test_materialized_operator() {
1252        let rows = vec![
1253            Row::from_values(vec![Value::integer(1)]),
1254            Row::from_values(vec![Value::integer(2)]),
1255            Row::from_values(vec![Value::integer(3)]),
1256        ];
1257        let schema = vec![ColumnInfo::new("id")];
1258
1259        let mut op = MaterializedOperator::new(rows, schema);
1260        op.open().unwrap();
1261
1262        let row1 = op.next().unwrap().unwrap();
1263        assert_eq!(row1.get(0), Some(&Value::integer(1)));
1264
1265        let row2 = op.next().unwrap().unwrap();
1266        assert_eq!(row2.get(0), Some(&Value::integer(2)));
1267
1268        let row3 = op.next().unwrap().unwrap();
1269        assert_eq!(row3.get(0), Some(&Value::integer(3)));
1270
1271        assert!(op.next().unwrap().is_none());
1272
1273        op.close().unwrap();
1274    }
1275}