Skip to main content

radixdb_executor/
index_optimizer.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//! Index-based Query Optimizations
16//!
17//! This module contains optimizations that leverage indexes to speed up queries:
18//! - MIN/MAX on indexed columns (O(1) instead of O(n))
19//! - COUNT(*) using row_count() (O(1) instead of O(n))
20//! - ORDER BY + LIMIT using index-ordered scan
21//! - IN list/subquery/hashset using index probe
22//! - Window function pre-sorting detection
23
24use std::sync::Arc;
25
26use radixdb_core::{CompactArc, I64Set};
27use radixdb_core::{Result, Row, RowVec, Value, ValueSet};
28use radixdb_sql::ast::*;
29use radixdb_storage::traits::{Index, QueryResult, Table};
30
31use super::context::{
32    cache_in_subquery, extract_table_names_for_cache, get_cached_in_subquery, ExecutionContext,
33};
34use super::expression::{ExpressionEval, RowFilter};
35use super::planner::QueryPlanner;
36use super::query_classification::QueryClassification;
37use super::result::ExecutorResult;
38use crate::lookup_key::exact_integer_pk_value;
39
40/// Narrow callbacks joining independently bounded optimizer helpers to the
41/// concrete executor owner.
42#[doc(hidden)]
43pub trait IndexOptimizerHost {
44    fn index_project_rows(
45        &self,
46        select_exprs: &[Expression],
47        rows: RowVec,
48        all_columns: &[String],
49        ctx: &ExecutionContext,
50    ) -> Result<RowVec>;
51
52    fn index_project_rows_with_alias(
53        &self,
54        select_exprs: &[Expression],
55        rows: RowVec,
56        all_columns: &[String],
57        all_columns_lower: Option<&[String]>,
58        ctx: &ExecutionContext,
59        table_alias: Option<&str>,
60    ) -> Result<RowVec>;
61
62    fn index_output_column_names(
63        &self,
64        select_exprs: &[Expression],
65        all_columns: &[String],
66        table_alias: Option<&str>,
67    ) -> Vec<String>;
68
69    fn index_query_planner(&self) -> &QueryPlanner;
70
71    fn index_execute_select(
72        &self,
73        stmt: &SelectStatement,
74        ctx: &ExecutionContext,
75    ) -> Result<Box<dyn QueryResult>>;
76
77    fn index_process_where_subqueries(
78        &self,
79        expr: &Expression,
80        ctx: &ExecutionContext,
81    ) -> Result<Expression>;
82
83    fn index_has_subqueries(expr: &Expression) -> bool;
84
85    fn index_is_subquery_correlated(subquery: &SelectStatement) -> bool;
86}
87
88#[doc(hidden)]
89#[derive(Debug, Clone, Copy, PartialEq, Eq)]
90pub enum IntegerPkKeysetBound {
91    After(i64),
92    From(i64),
93    Empty,
94}
95
96impl IntegerPkKeysetBound {
97    /// Return the stronger of two lower bounds without converting an exclusive
98    /// bound into an adjacent inclusive value. Avoiding `value + 1` keeps the
99    /// full i64 domain exact at `i64::MAX`.
100    fn strongest_lower(self, right: Self) -> Self {
101        match (self, right) {
102            (Self::Empty, _) | (_, Self::Empty) => Self::Empty,
103            (Self::After(left), Self::After(right)) => Self::After(left.max(right)),
104            (Self::From(left), Self::From(right)) => Self::From(left.max(right)),
105            (Self::After(exclusive), Self::From(inclusive)) => {
106                if exclusive >= inclusive {
107                    Self::After(exclusive)
108                } else {
109                    Self::From(inclusive)
110                }
111            }
112            (Self::From(inclusive), Self::After(exclusive)) => {
113                if exclusive >= inclusive {
114                    Self::After(exclusive)
115                } else {
116                    Self::From(inclusive)
117                }
118            }
119        }
120    }
121
122    fn from_parts(start_after: Option<i64>, start_from: Option<i64>) -> Option<Self> {
123        match (start_after, start_from) {
124            (Some(after), Some(from)) => Some(Self::After(after).strongest_lower(Self::From(from))),
125            (Some(after), None) => Some(Self::After(after)),
126            (None, Some(from)) => Some(Self::From(from)),
127            (None, None) => None,
128        }
129    }
130
131    fn into_parts(self) -> (Option<i64>, Option<i64>, bool) {
132        match self {
133            Self::After(value) => (Some(value), None, false),
134            Self::From(value) => (None, Some(value), false),
135            Self::Empty => (None, None, true),
136        }
137    }
138}
139
140/// Pre-compiled projection slot for vector search results.
141/// Built once, applied per row — avoids per-row expression compilation.
142#[doc(hidden)]
143pub enum VectorProjectionSlot {
144    /// SELECT * — copy all columns from base row
145    Star,
146    /// Column reference by index
147    Column(usize),
148    /// Pre-computed distance value
149    Distance,
150    /// Pre-compiled expression evaluator (boxed to reduce enum size)
151    Compiled(Box<ExpressionEval>),
152}
153
154/// Index-access planning and execution shortcuts shared by executor hosts.
155#[doc(hidden)]
156pub trait IndexOptimizerExt: IndexOptimizerHost {
157    /// Try to optimize simple MIN/MAX aggregates using index
158    ///
159    /// For queries like `SELECT MIN(col) FROM table` or `SELECT MAX(col) FROM table`
160    /// without WHERE or GROUP BY, use the index's O(1) min/max lookup instead of O(n) scan.
161    #[allow(clippy::type_complexity)]
162    fn try_min_max_index_optimization(
163        &self,
164        stmt: &SelectStatement,
165        table: &dyn Table,
166        _all_columns: &[String],
167    ) -> Result<Option<(Box<dyn QueryResult>, CompactArc<Vec<String>>)>> {
168        // Only optimize single MIN or MAX without DISTINCT
169        if stmt.columns.len() != 1 {
170            return Ok(None);
171        }
172
173        let col_expr = &stmt.columns[0];
174
175        // Extract function info (handle aliased case too)
176        let (func, alias) = match col_expr {
177            Expression::FunctionCall(func) => (func, None),
178            Expression::Aliased(aliased) => {
179                if let Expression::FunctionCall(func) = aliased.expression.as_ref() {
180                    (func, Some(aliased.alias.value.to_string()))
181                } else {
182                    return Ok(None);
183                }
184            }
185            _ => return Ok(None),
186        };
187
188        // Check if it's MIN or MAX
189        // OPTIMIZATION: func.function is already uppercase from parsing
190        if func.function != "MIN" && func.function != "MAX" {
191            return Ok(None);
192        }
193
194        // The shortcut owns only the exact, unfiltered scalar shape. FILTER,
195        // HAVING, DISTINCT, and malformed arity must use normal aggregate
196        // binding/execution rather than being silently discarded here.
197        if func.is_distinct
198            || func.filter.is_some()
199            || func.arguments.len() != 1
200            || stmt.having.is_some()
201        {
202            return Ok(None);
203        }
204
205        let column_name = match &func.arguments[0] {
206            Expression::Identifier(id) => id.value.to_string(),
207            Expression::QualifiedIdentifier(qid) => qid.name.value.to_string(),
208            _ => return Ok(None),
209        };
210
211        // Try to get value from index
212        let value = if func.function == "MIN" {
213            table.get_index_min_value(&column_name)
214        } else {
215            table.get_index_max_value(&column_name)
216        };
217
218        if let Some(val) = value {
219            // Build result - wrap columns in CompactArc once for zero-copy sharing
220            let col_name = alias.unwrap_or_else(|| format!("{}({})", func.function, column_name));
221            let columns = CompactArc::new(vec![col_name]);
222            let mut rows = RowVec::with_capacity(1);
223            rows.push((0, Row::from_values(vec![val])));
224            let result: Box<dyn QueryResult> = Box::new(ExecutorResult::with_arc_columns(
225                CompactArc::clone(&columns),
226                rows,
227            ));
228            return Ok(Some((result, columns)));
229        }
230
231        Ok(None)
232    }
233
234    /// Try to optimize simple COUNT(*) queries using table row_count
235    ///
236    /// For queries like `SELECT COUNT(*) FROM table` without WHERE or GROUP BY,
237    /// use the table's O(1) row_count() method instead of O(n) scan.
238    #[allow(clippy::type_complexity)]
239    fn try_count_star_optimization(
240        &self,
241        stmt: &SelectStatement,
242        table: &dyn Table,
243    ) -> Result<Option<(Box<dyn QueryResult>, CompactArc<Vec<String>>)>> {
244        // Only optimize single COUNT(*) without DISTINCT
245        if stmt.columns.len() != 1 {
246            return Ok(None);
247        }
248
249        let col_expr = &stmt.columns[0];
250
251        // Extract function info (handle aliased case too)
252        let (func, alias) = match col_expr {
253            Expression::FunctionCall(func) => (func, None),
254            Expression::Aliased(aliased) => {
255                if let Expression::FunctionCall(func) = aliased.expression.as_ref() {
256                    (func, Some(aliased.alias.value.to_string()))
257                } else {
258                    return Ok(None);
259                }
260            }
261            _ => return Ok(None),
262        };
263
264        // Check if it's COUNT
265        // OPTIMIZATION: func.function is already uppercase from parsing
266        if func.function != "COUNT" {
267            return Ok(None);
268        }
269
270        // Don't optimize DISTINCT
271        if func.is_distinct || stmt.having.is_some() {
272            return Ok(None);
273        }
274
275        // Don't optimize if FILTER clause is present - requires row-by-row evaluation
276        if func.filter.is_some() {
277            return Ok(None);
278        }
279
280        // Must be COUNT(*) - either empty args or Star expression
281        let is_count_star = func.arguments.is_empty()
282            || (func.arguments.len() == 1 && matches!(func.arguments[0], Expression::Star(_)));
283
284        if !is_count_star {
285            return Ok(None);
286        }
287
288        // Use table's row_count method (O(1) instead of O(n))
289        let count = table.row_count();
290
291        // Build result - wrap columns in CompactArc once for zero-copy sharing
292        let col_name = alias.unwrap_or_else(|| "COUNT(*)".to_string());
293        let columns = CompactArc::new(vec![col_name]);
294        let mut rows = RowVec::with_capacity(1);
295        rows.push((0, Row::from_values(vec![Value::Integer(count as i64)])));
296        let result: Box<dyn QueryResult> = Box::new(ExecutorResult::with_arc_columns(
297            CompactArc::clone(&columns),
298            rows,
299        ));
300        Ok(Some((result, columns)))
301    }
302
303    /// Try to optimize ORDER BY + LIMIT using index-ordered scan
304    ///
305    /// For queries like `SELECT * FROM table ORDER BY col LIMIT 10`,
306    /// use the index to get rows in sorted order directly (O(limit) instead of O(n log n)).
307    #[allow(clippy::type_complexity)]
308    fn try_order_by_index_optimization(
309        &self,
310        stmt: &SelectStatement,
311        table: &dyn Table,
312        all_columns: &[String],
313        ctx: &ExecutionContext,
314    ) -> Result<Option<(Box<dyn QueryResult>, CompactArc<Vec<String>>)>> {
315        // Get the ORDER BY column name
316        let order_by = &stmt.order_by[0];
317        let column_name = match &order_by.expression {
318            Expression::Identifier(id) => id.value.clone(),
319            Expression::QualifiedIdentifier(qid) => qid.name.value.clone(),
320            _ => return Ok(None), // Can't optimize complex ORDER BY expressions
321        };
322
323        // Determine sort order
324        let ascending = order_by.ascending;
325
326        // The current hot B-tree API exposes structural Value ordering, where
327        // NULL is the minimum value.  Therefore it proves ASC NULLS FIRST and
328        // DESC NULLS LAST, but not PostgreSQL's opposite defaults.  A nullable
329        // column with a mismatching SQL NULL placement must take the ordinary
330        // sort path; otherwise LIMIT can return the wrong rows, not merely a
331        // differently ordered tie.
332        let requested_nulls_first = order_by.nulls_first.unwrap_or(!ascending);
333        let structural_nulls_first = ascending;
334        if requested_nulls_first != structural_nulls_first
335            && table
336                .schema()
337                .find_column(&column_name)
338                .is_some_and(|(_, column)| column.nullable)
339        {
340            return Ok(None);
341        }
342
343        // Evaluate limit and offset
344        let limit = if let Some(ref limit_expr) = stmt.limit {
345            match ExpressionEval::compile(limit_expr, &[])
346                .ok()
347                .and_then(|e| e.with_context(ctx).eval_slice(&Row::new()).ok())
348            {
349                Some(Value::Integer(l)) => l as usize,
350                Some(Value::Float(f)) => f as usize,
351                _ => return Ok(None),
352            }
353        } else {
354            return Ok(None);
355        };
356
357        let offset = if let Some(ref offset_expr) = stmt.offset {
358            match ExpressionEval::compile(offset_expr, &[])
359                .ok()
360                .and_then(|e| e.with_context(ctx).eval_slice(&Row::new()).ok())
361            {
362                Some(Value::Integer(o)) => o as usize,
363                Some(Value::Float(f)) => f as usize,
364                _ => 0,
365            }
366        } else {
367            0
368        };
369
370        // Try to use index-ordered scan
371        if let Some(rows) =
372            table.collect_rows_ordered_by_index(&column_name, ascending, limit, offset)
373        {
374            // Project rows according to SELECT expressions
375            let projected_rows = self.index_project_rows(&stmt.columns, rows, all_columns, ctx)?;
376
377            // Note: This optimization path doesn't have table_alias available,
378            // so we pass None. The prefix-based matching will still work for JOINs.
379            let output_columns =
380                CompactArc::new(self.index_output_column_names(&stmt.columns, all_columns, None));
381
382            let result = ExecutorResult::with_arc_columns(
383                CompactArc::clone(&output_columns),
384                projected_rows,
385            );
386            return Ok(Some((Box::new(result), output_columns)));
387        }
388
389        Ok(None)
390    }
391
392    /// Keyset pagination optimization for PRIMARY KEY columns
393    ///
394    /// For queries like `SELECT * FROM table WHERE id > X ORDER BY id LIMIT Y`,
395    /// uses the PK's natural ordering to start iteration from X and return only Y rows.
396    /// This provides O(limit) complexity instead of O(n) for full table scans.
397    #[allow(clippy::type_complexity)]
398    fn try_keyset_pagination_optimization(
399        &self,
400        stmt: &SelectStatement,
401        where_expr: Option<&Expression>,
402        table: &dyn Table,
403        all_columns: &[String],
404        table_alias: Option<&str>,
405        ctx: &ExecutionContext,
406    ) -> Result<Option<(Box<dyn QueryResult>, CompactArc<Vec<String>>)>> {
407        // Must have WHERE clause
408        let where_clause = match where_expr {
409            Some(expr) => expr,
410            None => return Ok(None),
411        };
412
413        // Get the ORDER BY column name - must be single column
414        let order_by = &stmt.order_by[0];
415        let order_column = match &order_by.expression {
416            Expression::Identifier(id) => id.value.clone(),
417            Expression::QualifiedIdentifier(qid) => qid.name.value.clone(),
418            _ => return Ok(None),
419        };
420
421        // Must be ascending order (most common case for keyset pagination)
422        let ascending = order_by.ascending;
423
424        // Get table schema to check if ORDER BY is on PK
425        let schema = table.schema();
426        let pk_indices = schema.primary_key_indices();
427        if pk_indices.len() != 1 {
428            return Ok(None); // Only single-column PK supported
429        }
430
431        let pk_idx = pk_indices[0];
432        if schema.columns[pk_idx].data_type != radixdb_core::DataType::Integer {
433            return Ok(None);
434        }
435        let pk_column = &schema.columns[pk_idx].name;
436
437        // ORDER BY must be on PK column
438        if !order_column.eq_ignore_ascii_case(pk_column) {
439            return Ok(None);
440        }
441
442        // Extract keyset condition from WHERE clause
443        // Supports: id > X, id >= X, id < X, id <= X
444        let (start_after, start_from, is_pure_keyset, is_empty) =
445            self.extract_pk_keyset_bounds(where_clause, pk_column, ctx)?;
446
447        if is_empty && is_pure_keyset {
448            let output_columns = CompactArc::new(self.index_output_column_names(
449                &stmt.columns,
450                all_columns,
451                table_alias,
452            ));
453            return Ok(Some((
454                Box::new(ExecutorResult::with_arc_columns(
455                    CompactArc::clone(&output_columns),
456                    RowVec::new(),
457                )),
458                output_columns,
459            )));
460        }
461
462        // Must have at least one bound AND be a pure keyset predicate
463        // (no additional predicates like `AND status = 'active'`)
464        if start_after.is_none() && start_from.is_none() {
465            return Ok(None);
466        }
467        if !is_pure_keyset {
468            return Ok(None);
469        }
470
471        // Evaluate limit
472        let limit = if let Some(ref limit_expr) = stmt.limit {
473            match ExpressionEval::compile(limit_expr, &[])
474                .ok()
475                .and_then(|e| e.with_context(ctx).eval_slice(&Row::new()).ok())
476            {
477                Some(Value::Integer(l)) => l as usize,
478                Some(Value::Float(f)) => f as usize,
479                _ => return Ok(None),
480            }
481        } else {
482            return Ok(None);
483        };
484
485        // Use keyset pagination from storage layer
486        if let Some(rows) = table.collect_rows_pk_keyset(start_after, start_from, ascending, limit)
487        {
488            // Project rows according to SELECT expressions
489            // Use schema cache for lowercase column names
490            let all_columns_lower = schema.column_names_lower_arc();
491            let projected_rows = self.index_project_rows_with_alias(
492                &stmt.columns,
493                rows,
494                all_columns,
495                Some(&all_columns_lower),
496                ctx,
497                table_alias,
498            )?;
499            let output_columns = CompactArc::new(self.index_output_column_names(
500                &stmt.columns,
501                all_columns,
502                table_alias,
503            ));
504
505            let result = ExecutorResult::with_arc_columns(
506                CompactArc::clone(&output_columns),
507                projected_rows,
508            );
509            return Ok(Some((Box::new(result), output_columns)));
510        }
511
512        Ok(None)
513    }
514
515    /// Execute a bounded equality-prefix + range query in composite-index order.
516    #[allow(clippy::type_complexity)]
517    fn try_composite_ordered_range_optimization(
518        &self,
519        stmt: &SelectStatement,
520        where_expr: &dyn radixdb_storage::expression::Expression,
521        table: &dyn Table,
522        all_columns: &[String],
523        table_alias: Option<&str>,
524        ctx: &ExecutionContext,
525    ) -> Result<Option<(Box<dyn QueryResult>, CompactArc<Vec<String>>)>> {
526        let order_by = &stmt.order_by[0];
527        let order_column = match &order_by.expression {
528            Expression::Identifier(id) => id.value.as_str(),
529            Expression::QualifiedIdentifier(qid) => qid.name.value.as_str(),
530            _ => return Ok(None),
531        };
532        let limit = match stmt
533            .limit
534            .as_ref()
535            .and_then(|expr| ExpressionEval::compile(expr, &[]).ok())
536            .and_then(|expr| expr.with_context(ctx).eval_slice(&Row::new()).ok())
537        {
538            Some(Value::Integer(value)) if value >= 0 => value as usize,
539            Some(Value::Float(value)) if value >= 0.0 => value as usize,
540            _ => return Ok(None),
541        };
542        let offset = match stmt
543            .offset
544            .as_ref()
545            .and_then(|expr| ExpressionEval::compile(expr, &[]).ok())
546            .and_then(|expr| expr.with_context(ctx).eval_slice(&Row::new()).ok())
547        {
548            Some(Value::Integer(value)) if value >= 0 => value as usize,
549            Some(Value::Float(value)) if value >= 0.0 => value as usize,
550            None => 0,
551            _ => return Ok(None),
552        };
553
554        let Some(rows) = table.collect_rows_composite_ordered_range(
555            where_expr,
556            order_column,
557            order_by.ascending,
558            limit,
559            offset,
560        ) else {
561            return Ok(None);
562        };
563        let rows = rows?;
564        let lower = table.schema().column_names_lower_arc();
565        let projected_rows = self.index_project_rows_with_alias(
566            &stmt.columns,
567            rows,
568            all_columns,
569            Some(&lower),
570            ctx,
571            table_alias,
572        )?;
573        let output_columns = CompactArc::new(self.index_output_column_names(
574            &stmt.columns,
575            all_columns,
576            table_alias,
577        ));
578        let result =
579            ExecutorResult::with_arc_columns(CompactArc::clone(&output_columns), projected_rows);
580        Ok(Some((Box::new(result), output_columns)))
581    }
582
583    /// Extract PK keyset bounds from a WHERE clause
584    ///
585    /// Returns (start_after, start_from, is_pure_keyset) where:
586    /// - start_after is for `id > X` (exclusive bound)
587    /// - start_from is for `id >= X` (inclusive bound)
588    /// - is_pure_keyset is true if the expression ONLY contains keyset predicates
589    ///   (no additional predicates like `AND status = 'active'`)
590    fn extract_pk_keyset_bounds(
591        &self,
592        expr: &Expression,
593        pk_column: &str,
594        ctx: &ExecutionContext,
595    ) -> Result<(Option<i64>, Option<i64>, bool, bool)> {
596        if let Expression::Infix(infix) = expr {
597            let op = infix.operator.as_str();
598
599            // Handle comparison operators
600            match op {
601                ">" | ">=" | "<" | "<=" => {
602                    // Check if left side is the PK column
603                    let left_is_pk = match &*infix.left {
604                        Expression::Identifier(id) => id.value.eq_ignore_ascii_case(pk_column),
605                        Expression::QualifiedIdentifier(qid) => {
606                            qid.name.value.eq_ignore_ascii_case(pk_column)
607                        }
608                        _ => false,
609                    };
610
611                    if left_is_pk {
612                        // pk OP value
613                        if matches!(op, ">" | ">=") {
614                            if let Some(value) = self.eval_pk_keyset_scalar(&infix.right, ctx) {
615                                if let Some(bound) =
616                                    Self::integer_pk_keyset_lower_bound(&value, op == ">=")
617                                {
618                                    return Ok(match bound {
619                                        IntegerPkKeysetBound::After(value) => {
620                                            (Some(value), None, true, false)
621                                        }
622                                        IntegerPkKeysetBound::From(value) => {
623                                            (None, Some(value), true, false)
624                                        }
625                                        IntegerPkKeysetBound::Empty => (None, None, true, true),
626                                    });
627                                }
628                            }
629                        }
630                    } else {
631                        // value OP pk (reversed)
632                        let right_is_pk = match &*infix.right {
633                            Expression::Identifier(id) => id.value.eq_ignore_ascii_case(pk_column),
634                            Expression::QualifiedIdentifier(qid) => {
635                                qid.name.value.eq_ignore_ascii_case(pk_column)
636                            }
637                            _ => false,
638                        };
639
640                        if right_is_pk && matches!(op, "<" | "<=") {
641                            if let Some(value) = self.eval_pk_keyset_scalar(&infix.left, ctx) {
642                                if let Some(bound) =
643                                    Self::integer_pk_keyset_lower_bound(&value, op == "<=")
644                                {
645                                    return Ok(match bound {
646                                        IntegerPkKeysetBound::After(value) => {
647                                            (Some(value), None, true, false)
648                                        }
649                                        IntegerPkKeysetBound::From(value) => {
650                                            (None, Some(value), true, false)
651                                        }
652                                        IntegerPkKeysetBound::Empty => (None, None, true, true),
653                                    });
654                                }
655                            }
656                        }
657                    }
658                }
659                "AND" => {
660                    // For AND, try to extract bounds from both sides
661                    let (left_after, left_from, left_pure, left_empty) =
662                        self.extract_pk_keyset_bounds(&infix.left, pk_column, ctx)?;
663                    let (right_after, right_from, right_pure, right_empty) =
664                        self.extract_pk_keyset_bounds(&infix.right, pk_column, ctx)?;
665
666                    // Expression is pure keyset only if BOTH sides are pure keyset predicates
667                    // (e.g., `id > 100 AND id > 200` is pure,
668                    // `id > 100 AND status = 'active'` is not).
669                    let is_pure = left_pure && right_pure;
670
671                    if left_empty || right_empty {
672                        return Ok((None, None, is_pure, true));
673                    }
674
675                    let left_bound = IntegerPkKeysetBound::from_parts(left_after, left_from);
676                    let right_bound = IntegerPkKeysetBound::from_parts(right_after, right_from);
677                    let strongest = match (left_bound, right_bound) {
678                        (Some(left), Some(right)) => Some(left.strongest_lower(right)),
679                        (Some(bound), None) | (None, Some(bound)) => Some(bound),
680                        (None, None) => None,
681                    };
682
683                    let (start_after, start_from, is_empty) = strongest
684                        .map(IntegerPkKeysetBound::into_parts)
685                        .unwrap_or((None, None, false));
686
687                    return Ok((start_after, start_from, is_pure, is_empty));
688                }
689                _ => {}
690            }
691        }
692
693        // Not a keyset predicate - this is an additional filter
694        Ok((None, None, false, false))
695    }
696
697    /// Evaluate a keyset boundary without coercing it to the PK type.
698    fn eval_pk_keyset_scalar(&self, expr: &Expression, ctx: &ExecutionContext) -> Option<Value> {
699        match expr {
700            Expression::IntegerLiteral(lit) => Some(Value::Integer(lit.value)),
701            Expression::FloatLiteral(lit) => Some(Value::Float(lit.value)),
702            Expression::Parameter(param) => {
703                // Named parameters (e.g., :name) use get_named_param()
704                // Positional parameters ($1, $2, ...) are 1-indexed, array is 0-indexed
705                if param.name.starts_with(':') {
706                    let name = &param.name[1..];
707                    ctx.get_named_param(name).cloned()
708                } else {
709                    let params = ctx.params();
710                    let param_idx = if param.index > 0 {
711                        param.index - 1
712                    } else {
713                        return None;
714                    };
715                    if param_idx < params.len() {
716                        Some(params[param_idx].clone())
717                    } else {
718                        None
719                    }
720                }
721            }
722            _ => {
723                // Try to evaluate the expression
724                ExpressionEval::compile(expr, &[])
725                    .ok()
726                    .and_then(|e| e.with_context(ctx).eval_slice(&Row::new()).ok())
727            }
728        }
729    }
730
731    /// Derive the exact lower boundary of an INTEGER domain without using a
732    /// truncating/saturating Float cast as a PK key.
733    fn integer_pk_keyset_lower_bound(
734        value: &Value,
735        inclusive: bool,
736    ) -> Option<IntegerPkKeysetBound> {
737        if let Some(integer) = exact_integer_pk_value(value) {
738            return Some(if inclusive {
739                IntegerPkKeysetBound::From(integer)
740            } else {
741                IntegerPkKeysetBound::After(integer)
742            });
743        }
744
745        let Value::Float(float) = value else {
746            return None;
747        };
748        if float.is_nan() || *float == f64::INFINITY {
749            return Some(IntegerPkKeysetBound::Empty);
750        }
751        if *float == f64::NEG_INFINITY || *float < i64::MIN as f64 {
752            return Some(IntegerPkKeysetBound::From(i64::MIN));
753        }
754        if *float >= i64::MAX as f64 {
755            return Some(IntegerPkKeysetBound::Empty);
756        }
757
758        debug_assert!(float.is_finite() && float.fract() != 0.0);
759        Some(IntegerPkKeysetBound::From(float.ceil() as i64))
760    }
761
762    /// Extract window ORDER BY information for optimization
763    /// Returns (column_name, ascending) if a simple optimizable case is found
764    fn extract_window_order_info(stmt: &SelectStatement) -> Option<(String, bool)> {
765        // Look for window functions in SELECT columns
766        for col_expr in &stmt.columns {
767            if let Some(info) = Self::find_window_order_in_expr(col_expr) {
768                return Some(info);
769            }
770        }
771        None
772    }
773
774    /// Check if all window functions in the statement are safe for LIMIT pushdown.
775    ///
776    /// Safe functions (don't depend on total row count):
777    /// - ROW_NUMBER, RANK, DENSE_RANK
778    /// - LAG, LEAD
779    /// - FIRST_VALUE, LAST_VALUE, NTH_VALUE
780    ///
781    /// Unsafe functions (depend on total row count):
782    /// - NTILE (bucket size depends on total count)
783    /// - PERCENT_RANK (formula: (rank-1)/(total-1))
784    /// - CUME_DIST (formula: rows_up_to_current/total)
785    fn is_window_safe_for_limit_pushdown(stmt: &SelectStatement) -> bool {
786        for col_expr in &stmt.columns {
787            if !Self::is_expr_window_safe(col_expr) {
788                return false;
789            }
790        }
791        true
792    }
793
794    /// Check if a single expression's window function (if any) is safe for LIMIT pushdown
795    /// Recursively checks all nested expressions for unsafe window functions
796    fn is_expr_window_safe(expr: &Expression) -> bool {
797        match expr {
798            Expression::Window(window_expr) => {
799                let func_name = window_expr.function.function.to_uppercase();
800                // These functions depend on total row count - NOT safe
801                !matches!(func_name.as_str(), "NTILE" | "PERCENT_RANK" | "CUME_DIST")
802            }
803            Expression::Aliased(aliased) => Self::is_expr_window_safe(&aliased.expression),
804            // Recursively check expressions that can contain window functions
805            Expression::Case(case_expr) => {
806                // Check operand if present (simple CASE expression)
807                if let Some(value) = &case_expr.value {
808                    if !Self::is_expr_window_safe(value) {
809                        return false;
810                    }
811                }
812                // Check all WHEN conditions and results
813                for when_clause in &case_expr.when_clauses {
814                    if !Self::is_expr_window_safe(&when_clause.condition)
815                        || !Self::is_expr_window_safe(&when_clause.then_result)
816                    {
817                        return false;
818                    }
819                }
820                // Check ELSE clause if present
821                if let Some(else_expr) = &case_expr.else_value {
822                    if !Self::is_expr_window_safe(else_expr) {
823                        return false;
824                    }
825                }
826                true
827            }
828            Expression::FunctionCall(func) => {
829                // Check all function arguments
830                func.arguments.iter().all(Self::is_expr_window_safe)
831            }
832            Expression::Infix(infix) => {
833                // Check both sides of binary expression (e.g., window_func + 1)
834                Self::is_expr_window_safe(&infix.left) && Self::is_expr_window_safe(&infix.right)
835            }
836            Expression::Cast(cast) => Self::is_expr_window_safe(&cast.expr),
837            Expression::Prefix(prefix) => Self::is_expr_window_safe(&prefix.right),
838            Expression::ScalarSubquery(_) => {
839                // Scalar subqueries have their own evaluation context, their window functions
840                // don't affect the outer query's LIMIT pushdown safety
841                true
842            }
843            Expression::In(in_expr) => {
844                // Check the left expression and right expression (which contains the list)
845                Self::is_expr_window_safe(&in_expr.left)
846                    && Self::is_expr_window_safe(&in_expr.right)
847            }
848            Expression::Between(between) => {
849                Self::is_expr_window_safe(&between.expr)
850                    && Self::is_expr_window_safe(&between.lower)
851                    && Self::is_expr_window_safe(&between.upper)
852            }
853            Expression::List(list) => {
854                // Check all expressions in the list
855                list.elements.iter().all(Self::is_expr_window_safe)
856            }
857            Expression::ExpressionList(expr_list) => {
858                // Check all expressions in the list
859                expr_list.expressions.iter().all(Self::is_expr_window_safe)
860            }
861            Expression::Like(like) => {
862                Self::is_expr_window_safe(&like.left)
863                    && Self::is_expr_window_safe(&like.pattern)
864                    && like
865                        .escape
866                        .as_ref()
867                        .is_none_or(|e| Self::is_expr_window_safe(e))
868            }
869            Expression::Exists(_) | Expression::AllAny(_) => {
870                // EXISTS and ALL/ANY have their own subquery context
871                true
872            }
873            Expression::Distinct(distinct) => Self::is_expr_window_safe(&distinct.expr),
874            // Leaf expressions - no nested window functions
875            Expression::Identifier(_)
876            | Expression::QualifiedIdentifier(_)
877            | Expression::IntegerLiteral(_)
878            | Expression::FloatLiteral(_)
879            | Expression::StringLiteral(_)
880            | Expression::BooleanLiteral(_)
881            | Expression::NullLiteral(_)
882            | Expression::Parameter(_)
883            | Expression::IntervalLiteral(_)
884            | Expression::BoundValue(_)
885            | Expression::Star(_)
886            | Expression::QualifiedStar(_)
887            | Expression::Default(_)
888            | Expression::InHashSet(_)
889            | Expression::TableSource(_)
890            | Expression::JoinSource(_)
891            | Expression::SubquerySource(_)
892            | Expression::ValuesSource(_)
893            | Expression::CteReference(_)
894            | Expression::FunctionTableSource(_) => true,
895        }
896    }
897
898    /// Check if an expression contains an equality condition (for hash join eligibility).
899    ///
900    /// Returns true if the expression contains at least one `=` comparison
901    /// that is NOT `!=`, `<>`, `>=`, or `<=`. This is used to determine if
902    /// streaming hash join can be used.
903    fn has_equality_condition(expr: &Expression) -> bool {
904        match expr {
905            // Direct equality comparison: col1 = col2
906            Expression::Infix(infix) => {
907                let op = infix.operator.as_str();
908                if op == "=" {
909                    // This is a pure equality, not >=, <=, !=, <>
910                    return true;
911                }
912                // Recurse into AND/OR branches
913                if op.eq_ignore_ascii_case("AND") || op.eq_ignore_ascii_case("OR") {
914                    return Self::has_equality_condition(&infix.left)
915                        || Self::has_equality_condition(&infix.right);
916                }
917                false
918            }
919            // Note: The parser inlines parenthesized expressions, so no Grouped variant needed
920            _ => false,
921        }
922    }
923
924    /// Estimate cardinality of a table expression for join build side selection.
925    ///
926    /// Uses table statistics and filter selectivity to estimate output rows.
927    /// Returns u64::MAX for complex expressions (subqueries, CTEs) that can't be estimated.
928    ///
929    /// This is used to choose the smaller side as build in streaming hash joins.
930    fn estimate_table_expr_cardinality(
931        &self,
932        expr: &Expression,
933        filter: Option<&Expression>,
934    ) -> u64 {
935        // Extract table name from expression (use pre-computed lowercase)
936        let table_name: &str = match expr {
937            Expression::TableSource(ts) => ts.name.value_lower.as_str(),
938            Expression::Aliased(aliased) => {
939                if let Expression::TableSource(ts) = aliased.expression.as_ref() {
940                    ts.name.value_lower.as_str()
941                } else {
942                    return u64::MAX; // Can't estimate subqueries/complex sources
943                }
944            }
945            _ => return u64::MAX, // Can't estimate joins, subqueries, CTEs
946        };
947
948        // Use the planner's estimate_scan_rows for accurate estimation
949        self.index_query_planner()
950            .estimate_scan_rows(table_name, filter)
951            .unwrap_or(u64::MAX)
952    }
953
954    /// Find window ORDER BY info in an expression
955    fn find_window_order_in_expr(expr: &Expression) -> Option<(String, bool)> {
956        match expr {
957            Expression::Window(window_expr) => {
958                // Check for no PARTITION BY (single partition case)
959                if !window_expr.partition_by.is_empty() {
960                    return None; // Pre-sorting doesn't help with partitions
961                }
962
963                // Check if using a window reference (can't analyze those)
964                if window_expr.window_ref.is_some() {
965                    return None;
966                }
967
968                // Get ORDER BY info
969                let order_by = &window_expr.order_by;
970
971                // Only optimize if exactly one simple ORDER BY column
972                if order_by.len() != 1 {
973                    return None;
974                }
975
976                let order = &order_by[0];
977                let column_name = match &order.expression {
978                    Expression::Identifier(id) => id.value.to_string(),
979                    Expression::QualifiedIdentifier(qid) => qid.name.value.to_string(),
980                    _ => return None, // Complex expression, can't optimize
981                };
982
983                Some((column_name, order.ascending))
984            }
985            Expression::Aliased(aliased) => Self::find_window_order_in_expr(&aliased.expression),
986            _ => None,
987        }
988    }
989
990    /// Extract window PARTITION BY information for optimization
991    /// Returns column_name if a simple single-column PARTITION BY is found
992    fn extract_window_partition_info(stmt: &SelectStatement) -> Option<String> {
993        // All window functions (including nested ones) must share the same single
994        // simple partition column. If any window is unpartitioned (global), uses
995        // complex expressions, or partitions by a different column, bail out.
996        let mut canonical: Option<String> = None;
997        for col_expr in &stmt.columns {
998            if !Self::collect_window_partitions(col_expr, &mut canonical) {
999                return None;
1000            }
1001        }
1002        canonical
1003    }
1004
1005    /// Recursively walk an expression tree, classifying every Window node found.
1006    /// Returns false (bail out) if any window is incompatible with the optimization.
1007    /// Updates `canonical` with the shared partition column.
1008    fn collect_window_partitions(expr: &Expression, canonical: &mut Option<String>) -> bool {
1009        match expr {
1010            Expression::Window(window_expr) => {
1011                if window_expr.partition_by.is_empty()
1012                    || window_expr.partition_by.len() != 1
1013                    || window_expr.window_ref.is_some()
1014                {
1015                    return false;
1016                }
1017                let col_name = match &window_expr.partition_by[0] {
1018                    Expression::Identifier(id) => id.value.to_string(),
1019                    Expression::QualifiedIdentifier(qid) => qid.name.value.to_string(),
1020                    _ => return false,
1021                };
1022                match canonical {
1023                    None => *canonical = Some(col_name),
1024                    Some(c) if !c.eq_ignore_ascii_case(&col_name) => return false,
1025                    _ => {}
1026                }
1027                true
1028            }
1029            Expression::Aliased(aliased) => {
1030                Self::collect_window_partitions(&aliased.expression, canonical)
1031            }
1032            Expression::Infix(infix) => {
1033                Self::collect_window_partitions(&infix.left, canonical)
1034                    && Self::collect_window_partitions(&infix.right, canonical)
1035            }
1036            Expression::Prefix(prefix) => Self::collect_window_partitions(&prefix.right, canonical),
1037            Expression::Case(case) => {
1038                if let Some(v) = &case.value {
1039                    if !Self::collect_window_partitions(v, canonical) {
1040                        return false;
1041                    }
1042                }
1043                for w in &case.when_clauses {
1044                    if !Self::collect_window_partitions(&w.condition, canonical)
1045                        || !Self::collect_window_partitions(&w.then_result, canonical)
1046                    {
1047                        return false;
1048                    }
1049                }
1050                if let Some(e) = &case.else_value {
1051                    if !Self::collect_window_partitions(e, canonical) {
1052                        return false;
1053                    }
1054                }
1055                true
1056            }
1057            Expression::Cast(cast) => Self::collect_window_partitions(&cast.expr, canonical),
1058            Expression::FunctionCall(func) => {
1059                for arg in &func.arguments {
1060                    if !Self::collect_window_partitions(arg, canonical) {
1061                        return false;
1062                    }
1063                }
1064                true
1065            }
1066            Expression::Between(between) => {
1067                Self::collect_window_partitions(&between.expr, canonical)
1068                    && Self::collect_window_partitions(&between.lower, canonical)
1069                    && Self::collect_window_partitions(&between.upper, canonical)
1070            }
1071            Expression::In(in_expr) => {
1072                Self::collect_window_partitions(&in_expr.left, canonical)
1073                    && Self::collect_window_partitions(&in_expr.right, canonical)
1074            }
1075            Expression::Like(l) => {
1076                Self::collect_window_partitions(&l.left, canonical)
1077                    && Self::collect_window_partitions(&l.pattern, canonical)
1078                    && l.escape
1079                        .as_ref()
1080                        .is_none_or(|e| Self::collect_window_partitions(e, canonical))
1081            }
1082            Expression::Distinct(d) => Self::collect_window_partitions(&d.expr, canonical),
1083            Expression::List(l) => l
1084                .elements
1085                .iter()
1086                .all(|e| Self::collect_window_partitions(e, canonical)),
1087            Expression::ExpressionList(l) => l
1088                .expressions
1089                .iter()
1090                .all(|e| Self::collect_window_partitions(e, canonical)),
1091            // Leaf nodes that cannot contain window expressions
1092            _ => true,
1093        }
1094    }
1095
1096    /// IN subquery index optimization
1097    ///
1098    /// For queries like `SELECT * FROM users WHERE id IN (SELECT user_id FROM orders WHERE ...)`
1099    /// where `id` has an index, probe the index for each subquery value instead of scanning all rows.
1100    /// This is O(k log n) where k = subquery result size, vs O(n) for full table scan.
1101    #[allow(clippy::type_complexity)]
1102    #[allow(clippy::too_many_arguments)]
1103    fn try_in_subquery_index_optimization(
1104        &self,
1105        stmt: &SelectStatement,
1106        where_expr: &Expression,
1107        table: &dyn Table,
1108        all_columns: &[String],
1109        table_alias: Option<&str>,
1110        ctx: &ExecutionContext,
1111        classification: &Arc<QueryClassification>,
1112    ) -> Result<Option<(Box<dyn QueryResult>, CompactArc<Vec<String>>)>> {
1113        // Extract IN subquery info: (column_name, subquery, is_negated, remaining_predicate)
1114        let (column_name, subquery, is_negated, remaining_predicate) =
1115            match Self::extract_in_subquery_info(where_expr) {
1116                Some(info) => info,
1117                None => return Ok(None),
1118            };
1119
1120        // INTEGER primary keys are sparse values, not a dense row-id range.
1121        // The ordinary evaluator preserves both the real key domain and NULL
1122        // three-valued logic for NOT IN.
1123        if is_negated {
1124            return Ok(None);
1125        }
1126
1127        // Skip correlated subqueries - they can't be pre-evaluated
1128        if <Self as IndexOptimizerHost>::index_is_subquery_correlated(&subquery.subquery) {
1129            return Ok(None);
1130        }
1131
1132        // Skip if SELECT columns have correlated subqueries (need per-row context)
1133        // classification is passed from caller to avoid redundant cache lookups
1134        if classification.select_has_correlated_subqueries {
1135            return Ok(None);
1136        }
1137
1138        // Check if this is a PRIMARY KEY column (O(1) lookup) or has an index
1139        let schema = table.schema();
1140        let pk_indices = schema.primary_key_indices();
1141        let is_pk_column = pk_indices.len() == 1 && {
1142            let pk_col_idx = pk_indices[0];
1143            schema.columns[pk_col_idx].data_type == radixdb_core::DataType::Integer
1144                && schema.columns[pk_col_idx].name_lower == column_name
1145        };
1146
1147        // Execute the subquery to get values (with caching for non-correlated subqueries)
1148        let cache_key = subquery.subquery.to_string();
1149        let values = if let Some(cached) = get_cached_in_subquery(&cache_key) {
1150            cached
1151        } else {
1152            let subquery_ctx = ctx.with_incremented_query_depth();
1153            let mut result = self.index_execute_select(&subquery.subquery, &subquery_ctx)?;
1154
1155            // Collect all values from the first column
1156            let mut values = Vec::new();
1157            while result.next() {
1158                let row = result.row();
1159                if !row.is_empty() {
1160                    values.push(
1161                        row.get(0)
1162                            .cloned()
1163                            .unwrap_or_else(radixdb_core::Value::null_unknown),
1164                    );
1165                }
1166            }
1167            if let Some(err) = result.last_error() {
1168                return Err(err);
1169            }
1170            // Cache for future use
1171            cache_in_subquery(
1172                cache_key,
1173                extract_table_names_for_cache(&subquery.subquery),
1174                values.clone(),
1175            );
1176            values
1177        };
1178
1179        // A direct mutable Index handle only covers the hot tier. Ask the
1180        // table for a complete physical candidate set so segmented tables can
1181        // merge persisted postings, and transaction-local tables can decline
1182        // the shortcut when private changes make shared indexes incomplete.
1183        let indexed_row_ids = if !is_pk_column && !is_negated {
1184            match table.collect_row_ids_by_index_values(&column_name, &values) {
1185                Some(row_ids) => Some(row_ids?),
1186                None => return Ok(None),
1187            }
1188        } else {
1189            None
1190        };
1191
1192        if values.is_empty() {
1193            // Empty subquery result
1194            if is_negated {
1195                // NOT IN empty set = all rows match (fall through to normal scan)
1196                return Ok(None);
1197            } else {
1198                // IN empty set = no rows match
1199                let output_columns = CompactArc::new(self.index_output_column_names(
1200                    &stmt.columns,
1201                    all_columns,
1202                    table_alias,
1203                ));
1204                let result = ExecutorResult::with_arc_columns(
1205                    CompactArc::clone(&output_columns),
1206                    RowVec::new(),
1207                );
1208                return Ok(Some((Box::new(result), output_columns)));
1209            }
1210        }
1211
1212        // Collect row_ids: either from PK (direct) or from index probe
1213        // Pre-allocate based on expected size to avoid reallocations
1214        let mut all_row_ids = Vec::with_capacity(values.len());
1215        if is_pk_column {
1216            if is_negated {
1217                // NOT IN optimization for INTEGER PRIMARY KEY:
1218                // Instead of scanning the table, iterate through row_ids and exclude
1219                // This is O(row_count) but with O(1) hashset lookup, which is faster
1220                // than table scan because we only touch row_ids, not full rows
1221                let exclusion_set: I64Set =
1222                    values.iter().filter_map(exact_integer_pk_value).collect();
1223
1224                // Calculate LIMIT + OFFSET to know how many row_ids we need
1225                let offset = stmt
1226                    .offset
1227                    .as_ref()
1228                    .and_then(|e| {
1229                        ExpressionEval::compile(e, &[])
1230                            .ok()
1231                            .and_then(|eval| eval.with_context(ctx).eval_slice(&Row::new()).ok())
1232                            .and_then(|v| {
1233                                if let Value::Integer(o) = v {
1234                                    Some(o.max(0) as usize)
1235                                } else {
1236                                    None
1237                                }
1238                            })
1239                    })
1240                    .unwrap_or(0);
1241
1242                let limit = stmt
1243                    .limit
1244                    .as_ref()
1245                    .and_then(|e| {
1246                        ExpressionEval::compile(e, &[])
1247                            .ok()
1248                            .and_then(|eval| eval.with_context(ctx).eval_slice(&Row::new()).ok())
1249                            .and_then(|v| {
1250                                if let Value::Integer(l) = v {
1251                                    Some(l.max(0) as usize)
1252                                } else {
1253                                    None
1254                                }
1255                            })
1256                    })
1257                    .unwrap_or(usize::MAX);
1258
1259                let target = if limit == usize::MAX {
1260                    usize::MAX
1261                } else {
1262                    offset.saturating_add(limit)
1263                };
1264
1265                // Iterate through row_ids and collect non-excluded ones
1266                // Row IDs are typically 1-based and sequential
1267                let row_count = table.row_count();
1268                for row_id in 1..=(row_count as i64) {
1269                    if !exclusion_set.contains(row_id) {
1270                        all_row_ids.push(row_id);
1271                        if all_row_ids.len() >= target {
1272                            break;
1273                        }
1274                    }
1275                }
1276
1277                // Apply offset
1278                if offset > 0 && offset < all_row_ids.len() {
1279                    all_row_ids = all_row_ids.split_off(offset);
1280                } else if offset >= all_row_ids.len() {
1281                    all_row_ids.clear();
1282                }
1283
1284                // Apply limit
1285                if limit < all_row_ids.len() {
1286                    all_row_ids.truncate(limit);
1287                }
1288            } else {
1289                // IN: PRIMARY KEY - the value IS the row_id (for INTEGER PK)
1290                for value in &values {
1291                    if let Some(id) = exact_integer_pk_value(value) {
1292                        all_row_ids.push(id);
1293                    }
1294                }
1295            }
1296        } else if is_negated {
1297            // NOT IN with non-PK index: fall back to normal scan
1298            return Ok(None);
1299        } else if let Some(row_ids) = indexed_row_ids {
1300            all_row_ids.extend(row_ids);
1301        }
1302
1303        // Sort row_ids for better cache locality during version lookup
1304        // and deduplicate. More efficient than HashSet when sorted output is needed anyway.
1305        // (only for IN, NOT IN is already ordered)
1306        if !is_negated {
1307            all_row_ids.sort_unstable();
1308            all_row_ids.dedup();
1309        }
1310
1311        // EARLY LIMIT OPTIMIZATION: When there's no ORDER BY and no remaining predicate,
1312        // we can apply LIMIT early to avoid fetching unnecessary rows
1313        let (early_limit_applied, early_limit, early_offset) =
1314            if stmt.order_by.is_empty() && remaining_predicate.is_none() {
1315                let offset = if let Some(ref offset_expr) = stmt.offset {
1316                    match ExpressionEval::compile(offset_expr, &[])
1317                        .ok()
1318                        .and_then(|e| e.with_context(ctx).eval_slice(&Row::new()).ok())
1319                    {
1320                        Some(Value::Integer(o)) if o >= 0 => o as usize,
1321                        _ => 0,
1322                    }
1323                } else {
1324                    0
1325                };
1326
1327                let limit = if let Some(ref limit_expr) = stmt.limit {
1328                    match ExpressionEval::compile(limit_expr, &[])
1329                        .ok()
1330                        .and_then(|e| e.with_context(ctx).eval_slice(&Row::new()).ok())
1331                    {
1332                        Some(Value::Integer(l)) if l >= 0 => l as usize,
1333                        _ => usize::MAX,
1334                    }
1335                } else {
1336                    usize::MAX
1337                };
1338
1339                // Truncate row_ids to avoid fetching unnecessary rows
1340                if limit < usize::MAX {
1341                    let take_count = (offset + limit).min(all_row_ids.len());
1342                    all_row_ids.truncate(take_count);
1343                }
1344                (true, limit, offset)
1345            } else {
1346                (false, usize::MAX, 0)
1347            };
1348
1349        // Create a filter expression for remaining predicate + visibility
1350        use radixdb_storage::expression::logical::ConstBoolExpr;
1351        let filter: Box<dyn radixdb_storage::expression::Expression> =
1352            Box::new(ConstBoolExpr::true_expr());
1353
1354        // Fetch rows by row_ids - returns RowVec directly
1355        let mut rows = table.fetch_rows_by_ids(&all_row_ids, filter.as_ref())?;
1356
1357        // Apply remaining predicate if any
1358        if let Some(ref remaining) = remaining_predicate {
1359            // Process any subqueries in the remaining predicate
1360            let processed_remaining =
1361                if <Self as IndexOptimizerHost>::index_has_subqueries(remaining) {
1362                    self.index_process_where_subqueries(remaining, ctx)?
1363                } else {
1364                    remaining.clone()
1365                };
1366
1367            // Compile the filter using RowFilter (with params for $1 etc.)
1368            let columns_slice: Vec<String> = all_columns.to_vec();
1369            let row_filter =
1370                RowFilter::new(&processed_remaining, &columns_slice)?.with_context(ctx);
1371
1372            // Filter rows (retain works on (i64, Row) tuples via Deref)
1373            row_filter.retain_checked(&mut rows)?;
1374        }
1375
1376        // Apply LIMIT/OFFSET if present (and no ORDER BY)
1377        // Skip if we already applied early limit optimization
1378        if early_limit_applied {
1379            // Early optimization already truncated row_ids, but we still need to apply offset
1380            if early_offset > 0 {
1381                rows = rows
1382                    .into_iter()
1383                    .skip(early_offset)
1384                    .take(early_limit)
1385                    .collect();
1386            } else if early_limit < rows.len() {
1387                rows.truncate(early_limit);
1388            }
1389        } else if stmt.order_by.is_empty() {
1390            let offset = if let Some(ref offset_expr) = stmt.offset {
1391                match ExpressionEval::compile(offset_expr, &[])
1392                    .ok()
1393                    .and_then(|e| e.with_context(ctx).eval_slice(&Row::new()).ok())
1394                {
1395                    Some(Value::Integer(o)) if o >= 0 => o as usize,
1396                    _ => 0,
1397                }
1398            } else {
1399                0
1400            };
1401
1402            let limit = if let Some(ref limit_expr) = stmt.limit {
1403                match ExpressionEval::compile(limit_expr, &[])
1404                    .ok()
1405                    .and_then(|e| e.with_context(ctx).eval_slice(&Row::new()).ok())
1406                {
1407                    Some(Value::Integer(l)) if l >= 0 => l as usize,
1408                    _ => usize::MAX,
1409                }
1410            } else {
1411                usize::MAX
1412            };
1413
1414            if offset > 0 || limit < usize::MAX {
1415                rows = rows.into_iter().skip(offset).take(limit).collect();
1416            }
1417        }
1418
1419        // Project rows according to SELECT expressions
1420        // Use schema cache for lowercase column names
1421        let all_columns_lower = schema.column_names_lower_arc();
1422        let projected_rows = self.index_project_rows_with_alias(
1423            &stmt.columns,
1424            rows,
1425            all_columns,
1426            Some(&all_columns_lower),
1427            ctx,
1428            table_alias,
1429        )?;
1430        let output_columns = CompactArc::new(self.index_output_column_names(
1431            &stmt.columns,
1432            all_columns,
1433            table_alias,
1434        ));
1435        let result =
1436            ExecutorResult::with_arc_columns(CompactArc::clone(&output_columns), projected_rows);
1437        Ok(Some((Box::new(result), output_columns)))
1438    }
1439
1440    /// IN list literal index optimization
1441    ///
1442    /// For queries like `SELECT * FROM users WHERE id IN (1, 2, 3, 5, 8)`
1443    /// where `id` has an index or is PRIMARY KEY, probe the index directly for each value
1444    /// instead of scanning all rows. This is O(k log n) where k = list size.
1445    #[allow(clippy::type_complexity)]
1446    #[allow(clippy::too_many_arguments)]
1447    fn try_in_list_index_optimization(
1448        &self,
1449        stmt: &SelectStatement,
1450        where_expr: &Expression,
1451        table: &dyn Table,
1452        all_columns: &[String],
1453        table_alias: Option<&str>,
1454        ctx: &ExecutionContext,
1455        classification: &Arc<QueryClassification>,
1456    ) -> Result<Option<(Box<dyn QueryResult>, CompactArc<Vec<String>>)>> {
1457        // Extract IN list info: (column_name, values, is_negated, remaining_predicate)
1458        let (column_name, values, is_negated, remaining_predicate) =
1459            match Self::extract_in_list_info(where_expr, ctx) {
1460                Some(info) => info,
1461                None => return Ok(None),
1462            };
1463
1464        // Skip if SELECT columns have correlated subqueries (need per-row context)
1465        // classification is passed from caller to avoid redundant cache lookups
1466        if classification.select_has_correlated_subqueries {
1467            return Ok(None);
1468        }
1469
1470        // Check if this is a PRIMARY KEY column (O(1) lookup) or has an index
1471        let schema = table.schema();
1472        let pk_indices = schema.primary_key_indices();
1473        let is_pk_column = pk_indices.len() == 1 && {
1474            let pk_col_idx = pk_indices[0];
1475            schema.columns[pk_col_idx].data_type == radixdb_core::DataType::Integer
1476                && schema.columns[pk_col_idx].name_lower == column_name
1477        };
1478
1479        // Ask the table for a complete physical candidate set. A segmented
1480        // implementation includes persisted postings and the hot index; when
1481        // it cannot prove complete coverage it returns None and we scan.
1482        let indexed_row_ids = if !is_pk_column {
1483            match table.collect_row_ids_by_index_values(&column_name, &values) {
1484                Some(row_ids) => Some(row_ids?),
1485                None => return Ok(None),
1486            }
1487        } else {
1488            None
1489        };
1490
1491        if values.is_empty() {
1492            // Empty IN list
1493            if is_negated {
1494                // NOT IN empty set = all rows match (fall through to normal scan)
1495                return Ok(None);
1496            } else {
1497                // IN empty set = no rows match
1498                let output_columns = CompactArc::new(self.index_output_column_names(
1499                    &stmt.columns,
1500                    all_columns,
1501                    table_alias,
1502                ));
1503                let result = ExecutorResult::with_arc_columns(
1504                    CompactArc::clone(&output_columns),
1505                    RowVec::new(),
1506                );
1507                return Ok(Some((Box::new(result), output_columns)));
1508            }
1509        }
1510
1511        // For NOT IN, we can't easily use the index (would need to exclude rows)
1512        // Fall back to normal scan
1513        if is_negated {
1514            return Ok(None);
1515        }
1516
1517        // Collect row_ids: either from PK (direct) or from index probe
1518        // Pre-allocate based on expected size to avoid reallocations
1519        let mut all_row_ids = Vec::with_capacity(values.len());
1520        if is_pk_column {
1521            // PRIMARY KEY: the value IS the row_id (for INTEGER PK)
1522            for value in &values {
1523                if let Some(id) = exact_integer_pk_value(value) {
1524                    all_row_ids.push(id);
1525                }
1526            }
1527        } else if let Some(row_ids) = indexed_row_ids {
1528            all_row_ids = row_ids;
1529        }
1530
1531        // Sort row_ids for better cache locality during version lookup
1532        // and deduplicate. More efficient than HashSet when sorted output is needed anyway.
1533        all_row_ids.sort_unstable();
1534        all_row_ids.dedup();
1535
1536        // EARLY LIMIT OPTIMIZATION: When there's no ORDER BY and no remaining predicate,
1537        // we can apply LIMIT early to avoid fetching unnecessary rows
1538        let (early_limit_applied, early_limit, early_offset) = if stmt.order_by.is_empty()
1539            && remaining_predicate.is_none()
1540            && !table.has_cold_segments()
1541        {
1542            let offset = if let Some(ref offset_expr) = stmt.offset {
1543                match ExpressionEval::compile(offset_expr, &[])
1544                    .ok()
1545                    .and_then(|e| e.with_context(ctx).eval_slice(&Row::new()).ok())
1546                {
1547                    Some(Value::Integer(o)) if o >= 0 => o as usize,
1548                    _ => 0,
1549                }
1550            } else {
1551                0
1552            };
1553
1554            let limit = if let Some(ref limit_expr) = stmt.limit {
1555                match ExpressionEval::compile(limit_expr, &[])
1556                    .ok()
1557                    .and_then(|e| e.with_context(ctx).eval_slice(&Row::new()).ok())
1558                {
1559                    Some(Value::Integer(l)) if l >= 0 => l as usize,
1560                    _ => usize::MAX,
1561                }
1562            } else {
1563                usize::MAX
1564            };
1565
1566            // Truncate row_ids to avoid fetching unnecessary rows
1567            if limit < usize::MAX {
1568                let take_count = (offset + limit).min(all_row_ids.len());
1569                all_row_ids.truncate(take_count);
1570            }
1571            (true, limit, offset)
1572        } else {
1573            (false, usize::MAX, 0)
1574        };
1575
1576        // Create a filter expression for remaining predicate + visibility
1577        use radixdb_storage::expression::logical::ConstBoolExpr;
1578        let filter: Box<dyn radixdb_storage::expression::Expression> =
1579            Box::new(ConstBoolExpr::true_expr());
1580
1581        // Fetch rows by row_ids - returns RowVec directly
1582        let mut rows = table.fetch_rows_by_ids(&all_row_ids, filter.as_ref())?;
1583
1584        // Persisted exact postings are hash candidates and a cold row may have
1585        // an authoritative hot shadow with a changed key. Recheck the requested
1586        // values before applying residual predicates.
1587        let (indexed_column_idx, indexed_column) = schema
1588            .find_column(&column_name)
1589            .expect("indexed IN column must belong to the table schema");
1590        let typed_values: Vec<Value> = values
1591            .iter()
1592            .map(|value| value.coerce_to_type(indexed_column.data_type))
1593            .filter(|value| !value.is_null())
1594            .collect();
1595        rows.retain(|(_, row)| {
1596            row.get(indexed_column_idx).is_some_and(|candidate| {
1597                typed_values
1598                    .iter()
1599                    .any(|value| matches!(candidate.compare(value), Ok(std::cmp::Ordering::Equal)))
1600            })
1601        });
1602
1603        // Apply remaining predicate if any
1604        if let Some(ref remaining) = remaining_predicate {
1605            // Process any subqueries in the remaining predicate
1606            let processed_remaining =
1607                if <Self as IndexOptimizerHost>::index_has_subqueries(remaining) {
1608                    self.index_process_where_subqueries(remaining, ctx)?
1609                } else {
1610                    remaining.clone()
1611                };
1612
1613            // Compile the filter using RowFilter (with params for $1 etc.)
1614            let columns_slice: Vec<String> = all_columns.to_vec();
1615            let row_filter =
1616                RowFilter::new(&processed_remaining, &columns_slice)?.with_context(ctx);
1617
1618            // Filter rows (retain works on (i64, Row) tuples via Deref)
1619            row_filter.retain_checked(&mut rows)?;
1620        }
1621
1622        // Apply LIMIT/OFFSET if present (and no ORDER BY)
1623        // Skip if we already applied early limit optimization
1624        if early_limit_applied {
1625            // Early optimization already truncated row_ids, but we still need to apply offset
1626            if early_offset > 0 {
1627                rows = rows
1628                    .into_iter()
1629                    .skip(early_offset)
1630                    .take(early_limit)
1631                    .collect();
1632            } else if early_limit < rows.len() {
1633                rows.truncate(early_limit);
1634            }
1635        } else if stmt.order_by.is_empty() {
1636            let offset = if let Some(ref offset_expr) = stmt.offset {
1637                match ExpressionEval::compile(offset_expr, &[])
1638                    .ok()
1639                    .and_then(|e| e.with_context(ctx).eval_slice(&Row::new()).ok())
1640                {
1641                    Some(Value::Integer(o)) if o >= 0 => o as usize,
1642                    _ => 0,
1643                }
1644            } else {
1645                0
1646            };
1647
1648            let limit = if let Some(ref limit_expr) = stmt.limit {
1649                match ExpressionEval::compile(limit_expr, &[])
1650                    .ok()
1651                    .and_then(|e| e.with_context(ctx).eval_slice(&Row::new()).ok())
1652                {
1653                    Some(Value::Integer(l)) if l >= 0 => l as usize,
1654                    _ => usize::MAX,
1655                }
1656            } else {
1657                usize::MAX
1658            };
1659
1660            if offset > 0 || limit < usize::MAX {
1661                rows = rows.into_iter().skip(offset).take(limit).collect();
1662            }
1663        }
1664
1665        // Project rows according to SELECT expressions
1666        // Use schema cache for lowercase column names
1667        let all_columns_lower = schema.column_names_lower_arc();
1668        let projected_rows = self.index_project_rows_with_alias(
1669            &stmt.columns,
1670            rows,
1671            all_columns,
1672            Some(&all_columns_lower),
1673            ctx,
1674            table_alias,
1675        )?;
1676        let output_columns = CompactArc::new(self.index_output_column_names(
1677            &stmt.columns,
1678            all_columns,
1679            table_alias,
1680        ));
1681        let result =
1682            ExecutorResult::with_arc_columns(CompactArc::clone(&output_columns), projected_rows);
1683        Ok(Some((Box::new(result), output_columns)))
1684    }
1685
1686    /// Extract IN list literal information from a WHERE clause.
1687    /// Returns (column_name, values, is_negated, remaining_predicate)
1688    fn extract_in_list_info(
1689        expr: &Expression,
1690        ctx: &ExecutionContext,
1691    ) -> Option<(String, Vec<Value>, bool, Option<Expression>)> {
1692        match expr {
1693            // Direct IN list: column IN (v1, v2, ...)
1694            Expression::In(in_expr) => {
1695                // Get the column name from the left side (lowercase for case-insensitive match)
1696                let column_name = match in_expr.left.as_ref() {
1697                    Expression::Identifier(id) => id.value_lower.to_string(),
1698                    Expression::QualifiedIdentifier(qid) => qid.name.value_lower.to_string(),
1699                    _ => return None, // Can't optimize complex left expressions
1700                };
1701
1702                // Get the values from the right side (must be a literal list, not subquery)
1703                let values = match in_expr.right.as_ref() {
1704                    Expression::List(list) => {
1705                        // ListExpression has Vec<Expression>
1706                        Self::extract_literal_values(&list.elements, ctx)
1707                    }
1708                    Expression::ExpressionList(list) => {
1709                        // ExpressionList has Vec<Expression>
1710                        Self::extract_literal_values(&list.expressions, ctx)
1711                    }
1712                    _ => return None, // Not a literal list (might be subquery)
1713                };
1714
1715                values.map(|v| (column_name, v, in_expr.not, None))
1716            }
1717
1718            // IN list with AND: column IN (...) AND other_condition
1719            Expression::Infix(infix) if infix.op_type == InfixOperator::And => {
1720                // Try left side as IN list
1721                if let Some((col, vals, neg, _)) = Self::extract_in_list_info(&infix.left, ctx) {
1722                    return Some((col, vals, neg, Some((*infix.right).clone())));
1723                }
1724                // Try right side as IN list
1725                if let Some((col, vals, neg, _)) = Self::extract_in_list_info(&infix.right, ctx) {
1726                    return Some((col, vals, neg, Some((*infix.left).clone())));
1727                }
1728                None
1729            }
1730
1731            _ => None,
1732        }
1733    }
1734
1735    /// Extract literal values from a list of expressions.
1736    /// Returns None if any expression is not a literal (e.g., column reference, subquery).
1737    fn extract_literal_values(exprs: &[Expression], ctx: &ExecutionContext) -> Option<Vec<Value>> {
1738        let mut values = Vec::with_capacity(exprs.len());
1739        for expr in exprs {
1740            // Try to evaluate as a constant expression
1741            match ExpressionEval::compile(expr, &[]) {
1742                Ok(compiled) => match compiled.with_context(ctx).eval_slice(&Row::new()) {
1743                    Ok(val) => values.push(val),
1744                    Err(_) => return None, // Can't evaluate as constant
1745                },
1746                Err(_) => return None, // Can't compile (e.g., column reference)
1747            }
1748        }
1749        Some(values)
1750    }
1751
1752    /// Extract IN subquery information from a WHERE clause.
1753    /// Returns (column_name, subquery, is_negated, remaining_predicate)
1754    fn extract_in_subquery_info(
1755        expr: &Expression,
1756    ) -> Option<(String, &ScalarSubquery, bool, Option<Expression>)> {
1757        match expr {
1758            // Direct IN subquery: column IN (SELECT ...)
1759            Expression::In(in_expr) => {
1760                // Get the column name from the left side (lowercase for case-insensitive match)
1761                let column_name = match in_expr.left.as_ref() {
1762                    Expression::Identifier(id) => id.value_lower.to_string(),
1763                    Expression::QualifiedIdentifier(qid) => qid.name.value_lower.to_string(),
1764                    _ => return None, // Can't optimize complex left expressions
1765                };
1766
1767                // Get the subquery from the right side
1768                if let Expression::ScalarSubquery(subquery) = in_expr.right.as_ref() {
1769                    return Some((column_name, subquery, in_expr.not, None));
1770                }
1771                None
1772            }
1773
1774            // IN subquery with AND: column IN (SELECT ...) AND other_condition
1775            Expression::Infix(infix) if infix.op_type == InfixOperator::And => {
1776                // Try left side as IN subquery
1777                if let Some((col, sq, neg, _)) = Self::extract_in_subquery_info(&infix.left) {
1778                    return Some((col, sq, neg, Some((*infix.right).clone())));
1779                }
1780                // Try right side as IN subquery
1781                if let Some((col, sq, neg, _)) = Self::extract_in_subquery_info(&infix.right) {
1782                    return Some((col, sq, neg, Some((*infix.left).clone())));
1783                }
1784                None
1785            }
1786
1787            _ => None,
1788        }
1789    }
1790
1791    /// Try to optimize InHashSet expressions (from EXISTS → semi-join transformation).
1792    ///
1793    /// When EXISTS is transformed to InHashSet via semi-join, we can further optimize
1794    /// by probing the PRIMARY KEY or index directly instead of scanning all rows.
1795    ///
1796    /// For example: `SELECT * FROM users WHERE users.id IN {1, 2, 3} LIMIT 100`
1797    /// Instead of scanning all 10,000 users, probe the PK for ids 1, 2, 3 directly.
1798    #[allow(clippy::type_complexity)]
1799    fn try_in_hashset_index_optimization(
1800        &self,
1801        stmt: &SelectStatement,
1802        where_expr: &Expression,
1803        table: &dyn Table,
1804        all_columns: &[String],
1805        table_alias: Option<&str>,
1806        ctx: &ExecutionContext,
1807    ) -> Result<Option<(Box<dyn QueryResult>, CompactArc<Vec<String>>)>> {
1808        // Extract InHashSet info: (column_name, values, is_negated, remaining_predicate)
1809        let (column_name, values, is_negated, remaining_predicate) =
1810            match Self::extract_in_hashset_info(where_expr) {
1811                Some(info) => info,
1812                None => return Ok(None),
1813            };
1814
1815        // A negated hash set also needs the complete visible key domain; the
1816        // former 1..row_count complement was invalid for sparse primary keys.
1817        if is_negated {
1818            return Ok(None);
1819        }
1820
1821        // Check if this is a PRIMARY KEY column (O(1) lookup) or has an index
1822        let schema = table.schema();
1823        let pk_indices = schema.primary_key_indices();
1824        let is_pk_column = pk_indices.len() == 1 && {
1825            let pk_col_idx = pk_indices[0];
1826            schema.columns[pk_col_idx].data_type == radixdb_core::DataType::Integer
1827                && schema.columns[pk_col_idx].name_lower == column_name
1828        };
1829
1830        // For NOT IN (negated), only optimize if it's a PK column
1831        // Non-PK NOT IN would require full index scan which isn't much better than table scan
1832        if is_negated && !is_pk_column {
1833            return Ok(None);
1834        }
1835
1836        // Resolve the complete hot+cold candidate set up front. A mutable
1837        // index handle is never sufficient evidence for a segmented table.
1838        let indexed_row_ids = if !is_pk_column {
1839            let values_vec: Vec<Value> = values.iter().cloned().collect();
1840            match table.collect_row_ids_by_index_values(&column_name, &values_vec) {
1841                Some(row_ids) => Some(row_ids?),
1842                None => return Ok(None),
1843            }
1844        } else {
1845            None
1846        };
1847
1848        // EARLY LIMIT CHECK: Compute limit+offset before collecting row_ids
1849        // This allows us to stop collection early when there's no ORDER BY
1850        let early_termination_target = if stmt.order_by.is_empty() && remaining_predicate.is_none()
1851        {
1852            let offset = stmt
1853                .offset
1854                .as_ref()
1855                .and_then(|offset_expr| {
1856                    ExpressionEval::compile(offset_expr, &[])
1857                        .ok()
1858                        .and_then(|e| e.with_context(ctx).eval_slice(&Row::new()).ok())
1859                        .and_then(|v| {
1860                            if let Value::Integer(o) = v {
1861                                Some(o.max(0) as usize)
1862                            } else {
1863                                None
1864                            }
1865                        })
1866                })
1867                .unwrap_or(0);
1868
1869            let limit = stmt.limit.as_ref().and_then(|limit_expr| {
1870                ExpressionEval::compile(limit_expr, &[])
1871                    .ok()
1872                    .and_then(|e| e.with_context(ctx).eval_slice(&Row::new()).ok())
1873                    .and_then(|v| {
1874                        if let Value::Integer(l) = v {
1875                            Some(l.max(0) as usize)
1876                        } else {
1877                            None
1878                        }
1879                    })
1880            });
1881
1882            limit.map(|l| offset + l)
1883        } else {
1884            None // Can't use early termination with ORDER BY or remaining predicate
1885        };
1886
1887        // Collect row_ids: either from PK (direct) or from index probe
1888        // With early termination target, stop once we have enough
1889        // Pre-allocate based on expected size to avoid reallocations
1890        let estimated_capacity = if let Some(target) = early_termination_target {
1891            target.min(values.len())
1892        } else {
1893            values.len()
1894        };
1895        let mut all_row_ids = Vec::with_capacity(estimated_capacity);
1896        if is_pk_column {
1897            if is_negated {
1898                // NOT IN optimization for INTEGER PRIMARY KEY (from NOT EXISTS semi-join):
1899                // Iterate through row_ids and exclude those in the hash set
1900                let exclusion_set: I64Set =
1901                    values.iter().filter_map(exact_integer_pk_value).collect();
1902
1903                let target = early_termination_target.unwrap_or(usize::MAX);
1904                let row_count = table.row_count();
1905
1906                for row_id in 1..=(row_count as i64) {
1907                    if !exclusion_set.contains(row_id) {
1908                        all_row_ids.push(row_id);
1909                        if all_row_ids.len() >= target {
1910                            break;
1911                        }
1912                    }
1913                }
1914            } else {
1915                // IN: PRIMARY KEY - the value IS the row_id (for INTEGER PK)
1916                for value in values.iter() {
1917                    // Early termination check
1918                    if let Some(target) = early_termination_target {
1919                        if all_row_ids.len() >= target {
1920                            break;
1921                        }
1922                    }
1923                    if let Some(id) = exact_integer_pk_value(value) {
1924                        all_row_ids.push(id);
1925                    }
1926                }
1927            }
1928        } else if let Some(row_ids) = indexed_row_ids {
1929            all_row_ids.extend(row_ids);
1930        }
1931
1932        // If no row_ids found, return empty result
1933        if all_row_ids.is_empty() {
1934            let output_columns = CompactArc::new(self.index_output_column_names(
1935                &stmt.columns,
1936                all_columns,
1937                table_alias,
1938            ));
1939            let result =
1940                ExecutorResult::with_arc_columns(CompactArc::clone(&output_columns), RowVec::new());
1941            return Ok(Some((Box::new(result), output_columns)));
1942        }
1943
1944        // Sort row_ids for better cache locality during version lookup
1945        // and deduplicate. More efficient than HashSet when sorted output is needed anyway.
1946        all_row_ids.sort_unstable();
1947        all_row_ids.dedup();
1948
1949        // EARLY LIMIT OPTIMIZATION: When there's no ORDER BY and no remaining predicate,
1950        // we can apply LIMIT early to avoid fetching unnecessary rows
1951        let (early_limit_applied, early_limit, early_offset) =
1952            if stmt.order_by.is_empty() && remaining_predicate.is_none() {
1953                let offset = if let Some(ref offset_expr) = stmt.offset {
1954                    match ExpressionEval::compile(offset_expr, &[])
1955                        .ok()
1956                        .and_then(|e| e.with_context(ctx).eval_slice(&Row::new()).ok())
1957                    {
1958                        Some(Value::Integer(o)) if o >= 0 => o as usize,
1959                        _ => 0,
1960                    }
1961                } else {
1962                    0
1963                };
1964
1965                let limit = if let Some(ref limit_expr) = stmt.limit {
1966                    match ExpressionEval::compile(limit_expr, &[])
1967                        .ok()
1968                        .and_then(|e| e.with_context(ctx).eval_slice(&Row::new()).ok())
1969                    {
1970                        Some(Value::Integer(l)) if l >= 0 => l as usize,
1971                        _ => usize::MAX,
1972                    }
1973                } else {
1974                    usize::MAX
1975                };
1976
1977                // Truncate row_ids to avoid fetching unnecessary rows
1978                if limit < usize::MAX {
1979                    let take_count = (offset + limit).min(all_row_ids.len());
1980                    all_row_ids.truncate(take_count);
1981                }
1982                (true, limit, offset)
1983            } else {
1984                (false, usize::MAX, 0)
1985            };
1986
1987        // Create a filter expression for remaining predicate + visibility
1988        use radixdb_storage::expression::logical::ConstBoolExpr;
1989        let filter: Box<dyn radixdb_storage::expression::Expression> =
1990            Box::new(ConstBoolExpr::true_expr());
1991
1992        // Fetch rows by row_ids - returns RowVec directly
1993        let mut rows = table.fetch_rows_by_ids(&all_row_ids, filter.as_ref())?;
1994
1995        // Apply remaining predicate if any
1996        if let Some(ref remaining) = remaining_predicate {
1997            // Compile the filter using RowFilter (with params for $1 etc.)
1998            let columns_slice: Vec<String> = all_columns.to_vec();
1999            let row_filter = RowFilter::new(remaining, &columns_slice)?.with_context(ctx);
2000
2001            // Filter rows (retain works on (i64, Row) tuples via Deref)
2002            row_filter.retain_checked(&mut rows)?;
2003        }
2004
2005        // Apply LIMIT/OFFSET if present (and no ORDER BY)
2006        // Skip if we already applied early limit optimization
2007        if early_limit_applied {
2008            // Early optimization already truncated row_ids, but we still need to apply offset
2009            if early_offset > 0 {
2010                rows = rows
2011                    .into_iter()
2012                    .skip(early_offset)
2013                    .take(early_limit)
2014                    .collect();
2015            } else if early_limit < rows.len() {
2016                rows.truncate(early_limit);
2017            }
2018        } else if stmt.order_by.is_empty() {
2019            let offset = if let Some(ref offset_expr) = stmt.offset {
2020                match ExpressionEval::compile(offset_expr, &[])
2021                    .ok()
2022                    .and_then(|e| e.with_context(ctx).eval_slice(&Row::new()).ok())
2023                {
2024                    Some(Value::Integer(o)) if o >= 0 => o as usize,
2025                    _ => 0,
2026                }
2027            } else {
2028                0
2029            };
2030
2031            let limit = if let Some(ref limit_expr) = stmt.limit {
2032                match ExpressionEval::compile(limit_expr, &[])
2033                    .ok()
2034                    .and_then(|e| e.with_context(ctx).eval_slice(&Row::new()).ok())
2035                {
2036                    Some(Value::Integer(l)) if l >= 0 => l as usize,
2037                    _ => usize::MAX,
2038                }
2039            } else {
2040                usize::MAX
2041            };
2042
2043            if offset > 0 || limit < usize::MAX {
2044                rows = rows.into_iter().skip(offset).take(limit).collect();
2045            }
2046        }
2047
2048        // Project rows according to SELECT expressions
2049        // Use schema cache for lowercase column names
2050        let all_columns_lower = schema.column_names_lower_arc();
2051        let projected_rows = self.index_project_rows_with_alias(
2052            &stmt.columns,
2053            rows,
2054            all_columns,
2055            Some(&all_columns_lower),
2056            ctx,
2057            table_alias,
2058        )?;
2059        let output_columns = CompactArc::new(self.index_output_column_names(
2060            &stmt.columns,
2061            all_columns,
2062            table_alias,
2063        ));
2064        let result =
2065            ExecutorResult::with_arc_columns(CompactArc::clone(&output_columns), projected_rows);
2066        Ok(Some((Box::new(result), output_columns)))
2067    }
2068
2069    /// Extract InHashSet information from a WHERE clause.
2070    /// Returns (column_name, hash_set_values, is_negated, remaining_predicate)
2071    #[allow(clippy::type_complexity)]
2072    fn extract_in_hashset_info(
2073        expr: &Expression,
2074    ) -> Option<(String, CompactArc<ValueSet>, bool, Option<Expression>)> {
2075        match expr {
2076            // Direct InHashSet: column IN {hash_set}
2077            Expression::InHashSet(in_hash) => {
2078                // Get the column name from the column expression (lowercase for case-insensitive match)
2079                let column_name = match in_hash.column.as_ref() {
2080                    Expression::Identifier(id) => id.value_lower.to_string(),
2081                    Expression::QualifiedIdentifier(qid) => qid.name.value_lower.to_string(),
2082                    _ => return None, // Can't optimize complex column expressions
2083                };
2084
2085                Some((column_name, in_hash.values.clone(), in_hash.not, None))
2086            }
2087
2088            // InHashSet with AND: column IN {hash_set} AND other_condition
2089            Expression::Infix(infix) if infix.op_type == InfixOperator::And => {
2090                // Try left side as InHashSet
2091                if let Some((col, vals, neg, _)) = Self::extract_in_hashset_info(&infix.left) {
2092                    return Some((col, vals, neg, Some((*infix.right).clone())));
2093                }
2094                // Try right side as InHashSet
2095                if let Some((col, vals, neg, _)) = Self::extract_in_hashset_info(&infix.right) {
2096                    return Some((col, vals, neg, Some((*infix.left).clone())));
2097                }
2098                None
2099            }
2100
2101            _ => None,
2102        }
2103    }
2104
2105    /// Exact vector Top-K optimization using parallel brute force
2106    ///
2107    /// For queries like:
2108    ///   SELECT id, VEC_DISTANCE_L2(embedding, '[1.0, 2.0, ...]') AS dist
2109    ///   FROM documents ORDER BY dist LIMIT 10;
2110    ///
2111    /// Detects the VEC_DISTANCE pattern in ORDER BY:
2112    /// HNSW is not selected for ordinary SQL because no approximate opt-in is
2113    /// present. The fused brute-force path preserves exact ORDER BY semantics.
2114    #[allow(clippy::type_complexity)]
2115    fn try_vector_search_optimization(
2116        &self,
2117        stmt: &SelectStatement,
2118        table: &dyn Table,
2119        all_columns: &[String],
2120        ctx: &ExecutionContext,
2121    ) -> Result<Option<(Box<dyn QueryResult>, CompactArc<Vec<String>>)>> {
2122        // Must have exactly 1 ORDER BY + LIMIT, ascending order
2123        if stmt.order_by.len() != 1 || stmt.limit.is_none() {
2124            return Ok(None);
2125        }
2126
2127        let order_by = &stmt.order_by[0];
2128        if !order_by.ascending {
2129            return Ok(None); // Vector search always returns closest first
2130        }
2131
2132        // Find the VEC_DISTANCE function call — either directly in ORDER BY, via alias,
2133        // or via the <=> infix operator (which is equivalent to VEC_DISTANCE_L2).
2134        // For <=> operator, we synthesize an equivalent FunctionCall to reuse the same logic.
2135        let synthesized_fc;
2136        let func_call = match &order_by.expression {
2137            Expression::FunctionCall(fc) => Some(fc.as_ref()),
2138            Expression::Identifier(id) => {
2139                // Look up alias in SELECT columns — try FunctionCall first, then <=> infix
2140                if let Some(fc) = Self::find_vec_distance_alias(&id.value_lower, &stmt.columns) {
2141                    Some(fc)
2142                } else if let Some(infix) =
2143                    Self::find_vec_distance_infix_alias(&id.value_lower, &stmt.columns)
2144                {
2145                    synthesized_fc = FunctionCall {
2146                        token: infix.token.clone(),
2147                        function: "VEC_DISTANCE_L2".into(),
2148                        arguments: vec![(*infix.left).clone(), (*infix.right).clone()],
2149                        is_distinct: false,
2150                        order_by: Vec::new(),
2151                        filter: None,
2152                    };
2153                    Some(&synthesized_fc)
2154                } else {
2155                    None
2156                }
2157            }
2158            Expression::Infix(infix) if infix.op_type == InfixOperator::VectorDistance => {
2159                // <=> operator is equivalent to VEC_DISTANCE_L2
2160                synthesized_fc = FunctionCall {
2161                    token: infix.token.clone(),
2162                    function: "VEC_DISTANCE_L2".into(),
2163                    arguments: vec![(*infix.left).clone(), (*infix.right).clone()],
2164                    is_distinct: false,
2165                    order_by: Vec::new(),
2166                    filter: None,
2167                };
2168                Some(&synthesized_fc)
2169            }
2170            _ => None,
2171        };
2172
2173        let func_call = match func_call {
2174            Some(fc) => fc,
2175            None => return Ok(None),
2176        };
2177
2178        // Check if it's a VEC_DISTANCE function
2179        let fn_name_upper = func_call.function.to_uppercase();
2180        let is_vec_distance = matches!(
2181            fn_name_upper.as_str(),
2182            "VEC_DISTANCE_L2" | "VEC_DISTANCE_COSINE" | "VEC_DISTANCE_IP"
2183        );
2184        if !is_vec_distance || func_call.arguments.len() != 2 {
2185            return Ok(None);
2186        }
2187
2188        // Extract vector column name from first argument
2189        let vec_col_name = match &func_call.arguments[0] {
2190            Expression::Identifier(id) => id.value.clone(),
2191            Expression::QualifiedIdentifier(qid) => qid.name.value.clone(),
2192            _ => return Ok(None),
2193        };
2194
2195        // Ordinary SQL ORDER BY ... LIMIT has exact semantics. HNSW is an
2196        // approximate candidate source and is therefore never selected without
2197        // an explicit approximate-query contract. Keep the exact brute-force
2198        // Top-K path below even when an HNSW index exists.
2199        let hnsw_index: Option<Arc<dyn Index>> = None;
2200
2201        // Extract query vector from second argument
2202        let query_vec_value = match &func_call.arguments[1] {
2203            Expression::StringLiteral(s) => {
2204                // Parse "[1.0, 2.0, ...]" string literal to vector
2205                match radixdb_core::value::parse_vector_str(&s.value) {
2206                    Some(floats) => Value::vector(floats),
2207                    None => return Ok(None),
2208                }
2209            }
2210            Expression::Parameter(_) => {
2211                // Evaluate parameter
2212                match ExpressionEval::compile(&func_call.arguments[1], &[])
2213                    .ok()
2214                    .and_then(|e| e.with_context(ctx).eval_slice(&Row::new()).ok())
2215                {
2216                    Some(v @ Value::Extension(_)) => v,
2217                    Some(Value::Text(s)) => {
2218                        match radixdb_core::value::parse_vector_str(s.as_ref()) {
2219                            Some(floats) => Value::vector(floats),
2220                            None => return Ok(None),
2221                        }
2222                    }
2223                    _ => return Ok(None),
2224                }
2225            }
2226            _ => return Ok(None),
2227        };
2228
2229        // Evaluate limit + offset
2230        let limit = match &stmt.limit {
2231            Some(limit_expr) => {
2232                match ExpressionEval::compile(limit_expr, &[])
2233                    .ok()
2234                    .and_then(|e| e.with_context(ctx).eval_slice(&Row::new()).ok())
2235                {
2236                    Some(Value::Integer(l)) if l >= 0 => l as usize,
2237                    Some(Value::Float(f)) if f >= 0.0 => f as usize,
2238                    _ => return Ok(None),
2239                }
2240            }
2241            None => return Ok(None),
2242        };
2243
2244        let offset = if let Some(ref offset_expr) = stmt.offset {
2245            match ExpressionEval::compile(offset_expr, &[])
2246                .ok()
2247                .and_then(|e| e.with_context(ctx).eval_slice(&Row::new()).ok())
2248            {
2249                Some(Value::Integer(o)) if o >= 0 => o as usize,
2250                Some(Value::Float(f)) if f >= 0.0 => f as usize,
2251                _ => 0,
2252            }
2253        } else {
2254            0
2255        };
2256
2257        let k = limit.saturating_add(offset);
2258
2259        // Compile WHERE filter if present
2260        let where_filter = if let Some(ref where_expr) = stmt.where_clause {
2261            match RowFilter::new(where_expr, all_columns) {
2262                Ok(filter) => Some(filter.with_context(ctx)),
2263                Err(_) => return Ok(None), // Can't compile WHERE — fall through to normal path
2264            }
2265        } else {
2266            None
2267        };
2268
2269        // Determine distance results: HNSW path or brute-force path
2270        // nn_results: Vec<(row_id, distance)> sorted by distance ascending
2271        let (nn_results, fetched_rows) = if let Some(ref idx) = hnsw_index {
2272            // HNSW index path — O(log N) approximate search
2273            // With WHERE: oversample by 4x, then post-filter
2274            let oversample = if where_filter.is_some() { 4 } else { 1 };
2275            let hnsw_k = k.saturating_mul(oversample);
2276            let default_ef = idx.default_ef_search().unwrap_or(64);
2277            let ef_search = std::cmp::max(hnsw_k.saturating_mul(2), default_ef);
2278            let results = match idx.search_nearest(&query_vec_value, hnsw_k, ef_search) {
2279                Some(r) => r,
2280                None => return Ok(None),
2281            };
2282            if results.is_empty() {
2283                let output_columns = CompactArc::new(self.index_output_column_names(
2284                    &stmt.columns,
2285                    all_columns,
2286                    None,
2287                ));
2288                let result = ExecutorResult::with_arc_columns(
2289                    CompactArc::clone(&output_columns),
2290                    RowVec::new(),
2291                );
2292                return Ok(Some((Box::new(result), output_columns)));
2293            }
2294            let row_ids: Vec<i64> = results.iter().map(|(rid, _)| *rid).collect();
2295            let rows = table.collect_rows_by_ids(&row_ids)?;
2296
2297            // Post-filter with WHERE if present
2298            if let Some(ref filter) = where_filter {
2299                // Build O(1) lookup map from row_id -> row
2300                let row_map: rustc_hash::FxHashMap<i64, &radixdb_core::Row> =
2301                    rows.iter().map(|(rid, row)| (*rid, row)).collect();
2302                let mut filtered_nn = Vec::with_capacity(k);
2303                let mut filtered_rows = RowVec::with_capacity(k);
2304                for (rid, dist) in &results {
2305                    if let Some(row) = row_map.get(rid) {
2306                        if filter.matches_checked(row)? {
2307                            filtered_nn.push((*rid, *dist));
2308                            filtered_rows.push((*rid, (*row).clone()));
2309                            if filtered_nn.len() >= k {
2310                                break; // We have enough results
2311                            }
2312                        }
2313                    }
2314                }
2315                // If HNSW post-filtering yielded fewer results than requested,
2316                // fall back to the normal brute-force query path to guarantee
2317                // correct result counts. The HNSW index may miss qualifying rows
2318                // because ANN search only explores a subset of the graph.
2319                if filtered_nn.len() < k {
2320                    return Ok(None);
2321                }
2322                (filtered_nn, filtered_rows)
2323            } else {
2324                (results, rows)
2325            }
2326        } else {
2327            // Brute-force path — parallel distance computation + top-K
2328            let query_bytes = match &query_vec_value {
2329                Value::Extension(data)
2330                    if data.first() == Some(&(radixdb_core::DataType::Vector as u8)) =>
2331                {
2332                    &data[1..]
2333                }
2334                _ => return Ok(None),
2335            };
2336
2337            // Find vector column index in schema
2338            let schema = table.schema();
2339            let vec_col_idx = match schema
2340                .columns
2341                .iter()
2342                .position(|c| c.name.eq_ignore_ascii_case(&vec_col_name))
2343            {
2344                Some(idx) => idx,
2345                None => return Ok(None),
2346            };
2347
2348            let metric = match fn_name_upper.as_str() {
2349                "VEC_DISTANCE_L2" => super::parallel::DistanceMetric::L2,
2350                "VEC_DISTANCE_COSINE" => super::parallel::DistanceMetric::Cosine,
2351                "VEC_DISTANCE_IP" => super::parallel::DistanceMetric::InnerProduct,
2352                _ => return Ok(None),
2353            };
2354
2355            // Collect rows — push WHERE to storage level for index-accelerated filtering
2356            let scan_rows = if let Some(ref where_expr) = stmt.where_clause {
2357                // Try storage-level pushdown (uses indexes when available)
2358                let storage_expr = super::expr_converter::convert_ast_to_storage_expr(where_expr);
2359                let rows = table.collect_all_rows(storage_expr.as_deref())?;
2360                // Storage may not handle all predicates — apply residual in-memory filter
2361                if let Some(ref filter) = where_filter {
2362                    let mut filtered = RowVec::with_capacity(rows.len());
2363                    for (id, row) in rows {
2364                        if filter.matches_checked(&row)? {
2365                            filtered.push((id, row));
2366                        }
2367                    }
2368                    filtered
2369                } else {
2370                    rows
2371                }
2372            } else {
2373                table.collect_all_rows_unsorted()?
2374            };
2375            if scan_rows.is_empty() {
2376                let output_columns = CompactArc::new(self.index_output_column_names(
2377                    &stmt.columns,
2378                    all_columns,
2379                    None,
2380                ));
2381                let result = ExecutorResult::with_arc_columns(
2382                    CompactArc::clone(&output_columns),
2383                    RowVec::new(),
2384                );
2385                return Ok(Some((Box::new(result), output_columns)));
2386            }
2387
2388            let config = super::parallel::ParallelConfig::default();
2389            let topn = super::parallel::parallel_topn_vector_search(
2390                scan_rows,
2391                vec_col_idx,
2392                query_bytes,
2393                k,
2394                metric,
2395                &config,
2396            )?;
2397
2398            // Convert to (row_id, distance) + fetched rows
2399            let nn: Vec<(i64, f64)> = topn.iter().map(|(rid, _, dist)| (*rid, *dist)).collect();
2400            let rows: RowVec = topn.into_iter().map(|(rid, row, _)| (rid, row)).collect();
2401            (nn, rows)
2402        };
2403
2404        // Build a map from row_id to fetched row for order preservation
2405        let mut row_map: radixdb_core::I64Map<Row> =
2406            radixdb_core::I64Map::with_capacity(fetched_rows.len());
2407        for (rid, row) in fetched_rows.into_iter() {
2408            row_map.insert(rid, row);
2409        }
2410
2411        // Pre-compile projection plan once (avoid per-row expression compilation)
2412        let mut projection_plan =
2413            self.compile_vector_projection_plan(&stmt.columns, all_columns, ctx, func_call)?;
2414
2415        // Project rows according to the pre-compiled plan
2416        let mut result_rows = RowVec::with_capacity(k);
2417        for (i, (row_id, dist)) in nn_results.iter().enumerate() {
2418            if i < offset {
2419                continue;
2420            }
2421            if result_rows.len() >= limit {
2422                break;
2423            }
2424            if let Some(base_row) = row_map.get(*row_id) {
2425                let projected =
2426                    Self::apply_vector_projection(&mut projection_plan, base_row, *dist)?;
2427                result_rows.push((*row_id, projected));
2428            }
2429        }
2430
2431        let output_columns =
2432            CompactArc::new(self.index_output_column_names(&stmt.columns, all_columns, None));
2433
2434        let result =
2435            ExecutorResult::with_arc_columns(CompactArc::clone(&output_columns), result_rows);
2436        Ok(Some((Box::new(result), output_columns)))
2437    }
2438
2439    /// Find a VEC_DISTANCE function call (or <=> operator) that's aliased in SELECT columns.
2440    /// For <=> operator, returns None and the caller handles it via the synthesized_fc path.
2441    fn find_vec_distance_alias<'a>(
2442        alias_lower: &str,
2443        columns: &'a [Expression],
2444    ) -> Option<&'a FunctionCall> {
2445        for col_expr in columns {
2446            if let Expression::Aliased(aliased) = col_expr {
2447                if aliased.alias.value_lower == alias_lower {
2448                    if let Expression::FunctionCall(fc) = &*aliased.expression {
2449                        let fn_upper = fc.function.to_uppercase();
2450                        if matches!(
2451                            fn_upper.as_str(),
2452                            "VEC_DISTANCE_L2" | "VEC_DISTANCE_COSINE" | "VEC_DISTANCE_IP"
2453                        ) {
2454                            return Some(fc.as_ref());
2455                        }
2456                    }
2457                }
2458            }
2459        }
2460        None
2461    }
2462
2463    /// Check if an alias in SELECT points to a <=> infix operator.
2464    /// Returns the InfixExpression if found, to synthesize a FunctionCall from.
2465    fn find_vec_distance_infix_alias<'a>(
2466        alias_lower: &str,
2467        columns: &'a [Expression],
2468    ) -> Option<&'a InfixExpression> {
2469        for col_expr in columns {
2470            if let Expression::Aliased(aliased) = col_expr {
2471                if aliased.alias.value_lower == alias_lower {
2472                    if let Expression::Infix(infix) = &*aliased.expression {
2473                        if infix.op_type == InfixOperator::VectorDistance {
2474                            return Some(infix);
2475                        }
2476                    }
2477                }
2478            }
2479        }
2480        None
2481    }
2482
2483    /// Check if two FunctionCall nodes represent the same call (same name + same arguments).
2484    /// Used to match ORDER BY distance with the correct SELECT column.
2485    fn func_calls_match(a: &FunctionCall, b: &FunctionCall) -> bool {
2486        if !a.function.eq_ignore_ascii_case(&b.function) {
2487            return false;
2488        }
2489        if a.arguments.len() != b.arguments.len() {
2490            return false;
2491        }
2492        // Compare each argument structurally for common patterns
2493        a.arguments.iter().zip(b.arguments.iter()).all(|(x, y)| {
2494            match (x, y) {
2495                (Expression::Identifier(a_id), Expression::Identifier(b_id)) => {
2496                    a_id.value.eq_ignore_ascii_case(&b_id.value)
2497                }
2498                (
2499                    Expression::QualifiedIdentifier(a_qid),
2500                    Expression::QualifiedIdentifier(b_qid),
2501                ) => a_qid.name.value.eq_ignore_ascii_case(&b_qid.name.value),
2502                (Expression::Identifier(a_id), Expression::QualifiedIdentifier(b_qid))
2503                | (Expression::QualifiedIdentifier(b_qid), Expression::Identifier(a_id)) => {
2504                    a_id.value.eq_ignore_ascii_case(&b_qid.name.value)
2505                }
2506                (Expression::StringLiteral(a_s), Expression::StringLiteral(b_s)) => {
2507                    a_s.value == b_s.value
2508                }
2509                (Expression::IntegerLiteral(a_i), Expression::IntegerLiteral(b_i)) => {
2510                    a_i.value == b_i.value
2511                }
2512                (Expression::FloatLiteral(a_f), Expression::FloatLiteral(b_f)) => {
2513                    a_f.value == b_f.value
2514                }
2515                (Expression::FunctionCall(a_fc), Expression::FunctionCall(b_fc)) => {
2516                    Self::func_calls_match(a_fc, b_fc)
2517                }
2518                // For other expression kinds, use pointer equality as last resort
2519                // (works when ORDER BY references alias that points into SELECT)
2520                _ => std::ptr::eq(x as *const Expression, y as *const Expression),
2521            }
2522        })
2523    }
2524
2525    /// Compile a projection plan from SELECT columns. Called once before the row loop.
2526    /// `order_by_fc` is the FunctionCall used in ORDER BY — only this exact call gets the
2527    /// precomputed Distance slot. Other VEC_DISTANCE_* calls are compiled for per-row eval.
2528    fn compile_vector_projection_plan(
2529        &self,
2530        select_columns: &[Expression],
2531        all_columns: &[String],
2532        ctx: &ExecutionContext,
2533        order_by_fc: &FunctionCall,
2534    ) -> Result<Vec<VectorProjectionSlot>> {
2535        let mut plan = Vec::with_capacity(select_columns.len());
2536
2537        for col_expr in select_columns {
2538            let expr = match col_expr {
2539                Expression::Aliased(aliased) => &*aliased.expression,
2540                other => other,
2541            };
2542
2543            match expr {
2544                Expression::Star(_) => {
2545                    plan.push(VectorProjectionSlot::Star);
2546                }
2547                Expression::Identifier(id) => {
2548                    if let Some(idx) = all_columns
2549                        .iter()
2550                        .position(|c| c.eq_ignore_ascii_case(&id.value))
2551                    {
2552                        plan.push(VectorProjectionSlot::Column(idx));
2553                    } else {
2554                        return Err(radixdb_core::Error::ColumnNotFound(id.value.to_string()));
2555                    }
2556                }
2557                Expression::QualifiedIdentifier(qid) => {
2558                    if let Some(idx) = all_columns
2559                        .iter()
2560                        .position(|c| c.eq_ignore_ascii_case(&qid.name.value))
2561                    {
2562                        plan.push(VectorProjectionSlot::Column(idx));
2563                    } else {
2564                        return Err(radixdb_core::Error::ColumnNotFound(
2565                            qid.name.value.to_string(),
2566                        ));
2567                    }
2568                }
2569                Expression::FunctionCall(fc) => {
2570                    let fn_upper = fc.function.to_uppercase();
2571                    if matches!(
2572                        fn_upper.as_str(),
2573                        "VEC_DISTANCE_L2" | "VEC_DISTANCE_COSINE" | "VEC_DISTANCE_IP"
2574                    ) && Self::func_calls_match(fc, order_by_fc)
2575                    {
2576                        plan.push(VectorProjectionSlot::Distance);
2577                    } else {
2578                        let eval = ExpressionEval::compile(expr, all_columns)?.with_context(ctx);
2579                        plan.push(VectorProjectionSlot::Compiled(Box::new(eval)));
2580                    }
2581                }
2582                Expression::Infix(infix) if infix.op_type == InfixOperator::VectorDistance => {
2583                    // <=> operator is equivalent to VEC_DISTANCE_L2.
2584                    // Check if it matches the ORDER BY distance expression.
2585                    let equiv_fc = FunctionCall {
2586                        token: infix.token.clone(),
2587                        function: "VEC_DISTANCE_L2".into(),
2588                        arguments: vec![(*infix.left).clone(), (*infix.right).clone()],
2589                        is_distinct: false,
2590                        order_by: Vec::new(),
2591                        filter: None,
2592                    };
2593                    if Self::func_calls_match(&equiv_fc, order_by_fc) {
2594                        plan.push(VectorProjectionSlot::Distance);
2595                    } else {
2596                        let eval = ExpressionEval::compile(expr, all_columns)?.with_context(ctx);
2597                        plan.push(VectorProjectionSlot::Compiled(Box::new(eval)));
2598                    }
2599                }
2600                _ => {
2601                    let eval = ExpressionEval::compile(expr, all_columns)?.with_context(ctx);
2602                    plan.push(VectorProjectionSlot::Compiled(Box::new(eval)));
2603                }
2604            }
2605        }
2606
2607        Ok(plan)
2608    }
2609
2610    /// Apply a pre-compiled projection plan to a single row.
2611    fn apply_vector_projection(
2612        plan: &mut [VectorProjectionSlot],
2613        base_row: &Row,
2614        distance: f64,
2615    ) -> Result<Row> {
2616        let mut values = Vec::with_capacity(plan.len());
2617
2618        for slot in plan.iter_mut() {
2619            match slot {
2620                VectorProjectionSlot::Star => {
2621                    for i in 0..base_row.len() {
2622                        values.push(
2623                            base_row
2624                                .get(i)
2625                                .cloned()
2626                                .unwrap_or(Value::Null(radixdb_core::DataType::Null)),
2627                        );
2628                    }
2629                }
2630                VectorProjectionSlot::Column(idx) => {
2631                    values.push(
2632                        base_row
2633                            .get(*idx)
2634                            .cloned()
2635                            .unwrap_or(Value::Null(radixdb_core::DataType::Null)),
2636                    );
2637                }
2638                VectorProjectionSlot::Distance => {
2639                    if distance.is_infinite() {
2640                        // Dimension mismatch or missing vector → NULL distance
2641                        values.push(Value::Null(radixdb_core::DataType::Float));
2642                    } else {
2643                        values.push(Value::Float(distance));
2644                    }
2645                }
2646                VectorProjectionSlot::Compiled(eval) => {
2647                    let val = eval.eval_slice(base_row)?;
2648                    values.push(val);
2649                }
2650            }
2651        }
2652
2653        Ok(Row::from_values(values))
2654    }
2655}
2656
2657impl<T: IndexOptimizerHost + ?Sized> IndexOptimizerExt for T {}
2658
2659#[cfg(test)]
2660mod keyset_bound_tests {
2661    use super::IntegerPkKeysetBound::{After, Empty, From};
2662
2663    #[test]
2664    fn strongest_lower_bound_is_order_independent_and_preserves_exclusivity() {
2665        assert_eq!(After(1).strongest_lower(After(5)), After(5));
2666        assert_eq!(After(5).strongest_lower(After(1)), After(5));
2667        assert_eq!(From(6).strongest_lower(After(5)), From(6));
2668        assert_eq!(After(5).strongest_lower(From(6)), From(6));
2669        assert_eq!(From(5).strongest_lower(After(5)), After(5));
2670        assert_eq!(After(5).strongest_lower(From(5)), After(5));
2671        assert_eq!(
2672            After(i64::MAX).strongest_lower(From(i64::MAX)),
2673            After(i64::MAX)
2674        );
2675        assert_eq!(Empty.strongest_lower(After(1)), Empty);
2676    }
2677}