Skip to main content

alopex_sql/executor/query/
mod.rs

1use alopex_core::kv::KVStore;
2use alopex_core::sql::stream::ByteSized;
3use std::collections::{HashMap, HashSet};
4
5use crate::ast::LITERAL_TABLE;
6use crate::catalog::{Catalog, StorageType};
7use crate::executor::evaluator::EvalContext;
8use crate::executor::memory::{MemoryPolicy, MemoryTracker, map_core_memory_error};
9use crate::executor::{ExecutionResult, ExecutorError, QueryResult, QueryRowIterator, Result};
10use crate::planner::logical_plan::{LogicalPlan, RecursiveCteLimits, SetOperator};
11use crate::planner::typed_expr::{Projection, SortExpr};
12use crate::storage::{SqlTxn, SqlValue};
13
14use super::{ColumnInfo, Row};
15
16pub mod aggregate;
17pub mod columnar_scan;
18pub mod iterator;
19pub mod join;
20mod knn;
21mod project;
22mod scan;
23pub mod subquery;
24pub mod window;
25
26pub use columnar_scan::{ColumnarScanIterator, create_columnar_scan_iterator};
27pub use iterator::{
28    DistinctOnIterator, FilterIterator, LimitIterator, RowIterator, ScanIterator, SortIterator,
29};
30pub use project::{project_row_values, projected_columns};
31pub use scan::{
32    create_fenced_range_scan_iterator, create_scan_iterator, execute_fenced_range_scan,
33};
34
35#[derive(Clone)]
36struct RecursiveWorkingTable {
37    rows: Vec<Vec<SqlValue>>,
38    schema: Vec<crate::catalog::ColumnMetadata>,
39}
40
41/// Per-query state supplied explicitly to operators that can read a recursive
42/// working table. A fresh context is created at each public execution entry.
43#[derive(Clone, Default)]
44struct QueryExecutionContext {
45    recursive_tables: HashMap<String, RecursiveWorkingTable>,
46}
47
48struct RecursiveCteExecution {
49    name: String,
50    anchor: LogicalPlan,
51    recursive_term: LogicalPlan,
52    union_all: bool,
53    schema: Vec<crate::catalog::ColumnMetadata>,
54    limits: RecursiveCteLimits,
55}
56
57impl QueryExecutionContext {
58    fn with_recursive_table(
59        &self,
60        name: String,
61        table: RecursiveWorkingTable,
62    ) -> QueryExecutionContext {
63        let mut next = self.clone();
64        next.recursive_tables.insert(name, table);
65        next
66    }
67}
68
69fn plan_contains_recursive_cte(plan: &LogicalPlan) -> bool {
70    match plan {
71        LogicalPlan::RecursiveCte { .. } | LogicalPlan::RecursiveReference { .. } => true,
72        LogicalPlan::Filter { input, .. }
73        | LogicalPlan::Project { input, .. }
74        | LogicalPlan::Aggregate { input, .. }
75        | LogicalPlan::Window { input, .. }
76        | LogicalPlan::Sort { input, .. }
77        | LogicalPlan::DistinctOn { input, .. }
78        | LogicalPlan::Limit { input, .. } => plan_contains_recursive_cte(input),
79        LogicalPlan::Join { left, right, .. }
80        | LogicalPlan::LateralJoin { left, right, .. }
81        | LogicalPlan::SetOperation { left, right, .. } => {
82            plan_contains_recursive_cte(left) || plan_contains_recursive_cte(right)
83        }
84        _ => false,
85    }
86}
87
88/// Whether the plan holds a LATERAL join or a table function.
89///
90/// Both are evaluated per outer row through the materializing path, which owns
91/// the transaction while it re-executes the correlated side. The streaming
92/// pipeline borrows the transaction exclusively and cannot do that, so plans
93/// containing either are routed the same way subquery plans are.
94fn plan_contains_lateral(plan: &LogicalPlan) -> bool {
95    match plan {
96        LogicalPlan::LateralJoin { .. } | LogicalPlan::TableFunction { .. } => true,
97        LogicalPlan::Filter { input, .. }
98        | LogicalPlan::Project { input, .. }
99        | LogicalPlan::Aggregate { input, .. }
100        | LogicalPlan::Window { input, .. }
101        | LogicalPlan::Sort { input, .. }
102        | LogicalPlan::DistinctOn { input, .. }
103        | LogicalPlan::Limit { input, .. } => plan_contains_lateral(input),
104        LogicalPlan::Join { left, right, .. } | LogicalPlan::SetOperation { left, right, .. } => {
105            plan_contains_lateral(left) || plan_contains_lateral(right)
106        }
107        LogicalPlan::RecursiveCte {
108            anchor,
109            recursive_term,
110            ..
111        } => plan_contains_lateral(anchor) || plan_contains_lateral(recursive_term),
112        _ => false,
113    }
114}
115
116/// Execute a SELECT logical plan and return a query result.
117///
118/// This function uses an iterator-based execution model that processes rows
119/// through a pipeline of operators. This approach:
120/// - Enables early termination for LIMIT queries
121/// - Provides streaming execution after the initial scan
122/// - Allows composable query operators
123///
124/// Note: The Scan stage reads all matching rows into memory, but subsequent
125/// operators (Filter, Sort, Limit) process rows through an iterator pipeline.
126/// Sort operations additionally require materializing all input rows.
127pub fn execute_query<'txn, S: KVStore + 'txn, C: Catalog + ?Sized, T: SqlTxn<'txn, S>>(
128    txn: &mut T,
129    catalog: &C,
130    plan: LogicalPlan,
131) -> Result<ExecutionResult> {
132    execute_query_with_policy(txn, catalog, plan, None)
133}
134
135pub fn execute_query_with_policy<
136    'txn,
137    S: KVStore + 'txn,
138    C: Catalog + ?Sized,
139    T: SqlTxn<'txn, S>,
140>(
141    txn: &mut T,
142    catalog: &C,
143    plan: LogicalPlan,
144    memory: Option<&MemoryPolicy>,
145) -> Result<ExecutionResult> {
146    if let Some((pattern, projection, filter)) = knn::extract_knn_context(&plan) {
147        return knn::execute_knn_query(txn, catalog, &pattern, &projection, filter.as_ref());
148    }
149
150    let result = execute_query_result_with_outer_and_policy(txn, catalog, plan, None, memory)?;
151    Ok(ExecutionResult::Query(result))
152}
153
154pub(crate) fn execute_query_result_with_outer<
155    'txn,
156    S: KVStore + 'txn,
157    C: Catalog + ?Sized,
158    T: SqlTxn<'txn, S>,
159>(
160    txn: &mut T,
161    catalog: &C,
162    plan: LogicalPlan,
163    outer: Option<&Row>,
164) -> Result<QueryResult> {
165    execute_query_result_with_outer_and_policy(txn, catalog, plan, outer, None)
166}
167
168fn execute_query_result_with_outer_and_policy<
169    'txn,
170    S: KVStore + 'txn,
171    C: Catalog + ?Sized,
172    T: SqlTxn<'txn, S>,
173>(
174    txn: &mut T,
175    catalog: &C,
176    plan: LogicalPlan,
177    outer: Option<&Row>,
178    memory: Option<&MemoryPolicy>,
179) -> Result<QueryResult> {
180    execute_query_result_with_context(
181        txn,
182        catalog,
183        plan,
184        outer,
185        memory,
186        &QueryExecutionContext::default(),
187    )
188}
189
190fn execute_query_result_with_context<
191    'txn,
192    S: KVStore + 'txn,
193    C: Catalog + ?Sized,
194    T: SqlTxn<'txn, S>,
195>(
196    txn: &mut T,
197    catalog: &C,
198    plan: LogicalPlan,
199    outer: Option<&Row>,
200    memory: Option<&MemoryPolicy>,
201    context: &QueryExecutionContext,
202) -> Result<QueryResult> {
203    let (mut iter, projection, schema) =
204        build_iterator_pipeline_with_outer(txn, catalog, plan, memory, outer, context)?;
205    let mut rows = Vec::new();
206    while let Some(result) = iter.next_row() {
207        rows.push(result?);
208    }
209    execute_project_with_subqueries(txn, catalog, rows, &projection, &schema, outer)
210}
211
212/// Execute a SELECT logical plan and return a streaming query result.
213///
214/// This function returns a `QueryRowIterator` that yields rows one at a time,
215/// enabling true streaming output without materializing all rows upfront.
216///
217/// # FR-7 Streaming Output
218///
219/// This function implements the FR-7 requirement for streaming output.
220/// Rows are yielded through an iterator interface, and projection is applied
221/// on-the-fly as each row is consumed.
222///
223/// # Note
224///
225/// KNN queries currently fall back to the non-streaming path as they require
226/// specialized handling.
227pub fn execute_query_streaming<'txn, S: KVStore + 'txn, C: Catalog + ?Sized, T: SqlTxn<'txn, S>>(
228    txn: &mut T,
229    catalog: &C,
230    plan: LogicalPlan,
231) -> Result<QueryRowIterator<'static>> {
232    execute_query_streaming_with_policy(txn, catalog, plan, None)
233}
234
235pub fn execute_query_streaming_with_policy<
236    'txn,
237    S: KVStore + 'txn,
238    C: Catalog + ?Sized,
239    T: SqlTxn<'txn, S>,
240>(
241    txn: &mut T,
242    catalog: &C,
243    plan: LogicalPlan,
244    memory: Option<&MemoryPolicy>,
245) -> Result<QueryRowIterator<'static>> {
246    // KNN queries not yet supported for streaming - fall back would need different handling
247    if knn::extract_knn_context(&plan).is_some() {
248        // For KNN, we materialize and wrap in VecIterator
249        let result = execute_query_with_policy(txn, catalog, plan, memory)?;
250        if let ExecutionResult::Query(qr) = result {
251            let (iter, projection, schema) = materialize_query_result(qr);
252            return Ok(QueryRowIterator::new(iter, projection, schema));
253        }
254        return Err(ExecutorError::InvalidOperation {
255            operation: "execute_query_streaming".into(),
256            reason: "KNN query did not return Query result".into(),
257        });
258    }
259
260    // Subqueries need transaction access during evaluation, which streaming
261    // iterators borrow exclusively. Execute through the materializing path
262    // (the same one used by `execute_query`) so results are identical to the
263    // non-streaming API instead of failing or silently dropping rows.
264    if subquery::plan_contains_subquery(&plan)
265        || plan_contains_recursive_cte(&plan)
266        || plan_contains_lateral(&plan)
267    {
268        let result = execute_query_result_with_outer_and_policy(txn, catalog, plan, None, memory)?;
269        let (iter, projection, schema) = materialize_query_result(result);
270        return Ok(QueryRowIterator::new(iter, projection, schema));
271    }
272
273    let (iter, projection, schema) = build_iterator_pipeline(txn, catalog, plan, memory)?;
274
275    Ok(QueryRowIterator::new(iter, projection, schema))
276}
277
278/// Convert a materialized `QueryResult` into pipeline outputs.
279///
280/// The resulting rows are already fully projected, so the returned projection
281/// is `Projection::All` over the output column names.
282fn materialize_query_result(
283    result: QueryResult,
284) -> (
285    Box<dyn RowIterator>,
286    Projection,
287    Vec<crate::catalog::ColumnMetadata>,
288) {
289    let column_names: Vec<String> = result.columns.iter().map(|c| c.name.clone()).collect();
290    let schema: Vec<crate::catalog::ColumnMetadata> = result
291        .columns
292        .iter()
293        .map(|c| crate::catalog::ColumnMetadata::new(&c.name, c.data_type.clone()))
294        .collect();
295    let rows: Vec<Row> = result
296        .rows
297        .into_iter()
298        .enumerate()
299        .map(|(i, values)| Row::new(i as u64, values))
300        .collect();
301    let iter = iterator::VecIterator::new(rows, schema.clone());
302    (Box::new(iter), Projection::All(column_names), schema)
303}
304
305fn execute_recursive_cte_result<
306    'txn,
307    S: KVStore + 'txn,
308    C: Catalog + ?Sized,
309    T: SqlTxn<'txn, S>,
310>(
311    txn: &mut T,
312    catalog: &C,
313    context: &QueryExecutionContext,
314    outer: Option<&Row>,
315    memory: Option<&MemoryPolicy>,
316    execution: RecursiveCteExecution,
317) -> Result<QueryResult> {
318    let RecursiveCteExecution {
319        name,
320        anchor,
321        recursive_term,
322        union_all,
323        schema,
324        limits,
325    } = execution;
326    let anchor_result =
327        execute_query_result_with_context(txn, catalog, anchor, outer, memory, context)?;
328    let mut accumulated = Vec::new();
329    let mut seen = HashSet::new();
330    let mut accumulated_bytes = 0u64;
331    let mut seen_bytes = 0u64;
332    for row in anchor_result.rows {
333        let should_accumulate = if union_all {
334            true
335        } else {
336            let key = aggregate::encode_group_key(&row)?;
337            let key_bytes = key.len() as u64;
338            if seen.insert(key) {
339                seen_bytes = seen_bytes.saturating_add(key_bytes);
340                true
341            } else {
342                false
343            }
344        };
345        if should_accumulate {
346            accumulated_bytes = accumulated_bytes.saturating_add(estimated_row_bytes(&row));
347            accumulated.push(row);
348        }
349    }
350    ensure_recursive_row_limit(&name, accumulated.len(), limits.max_rows)?;
351
352    let mut working = accumulated.clone();
353    let mut working_bytes = accumulated_bytes;
354    enforce_recursive_memory(
355        memory,
356        accumulated_bytes
357            .saturating_add(working_bytes)
358            .saturating_add(seen_bytes),
359    )?;
360    let mut iterations = 0usize;
361    while !working.is_empty() {
362        if iterations >= limits.max_iterations {
363            return Err(ExecutorError::ResourceExhausted {
364                message: format!(
365                    "recursive CTE '{name}' reached iteration limit {}",
366                    limits.max_iterations
367                ),
368            });
369        }
370        iterations += 1;
371
372        // The iteration context owns the delta rows, and RecursiveReference
373        // clones them into its iterator. Account for both retained copies at
374        // the point where their lifetimes overlap.
375        enforce_recursive_memory(
376            memory,
377            accumulated_bytes
378                .saturating_add(working_bytes.saturating_mul(2))
379                .saturating_add(seen_bytes),
380        )?;
381
382        let iteration_context = context.with_recursive_table(
383            name.clone(),
384            RecursiveWorkingTable {
385                rows: working,
386                schema: schema.clone(),
387            },
388        );
389        let recursive_result = execute_query_result_with_context(
390            txn,
391            catalog,
392            recursive_term.clone(),
393            outer,
394            memory,
395            &iteration_context,
396        )?;
397        let recursive_result_bytes = recursive_result
398            .rows
399            .iter()
400            .map(|row| estimated_row_bytes(row))
401            .sum::<u64>();
402        // The materialized recursive result and the context's delta rows are
403        // both retained until the recursive operator returns. Inner operator
404        // buffers enforce the same MemoryPolicy independently; the current
405        // policy API has no shared remaining-budget tracker to combine their
406        // transient high-water marks with these retained recursive sets.
407        enforce_recursive_memory(
408            memory,
409            accumulated_bytes
410                .saturating_add(working_bytes)
411                .saturating_add(recursive_result_bytes)
412                .saturating_add(seen_bytes),
413        )?;
414        drop(iteration_context);
415        let mut next = Vec::new();
416        let mut next_bytes = 0u64;
417        for row in recursive_result.rows {
418            let should_accumulate = if union_all {
419                true
420            } else {
421                let key = aggregate::encode_group_key(&row)?;
422                let key_bytes = key.len() as u64;
423                if seen.insert(key) {
424                    seen_bytes = seen_bytes.saturating_add(key_bytes);
425                    true
426                } else {
427                    false
428                }
429            };
430            if should_accumulate {
431                next_bytes = next_bytes.saturating_add(estimated_row_bytes(&row));
432                next.push(row);
433            }
434        }
435        ensure_recursive_row_limit(
436            &name,
437            accumulated.len().saturating_add(next.len()),
438            limits.max_rows,
439        )?;
440        accumulated.extend(next.iter().cloned());
441        accumulated_bytes = accumulated_bytes.saturating_add(next_bytes);
442        working_bytes = next_bytes;
443        enforce_recursive_memory(
444            memory,
445            accumulated_bytes
446                .saturating_add(working_bytes)
447                .saturating_add(seen_bytes),
448        )?;
449        working = next;
450    }
451
452    let columns = schema
453        .into_iter()
454        .map(|column| ColumnInfo::new(column.name, column.data_type))
455        .collect();
456    Ok(QueryResult::new(columns, accumulated))
457}
458
459fn estimated_row_bytes(row: &[SqlValue]) -> u64 {
460    row.iter().map(ByteSized::estimated_bytes).sum()
461}
462
463fn enforce_recursive_memory(memory: Option<&MemoryPolicy>, bytes: u64) -> Result<()> {
464    let Some(policy) = memory else {
465        return Ok(());
466    };
467    let mut tracker = MemoryTracker::new(policy.clone());
468    tracker.add_bytes(bytes).map_err(map_core_memory_error)?;
469    if tracker.over_limit() {
470        return Err(ExecutorError::ResourceExhausted {
471            message: format!(
472                "query memory limit exceeded by recursive CTE materialization at {} bytes",
473                tracker.used_bytes()
474            ),
475        });
476    }
477    Ok(())
478}
479
480fn ensure_recursive_row_limit(name: &str, rows: usize, max_rows: usize) -> Result<()> {
481    if rows <= max_rows {
482        return Ok(());
483    }
484    Err(ExecutorError::ResourceExhausted {
485        message: format!("recursive CTE '{name}' reached row limit {max_rows}"),
486    })
487}
488
489/// Build an iterator pipeline from a logical plan.
490///
491/// This recursively constructs a tree of iterators that mirrors the logical plan
492/// structure. The scan phase reads rows into memory, then subsequent operators
493/// process them through an iterator pipeline enabling streaming execution and
494/// early termination.
495fn build_iterator_pipeline<'txn, S: KVStore + 'txn, C: Catalog + ?Sized, T: SqlTxn<'txn, S>>(
496    txn: &mut T,
497    catalog: &C,
498    plan: LogicalPlan,
499    memory: Option<&MemoryPolicy>,
500) -> Result<(
501    Box<dyn RowIterator>,
502    Projection,
503    Vec<crate::catalog::ColumnMetadata>,
504)> {
505    build_iterator_pipeline_with_outer(
506        txn,
507        catalog,
508        plan,
509        memory,
510        None,
511        &QueryExecutionContext::default(),
512    )
513}
514
515fn build_iterator_pipeline_with_outer<
516    'txn,
517    S: KVStore + 'txn,
518    C: Catalog + ?Sized,
519    T: SqlTxn<'txn, S>,
520>(
521    txn: &mut T,
522    catalog: &C,
523    plan: LogicalPlan,
524    memory: Option<&MemoryPolicy>,
525    outer: Option<&Row>,
526    context: &QueryExecutionContext,
527) -> Result<(
528    Box<dyn RowIterator>,
529    Projection,
530    Vec<crate::catalog::ColumnMetadata>,
531)> {
532    match plan {
533        LogicalPlan::RecursiveReference { name, schema } => {
534            let table = context.recursive_tables.get(&name).ok_or_else(|| {
535                ExecutorError::InvalidOperation {
536                    operation: "recursive common table expression".into(),
537                    reason: format!("working table '{name}' is not active"),
538                }
539            })?;
540            if table.schema.len() != schema.len()
541                || table.schema.iter().zip(&schema).any(|(left, right)| {
542                    left.name != right.name || left.data_type != right.data_type
543                })
544            {
545                return Err(ExecutorError::InvalidOperation {
546                    operation: "recursive common table expression".into(),
547                    reason: format!("working table '{name}' schema changed during evaluation"),
548                });
549            }
550            let rows = table
551                .rows
552                .iter()
553                .cloned()
554                .enumerate()
555                .map(|(index, values)| Row::new(index as u64, values))
556                .collect();
557            let projection =
558                Projection::All(schema.iter().map(|column| column.name.clone()).collect());
559            Ok((
560                Box::new(iterator::VecIterator::new(rows, schema.clone())),
561                projection,
562                schema,
563            ))
564        }
565        LogicalPlan::RecursiveCte {
566            name,
567            anchor,
568            recursive_term,
569            union_all,
570            schema,
571            limits,
572        } => {
573            let result = execute_recursive_cte_result(
574                txn,
575                catalog,
576                context,
577                outer,
578                memory,
579                RecursiveCteExecution {
580                    name,
581                    anchor: *anchor,
582                    recursive_term: *recursive_term,
583                    union_all,
584                    schema,
585                    limits,
586                },
587            )?;
588            Ok(materialize_query_result(result))
589        }
590        LogicalPlan::Scan { table, projection } => {
591            if table == LITERAL_TABLE {
592                let schema = Vec::new();
593                let rows = vec![Row::new(0, Vec::new())];
594                let iter = iterator::VecIterator::new(rows, schema.clone());
595                return Ok((Box::new(iter), projection, schema));
596            }
597            let table_meta = catalog
598                .get_table(&table)
599                .cloned()
600                .ok_or_else(|| ExecutorError::TableNotFound(table.clone()))?;
601
602            if table_meta.storage_options.storage_type == StorageType::Columnar {
603                let columnar_scan = columnar_scan::build_columnar_scan(&table_meta, &projection);
604                let rows = columnar_scan::execute_columnar_scan(txn, &table_meta, &columnar_scan)?;
605                let schema = table_meta.columns.clone();
606                let iter = iterator::VecIterator::new(rows, schema.clone());
607                return Ok((Box::new(iter), projection, schema));
608            }
609
610            // TODO: 現状は Scan で一度全件をメモリに載せてから iterator に渡しています。
611            // 将来ストリーミングを徹底する場合は、ScanIterator を活用できるよう
612            // トランザクションのライフタイム設計を見直すとよいです。
613            let rows = scan::execute_scan(txn, &table_meta)?;
614            let schema = table_meta.columns.clone();
615
616            // Wrap in VecIterator for consistent iterator-based processing
617            let iter = iterator::VecIterator::new(rows, schema.clone());
618            Ok((Box::new(iter), projection, schema))
619        }
620        LogicalPlan::Values { rows, schema } => {
621            let projection =
622                Projection::All(schema.iter().map(|column| column.name.clone()).collect());
623            let iterator = iterator::ValuesIterator::new(rows, schema.clone(), outer);
624            Ok((Box::new(iterator), projection, schema))
625        }
626        LogicalPlan::Filter { input, predicate } => {
627            if let LogicalPlan::Scan { table, projection } = input.as_ref()
628                && let Some(table_meta) = catalog.get_table(table)
629                && table_meta.storage_options.storage_type == StorageType::Columnar
630            {
631                // Columnar filter fusion evaluates the predicate inside the
632                // scan, which resolves every column index against the scanned
633                // table. A correlated predicate also carries outer-row indexes
634                // past that width, and a subquery predicate needs transaction
635                // access the scan does not have, so both are evaluated here
636                // over a scan widened to the local columns they read
637                // (issue #151, D15).
638                let evaluated_here = outer.is_some() || subquery::contains_subquery(&predicate);
639                let projection = projection.clone();
640                let schema = table_meta.columns.clone();
641                let columnar_scan = if evaluated_here {
642                    columnar_scan::build_columnar_scan_for_external_filter(
643                        table_meta,
644                        projection.clone(),
645                        &predicate,
646                    )
647                } else {
648                    columnar_scan::build_columnar_scan_for_filter(
649                        table_meta,
650                        projection.clone(),
651                        &predicate,
652                    )
653                };
654                let rows = columnar_scan::execute_columnar_scan(txn, table_meta, &columnar_scan)?;
655                if !evaluated_here {
656                    let iter = iterator::VecIterator::new(rows, schema.clone());
657                    return Ok((Box::new(iter), projection, schema));
658                }
659                let mut kept = Vec::new();
660                for row in rows {
661                    let eval_row = combine_outer_for_eval(&row, outer);
662                    if let SqlValue::Boolean(true) = subquery::evaluate_expr_with_subqueries(
663                        txn, catalog, &predicate, &eval_row,
664                    )? {
665                        kept.push(row);
666                    }
667                }
668                let iter = iterator::VecIterator::new(kept, schema.clone());
669                return Ok((Box::new(iter), projection, schema));
670            }
671            let (mut input_iter, projection, schema) =
672                build_iterator_pipeline_with_outer(txn, catalog, *input, memory, outer, context)?;
673            if outer.is_some() || subquery::contains_subquery(&predicate) {
674                let mut rows = Vec::new();
675                while let Some(result) = input_iter.next_row() {
676                    let row = result?;
677                    let eval_row = combine_outer_for_eval(&row, outer);
678                    if let SqlValue::Boolean(true) = subquery::evaluate_expr_with_subqueries(
679                        txn, catalog, &predicate, &eval_row,
680                    )? {
681                        rows.push(row);
682                    }
683                }
684                let iter = iterator::VecIterator::new(rows, schema.clone());
685                return Ok((Box::new(iter), projection, schema));
686            }
687            let filter_iter = FilterIterator::new(input_iter, predicate);
688            Ok((Box::new(filter_iter), projection, schema))
689        }
690        LogicalPlan::Project { input, projection } => {
691            let input_result =
692                execute_query_result_with_context(txn, catalog, *input, outer, memory, context)?;
693            let (mut input_iter, _input_projection, schema) =
694                materialize_query_result(input_result);
695            let mut rows = Vec::new();
696            while let Some(result) = input_iter.next_row() {
697                rows.push(result?);
698            }
699            let projected =
700                execute_project_with_subqueries(txn, catalog, rows, &projection, &schema, outer)?;
701            let output_schema = projected
702                .columns
703                .iter()
704                .map(|col| crate::catalog::ColumnMetadata::new(&col.name, col.data_type.clone()))
705                .collect::<Vec<_>>();
706            let rows = projected
707                .rows
708                .into_iter()
709                .enumerate()
710                .map(|(idx, values)| Row::new(idx as u64, values))
711                .collect::<Vec<_>>();
712            let output_projection =
713                Projection::All(output_schema.iter().map(|col| col.name.clone()).collect());
714            let iter = iterator::VecIterator::new(rows, output_schema.clone());
715            Ok((Box::new(iter), output_projection, output_schema))
716        }
717        LogicalPlan::Join {
718            left,
719            right,
720            join_type,
721            condition,
722            using: _,
723        } => {
724            let (mut left_iter, _left_projection, left_schema) =
725                build_iterator_pipeline_with_outer(txn, catalog, *left, memory, outer, context)?;
726            let (mut right_iter, _right_projection, right_schema) =
727                build_iterator_pipeline_with_outer(txn, catalog, *right, memory, outer, context)?;
728            let mut left_rows = Vec::new();
729            while let Some(result) = left_iter.next_row() {
730                left_rows.push(result?);
731            }
732            let mut right_rows = Vec::new();
733            while let Some(result) = right_iter.next_row() {
734                right_rows.push(result?);
735            }
736            let left_width = left_schema.len();
737            let right_width = right_schema.len();
738            let rows = join::execute_join_with_widths(
739                left_rows,
740                right_rows,
741                join_type,
742                condition.as_ref(),
743                left_width,
744                right_width,
745            )?;
746            let mut schema = left_schema;
747            schema.extend(right_schema);
748            let projection = Projection::All(schema.iter().map(|col| col.name.clone()).collect());
749            let iter = iterator::VecIterator::new(rows, schema.clone());
750            Ok((Box::new(iter), projection, schema))
751        }
752        LogicalPlan::LateralJoin {
753            left,
754            right,
755            join_type,
756            condition,
757            right_schema,
758        } => {
759            let (mut left_iter, _left_projection, left_schema) =
760                build_iterator_pipeline_with_outer(txn, catalog, *left, memory, outer, context)?;
761            let mut left_rows = Vec::new();
762            while let Some(result) = left_iter.next_row() {
763                left_rows.push(result?);
764            }
765            drop(left_iter);
766            let rows = execute_lateral_join(
767                txn,
768                catalog,
769                left_rows,
770                &right,
771                join_type,
772                condition.as_ref(),
773                right_schema.len(),
774                outer,
775                memory,
776                context,
777            )?;
778            let mut schema = left_schema;
779            schema.extend(right_schema);
780            let projection = Projection::All(schema.iter().map(|col| col.name.clone()).collect());
781            let iter = iterator::VecIterator::new(rows, schema.clone());
782            Ok((Box::new(iter), projection, schema))
783        }
784        LogicalPlan::TableFunction {
785            function,
786            args,
787            schema,
788        } => {
789            let rows = execute_table_function(txn, catalog, function, &args, outer)?;
790            let projection =
791                Projection::All(schema.iter().map(|column| column.name.clone()).collect());
792            let iter = iterator::VecIterator::new(rows, schema.clone());
793            Ok((Box::new(iter), projection, schema))
794        }
795        LogicalPlan::Aggregate {
796            input,
797            group_keys,
798            aggregates,
799            having,
800            projection,
801            grouping_sets,
802        } => {
803            let (input_iter, _projection, input_schema) =
804                build_iterator_pipeline_with_outer(txn, catalog, *input, memory, outer, context)?;
805            // A correlated group key, aggregate argument, aggregate FILTER
806            // predicate or aggregate-local ORDER BY key addresses the outer
807            // row past the input width, so widen the input once (D16). The
808            // aggregate output is the group-key/aggregate schema, so no
809            // narrowing is needed afterwards.
810            let input_iter: Box<dyn RowIterator> = match outer {
811                Some(outer_row) => {
812                    let rows = widen_rows_with_outer(input_iter, outer_row)?;
813                    Box::new(iterator::VecIterator::new(rows, input_schema))
814                }
815                None => input_iter,
816            };
817            let mut schema = aggregate::build_aggregate_schema(&group_keys, &aggregates);
818            if grouping_sets.is_some() {
819                schema.push(crate::catalog::ColumnMetadata::new(
820                    crate::planner::GROUPING_ID_COLUMN,
821                    crate::planner::ResolvedType::BigInt,
822                ));
823            }
824            // GROUPING SETS runs single-pass on AggregateIterator only
825            // (issue #149, D9): spill/streaming and parallel execution are
826            // mask-unaware and stay on the grouping_sets == None path.
827            if grouping_sets.is_none()
828                && let Some(policy) = memory
829                && policy.spill_directory().is_some()
830            {
831                if group_keys.is_empty() {
832                    let iter = aggregate::StreamingAggregateIterator::new(
833                        input_iter,
834                        group_keys,
835                        aggregates,
836                        having,
837                        schema.clone(),
838                    );
839                    return Ok((Box::new(iter), projection, schema));
840                }
841                let order_by = group_keys
842                    .iter()
843                    .cloned()
844                    .map(|expr| SortExpr {
845                        expr,
846                        asc: true,
847                        nulls_first: false,
848                    })
849                    .collect::<Vec<_>>();
850                let sort_iter =
851                    SortIterator::new_with_policy(input_iter, &order_by, Some(policy.clone()))?;
852                let iter = aggregate::StreamingAggregateIterator::new(
853                    Box::new(sort_iter),
854                    group_keys,
855                    aggregates,
856                    having,
857                    schema.clone(),
858                );
859                return Ok((Box::new(iter), projection, schema));
860            }
861
862            let parallelism = std::thread::available_parallelism()
863                .map(usize::from)
864                .unwrap_or(1);
865            if grouping_sets.is_none()
866                && !aggregate::should_use_single_for_parallel(parallelism, &aggregates)
867            {
868                let rows = aggregate::execute_parallel_aggregate_rows_with_policy(
869                    input_iter,
870                    group_keys,
871                    aggregates,
872                    having,
873                    schema.clone(),
874                    parallelism,
875                    memory.cloned(),
876                    1_000_000,
877                )?;
878                let iter = iterator::VecIterator::new(rows, schema.clone());
879                return Ok((Box::new(iter), projection, schema));
880            }
881
882            let mut iter = aggregate::AggregateIterator::new(
883                input_iter,
884                group_keys,
885                aggregates,
886                having,
887                schema.clone(),
888            )
889            .with_grouping_sets(grouping_sets);
890            if let Some(policy) = memory {
891                iter = iter.with_memory_policy(Some(policy.clone()));
892            }
893            Ok((Box::new(iter), projection, schema))
894        }
895        LogicalPlan::Window { input, windows } => {
896            let (input_iter, _projection, input_schema) =
897                build_iterator_pipeline_with_outer(txn, catalog, *input, memory, outer, context)?;
898            // A correlated PARTITION BY / ORDER BY key addresses the outer row
899            // past the input width (D16). The window iterator appends its
900            // results after the whole input row, so the outer values are cut
901            // back out of the middle and the declared schema stays
902            // `input columns + window columns`.
903            if let Some(outer_row) = outer {
904                let inner_width = input_schema.len();
905                let outer_width = outer_row.len();
906                let mut wide_schema = input_schema.clone();
907                wide_schema.extend((0..outer_width).map(|index| {
908                    crate::catalog::ColumnMetadata::new(
909                        format!("__outer_{index}"),
910                        crate::planner::ResolvedType::Text,
911                    )
912                }));
913                let rows = widen_rows_with_outer(input_iter, outer_row)?;
914                let wide_iter = iterator::VecIterator::new(rows, wide_schema.clone());
915                let window_iter = window::WindowIterator::new(wide_iter, windows, memory)?;
916                let mut schema = input_schema;
917                schema.extend_from_slice(&window_iter.schema()[wide_schema.len()..]);
918                let rows = drain_rows(Box::new(window_iter))?
919                    .into_iter()
920                    .map(|mut row| {
921                        let end = (inner_width + outer_width).min(row.values.len());
922                        row.values.drain(inner_width.min(end)..end);
923                        row
924                    })
925                    .collect::<Vec<_>>();
926                let projection =
927                    Projection::All(schema.iter().map(|column| column.name.clone()).collect());
928                let iter = iterator::VecIterator::new(rows, schema.clone());
929                return Ok((Box::new(iter), projection, schema));
930            }
931            let iter = window::WindowIterator::new(input_iter, windows, memory)?;
932            let schema = iter.schema().to_vec();
933            let projection =
934                Projection::All(schema.iter().map(|column| column.name.clone()).collect());
935            Ok((Box::new(iter), projection, schema))
936        }
937        LogicalPlan::SetOperation {
938            left,
939            right,
940            operator,
941            all,
942        } => {
943            let left_result =
944                execute_query_result_with_context(txn, catalog, *left, outer, memory, context)?;
945            let right_result =
946                execute_query_result_with_context(txn, catalog, *right, outer, memory, context)?;
947            let rows = execute_set_operation(operator, all, left_result.rows, right_result.rows)?;
948            let schema = left_result
949                .columns
950                .iter()
951                .map(|column| {
952                    crate::catalog::ColumnMetadata::new(&column.name, column.data_type.clone())
953                })
954                .collect::<Vec<_>>();
955            let projection = Projection::All(
956                left_result
957                    .columns
958                    .iter()
959                    .map(|column| column.name.clone())
960                    .collect(),
961            );
962            let rows = rows
963                .into_iter()
964                .enumerate()
965                .map(|(index, values)| Row::new(index as u64, values))
966                .collect();
967            Ok((
968                Box::new(iterator::VecIterator::new(rows, schema.clone())),
969                projection,
970                schema,
971            ))
972        }
973        LogicalPlan::Sort { input, order_by } => {
974            let (input_iter, projection, schema) =
975                build_iterator_pipeline_with_outer(txn, catalog, *input, memory, outer, context)?;
976            // A correlated sort key addresses the outer row past the input
977            // width, so sort over widened rows and narrow the output back to
978            // the operator's own width (D16).
979            if let Some(outer_row) = outer {
980                let rows = widen_rows_with_outer(input_iter, outer_row)?;
981                let wide_iter = iterator::VecIterator::new(rows, schema.clone());
982                let sort_iter = if let Some(policy) = memory {
983                    SortIterator::new_with_policy(wide_iter, &order_by, Some(policy.clone()))?
984                } else {
985                    SortIterator::new(wide_iter, &order_by)?
986                };
987                let rows =
988                    narrow_rows_from_outer(drain_rows(Box::new(sort_iter))?, outer_row.len());
989                let iter = iterator::VecIterator::new(rows, schema.clone());
990                return Ok((Box::new(iter), projection, schema));
991            }
992            let sort_iter = if let Some(policy) = memory {
993                SortIterator::new_with_policy(input_iter, &order_by, Some(policy.clone()))?
994            } else {
995                SortIterator::new(input_iter, &order_by)?
996            };
997            Ok((Box::new(sort_iter), projection, schema))
998        }
999        LogicalPlan::DistinctOn {
1000            input,
1001            key_count,
1002            order_by,
1003        } => {
1004            let (input_iter, projection, schema) =
1005                build_iterator_pipeline_with_outer(txn, catalog, *input, memory, outer, context)?;
1006            if let Some(outer_row) = outer {
1007                let rows = widen_rows_with_outer(input_iter, outer_row)?;
1008                let wide_iter = iterator::VecIterator::new(rows, schema.clone());
1009                let distinct_iter =
1010                    DistinctOnIterator::new(wide_iter, &order_by, key_count, memory.cloned())?;
1011                let rows =
1012                    narrow_rows_from_outer(drain_rows(Box::new(distinct_iter))?, outer_row.len());
1013                let iter = iterator::VecIterator::new(rows, schema.clone());
1014                return Ok((Box::new(iter), projection, schema));
1015            }
1016            let distinct_iter =
1017                DistinctOnIterator::new(input_iter, &order_by, key_count, memory.cloned())?;
1018            Ok((Box::new(distinct_iter), projection, schema))
1019        }
1020        LogicalPlan::Limit {
1021            input,
1022            limit,
1023            offset,
1024            ties,
1025        } => {
1026            let (input_iter, projection, schema) =
1027                build_iterator_pipeline_with_outer(txn, catalog, *input, memory, outer, context)?;
1028            // Only WITH TIES evaluates expressions here, so a plain
1029            // LIMIT/OFFSET needs no widening even when correlated (D16).
1030            if let (Some(outer_row), Some(tie_keys)) = (outer, ties.as_ref()) {
1031                let rows = widen_rows_with_outer(input_iter, outer_row)?;
1032                let wide_iter = iterator::VecIterator::new(rows, schema.clone());
1033                let limit_iter =
1034                    LimitIterator::with_ties(wide_iter, limit, offset, tie_keys.clone());
1035                let rows =
1036                    narrow_rows_from_outer(drain_rows(Box::new(limit_iter))?, outer_row.len());
1037                let iter = iterator::VecIterator::new(rows, schema.clone());
1038                return Ok((Box::new(iter), projection, schema));
1039            }
1040            let limit_iter = match ties {
1041                Some(tie_keys) => LimitIterator::with_ties(input_iter, limit, offset, tie_keys),
1042                None => LimitIterator::new(input_iter, limit, offset),
1043            };
1044            Ok((Box::new(limit_iter), projection, schema))
1045        }
1046        other => Err(ExecutorError::UnsupportedOperation(format!(
1047            "unsupported query plan: {other:?}"
1048        ))),
1049    }
1050}
1051
1052/// Build a streaming iterator pipeline from a logical plan (FR-7).
1053///
1054/// This version uses `ScanIterator` for row-based tables to enable true
1055/// streaming without materializing all rows upfront. The returned iterator
1056/// has lifetime `'a` tied to the transaction borrow.
1057///
1058/// # Limitations
1059///
1060/// - Columnar storage still materializes rows (uses VecIterator)
1061/// - Sort operations materialize all input rows
1062/// - KNN queries are not supported (use `build_iterator_pipeline` instead)
1063pub fn build_streaming_pipeline<
1064    'a,
1065    'txn: 'a,
1066    S: KVStore + 'txn,
1067    C: Catalog + ?Sized,
1068    T: SqlTxn<'txn, S>,
1069>(
1070    txn: &'a mut T,
1071    catalog: &C,
1072    plan: LogicalPlan,
1073) -> Result<(
1074    Box<dyn RowIterator + 'a>,
1075    Projection,
1076    Vec<crate::catalog::ColumnMetadata>,
1077)> {
1078    build_streaming_pipeline_with_policy(txn, catalog, plan, None)
1079}
1080
1081pub fn build_streaming_pipeline_with_policy<
1082    'a,
1083    'txn: 'a,
1084    S: KVStore + 'txn,
1085    C: Catalog + ?Sized,
1086    T: SqlTxn<'txn, S>,
1087>(
1088    txn: &'a mut T,
1089    catalog: &C,
1090    plan: LogicalPlan,
1091    memory: Option<&MemoryPolicy>,
1092) -> Result<(
1093    Box<dyn RowIterator + 'a>,
1094    Projection,
1095    Vec<crate::catalog::ColumnMetadata>,
1096)> {
1097    // Subqueries need transaction access during evaluation, which streaming
1098    // iterators borrow exclusively. Execute through the materializing path
1099    // (the same one used by `execute_query`) so results are identical to the
1100    // non-streaming API instead of failing or silently dropping rows
1101    // (GitHub issues #23 / #24).
1102    if subquery::plan_contains_subquery(&plan)
1103        || plan_contains_recursive_cte(&plan)
1104        || plan_contains_lateral(&plan)
1105    {
1106        let result = execute_query_result_with_outer_and_policy(txn, catalog, plan, None, memory)?;
1107        return Ok(materialize_query_result(result));
1108    }
1109
1110    match plan {
1111        LogicalPlan::SetOperation {
1112            left,
1113            right,
1114            operator,
1115            all,
1116        } => build_streaming_set_operation(txn, catalog, *left, *right, operator, all, memory),
1117        other => build_streaming_pipeline_inner(txn, catalog, other, memory),
1118    }
1119}
1120
1121fn build_streaming_set_operation<
1122    'a,
1123    'txn: 'a,
1124    S: KVStore + 'txn,
1125    C: Catalog + ?Sized,
1126    T: SqlTxn<'txn, S>,
1127>(
1128    txn: &'a mut T,
1129    catalog: &C,
1130    left: LogicalPlan,
1131    right: LogicalPlan,
1132    operator: SetOperator,
1133    all: bool,
1134    memory: Option<&MemoryPolicy>,
1135) -> Result<(
1136    Box<dyn RowIterator + 'a>,
1137    Projection,
1138    Vec<crate::catalog::ColumnMetadata>,
1139)> {
1140    let (mut left_iter, left_projection, left_schema) =
1141        build_streaming_pipeline_with_policy(txn, catalog, left, memory)?;
1142    let mut left_rows = Vec::new();
1143    while let Some(result) = left_iter.next_row() {
1144        left_rows.push(result?);
1145    }
1146    drop(left_iter);
1147    let left_result = project::execute_project(left_rows, &left_projection, &left_schema)?;
1148
1149    let (mut right_iter, right_projection, right_schema) =
1150        build_streaming_pipeline_with_policy(txn, catalog, right, memory)?;
1151    let mut right_rows = Vec::new();
1152    while let Some(result) = right_iter.next_row() {
1153        right_rows.push(result?);
1154    }
1155    let right_result = project::execute_project(right_rows, &right_projection, &right_schema)?;
1156
1157    let rows = execute_set_operation(operator, all, left_result.rows, right_result.rows)?;
1158    let schema = left_result
1159        .columns
1160        .iter()
1161        .map(|column| crate::catalog::ColumnMetadata::new(&column.name, column.data_type.clone()))
1162        .collect::<Vec<_>>();
1163    let projection = Projection::All(
1164        left_result
1165            .columns
1166            .iter()
1167            .map(|column| column.name.clone())
1168            .collect(),
1169    );
1170    let rows = rows
1171        .into_iter()
1172        .enumerate()
1173        .map(|(index, values)| Row::new(index as u64, values))
1174        .collect();
1175    Ok((
1176        Box::new(iterator::VecIterator::new(rows, schema.clone())),
1177        projection,
1178        schema,
1179    ))
1180}
1181
1182fn execute_set_operation(
1183    operator: SetOperator,
1184    all: bool,
1185    mut left: Vec<Vec<SqlValue>>,
1186    right: Vec<Vec<SqlValue>>,
1187) -> Result<Vec<Vec<SqlValue>>> {
1188    if all {
1189        match operator {
1190            SetOperator::Union => {
1191                left.extend(right);
1192                return Ok(left);
1193            }
1194            SetOperator::Intersect | SetOperator::Except => {
1195                let mut right_counts = HashMap::<Vec<u8>, usize>::new();
1196                for row in right {
1197                    *right_counts
1198                        .entry(aggregate::encode_group_key(&row)?)
1199                        .or_default() += 1;
1200                }
1201                let mut output = Vec::new();
1202                for row in left {
1203                    let key = aggregate::encode_group_key(&row)?;
1204                    let remaining = right_counts.entry(key).or_default();
1205                    if *remaining > 0 {
1206                        *remaining -= 1;
1207                        if operator == SetOperator::Intersect {
1208                            output.push(row);
1209                        }
1210                    } else if operator == SetOperator::Except {
1211                        output.push(row);
1212                    }
1213                }
1214                return Ok(output);
1215            }
1216        }
1217    }
1218
1219    let right_keys = right
1220        .iter()
1221        .map(|row| aggregate::encode_group_key(row))
1222        .collect::<Result<HashSet<_>>>()?;
1223    let mut seen = HashSet::new();
1224    let mut output = Vec::new();
1225    match operator {
1226        SetOperator::Union => {
1227            for row in left.into_iter().chain(right) {
1228                if seen.insert(aggregate::encode_group_key(&row)?) {
1229                    output.push(row);
1230                }
1231            }
1232        }
1233        SetOperator::Intersect => {
1234            for row in left {
1235                let key = aggregate::encode_group_key(&row)?;
1236                if right_keys.contains(&key) && seen.insert(key) {
1237                    output.push(row);
1238                }
1239            }
1240        }
1241        SetOperator::Except => {
1242            for row in left {
1243                let key = aggregate::encode_group_key(&row)?;
1244                if !right_keys.contains(&key) && seen.insert(key) {
1245                    output.push(row);
1246                }
1247            }
1248        }
1249    }
1250    Ok(output)
1251}
1252
1253/// Inner implementation of streaming pipeline builder.
1254fn build_streaming_pipeline_inner<
1255    'a,
1256    'txn: 'a,
1257    S: KVStore + 'txn,
1258    C: Catalog + ?Sized,
1259    T: SqlTxn<'txn, S>,
1260>(
1261    txn: &'a mut T,
1262    catalog: &C,
1263    plan: LogicalPlan,
1264    memory: Option<&MemoryPolicy>,
1265) -> Result<(
1266    Box<dyn RowIterator + 'a>,
1267    Projection,
1268    Vec<crate::catalog::ColumnMetadata>,
1269)> {
1270    match plan {
1271        LogicalPlan::Scan { table, projection } => {
1272            if table == LITERAL_TABLE {
1273                let schema = Vec::new();
1274                let rows = vec![Row::new(0, Vec::new())];
1275                let iter = iterator::VecIterator::new(rows, schema.clone());
1276                return Ok((Box::new(iter), projection, schema));
1277            }
1278            let table_meta = catalog
1279                .get_table(&table)
1280                .cloned()
1281                .ok_or_else(|| ExecutorError::TableNotFound(table.clone()))?;
1282
1283            if table_meta.storage_options.storage_type == StorageType::Columnar {
1284                // Columnar storage: use ColumnarScanIterator for FR-7 streaming
1285                let columnar_scan = columnar_scan::build_columnar_scan(&table_meta, &projection);
1286                let schema = table_meta.columns.clone();
1287                let iter =
1288                    columnar_scan::create_columnar_scan_iterator(txn, &table_meta, &columnar_scan)?;
1289                return Ok((Box::new(iter), projection, schema));
1290            }
1291
1292            // Row-based storage: use ScanIterator for true streaming (FR-7)
1293            let schema = table_meta.columns.clone();
1294            let scan_iter = scan::create_scan_iterator(txn, &table_meta)?;
1295            Ok((Box::new(scan_iter), projection, schema))
1296        }
1297        LogicalPlan::Values { rows, schema } => {
1298            let projection =
1299                Projection::All(schema.iter().map(|column| column.name.clone()).collect());
1300            let iterator = iterator::ValuesIterator::new(rows, schema.clone(), None);
1301            Ok((Box::new(iterator), projection, schema))
1302        }
1303        LogicalPlan::Filter { input, predicate } => {
1304            if let LogicalPlan::Scan { table, projection } = input.as_ref()
1305                && let Some(table_meta) = catalog.get_table(table)
1306                && table_meta.storage_options.storage_type == StorageType::Columnar
1307            {
1308                // Columnar storage with filter: use ColumnarScanIterator for FR-7 streaming
1309                let columnar_scan = columnar_scan::build_columnar_scan_for_filter(
1310                    table_meta,
1311                    projection.clone(),
1312                    &predicate,
1313                );
1314                let schema = table_meta.columns.clone();
1315                let iter =
1316                    columnar_scan::create_columnar_scan_iterator(txn, table_meta, &columnar_scan)?;
1317                return Ok((Box::new(iter), projection.clone(), schema));
1318            }
1319            let (input_iter, projection, schema) =
1320                build_streaming_pipeline_with_policy(txn, catalog, *input, memory)?;
1321            let filter_iter = FilterIterator::new(input_iter, predicate);
1322            Ok((Box::new(filter_iter), projection, schema))
1323        }
1324        LogicalPlan::Project { input, projection } => {
1325            let (mut input_iter, _input_projection, schema) =
1326                build_streaming_pipeline_with_policy(txn, catalog, *input, memory)?;
1327            let mut rows = Vec::new();
1328            while let Some(result) = input_iter.next_row() {
1329                rows.push(result?);
1330            }
1331            let projected = project::execute_project(rows, &projection, &schema)?;
1332            let output_schema = projected
1333                .columns
1334                .iter()
1335                .map(|col| crate::catalog::ColumnMetadata::new(&col.name, col.data_type.clone()))
1336                .collect::<Vec<_>>();
1337            let rows = projected
1338                .rows
1339                .into_iter()
1340                .enumerate()
1341                .map(|(idx, values)| Row::new(idx as u64, values))
1342                .collect::<Vec<_>>();
1343            let output_projection =
1344                Projection::All(output_schema.iter().map(|col| col.name.clone()).collect());
1345            let iter = iterator::VecIterator::new(rows, output_schema.clone());
1346            Ok((Box::new(iter), output_projection, output_schema))
1347        }
1348        LogicalPlan::Join {
1349            left,
1350            right,
1351            join_type,
1352            condition,
1353            using: _,
1354        } => {
1355            let (mut left_iter, _left_projection, left_schema) =
1356                build_streaming_pipeline_with_policy(txn, catalog, *left, memory)?;
1357            let mut left_rows = Vec::new();
1358            while let Some(result) = left_iter.next_row() {
1359                left_rows.push(result?);
1360            }
1361            drop(left_iter);
1362            let (mut right_iter, _right_projection, right_schema) =
1363                build_streaming_pipeline_with_policy(txn, catalog, *right, memory)?;
1364            let mut right_rows = Vec::new();
1365            while let Some(result) = right_iter.next_row() {
1366                right_rows.push(result?);
1367            }
1368            let rows = join::execute_join_with_widths(
1369                left_rows,
1370                right_rows,
1371                join_type,
1372                condition.as_ref(),
1373                left_schema.len(),
1374                right_schema.len(),
1375            )?;
1376            let mut schema = left_schema;
1377            schema.extend(right_schema);
1378            let projection = Projection::All(schema.iter().map(|col| col.name.clone()).collect());
1379            let iter = iterator::VecIterator::new(rows, schema.clone());
1380            Ok((Box::new(iter), projection, schema))
1381        }
1382        LogicalPlan::Aggregate {
1383            input,
1384            group_keys,
1385            aggregates,
1386            having,
1387            projection,
1388            grouping_sets,
1389        } => {
1390            let (input_iter, _projection, _schema) =
1391                build_streaming_pipeline_with_policy(txn, catalog, *input, memory)?;
1392            let mut schema = aggregate::build_aggregate_schema(&group_keys, &aggregates);
1393            if grouping_sets.is_some() {
1394                schema.push(crate::catalog::ColumnMetadata::new(
1395                    crate::planner::GROUPING_ID_COLUMN,
1396                    crate::planner::ResolvedType::BigInt,
1397                ));
1398            }
1399            // Same single-pass gate as the materialized pipeline (D9).
1400            if grouping_sets.is_none()
1401                && let Some(policy) = memory
1402                && policy.spill_directory().is_some()
1403            {
1404                if group_keys.is_empty() {
1405                    let iter = aggregate::StreamingAggregateIterator::new(
1406                        input_iter,
1407                        group_keys,
1408                        aggregates,
1409                        having,
1410                        schema.clone(),
1411                    );
1412                    return Ok((Box::new(iter), projection, schema));
1413                }
1414                let order_by = group_keys
1415                    .iter()
1416                    .cloned()
1417                    .map(|expr| SortExpr {
1418                        expr,
1419                        asc: true,
1420                        nulls_first: false,
1421                    })
1422                    .collect::<Vec<_>>();
1423                let sort_iter =
1424                    SortIterator::new_with_policy(input_iter, &order_by, Some(policy.clone()))?;
1425                let iter = aggregate::StreamingAggregateIterator::new(
1426                    Box::new(sort_iter),
1427                    group_keys,
1428                    aggregates,
1429                    having,
1430                    schema.clone(),
1431                );
1432                return Ok((Box::new(iter), projection, schema));
1433            }
1434
1435            let parallelism = std::thread::available_parallelism()
1436                .map(usize::from)
1437                .unwrap_or(1);
1438            if grouping_sets.is_none()
1439                && !aggregate::should_use_single_for_parallel(parallelism, &aggregates)
1440            {
1441                let rows = aggregate::execute_parallel_aggregate_rows_with_policy(
1442                    input_iter,
1443                    group_keys,
1444                    aggregates,
1445                    having,
1446                    schema.clone(),
1447                    parallelism,
1448                    memory.cloned(),
1449                    1_000_000,
1450                )?;
1451                let iter = iterator::VecIterator::new(rows, schema.clone());
1452                return Ok((Box::new(iter), projection, schema));
1453            }
1454
1455            let mut iter = aggregate::AggregateIterator::new(
1456                input_iter,
1457                group_keys,
1458                aggregates,
1459                having,
1460                schema.clone(),
1461            )
1462            .with_grouping_sets(grouping_sets);
1463            if let Some(policy) = memory {
1464                iter = iter.with_memory_policy(Some(policy.clone()));
1465            }
1466            Ok((Box::new(iter), projection, schema))
1467        }
1468        LogicalPlan::Window { input, windows } => {
1469            let (input_iter, _projection, _schema) =
1470                build_streaming_pipeline_inner(txn, catalog, *input, memory)?;
1471            let iter = window::WindowIterator::new(input_iter, windows, memory)?;
1472            let schema = iter.schema().to_vec();
1473            let projection =
1474                Projection::All(schema.iter().map(|column| column.name.clone()).collect());
1475            Ok((Box::new(iter), projection, schema))
1476        }
1477        LogicalPlan::Sort { input, order_by } => {
1478            let (input_iter, projection, schema) =
1479                build_streaming_pipeline_with_policy(txn, catalog, *input, memory)?;
1480            let sort_iter = if let Some(policy) = memory {
1481                SortIterator::new_with_policy(input_iter, &order_by, Some(policy.clone()))?
1482            } else {
1483                SortIterator::new(input_iter, &order_by)?
1484            };
1485            Ok((Box::new(sort_iter), projection, schema))
1486        }
1487        LogicalPlan::DistinctOn {
1488            input,
1489            key_count,
1490            order_by,
1491        } => {
1492            let (input_iter, projection, schema) =
1493                build_streaming_pipeline_with_policy(txn, catalog, *input, memory)?;
1494            let distinct_iter =
1495                DistinctOnIterator::new(input_iter, &order_by, key_count, memory.cloned())?;
1496            Ok((Box::new(distinct_iter), projection, schema))
1497        }
1498        LogicalPlan::Limit {
1499            input,
1500            limit,
1501            offset,
1502            ties,
1503        } => {
1504            let (input_iter, projection, schema) =
1505                build_streaming_pipeline_with_policy(txn, catalog, *input, memory)?;
1506            let limit_iter = match ties {
1507                Some(tie_keys) => LimitIterator::with_ties(input_iter, limit, offset, tie_keys),
1508                None => LimitIterator::new(input_iter, limit, offset),
1509            };
1510            Ok((Box::new(limit_iter), projection, schema))
1511        }
1512        other => Err(ExecutorError::UnsupportedOperation(format!(
1513            "unsupported query plan: {other:?}"
1514        ))),
1515    }
1516}
1517
1518/// Run a LATERAL join by re-executing the correlated right side per left row.
1519///
1520/// The right plan is correlated, so it cannot be materialized once and joined:
1521/// each left row is supplied as the outer row and the right side is executed
1522/// again. `right_width` comes from the planned right schema so a LEFT join can
1523/// pad even when the right side produced nothing.
1524#[allow(clippy::too_many_arguments)]
1525fn execute_lateral_join<'txn, S: KVStore + 'txn, C: Catalog + ?Sized, T: SqlTxn<'txn, S>>(
1526    txn: &mut T,
1527    catalog: &C,
1528    left_rows: Vec<Row>,
1529    right: &LogicalPlan,
1530    join_type: crate::planner::JoinType,
1531    condition: Option<&crate::planner::typed_expr::TypedExpr>,
1532    right_width: usize,
1533    outer: Option<&Row>,
1534    memory: Option<&MemoryPolicy>,
1535    context: &QueryExecutionContext,
1536) -> Result<Vec<Row>> {
1537    use crate::planner::JoinType;
1538
1539    let mut rows = Vec::new();
1540    let mut row_id = 0u64;
1541    for left_row in left_rows {
1542        // The correlated side reads the left row through the outer-row
1543        // convention shared with correlated subqueries.
1544        let eval_outer = combine_outer_for_eval(&left_row, outer);
1545        let right_result = execute_query_result_with_context(
1546            txn,
1547            catalog,
1548            right.clone(),
1549            Some(&eval_outer),
1550            memory,
1551            context,
1552        )?;
1553        let mut matched = false;
1554        for right_values in right_result.rows {
1555            let mut values = left_row.values.clone();
1556            values.extend(right_values);
1557            let combined = Row::new(row_id, values);
1558            if let Some(condition) = condition {
1559                let eval_row = combine_outer_for_eval(&combined, outer);
1560                let keep =
1561                    subquery::evaluate_expr_with_subqueries(txn, catalog, condition, &eval_row)?;
1562                if !matches!(keep, SqlValue::Boolean(true)) {
1563                    continue;
1564                }
1565            }
1566            matched = true;
1567            rows.push(combined);
1568            row_id += 1;
1569        }
1570        if !matched && matches!(join_type, JoinType::Left) {
1571            let mut values = left_row.values.clone();
1572            values.extend(std::iter::repeat_n(SqlValue::Null, right_width));
1573            rows.push(Row::new(row_id, values));
1574            row_id += 1;
1575        }
1576    }
1577    Ok(rows)
1578}
1579
1580/// Evaluate a FROM-clause table function.
1581///
1582/// Arguments are evaluated against the outer row alone: the node has no input
1583/// of its own, so its expression indexes address the outer row directly.
1584fn execute_table_function<'txn, S: KVStore + 'txn, C: Catalog + ?Sized, T: SqlTxn<'txn, S>>(
1585    txn: &mut T,
1586    catalog: &C,
1587    function: crate::planner::TableFunctionKind,
1588    args: &[crate::planner::typed_expr::TypedExpr],
1589    outer: Option<&Row>,
1590) -> Result<Vec<Row>> {
1591    use crate::planner::TableFunctionKind;
1592
1593    let empty = Row::new(0, Vec::new());
1594    let eval_row = combine_outer_for_eval(&empty, outer);
1595    let mut values = Vec::with_capacity(args.len());
1596    for arg in args {
1597        values.push(subquery::evaluate_expr_with_subqueries(
1598            txn, catalog, arg, &eval_row,
1599        )?);
1600    }
1601
1602    match function {
1603        TableFunctionKind::Unnest => {
1604            let Some(argument) = values.into_iter().next() else {
1605                return Err(ExecutorError::InvalidOperation {
1606                    operation: "UNNEST".into(),
1607                    reason: "UNNEST requires exactly one argument".into(),
1608                });
1609            };
1610            match argument {
1611                // A NULL argument yields no rows, as it does in PostgreSQL.
1612                SqlValue::Null => Ok(Vec::new()),
1613                SqlValue::Vector(elements) => Ok(elements
1614                    .into_iter()
1615                    .enumerate()
1616                    .map(|(index, element)| Row::new(index as u64, vec![SqlValue::Float(element)]))
1617                    .collect()),
1618                other => Err(ExecutorError::InvalidOperation {
1619                    operation: "UNNEST".into(),
1620                    reason: format!(
1621                        "UNNEST requires a VECTOR argument, found {}",
1622                        other.type_name()
1623                    ),
1624                }),
1625            }
1626        }
1627        TableFunctionKind::GenerateSeries => generate_integer_series(&values),
1628    }
1629}
1630
1631const MAX_GENERATED_SERIES_ROWS: usize = 100_000;
1632
1633fn generate_integer_series(values: &[SqlValue]) -> Result<Vec<Row>> {
1634    if values.iter().any(SqlValue::is_null) {
1635        return Ok(Vec::new());
1636    }
1637    let integer = |index: usize| match values.get(index) {
1638        Some(SqlValue::Integer(value)) => Ok(i64::from(*value)),
1639        Some(SqlValue::BigInt(value)) => Ok(*value),
1640        Some(value) => Err(ExecutorError::InvalidOperation {
1641            operation: "GENERATE_SERIES".into(),
1642            reason: format!("expected INTEGER, found {}", value.type_name()),
1643        }),
1644        None => Err(ExecutorError::InvalidOperation {
1645            operation: "GENERATE_SERIES".into(),
1646            reason: "missing argument".into(),
1647        }),
1648    };
1649    let start = integer(0)?;
1650    let stop = integer(1)?;
1651    let step = if values.len() == 3 { integer(2)? } else { 1 };
1652    if step == 0 {
1653        return Err(ExecutorError::InvalidOperation {
1654            operation: "GENERATE_SERIES".into(),
1655            reason: "step must not be zero".into(),
1656        });
1657    }
1658    if (step > 0 && start > stop) || (step < 0 && start < stop) {
1659        return Ok(Vec::new());
1660    }
1661
1662    let use_bigint = values
1663        .iter()
1664        .any(|value| matches!(value, SqlValue::BigInt(_)));
1665    let mut rows = Vec::new();
1666    let mut current = start;
1667    loop {
1668        if rows.len() == MAX_GENERATED_SERIES_ROWS {
1669            return Err(ExecutorError::ResourceExhausted {
1670                message: format!("GENERATE_SERIES is limited to {MAX_GENERATED_SERIES_ROWS} rows"),
1671            });
1672        }
1673        let value = if use_bigint {
1674            SqlValue::BigInt(current)
1675        } else {
1676            SqlValue::Integer(i32::try_from(current).map_err(|_| {
1677                ExecutorError::Evaluation(crate::executor::EvaluationError::Overflow)
1678            })?)
1679        };
1680        rows.push(Row::new(rows.len() as u64, vec![value]));
1681        if current == stop {
1682            break;
1683        }
1684        current = current.checked_add(step).ok_or(ExecutorError::Evaluation(
1685            crate::executor::EvaluationError::Overflow,
1686        ))?;
1687        if (step > 0 && current > stop) || (step < 0 && current < stop) {
1688            break;
1689        }
1690    }
1691    Ok(rows)
1692}
1693
1694/// Evaluate a typed expression against a row, returning SqlValue.
1695fn eval_expr(expr: &crate::planner::typed_expr::TypedExpr, row: &Row) -> Result<SqlValue> {
1696    let ctx = EvalContext::new(&row.values);
1697    crate::executor::evaluator::evaluate(expr, &ctx)
1698}
1699
1700fn combine_outer_for_eval(row: &Row, outer: Option<&Row>) -> Row {
1701    let Some(outer) = outer else {
1702        return row.clone();
1703    };
1704    let mut values = Vec::with_capacity(row.len() + outer.len());
1705    values.extend(row.values.clone());
1706    values.extend(outer.values.clone());
1707    Row::new(row.row_id, values)
1708}
1709
1710/// Materialize an operator's input with the outer row appended to every row.
1711///
1712/// The planner addresses a correlated reference as `inner_width + outer_index`
1713/// (the `combine_outer_for_eval` convention shared with correlated
1714/// subqueries). Operators that build their own `EvalContext` straight from
1715/// `row.values` — Aggregate (group keys, aggregate arguments, `FILTER`
1716/// predicates and aggregate-local `ORDER BY`), Sort, DistinctOn and
1717/// `LIMIT ... WITH TIES` — would otherwise index past the row and fail with an
1718/// internal `invalid column reference` (issue #151, D16). Widening the input
1719/// once makes every one of those indexes resolve without teaching each
1720/// operator about correlation.
1721fn widen_rows_with_outer(mut input: Box<dyn RowIterator>, outer: &Row) -> Result<Vec<Row>> {
1722    let mut rows = Vec::new();
1723    while let Some(result) = input.next_row() {
1724        rows.push(combine_outer_for_eval(&result?, Some(outer)));
1725    }
1726    Ok(rows)
1727}
1728
1729/// Undo [`widen_rows_with_outer`] on an operator's output rows.
1730///
1731/// Row-preserving operators (Sort, DistinctOn, Limit) emit the widened rows
1732/// themselves, so the trailing outer values are dropped again before the rows
1733/// leave the operator and the declared schema keeps matching the row width.
1734fn narrow_rows_from_outer(rows: Vec<Row>, outer_width: usize) -> Vec<Row> {
1735    rows.into_iter()
1736        .map(|mut row| {
1737            let keep = row.values.len().saturating_sub(outer_width);
1738            row.values.truncate(keep);
1739            row
1740        })
1741        .collect()
1742}
1743
1744fn drain_rows(mut input: Box<dyn RowIterator>) -> Result<Vec<Row>> {
1745    let mut rows = Vec::new();
1746    while let Some(result) = input.next_row() {
1747        rows.push(result?);
1748    }
1749    Ok(rows)
1750}
1751
1752fn execute_project_with_subqueries<
1753    'txn,
1754    S: KVStore + 'txn,
1755    C: Catalog + ?Sized,
1756    T: SqlTxn<'txn, S>,
1757>(
1758    txn: &mut T,
1759    catalog: &C,
1760    rows: Vec<Row>,
1761    projection: &Projection,
1762    schema: &[crate::catalog::ColumnMetadata],
1763    outer: Option<&Row>,
1764) -> Result<QueryResult> {
1765    match projection {
1766        Projection::All(_) => project::execute_project(rows, projection, schema),
1767        Projection::Columns(cols)
1768            if outer.is_some() || cols.iter().any(|c| subquery::contains_subquery(&c.expr)) =>
1769        {
1770            let columns: Vec<_> = cols
1771                .iter()
1772                .enumerate()
1773                .map(|(i, c)| column_info_from_projection(c, i))
1774                .collect();
1775            let mut projected_rows = Vec::with_capacity(rows.len());
1776            for row in rows {
1777                let eval_row = combine_outer_for_eval(&row, outer);
1778                let mut values = Vec::with_capacity(cols.len());
1779                for col in cols {
1780                    values.push(subquery::evaluate_expr_with_subqueries(
1781                        txn, catalog, &col.expr, &eval_row,
1782                    )?);
1783                }
1784                projected_rows.push(values);
1785            }
1786            Ok(QueryResult::new(columns, projected_rows))
1787        }
1788        Projection::Columns(_) => project::execute_project(rows, projection, schema),
1789    }
1790}
1791
1792/// Build column info name using alias fallback.
1793fn column_name_from_projection(
1794    projected: &crate::planner::typed_expr::ProjectedColumn,
1795    idx: usize,
1796) -> String {
1797    use crate::planner::typed_expr::TypedExprKind;
1798
1799    projected
1800        .alias
1801        .clone()
1802        .or_else(|| match &projected.expr.kind {
1803            TypedExprKind::ColumnRef { column, .. } => Some(column.clone()),
1804            // A USING/NATURAL common column is planned as
1805            // COALESCE(left, right); it still names the merged column.
1806            TypedExprKind::FunctionCall { name, args, .. }
1807                if name == "coalesce" && !args.is_empty() =>
1808            {
1809                let first_column = match &args[0].kind {
1810                    TypedExprKind::ColumnRef { column, .. } => Some(column),
1811                    _ => None,
1812                };
1813                first_column
1814                    .filter(|column| {
1815                        args.iter().all(|arg| {
1816                            matches!(
1817                                &arg.kind,
1818                                TypedExprKind::ColumnRef { column: other, .. } if other == *column
1819                            )
1820                        })
1821                    })
1822                    .cloned()
1823            }
1824            _ => None,
1825        })
1826        .unwrap_or_else(|| format!("col_{idx}"))
1827}
1828
1829/// Build ColumnInfo from projection.
1830fn column_info_from_projection(
1831    projected: &crate::planner::typed_expr::ProjectedColumn,
1832    idx: usize,
1833) -> ColumnInfo {
1834    ColumnInfo::new(
1835        column_name_from_projection(projected, idx),
1836        projected.expr.resolved_type.clone(),
1837    )
1838}
1839
1840/// Build ColumnInfo for Projection::All using schema.
1841fn column_infos_from_all(
1842    schema: &[crate::catalog::ColumnMetadata],
1843    names: &[String],
1844) -> Result<Vec<ColumnInfo>> {
1845    if names.len() == schema.len() {
1846        return Ok(names
1847            .iter()
1848            .zip(schema)
1849            .map(|(name, column)| ColumnInfo::new(name.clone(), column.data_type.clone()))
1850            .collect());
1851    }
1852    names
1853        .iter()
1854        .map(|name| {
1855            let col = schema
1856                .iter()
1857                .find(|c| &c.name == name)
1858                .ok_or_else(|| ExecutorError::ColumnNotFound(name.clone()))?;
1859            Ok(ColumnInfo::new(name.clone(), col.data_type.clone()))
1860        })
1861        .collect()
1862}
1863
1864#[cfg(test)]
1865mod tests {
1866    use super::*;
1867    use crate::catalog::{ColumnMetadata, MemoryCatalog, TableMetadata};
1868    use crate::executor::SpillPolicy;
1869    use crate::executor::ddl::create_table::execute_create_table;
1870    use crate::planner::typed_expr::{ProjectedColumn, TypedExpr};
1871    use crate::planner::types::ResolvedType;
1872    use crate::storage::TxnBridge;
1873    use alopex_core::kv::memory::MemoryKV;
1874    use std::sync::Arc;
1875
1876    fn text_literal_plan(value: &str) -> LogicalPlan {
1877        LogicalPlan::Project {
1878            input: Box::new(LogicalPlan::Scan {
1879                table: LITERAL_TABLE.into(),
1880                projection: Projection::All(Vec::new()),
1881            }),
1882            projection: Projection::Columns(vec![ProjectedColumn::new(TypedExpr::literal(
1883                crate::ast::expr::Literal::String(value.into()),
1884                ResolvedType::Text,
1885                crate::Span::default(),
1886            ))]),
1887        }
1888    }
1889
1890    #[test]
1891    fn execute_query_scan_only_returns_rows() {
1892        let bridge = TxnBridge::new(Arc::new(MemoryKV::new()));
1893        let mut catalog = MemoryCatalog::new();
1894        let table = TableMetadata::new(
1895            "users",
1896            vec![
1897                ColumnMetadata::new("id", ResolvedType::Integer),
1898                ColumnMetadata::new("name", ResolvedType::Text),
1899            ],
1900        );
1901        let mut ddl_txn = bridge.begin_write().unwrap();
1902        execute_create_table(&mut ddl_txn, &mut catalog, table.clone(), vec![], false).unwrap();
1903        ddl_txn.commit().unwrap();
1904
1905        let mut txn = bridge.begin_write().unwrap();
1906        crate::executor::dml::execute_insert(
1907            &mut txn,
1908            &catalog,
1909            "users",
1910            vec!["id".into(), "name".into()],
1911            vec![vec![
1912                TypedExpr::literal(
1913                    crate::ast::expr::Literal::Number("1".into()),
1914                    ResolvedType::Integer,
1915                    crate::Span::default(),
1916                ),
1917                TypedExpr::literal(
1918                    crate::ast::expr::Literal::String("alice".into()),
1919                    ResolvedType::Text,
1920                    crate::Span::default(),
1921                ),
1922            ]],
1923        )
1924        .unwrap();
1925
1926        let result = execute_query(
1927            &mut txn,
1928            &catalog,
1929            LogicalPlan::scan(
1930                "users".into(),
1931                Projection::All(vec!["id".into(), "name".into()]),
1932            ),
1933        )
1934        .unwrap();
1935
1936        match result {
1937            ExecutionResult::Query(q) => {
1938                assert_eq!(q.rows.len(), 1);
1939                assert_eq!(q.columns.len(), 2);
1940                assert_eq!(
1941                    q.rows[0],
1942                    vec![SqlValue::Integer(1), SqlValue::Text("alice".into())]
1943                );
1944            }
1945            other => panic!("unexpected result {other:?}"),
1946        }
1947    }
1948
1949    #[test]
1950    fn recursive_cte_accounts_for_reference_clone_high_water() {
1951        let bridge = TxnBridge::new(Arc::new(MemoryKV::new()));
1952        let catalog = MemoryCatalog::new();
1953        let schema = vec![ColumnMetadata::new("value", ResolvedType::Text)];
1954        let anchor = text_literal_plan(&"x".repeat(32));
1955        let recursive_term = LogicalPlan::RecursiveReference {
1956            name: "memory_cycle".into(),
1957            schema: schema.clone(),
1958        };
1959        let plan = LogicalPlan::RecursiveCte {
1960            name: "memory_cycle".into(),
1961            anchor: Box::new(anchor),
1962            recursive_term: Box::new(recursive_term),
1963            union_all: false,
1964            schema,
1965            limits: RecursiveCteLimits::default(),
1966        };
1967        let policy = MemoryPolicy::new(Some(115), SpillPolicy::FailFast);
1968        let mut txn = bridge.begin_write().unwrap();
1969
1970        let error = execute_query_with_policy(&mut txn, &catalog, plan, Some(&policy))
1971            .expect_err("the working table and its iterator clone must both be accounted");
1972
1973        assert!(
1974            matches!(&error, ExecutorError::ResourceExhausted { message }
1975                if message.contains("query memory limit exceeded")),
1976            "expected recursive materialization to honor the query memory limit, got: {error}"
1977        );
1978    }
1979
1980    #[test]
1981    fn recursive_cte_enforces_accumulated_row_limit() {
1982        let bridge = TxnBridge::new(Arc::new(MemoryKV::new()));
1983        let catalog = MemoryCatalog::new();
1984        let schema = vec![ColumnMetadata::new("value", ResolvedType::Text)];
1985        let anchor = LogicalPlan::SetOperation {
1986            left: Box::new(text_literal_plan("first")),
1987            right: Box::new(text_literal_plan("second")),
1988            operator: SetOperator::Union,
1989            all: true,
1990        };
1991        let plan = LogicalPlan::RecursiveCte {
1992            name: "bounded".into(),
1993            anchor: Box::new(anchor),
1994            recursive_term: Box::new(LogicalPlan::RecursiveReference {
1995                name: "bounded".into(),
1996                schema: schema.clone(),
1997            }),
1998            union_all: true,
1999            schema,
2000            limits: RecursiveCteLimits {
2001                max_iterations: 10,
2002                max_rows: 1,
2003            },
2004        };
2005        let mut txn = bridge.begin_write().unwrap();
2006
2007        let error = execute_query(&mut txn, &catalog, plan)
2008            .expect_err("the accumulated row limit must be checked before iteration");
2009
2010        assert!(
2011            matches!(&error, ExecutorError::ResourceExhausted { message }
2012                if message.contains("recursive CTE 'bounded' reached row limit 1")),
2013            "expected a named recursive row-limit error, got: {error}"
2014        );
2015    }
2016
2017    #[test]
2018    fn recursive_cte_accounts_for_result_before_releasing_working_table() {
2019        let bridge = TxnBridge::new(Arc::new(MemoryKV::new()));
2020        let catalog = MemoryCatalog::new();
2021        let value = "y".repeat(16);
2022        let schema = vec![ColumnMetadata::new("value", ResolvedType::Text)];
2023        let reference = LogicalPlan::RecursiveReference {
2024            name: "result_overlap".into(),
2025            schema: schema.clone(),
2026        };
2027        let two_literals = LogicalPlan::SetOperation {
2028            left: Box::new(text_literal_plan(&value)),
2029            right: Box::new(text_literal_plan(&value)),
2030            operator: SetOperator::Union,
2031            all: true,
2032        };
2033        let recursive_term = LogicalPlan::SetOperation {
2034            left: Box::new(reference),
2035            right: Box::new(two_literals),
2036            operator: SetOperator::Union,
2037            all: true,
2038        };
2039        let plan = LogicalPlan::RecursiveCte {
2040            name: "result_overlap".into(),
2041            anchor: Box::new(text_literal_plan(&value)),
2042            recursive_term: Box::new(recursive_term),
2043            union_all: false,
2044            schema,
2045            limits: RecursiveCteLimits::default(),
2046        };
2047        let policy = MemoryPolicy::new(Some(92), SpillPolicy::FailFast);
2048        let mut txn = bridge.begin_write().unwrap();
2049
2050        let error = execute_query_with_policy(&mut txn, &catalog, plan, Some(&policy))
2051            .expect_err("recursive result must be counted before the working table is released");
2052
2053        assert!(
2054            matches!(&error, ExecutorError::ResourceExhausted { message }
2055                if message.contains("query memory limit exceeded")),
2056            "expected result/working-table overlap to honor the query memory limit, got: {error}"
2057        );
2058    }
2059}