Skip to main content

alopex_sql/executor/query/
columnar_scan.rs

1use alopex_core::columnar::encoding::Column;
2use alopex_core::columnar::encoding_v2::Bitmap;
3use alopex_core::columnar::kvs_bridge::key_layout;
4use alopex_core::columnar::segment_v2::{
5    ColumnSegmentV2, InMemorySegmentSource, RecordBatch, SegmentReaderV2,
6};
7use alopex_core::kv::{KVStore, KVTransaction};
8use alopex_core::storage::format::bincode_config;
9use bincode::config::Options;
10
11use crate::ast::expr::BinaryOp;
12use crate::catalog::{ColumnMetadata, RowIdMode, TableMetadata};
13use crate::columnar::statistics::RowGroupStatistics;
14use crate::executor::evaluator::{EvalContext, evaluate};
15use crate::executor::query::iterator::RowIterator;
16use crate::executor::{ExecutorError, Result, Row};
17use crate::planner::typed_expr::{Projection, TypedExpr, TypedExprKind};
18use crate::planner::types::ResolvedType;
19use crate::storage::{SqlTxn, SqlValue};
20use std::collections::BTreeSet;
21
22/// ColumnarScan オペレータ。
23#[derive(Debug, Clone)]
24pub struct ColumnarScan {
25    pub table_id: u32,
26    pub projected_columns: Vec<usize>,
27    pub pushed_filter: Option<PushdownFilter>,
28    pub residual_filter: Option<TypedExpr>,
29}
30
31/// プッシュダウン可能なフィルタ。
32#[derive(Debug, Clone, PartialEq)]
33pub enum PushdownFilter {
34    Eq {
35        column_idx: usize,
36        value: SqlValue,
37    },
38    Range {
39        column_idx: usize,
40        min: Option<SqlValue>,
41        max: Option<SqlValue>,
42    },
43    IsNull {
44        column_idx: usize,
45        is_null: bool,
46    },
47    And(Vec<PushdownFilter>),
48    Or(Vec<PushdownFilter>),
49}
50
51impl ColumnarScan {
52    pub fn new(
53        table_id: u32,
54        projected_columns: Vec<usize>,
55        pushed_filter: Option<PushdownFilter>,
56        residual_filter: Option<TypedExpr>,
57    ) -> Self {
58        Self {
59            table_id,
60            projected_columns,
61            pushed_filter,
62            residual_filter,
63        }
64    }
65
66    /// RowGroup をプルーニングするか判定する。
67    pub fn should_skip_row_group(&self, stats: &RowGroupStatistics) -> bool {
68        match &self.pushed_filter {
69            None => false,
70            Some(filter) => Self::evaluate_pushdown(filter, stats),
71        }
72    }
73
74    /// プッシュダウンフィルタを統計情報で評価する。
75    pub fn evaluate_pushdown(filter: &PushdownFilter, stats: &RowGroupStatistics) -> bool {
76        match filter {
77            PushdownFilter::Eq { column_idx, value } => match stats.columns.get(*column_idx) {
78                Some(col_stats) => {
79                    if col_stats.total_count == 0 {
80                        return true;
81                    }
82                    if matches!(
83                        value.partial_cmp(&col_stats.min),
84                        Some(std::cmp::Ordering::Less)
85                    ) {
86                        return true;
87                    }
88                    matches!(
89                        value.partial_cmp(&col_stats.max),
90                        Some(std::cmp::Ordering::Greater)
91                    )
92                }
93                None => false,
94            },
95
96            PushdownFilter::Range {
97                column_idx,
98                min,
99                max,
100            } => match stats.columns.get(*column_idx) {
101                Some(col_stats) => {
102                    if col_stats.total_count == 0 {
103                        return true;
104                    }
105                    if let Some(filter_min) = min
106                        && matches!(
107                            col_stats.max.partial_cmp(filter_min),
108                            Some(std::cmp::Ordering::Less)
109                        )
110                    {
111                        return true;
112                    }
113                    if let Some(filter_max) = max
114                        && matches!(
115                            col_stats.min.partial_cmp(filter_max),
116                            Some(std::cmp::Ordering::Greater)
117                        )
118                    {
119                        return true;
120                    }
121                    false
122                }
123                None => false,
124            },
125
126            PushdownFilter::IsNull {
127                column_idx,
128                is_null,
129            } => match stats.columns.get(*column_idx) {
130                Some(col_stats) => {
131                    if *is_null {
132                        col_stats.null_count == 0
133                    } else {
134                        col_stats.null_count == col_stats.total_count
135                    }
136                }
137                None => false,
138            },
139
140            PushdownFilter::And(filters) => {
141                if filters.is_empty() {
142                    return false;
143                }
144                filters.iter().any(|f| Self::evaluate_pushdown(f, stats))
145            }
146
147            PushdownFilter::Or(filters) => {
148                if filters.is_empty() {
149                    return false;
150                }
151                filters.iter().all(|f| Self::evaluate_pushdown(f, stats))
152            }
153        }
154    }
155}
156
157// ============================================================================
158// ColumnarScanIterator - FR-7 Streaming Iterator for Columnar Storage
159// ============================================================================
160
161/// Pre-loaded segment data for streaming iteration.
162struct LoadedSegment {
163    /// Segment reader for reading row groups.
164    reader: SegmentReaderV2,
165    /// Row group statistics for pruning (if available).
166    row_group_stats: Option<Vec<RowGroupStatistics>>,
167    /// Row IDs for RowIdMode::Direct.
168    row_ids: Vec<u64>,
169    /// Row group metadata for row ID slicing.
170    row_groups: Vec<alopex_core::columnar::segment_v2::RowGroupMeta>,
171}
172
173/// Streaming iterator for columnar storage (FR-7 compliant).
174///
175/// This iterator yields rows one at a time from columnar storage,
176/// avoiding the need to materialize all rows into a `Vec<Row>` upfront.
177/// Segments are pre-loaded during construction, but row conversion is
178/// performed lazily as rows are requested.
179pub struct ColumnarScanIterator {
180    /// Pre-loaded segments.
181    segments: Vec<LoadedSegment>,
182    /// Current segment index.
183    segment_idx: usize,
184    /// Current row group index within the segment.
185    row_group_idx: usize,
186    /// Current row index within the batch.
187    row_idx: usize,
188    /// Current loaded RecordBatch (lazy loaded per row group).
189    current_batch: Option<RecordBatch>,
190    /// Projected column indices.
191    projected: Vec<usize>,
192    /// Table metadata.
193    table_meta: TableMetadata,
194    /// Schema for RowIterator trait.
195    schema: Vec<ColumnMetadata>,
196    /// ColumnarScan operator for filter evaluation.
197    scan: ColumnarScan,
198    /// RowID column index for RowIdMode::Direct.
199    row_id_col_idx: Option<usize>,
200    /// Next synthetic row ID (for RowIdMode::None).
201    next_row_id: u64,
202}
203
204impl ColumnarScanIterator {
205    /// Advances to the next valid row, loading batches as needed.
206    ///
207    /// Returns `Some(Ok(row))` for valid rows, `Some(Err(_))` for errors,
208    /// or `None` when all rows have been consumed.
209    fn advance(&mut self) -> Option<Result<Row>> {
210        loop {
211            // Check if we need to load a new batch
212            if self.current_batch.is_none() && !self.load_next_batch() {
213                return None; // No more batches
214            }
215
216            // Get row count to check if exhausted
217            let row_count = match &self.current_batch {
218                Some(batch) => batch.num_rows(),
219                None => continue,
220            };
221
222            // Check if we've exhausted the current batch
223            if self.row_idx >= row_count {
224                self.current_batch = None;
225                self.row_idx = 0;
226                self.row_group_idx += 1;
227                continue;
228            }
229
230            // Convert current row (take batch temporarily to avoid borrow conflict)
231            let row_idx = self.row_idx;
232            match self.convert_current_row(row_idx) {
233                Ok(Some(row)) => {
234                    self.row_idx += 1;
235                    return Some(Ok(row));
236                }
237                Ok(None) => {
238                    // Row filtered out by residual filter
239                    self.row_idx += 1;
240                    continue;
241                }
242                Err(e) => {
243                    self.row_idx += 1;
244                    return Some(Err(e));
245                }
246            }
247        }
248    }
249
250    /// Loads the next row group batch, advancing segments as needed.
251    ///
252    /// Returns `true` if a batch was successfully loaded, `false` if no more data.
253    fn load_next_batch(&mut self) -> bool {
254        while self.segment_idx < self.segments.len() {
255            let segment = &self.segments[self.segment_idx];
256            let row_group_count = segment.row_groups.len();
257
258            while self.row_group_idx < row_group_count {
259                // Check if this row group should be skipped via pushdown
260                let should_skip = match segment.row_group_stats.as_ref() {
261                    Some(stats) if stats.len() == row_group_count => {
262                        self.scan.should_skip_row_group(&stats[self.row_group_idx])
263                    }
264                    _ => false,
265                };
266
267                if should_skip {
268                    self.row_group_idx += 1;
269                    continue;
270                }
271
272                // Load the batch
273                match segment
274                    .reader
275                    .read_row_group_by_index(&self.projected, self.row_group_idx)
276                {
277                    Ok(mut batch) => {
278                        // Attach row IDs if available
279                        if !segment.row_ids.is_empty()
280                            && let Some(meta) = segment.row_groups.get(self.row_group_idx)
281                        {
282                            let start = meta.row_start as usize;
283                            let end = start + meta.row_count as usize;
284                            if end <= segment.row_ids.len() {
285                                batch =
286                                    batch.with_row_ids(Some(segment.row_ids[start..end].to_vec()));
287                            }
288                        }
289                        self.current_batch = Some(batch);
290                        self.row_idx = 0;
291                        return true;
292                    }
293                    Err(_) => {
294                        // Skip this row group on error
295                        self.row_group_idx += 1;
296                        continue;
297                    }
298                }
299            }
300
301            // Move to next segment
302            self.segment_idx += 1;
303            self.row_group_idx = 0;
304        }
305
306        false
307    }
308
309    /// Converts a row from the current batch, applying residual filter.
310    ///
311    /// Returns `Ok(Some(row))` if row passes filter, `Ok(None)` if filtered out.
312    fn convert_current_row(&mut self, row_idx: usize) -> Result<Option<Row>> {
313        let batch = self
314            .current_batch
315            .as_ref()
316            .ok_or_else(|| ExecutorError::Columnar("no current batch".into()))?;
317
318        let column_count = self.table_meta.column_count();
319        let mut values = vec![SqlValue::Null; column_count];
320
321        for (pos, &table_col_idx) in self.projected.iter().enumerate() {
322            let column = batch
323                .columns
324                .get(pos)
325                .ok_or_else(|| ExecutorError::Columnar("missing projected column".into()))?;
326            let bitmap = batch.null_bitmaps.get(pos).and_then(|b| b.as_ref());
327            let col_meta = self
328                .table_meta
329                .columns
330                .get(table_col_idx)
331                .ok_or_else(|| ExecutorError::Columnar("column index out of bounds".into()))?;
332            let value = value_from_column(column, bitmap, row_idx, &col_meta.data_type)?;
333            values[table_col_idx] = value;
334        }
335
336        // Apply residual filter
337        if let Some(predicate) = self.scan.residual_filter.as_ref() {
338            let ctx = EvalContext::new(&values);
339            let keep = matches!(evaluate(predicate, &ctx)?, SqlValue::Boolean(true));
340            if !keep {
341                return Ok(None);
342            }
343        }
344
345        // Determine row ID - need to access batch again for row_ids
346        let batch = self
347            .current_batch
348            .as_ref()
349            .ok_or_else(|| ExecutorError::Columnar("no current batch".into()))?;
350
351        let row_id = match self.table_meta.storage_options.row_id_mode {
352            RowIdMode::Direct => {
353                if let Some(row_ids) = batch.row_ids.as_ref() {
354                    *row_ids.get(row_idx).ok_or_else(|| {
355                        ExecutorError::Columnar(
356                            "row_id missing for row in row_id_mode=direct".into(),
357                        )
358                    })?
359                } else if let Some(idx) = self.row_id_col_idx {
360                    let val = values.get(idx).ok_or_else(|| {
361                        ExecutorError::Columnar("row_id column missing in projected values".into())
362                    })?;
363                    match val {
364                        SqlValue::Integer(v) if *v >= 0 => *v as u64,
365                        SqlValue::BigInt(v) if *v >= 0 => *v as u64,
366                        other => {
367                            return Err(ExecutorError::Columnar(format!(
368                                "row_id column must be non-negative integer, got {}",
369                                other.type_name()
370                            )));
371                        }
372                    }
373                } else {
374                    let rid = self.next_row_id;
375                    self.next_row_id = self.next_row_id.saturating_add(1);
376                    rid
377                }
378            }
379            RowIdMode::None => {
380                let rid = self.next_row_id;
381                self.next_row_id = self.next_row_id.saturating_add(1);
382                rid
383            }
384        };
385
386        Ok(Some(Row::new(row_id, values)))
387    }
388}
389
390impl RowIterator for ColumnarScanIterator {
391    fn next_row(&mut self) -> Option<Result<Row>> {
392        self.advance()
393    }
394
395    fn schema(&self) -> &[ColumnMetadata] {
396        &self.schema
397    }
398}
399
400/// Create a streaming columnar scan iterator (FR-7 compliant).
401///
402/// This function pre-loads segment data during construction but yields rows
403/// one at a time during iteration, avoiding full materialization of all rows.
404///
405/// # Arguments
406///
407/// * `txn` - Transaction for loading segment data
408/// * `table_meta` - Table metadata
409/// * `scan` - ColumnarScan operator with projection and filters
410///
411/// # Returns
412///
413/// A `ColumnarScanIterator` that implements `RowIterator`.
414pub fn create_columnar_scan_iterator<'txn, S: KVStore + 'txn>(
415    txn: &mut impl SqlTxn<'txn, S>,
416    table_meta: &TableMetadata,
417    scan: &ColumnarScan,
418) -> Result<ColumnarScanIterator> {
419    debug_assert_eq!(scan.table_id, table_meta.table_id);
420
421    let projected: Vec<usize> = if scan.projected_columns.is_empty() {
422        (0..table_meta.columns.len()).collect()
423    } else {
424        scan.projected_columns.clone()
425    };
426
427    let segment_ids = load_segment_index(txn, table_meta.table_id)?;
428
429    let row_id_col_idx = if table_meta.storage_options.row_id_mode == RowIdMode::Direct {
430        table_meta
431            .columns
432            .iter()
433            .position(|c| c.name.eq_ignore_ascii_case("row_id"))
434    } else {
435        None
436    };
437
438    // Pre-load all segments
439    let mut segments = Vec::with_capacity(segment_ids.len());
440    for segment_id in segment_ids {
441        let segment = load_segment(txn, table_meta.table_id, segment_id)?;
442        let reader =
443            SegmentReaderV2::open(Box::new(InMemorySegmentSource::new(segment.data.clone())))
444                .map_err(|e| ExecutorError::Columnar(e.to_string()))?;
445        let row_group_stats = load_row_group_stats(txn, table_meta.table_id, segment_id);
446
447        segments.push(LoadedSegment {
448            reader,
449            row_group_stats,
450            row_ids: segment.row_ids.clone(),
451            row_groups: segment.meta.row_groups.clone(),
452        });
453    }
454
455    Ok(ColumnarScanIterator {
456        segments,
457        segment_idx: 0,
458        row_group_idx: 0,
459        row_idx: 0,
460        current_batch: None,
461        projected,
462        schema: table_meta.columns.clone(),
463        table_meta: table_meta.clone(),
464        scan: scan.clone(),
465        row_id_col_idx,
466        next_row_id: 0,
467    })
468}
469
470/// ColumnarScan を実行する。
471pub fn execute_columnar_scan<'txn, S: KVStore + 'txn>(
472    txn: &mut impl SqlTxn<'txn, S>,
473    table_meta: &TableMetadata,
474    scan: &ColumnarScan,
475) -> Result<Vec<Row>> {
476    debug_assert_eq!(scan.table_id, table_meta.table_id);
477    let projected: Vec<usize> = if scan.projected_columns.is_empty() {
478        (0..table_meta.columns.len()).collect()
479    } else {
480        scan.projected_columns.clone()
481    };
482
483    let segment_ids = load_segment_index(txn, table_meta.table_id)?;
484    if segment_ids.is_empty() {
485        return Ok(Vec::new());
486    }
487
488    let row_id_col_idx = if table_meta.storage_options.row_id_mode == RowIdMode::Direct {
489        table_meta
490            .columns
491            .iter()
492            .position(|c| c.name.eq_ignore_ascii_case("row_id"))
493    } else {
494        None
495    };
496
497    let mut results = Vec::new();
498    let mut next_row_id = 0u64;
499    for segment_id in segment_ids {
500        let segment = load_segment(txn, table_meta.table_id, segment_id)?;
501        let reader =
502            SegmentReaderV2::open(Box::new(InMemorySegmentSource::new(segment.data.clone())))
503                .map_err(|e| ExecutorError::Columnar(e.to_string()))?;
504
505        let row_group_stats = load_row_group_stats(txn, table_meta.table_id, segment_id);
506        let row_group_count = segment.meta.row_groups.len();
507        for rg_index in 0..row_group_count {
508            let should_skip = match row_group_stats.as_ref() {
509                Some(stats) if stats.len() == row_group_count => {
510                    scan.should_skip_row_group(&stats[rg_index])
511                }
512                _ => false,
513            };
514            if should_skip {
515                continue;
516            }
517
518            let batch = reader
519                .read_row_group_by_index(&projected, rg_index)
520                .map_err(|e| ExecutorError::Columnar(e.to_string()))?;
521            let batch = if !segment.row_ids.is_empty() {
522                if let Some(meta) = segment.meta.row_groups.get(rg_index) {
523                    let start = meta.row_start as usize;
524                    let end = start + meta.row_count as usize;
525                    if end <= segment.row_ids.len() {
526                        batch.with_row_ids(Some(segment.row_ids[start..end].to_vec()))
527                    } else {
528                        batch
529                    }
530                } else {
531                    batch
532                }
533            } else {
534                batch
535            };
536            append_rows_from_batch(
537                &mut results,
538                &batch,
539                table_meta,
540                &projected,
541                scan.residual_filter.as_ref(),
542                table_meta.storage_options.row_id_mode,
543                row_id_col_idx,
544                &mut next_row_id,
545            )?;
546        }
547    }
548
549    Ok(results)
550}
551
552/// ColumnarScan を実行し、フィルタ後の RowID のみを返す。
553///
554/// RowIdMode::Direct で columnar ストレージの場合、行本体の読み込みを避けて
555/// RowID 再フェッチ用の候補セットを得る目的で使用する。
556pub fn execute_columnar_row_ids<'txn, S: KVStore + 'txn>(
557    txn: &mut impl SqlTxn<'txn, S>,
558    table_meta: &TableMetadata,
559    scan: &ColumnarScan,
560) -> Result<Vec<u64>> {
561    if table_meta.storage_options.storage_type != crate::catalog::StorageType::Columnar {
562        return Err(ExecutorError::Columnar(
563            "execute_columnar_row_ids requires columnar storage".into(),
564        ));
565    }
566
567    let mut needed: BTreeSet<usize> = scan.projected_columns.iter().copied().collect();
568    if let Some(pred) = &scan.residual_filter {
569        collect_column_indices(pred, &mut needed);
570    }
571    let projected: Vec<usize> = needed.into_iter().collect();
572
573    let segment_ids = load_segment_index(txn, table_meta.table_id)?;
574    if segment_ids.is_empty() {
575        return Ok(Vec::new());
576    }
577
578    let row_id_col_idx = if table_meta.storage_options.row_id_mode == RowIdMode::Direct {
579        table_meta
580            .columns
581            .iter()
582            .position(|c| c.name.eq_ignore_ascii_case("row_id"))
583    } else {
584        None
585    };
586
587    let mut results = Vec::new();
588    let mut next_row_id = 0u64;
589    for segment_id in segment_ids {
590        let segment = load_segment(txn, table_meta.table_id, segment_id)?;
591        let reader =
592            SegmentReaderV2::open(Box::new(InMemorySegmentSource::new(segment.data.clone())))
593                .map_err(|e| ExecutorError::Columnar(e.to_string()))?;
594
595        let row_group_stats = load_row_group_stats(txn, table_meta.table_id, segment_id);
596        let row_group_count = segment.meta.row_groups.len();
597        for rg_index in 0..row_group_count {
598            let should_skip = match row_group_stats.as_ref() {
599                Some(stats) if stats.len() == row_group_count => {
600                    scan.should_skip_row_group(&stats[rg_index])
601                }
602                _ => false,
603            };
604            if should_skip {
605                continue;
606            }
607
608            let batch = reader
609                .read_row_group_by_index(&projected, rg_index)
610                .map_err(|e| ExecutorError::Columnar(e.to_string()))?;
611            let batch = if !segment.row_ids.is_empty() {
612                if let Some(meta) = segment.meta.row_groups.get(rg_index) {
613                    let start = meta.row_start as usize;
614                    let end = start + meta.row_count as usize;
615                    if end <= segment.row_ids.len() {
616                        batch.with_row_ids(Some(segment.row_ids[start..end].to_vec()))
617                    } else {
618                        batch
619                    }
620                } else {
621                    batch
622                }
623            } else {
624                batch
625            };
626
627            let row_count = batch.num_rows();
628            for row_idx in 0..row_count {
629                // 残余フィルタの評価に必要なカラムだけ値を復元する。
630                let mut values = vec![SqlValue::Null; table_meta.column_count()];
631                for (pos, &table_col_idx) in projected.iter().enumerate() {
632                    let column = batch.columns.get(pos).ok_or_else(|| {
633                        ExecutorError::Columnar("missing projected column".into())
634                    })?;
635                    let bitmap = batch.null_bitmaps.get(pos).and_then(|b| b.as_ref());
636                    let value = value_from_column(
637                        column,
638                        bitmap,
639                        row_idx,
640                        &table_meta
641                            .columns
642                            .get(table_col_idx)
643                            .ok_or_else(|| {
644                                ExecutorError::Columnar("column index out of bounds".into())
645                            })?
646                            .data_type,
647                    )?;
648                    values[table_col_idx] = value;
649                }
650
651                if let Some(predicate) = scan.residual_filter.as_ref() {
652                    let ctx = EvalContext::new(&values);
653                    let keep = matches!(evaluate(predicate, &ctx)?, SqlValue::Boolean(true));
654                    if !keep {
655                        continue;
656                    }
657                }
658
659                let row_id = match table_meta.storage_options.row_id_mode {
660                    RowIdMode::Direct => {
661                        if let Some(row_ids) = batch.row_ids.as_ref() {
662                            *row_ids.get(row_idx).ok_or_else(|| {
663                                ExecutorError::Columnar(
664                                    "row_id missing for row in row_id_mode=direct".into(),
665                                )
666                            })?
667                        } else if let Some(idx) = row_id_col_idx {
668                            let val = values.get(idx).ok_or_else(|| {
669                                ExecutorError::Columnar(
670                                    "row_id column missing in projected values".into(),
671                                )
672                            })?;
673                            match val {
674                                SqlValue::Integer(v) if *v >= 0 => *v as u64,
675                                SqlValue::BigInt(v) if *v >= 0 => *v as u64,
676                                other => {
677                                    return Err(ExecutorError::Columnar(format!(
678                                        "row_id column must be non-negative integer, got {}",
679                                        other.type_name()
680                                    )));
681                                }
682                            }
683                        } else {
684                            let rid = next_row_id;
685                            next_row_id = next_row_id.saturating_add(1);
686                            rid
687                        }
688                    }
689                    RowIdMode::None => {
690                        let rid = next_row_id;
691                        next_row_id = next_row_id.saturating_add(1);
692                        rid
693                    }
694                };
695                results.push(row_id);
696            }
697        }
698    }
699
700    Ok(results)
701}
702
703/// TypedExpr から PushdownFilter へ変換する(変換不可なら None)。
704pub fn expr_to_pushdown(expr: &TypedExpr) -> Option<PushdownFilter> {
705    match &expr.kind {
706        TypedExprKind::BinaryOp { left, op, right } => match op {
707            BinaryOp::And => {
708                let l = expr_to_pushdown(left)?;
709                let r = expr_to_pushdown(right)?;
710                Some(PushdownFilter::And(vec![l, r]))
711            }
712            BinaryOp::Or => {
713                let l = expr_to_pushdown(left)?;
714                let r = expr_to_pushdown(right)?;
715                Some(PushdownFilter::Or(vec![l, r]))
716            }
717            BinaryOp::Eq => extract_eq(left, right),
718            BinaryOp::Lt | BinaryOp::LtEq | BinaryOp::Gt | BinaryOp::GtEq => {
719                extract_range(op, left, right)
720            }
721            _ => None,
722        },
723        TypedExprKind::Between {
724            expr,
725            low,
726            high,
727            negated,
728        } => {
729            if *negated {
730                return None;
731            }
732            let (column_idx, value_min, value_max) = match expr.kind {
733                TypedExprKind::ColumnRef { column_index, .. } => {
734                    let low_v = literal_value(low)?;
735                    let high_v = literal_value(high)?;
736                    (column_index, low_v, high_v)
737                }
738                _ => return None,
739            };
740            Some(PushdownFilter::Range {
741                column_idx,
742                min: Some(value_min),
743                max: Some(value_max),
744            })
745        }
746        TypedExprKind::IsNull { expr, negated } => match expr.kind {
747            TypedExprKind::ColumnRef { column_index, .. } => Some(PushdownFilter::IsNull {
748                column_idx: column_index,
749                is_null: !negated,
750            }),
751            _ => None,
752        },
753        _ => None,
754    }
755}
756
757fn extract_eq(left: &TypedExpr, right: &TypedExpr) -> Option<PushdownFilter> {
758    if let Some((col_idx, value)) = extract_column_literal(left, right) {
759        return Some(PushdownFilter::Eq {
760            column_idx: col_idx,
761            value,
762        });
763    }
764    if let Some((col_idx, value)) = extract_column_literal(right, left) {
765        return Some(PushdownFilter::Eq {
766            column_idx: col_idx,
767            value,
768        });
769    }
770    None
771}
772
773fn extract_range(op: &BinaryOp, left: &TypedExpr, right: &TypedExpr) -> Option<PushdownFilter> {
774    match (
775        extract_column_literal(left, right),
776        extract_column_literal(right, left),
777    ) {
778        (Some((col_idx, value)), _) => match op {
779            BinaryOp::Lt | BinaryOp::LtEq => Some(PushdownFilter::Range {
780                column_idx: col_idx,
781                min: None,
782                max: Some(value),
783            }),
784            BinaryOp::Gt | BinaryOp::GtEq => Some(PushdownFilter::Range {
785                column_idx: col_idx,
786                min: Some(value),
787                max: None,
788            }),
789            _ => None,
790        },
791        (_, Some((col_idx, value))) => match op {
792            BinaryOp::Lt | BinaryOp::LtEq => Some(PushdownFilter::Range {
793                column_idx: col_idx,
794                min: Some(value),
795                max: None,
796            }),
797            BinaryOp::Gt | BinaryOp::GtEq => Some(PushdownFilter::Range {
798                column_idx: col_idx,
799                min: None,
800                max: Some(value),
801            }),
802            _ => None,
803        },
804        _ => None,
805    }
806}
807
808fn extract_column_literal(
809    column_expr: &TypedExpr,
810    literal_expr: &TypedExpr,
811) -> Option<(usize, SqlValue)> {
812    match column_expr.kind {
813        TypedExprKind::ColumnRef { column_index, .. } => {
814            let value = literal_value(literal_expr)?;
815            Some((column_index, value))
816        }
817        _ => None,
818    }
819}
820
821fn literal_value(expr: &TypedExpr) -> Option<SqlValue> {
822    match &expr.kind {
823        TypedExprKind::Literal(_) | TypedExprKind::VectorLiteral(_) => {
824            evaluate(expr, &EvalContext::new(&[])).ok()
825        }
826        _ => None,
827    }
828}
829
830/// projection 情報からカラムインデックスを推定する(現状は全カラム)。
831pub fn projection_to_columns(projection: &Projection, table_meta: &TableMetadata) -> Vec<usize> {
832    match projection {
833        Projection::All(names) => names
834            .iter()
835            .filter_map(|name| table_meta.columns.iter().position(|c| &c.name == name))
836            .collect(),
837        Projection::Columns(cols) => {
838            let mut indices = BTreeSet::new();
839            for col in cols {
840                collect_column_indices(&col.expr, &mut indices);
841            }
842            if indices.is_empty() {
843                return (0..table_meta.columns.len()).collect();
844            }
845            indices
846                .into_iter()
847                .filter(|idx| *idx < table_meta.columns.len())
848                .collect()
849        }
850    }
851}
852
853/// フィルタと Projection を ColumnarScan にまとめるユーティリティ。
854pub fn build_columnar_scan_for_filter(
855    table_meta: &TableMetadata,
856    projection: Projection,
857    predicate: &TypedExpr,
858) -> ColumnarScan {
859    let mut projected_columns = projection_to_columns(&projection, table_meta);
860    let mut predicate_indices = BTreeSet::new();
861    collect_column_indices(predicate, &mut predicate_indices);
862    for idx in predicate_indices {
863        if !projected_columns.contains(&idx) {
864            projected_columns.push(idx);
865        }
866    }
867    projected_columns.sort_unstable();
868    let pushed_filter = expr_to_pushdown(predicate);
869    ColumnarScan::new(
870        table_meta.table_id,
871        projected_columns,
872        pushed_filter,
873        Some(predicate.clone()),
874    )
875}
876
877/// Columnar scan that materializes the projection plus every *local* column the
878/// predicate reads, leaving predicate evaluation to the caller.
879///
880/// Filter fusion resolves each column index against the scanned table, so it
881/// cannot be used for a predicate the scan is unable to evaluate on its own:
882/// a correlated predicate also carries outer-row indexes past the table width,
883/// and a predicate holding a subquery needs transaction access. Indexes at or
884/// past the table width are dropped here rather than widening the scan - that
885/// is the correlation boundary (issue #151, D15).
886pub fn build_columnar_scan_for_external_filter(
887    table_meta: &TableMetadata,
888    projection: Projection,
889    predicate: &TypedExpr,
890) -> ColumnarScan {
891    let mut projected_columns = projection_to_columns(&projection, table_meta);
892    let mut predicate_indices = BTreeSet::new();
893    collect_column_indices(predicate, &mut predicate_indices);
894    for idx in predicate_indices {
895        if idx < table_meta.columns.len() && !projected_columns.contains(&idx) {
896            projected_columns.push(idx);
897        }
898    }
899    projected_columns.sort_unstable();
900    ColumnarScan::new(table_meta.table_id, projected_columns, None, None)
901}
902
903/// Projection だけを指定して ColumnarScan を構築する。
904pub fn build_columnar_scan(table_meta: &TableMetadata, projection: &Projection) -> ColumnarScan {
905    let projected_columns = projection_to_columns(projection, table_meta);
906    ColumnarScan::new(table_meta.table_id, projected_columns, None, None)
907}
908
909/// 式中に現れるカラムインデックスを収集する。
910fn collect_column_indices(expr: &TypedExpr, acc: &mut BTreeSet<usize>) {
911    match &expr.kind {
912        TypedExprKind::ColumnRef { column_index, .. } => {
913            acc.insert(*column_index);
914        }
915        TypedExprKind::BinaryOp { left, right, .. } => {
916            collect_column_indices(left, acc);
917            collect_column_indices(right, acc);
918        }
919        TypedExprKind::UnaryOp { operand, .. } => collect_column_indices(operand, acc),
920        TypedExprKind::Cast { expr, .. } | TypedExprKind::TryCast { expr, .. } => {
921            collect_column_indices(expr, acc)
922        }
923        TypedExprKind::Case {
924            operand,
925            branches,
926            else_expr,
927        } => {
928            if let Some(operand) = operand {
929                collect_column_indices(operand, acc);
930            }
931            for branch in branches {
932                collect_column_indices(&branch.when, acc);
933                collect_column_indices(&branch.then, acc);
934            }
935            if let Some(else_expr) = else_expr {
936                collect_column_indices(else_expr, acc);
937            }
938        }
939        TypedExprKind::Between {
940            expr, low, high, ..
941        } => {
942            collect_column_indices(expr, acc);
943            collect_column_indices(low, acc);
944            collect_column_indices(high, acc);
945        }
946        TypedExprKind::Like {
947            expr,
948            pattern,
949            escape,
950            ..
951        } => {
952            collect_column_indices(expr, acc);
953            collect_column_indices(pattern, acc);
954            if let Some(escape) = escape {
955                collect_column_indices(escape, acc);
956            }
957        }
958        TypedExprKind::InList { expr, list, .. } => {
959            collect_column_indices(expr, acc);
960            for item in list {
961                collect_column_indices(item, acc);
962            }
963        }
964        TypedExprKind::IsNull { expr, .. } => collect_column_indices(expr, acc),
965        TypedExprKind::FunctionCall {
966            args,
967            filter,
968            order_by,
969            over,
970            ..
971        } => {
972            for arg in args {
973                collect_column_indices(arg, acc);
974            }
975            // A columnar scan materializes exactly the columns collected here
976            // and fills every other column with NULL, so an aggregate FILTER
977            // predicate (issue #148), an aggregate-local ORDER BY key, or a
978            // window partition/order key that names a column appearing
979            // nowhere else in the projection must be collected too. Missing
980            // one is silent: the operator reads NULL instead of the value.
981            if let Some(filter) = filter {
982                collect_column_indices(filter, acc);
983            }
984            for sort in order_by {
985                collect_column_indices(&sort.expr, acc);
986            }
987            if let Some(over) = over {
988                for partition in &over.partition_by {
989                    collect_column_indices(partition, acc);
990                }
991                for sort in &over.order_by {
992                    collect_column_indices(&sort.expr, acc);
993                }
994            }
995        }
996        _ => {}
997    }
998}
999
1000fn load_segment_index<'txn, S: KVStore + 'txn>(
1001    txn: &mut impl SqlTxn<'txn, S>,
1002    table_id: u32,
1003) -> Result<Vec<u64>> {
1004    let key = key_layout::segment_index_key(table_id);
1005    let bytes = txn.inner_mut().get(&key)?;
1006    if let Some(raw) = bytes {
1007        bincode_config()
1008            .deserialize(&raw)
1009            .map_err(|e| ExecutorError::Columnar(e.to_string()))
1010    } else {
1011        Ok(Vec::new())
1012    }
1013}
1014
1015fn load_segment<'txn, S: KVStore + 'txn>(
1016    txn: &mut impl SqlTxn<'txn, S>,
1017    table_id: u32,
1018    segment_id: u64,
1019) -> Result<ColumnSegmentV2> {
1020    let key = key_layout::column_segment_key(table_id, segment_id, 0);
1021    let bytes = txn
1022        .inner_mut()
1023        .get(&key)?
1024        .ok_or_else(|| ExecutorError::Columnar(format!("segment {segment_id} missing")))?;
1025    bincode_config()
1026        .deserialize(&bytes)
1027        .map_err(|e| ExecutorError::Columnar(e.to_string()))
1028}
1029
1030fn load_row_group_stats<'txn, S: KVStore + 'txn>(
1031    txn: &mut impl SqlTxn<'txn, S>,
1032    table_id: u32,
1033    segment_id: u64,
1034) -> Option<Vec<RowGroupStatistics>> {
1035    let key = key_layout::row_group_stats_key(table_id, segment_id);
1036    match txn.inner_mut().get(&key) {
1037        Ok(Some(bytes)) => bincode_config().deserialize(&bytes).ok(),
1038        Ok(None) => None,
1039        Err(_) => None,
1040    }
1041}
1042
1043#[allow(clippy::too_many_arguments)]
1044fn append_rows_from_batch(
1045    out: &mut Vec<Row>,
1046    batch: &alopex_core::columnar::segment_v2::RecordBatch,
1047    table_meta: &TableMetadata,
1048    projected: &[usize],
1049    residual_filter: Option<&TypedExpr>,
1050    row_id_mode: RowIdMode,
1051    row_id_col_idx: Option<usize>,
1052    next_row_id: &mut u64,
1053) -> Result<()> {
1054    if batch.columns.len() != projected.len() {
1055        return Err(ExecutorError::Columnar(format!(
1056            "projected column count mismatch: requested {}, got {}",
1057            projected.len(),
1058            batch.columns.len()
1059        )));
1060    }
1061
1062    let row_count = batch.num_rows();
1063    for row_idx in 0..row_count {
1064        let mut values = vec![SqlValue::Null; table_meta.column_count()];
1065        for (pos, &table_col_idx) in projected.iter().enumerate() {
1066            let column = batch
1067                .columns
1068                .get(pos)
1069                .ok_or_else(|| ExecutorError::Columnar("missing projected column".into()))?;
1070            let bitmap = batch.null_bitmaps.get(pos).and_then(|b| b.as_ref());
1071            let value = value_from_column(
1072                column,
1073                bitmap,
1074                row_idx,
1075                &table_meta
1076                    .columns
1077                    .get(table_col_idx)
1078                    .ok_or_else(|| ExecutorError::Columnar("column index out of bounds".into()))?
1079                    .data_type,
1080            )?;
1081            values[table_col_idx] = value;
1082        }
1083
1084        if let Some(predicate) = residual_filter {
1085            let ctx = EvalContext::new(&values);
1086            let keep = matches!(evaluate(predicate, &ctx)?, SqlValue::Boolean(true));
1087            if !keep {
1088                continue;
1089            }
1090        }
1091
1092        let row_id = match row_id_mode {
1093            RowIdMode::Direct => {
1094                if let Some(row_ids) = batch.row_ids.as_ref() {
1095                    *row_ids.get(row_idx).ok_or_else(|| {
1096                        ExecutorError::Columnar(
1097                            "row_id missing for row in row_id_mode=direct".into(),
1098                        )
1099                    })?
1100                } else if let Some(idx) = row_id_col_idx {
1101                    let val = values.get(idx).ok_or_else(|| {
1102                        ExecutorError::Columnar("row_id column missing in projected values".into())
1103                    })?;
1104                    match val {
1105                        SqlValue::Integer(v) if *v >= 0 => *v as u64,
1106                        SqlValue::BigInt(v) if *v >= 0 => *v as u64,
1107                        other => {
1108                            return Err(ExecutorError::Columnar(format!(
1109                                "row_id column must be non-negative integer, got {}",
1110                                other.type_name()
1111                            )));
1112                        }
1113                    }
1114                } else {
1115                    let rid = *next_row_id;
1116                    *next_row_id = next_row_id.saturating_add(1);
1117                    rid
1118                }
1119            }
1120            RowIdMode::None => {
1121                let rid = *next_row_id;
1122                *next_row_id = next_row_id.saturating_add(1);
1123                rid
1124            }
1125        };
1126        out.push(Row::new(row_id, values));
1127    }
1128
1129    Ok(())
1130}
1131
1132fn value_from_column(
1133    column: &Column,
1134    bitmap: Option<&Bitmap>,
1135    row_idx: usize,
1136    ty: &ResolvedType,
1137) -> Result<SqlValue> {
1138    if let Some(bm) = bitmap
1139        && !bm.get(row_idx)
1140    {
1141        return Ok(SqlValue::Null);
1142    }
1143
1144    match (ty, column) {
1145        (ResolvedType::Integer, Column::Int64(values)) => {
1146            let v = *values
1147                .get(row_idx)
1148                .ok_or_else(|| ExecutorError::Columnar("row index out of bounds".into()))?;
1149            Ok(SqlValue::Integer(v as i32))
1150        }
1151        (ResolvedType::BigInt | ResolvedType::Timestamp, Column::Int64(values)) => {
1152            let v = *values
1153                .get(row_idx)
1154                .ok_or_else(|| ExecutorError::Columnar("row index out of bounds".into()))?;
1155            if matches!(ty, ResolvedType::Timestamp) {
1156                Ok(SqlValue::Timestamp(v))
1157            } else {
1158                Ok(SqlValue::BigInt(v))
1159            }
1160        }
1161        (ResolvedType::Float, Column::Float32(values)) => {
1162            let v = *values
1163                .get(row_idx)
1164                .ok_or_else(|| ExecutorError::Columnar("row index out of bounds".into()))?;
1165            Ok(SqlValue::Float(v))
1166        }
1167        (ResolvedType::Double, Column::Float64(values)) => {
1168            let v = *values
1169                .get(row_idx)
1170                .ok_or_else(|| ExecutorError::Columnar("row index out of bounds".into()))?;
1171            Ok(SqlValue::Double(v))
1172        }
1173        (ResolvedType::Boolean, Column::Bool(values)) => {
1174            let v = *values
1175                .get(row_idx)
1176                .ok_or_else(|| ExecutorError::Columnar("row index out of bounds".into()))?;
1177            Ok(SqlValue::Boolean(v))
1178        }
1179        (ResolvedType::Text, Column::Binary(values)) => {
1180            let raw = values
1181                .get(row_idx)
1182                .ok_or_else(|| ExecutorError::Columnar("row index out of bounds".into()))?;
1183            String::from_utf8(raw.clone())
1184                .map(SqlValue::Text)
1185                .map_err(|e| ExecutorError::Columnar(e.to_string()))
1186        }
1187        (ResolvedType::Blob, Column::Binary(values)) => {
1188            let raw = values
1189                .get(row_idx)
1190                .ok_or_else(|| ExecutorError::Columnar("row index out of bounds".into()))?;
1191            Ok(SqlValue::Blob(raw.clone()))
1192        }
1193        (ResolvedType::Vector { .. }, Column::Fixed { values, .. }) => {
1194            let raw = values
1195                .get(row_idx)
1196                .ok_or_else(|| ExecutorError::Columnar("row index out of bounds".into()))?;
1197            if raw.len() % 4 != 0 {
1198                return Err(ExecutorError::Columnar(
1199                    "invalid vector byte length in columnar segment".into(),
1200                ));
1201            }
1202            let floats: Vec<f32> = raw
1203                .as_chunks::<4>()
1204                .0
1205                .iter()
1206                .map(|bytes| f32::from_le_bytes(*bytes))
1207                .collect();
1208            Ok(SqlValue::Vector(floats))
1209        }
1210        (_, Column::Binary(values)) => {
1211            let raw = values
1212                .get(row_idx)
1213                .ok_or_else(|| ExecutorError::Columnar("row index out of bounds".into()))?;
1214            Ok(SqlValue::Blob(raw.clone()))
1215        }
1216        _ => Err(ExecutorError::Columnar(
1217            "unsupported column type for columnar read".into(),
1218        )),
1219    }
1220}
1221#[cfg(test)]
1222mod tests {
1223    use super::*;
1224    use crate::ast::expr::Literal;
1225    use crate::ast::span::Span;
1226    use crate::catalog::{ColumnMetadata, RowIdMode, TableMetadata};
1227    use crate::columnar::statistics::ColumnStatistics;
1228    use crate::planner::TypedCaseWhen;
1229    use crate::planner::typed_expr::TypedExpr;
1230    use crate::planner::typed_expr::TypedExprKind;
1231    use crate::planner::types::ResolvedType;
1232    use crate::storage::TxnBridge;
1233    use alopex_core::kv::memory::MemoryKV;
1234    use bincode::config::Options;
1235    use std::sync::Arc;
1236
1237    #[test]
1238    fn case_promotion_cast_keeps_column_in_projection() {
1239        let span = Span::default();
1240        let column = TypedExpr {
1241            kind: TypedExprKind::ColumnRef {
1242                table: "items".to_string(),
1243                column: "value".to_string(),
1244                column_index: 3,
1245            },
1246            resolved_type: ResolvedType::Integer,
1247            span,
1248        };
1249        let promoted_column = TypedExpr {
1250            kind: TypedExprKind::Cast {
1251                expr: Box::new(column),
1252                target_type: ResolvedType::Double,
1253            },
1254            resolved_type: ResolvedType::Double,
1255            span,
1256        };
1257        let case = TypedExpr {
1258            kind: TypedExprKind::Case {
1259                operand: None,
1260                branches: vec![TypedCaseWhen {
1261                    when: TypedExpr {
1262                        kind: TypedExprKind::Literal(Literal::Boolean(true)),
1263                        resolved_type: ResolvedType::Boolean,
1264                        span,
1265                    },
1266                    then: promoted_column,
1267                }],
1268                else_expr: None,
1269            },
1270            resolved_type: ResolvedType::Double,
1271            span,
1272        };
1273
1274        let mut columns = BTreeSet::new();
1275        collect_column_indices(&case, &mut columns);
1276        assert_eq!(columns, BTreeSet::from([3]));
1277    }
1278
1279    #[test]
1280    fn evaluate_pushdown_eq_prunes_out_of_range() {
1281        let stats = RowGroupStatistics {
1282            row_count: 3,
1283            columns: vec![ColumnStatistics {
1284                min: SqlValue::Integer(1),
1285                max: SqlValue::Integer(3),
1286                null_count: 0,
1287                total_count: 3,
1288                distinct_count: None,
1289            }],
1290            row_id_min: None,
1291            row_id_max: None,
1292        };
1293        let filter = PushdownFilter::Eq {
1294            column_idx: 0,
1295            value: SqlValue::Integer(10),
1296        };
1297        assert!(ColumnarScan::evaluate_pushdown(&filter, &stats));
1298    }
1299
1300    #[test]
1301    fn evaluate_pushdown_range_allows_overlap() {
1302        let stats = RowGroupStatistics {
1303            row_count: 3,
1304            columns: vec![ColumnStatistics {
1305                min: SqlValue::Integer(5),
1306                max: SqlValue::Integer(10),
1307                null_count: 0,
1308                total_count: 3,
1309                distinct_count: None,
1310            }],
1311            row_id_min: None,
1312            row_id_max: None,
1313        };
1314        let filter = PushdownFilter::Range {
1315            column_idx: 0,
1316            min: Some(SqlValue::Integer(8)),
1317            max: Some(SqlValue::Integer(12)),
1318        };
1319        assert!(!ColumnarScan::evaluate_pushdown(&filter, &stats));
1320    }
1321
1322    #[test]
1323    fn evaluate_pushdown_is_null_skips_when_no_nulls() {
1324        let stats = RowGroupStatistics {
1325            row_count: 2,
1326            columns: vec![ColumnStatistics {
1327                min: SqlValue::Integer(1),
1328                max: SqlValue::Integer(2),
1329                null_count: 0,
1330                total_count: 2,
1331                distinct_count: None,
1332            }],
1333            row_id_min: None,
1334            row_id_max: None,
1335        };
1336        let filter = PushdownFilter::IsNull {
1337            column_idx: 0,
1338            is_null: true,
1339        };
1340        assert!(ColumnarScan::evaluate_pushdown(&filter, &stats));
1341    }
1342
1343    #[test]
1344    fn evaluate_pushdown_is_not_null_skips_when_all_null() {
1345        let stats = RowGroupStatistics {
1346            row_count: 2,
1347            columns: vec![ColumnStatistics {
1348                min: SqlValue::Null,
1349                max: SqlValue::Null,
1350                null_count: 2,
1351                total_count: 2,
1352                distinct_count: None,
1353            }],
1354            row_id_min: None,
1355            row_id_max: None,
1356        };
1357        let filter = PushdownFilter::IsNull {
1358            column_idx: 0,
1359            is_null: false,
1360        };
1361        assert!(ColumnarScan::evaluate_pushdown(&filter, &stats));
1362    }
1363
1364    #[test]
1365    fn evaluate_pushdown_and_prunes_if_any_branch_skips() {
1366        let stats = RowGroupStatistics {
1367            row_count: 3,
1368            columns: vec![ColumnStatistics {
1369                min: SqlValue::Integer(1),
1370                max: SqlValue::Integer(3),
1371                null_count: 0,
1372                total_count: 3,
1373                distinct_count: None,
1374            }],
1375            row_id_min: None,
1376            row_id_max: None,
1377        };
1378        let filter = PushdownFilter::And(vec![
1379            PushdownFilter::Eq {
1380                column_idx: 0,
1381                value: SqlValue::Integer(10),
1382            },
1383            PushdownFilter::Eq {
1384                column_idx: 0,
1385                value: SqlValue::Integer(2),
1386            },
1387        ]);
1388        assert!(ColumnarScan::evaluate_pushdown(&filter, &stats));
1389    }
1390
1391    #[test]
1392    fn evaluate_pushdown_or_keeps_if_any_branch_may_match() {
1393        let stats = RowGroupStatistics {
1394            row_count: 3,
1395            columns: vec![ColumnStatistics {
1396                min: SqlValue::Integer(1),
1397                max: SqlValue::Integer(3),
1398                null_count: 0,
1399                total_count: 3,
1400                distinct_count: None,
1401            }],
1402            row_id_min: None,
1403            row_id_max: None,
1404        };
1405        let filter = PushdownFilter::Or(vec![
1406            PushdownFilter::Eq {
1407                column_idx: 0,
1408                value: SqlValue::Integer(10),
1409            },
1410            PushdownFilter::Eq {
1411                column_idx: 0,
1412                value: SqlValue::Integer(2),
1413            },
1414        ]);
1415        assert!(!ColumnarScan::evaluate_pushdown(&filter, &stats));
1416    }
1417
1418    #[test]
1419    fn expr_to_pushdown_converts_eq() {
1420        let expr = TypedExpr {
1421            kind: TypedExprKind::BinaryOp {
1422                left: Box::new(TypedExpr::column_ref(
1423                    "t".into(),
1424                    "c".into(),
1425                    0,
1426                    ResolvedType::Integer,
1427                    crate::Span::default(),
1428                )),
1429                op: BinaryOp::Eq,
1430                right: Box::new(TypedExpr::literal(
1431                    Literal::Number("1".into()),
1432                    ResolvedType::Integer,
1433                    crate::Span::default(),
1434                )),
1435            },
1436            resolved_type: ResolvedType::Boolean,
1437            span: crate::Span::default(),
1438        };
1439        let filter = expr_to_pushdown(&expr).unwrap();
1440        assert_eq!(
1441            filter,
1442            PushdownFilter::Eq {
1443                column_idx: 0,
1444                value: SqlValue::Integer(1)
1445            }
1446        );
1447    }
1448
1449    #[test]
1450    fn execute_columnar_scan_applies_residual_filter() {
1451        let bridge = TxnBridge::new(Arc::new(MemoryKV::new()));
1452        let mut table = TableMetadata::new(
1453            "users",
1454            vec![
1455                ColumnMetadata::new("id", ResolvedType::Integer),
1456                ColumnMetadata::new("name", ResolvedType::Text),
1457            ],
1458        )
1459        .with_table_id(1);
1460        table.storage_options.storage_type = crate::catalog::StorageType::Columnar;
1461
1462        // Columnar セグメントを直接書き込む。
1463        let schema = alopex_core::columnar::segment_v2::Schema {
1464            columns: vec![
1465                alopex_core::columnar::segment_v2::ColumnSchema {
1466                    name: "id".into(),
1467                    logical_type: alopex_core::columnar::encoding::LogicalType::Int64,
1468                    nullable: false,
1469                    fixed_len: None,
1470                },
1471                alopex_core::columnar::segment_v2::ColumnSchema {
1472                    name: "name".into(),
1473                    logical_type: alopex_core::columnar::encoding::LogicalType::Binary,
1474                    nullable: false,
1475                    fixed_len: None,
1476                },
1477            ],
1478        };
1479        let batch = alopex_core::columnar::segment_v2::RecordBatch::new(
1480            schema.clone(),
1481            vec![
1482                alopex_core::columnar::encoding::Column::Int64(vec![1]),
1483                alopex_core::columnar::encoding::Column::Binary(vec![b"alice".to_vec()]),
1484            ],
1485            vec![None, None],
1486        );
1487        let mut writer =
1488            alopex_core::columnar::segment_v2::SegmentWriterV2::new(Default::default());
1489        writer.write_batch(batch).unwrap();
1490        let segment = writer.finish().unwrap();
1491
1492        let stats = vec![crate::columnar::statistics::compute_row_group_statistics(
1493            &[vec![SqlValue::Integer(1), SqlValue::Text("alice".into())]],
1494        )];
1495
1496        let mut txn = bridge.begin_write().unwrap();
1497        let segment_bytes = alopex_core::storage::format::bincode_config()
1498            .serialize(&segment)
1499            .unwrap();
1500        let meta_bytes = alopex_core::storage::format::bincode_config()
1501            .serialize(&segment.meta)
1502            .unwrap();
1503        let stats_bytes = alopex_core::storage::format::bincode_config()
1504            .serialize(&stats)
1505            .unwrap();
1506        txn.inner_mut()
1507            .put(
1508                alopex_core::columnar::kvs_bridge::key_layout::column_segment_key(1, 0, 0),
1509                segment_bytes,
1510            )
1511            .unwrap();
1512        txn.inner_mut()
1513            .put(
1514                alopex_core::columnar::kvs_bridge::key_layout::statistics_key(1, 0),
1515                meta_bytes,
1516            )
1517            .unwrap();
1518        txn.inner_mut()
1519            .put(
1520                alopex_core::columnar::kvs_bridge::key_layout::row_group_stats_key(1, 0),
1521                stats_bytes,
1522            )
1523            .unwrap();
1524        let index_bytes = alopex_core::storage::format::bincode_config()
1525            .serialize(&vec![0u64])
1526            .unwrap();
1527        txn.inner_mut()
1528            .put(
1529                alopex_core::columnar::kvs_bridge::key_layout::segment_index_key(1),
1530                index_bytes,
1531            )
1532            .unwrap();
1533        txn.commit().unwrap();
1534
1535        let scan = ColumnarScan::new(
1536            table.table_id,
1537            vec![0, 1],
1538            Some(PushdownFilter::Eq {
1539                column_idx: 0,
1540                value: SqlValue::Integer(1),
1541            }),
1542            Some(TypedExpr {
1543                kind: TypedExprKind::BinaryOp {
1544                    left: Box::new(TypedExpr::column_ref(
1545                        "users".into(),
1546                        "id".into(),
1547                        0,
1548                        ResolvedType::Integer,
1549                        crate::Span::default(),
1550                    )),
1551                    op: BinaryOp::Eq,
1552                    right: Box::new(TypedExpr::literal(
1553                        Literal::Number("1".into()),
1554                        ResolvedType::Integer,
1555                        crate::Span::default(),
1556                    )),
1557                },
1558                resolved_type: ResolvedType::Boolean,
1559                span: crate::Span::default(),
1560            }),
1561        );
1562
1563        let mut read_txn = bridge.begin_read().unwrap();
1564        let rows = execute_columnar_scan(&mut read_txn, &table, &scan).unwrap();
1565        assert_eq!(rows.len(), 1);
1566        assert_eq!(rows[0].values[1], SqlValue::Text("alice".into()));
1567    }
1568
1569    #[test]
1570    fn rowid_mode_direct_prefers_rowid_column() {
1571        let bridge = TxnBridge::new(Arc::new(MemoryKV::new()));
1572        let mut table = TableMetadata::new(
1573            "items",
1574            vec![
1575                ColumnMetadata::new("row_id", ResolvedType::BigInt),
1576                ColumnMetadata::new("val", ResolvedType::Integer),
1577            ],
1578        )
1579        .with_table_id(20);
1580        table.storage_options.storage_type = crate::catalog::StorageType::Columnar;
1581        table.storage_options.row_id_mode = RowIdMode::Direct;
1582
1583        let schema = alopex_core::columnar::segment_v2::Schema {
1584            columns: vec![
1585                alopex_core::columnar::segment_v2::ColumnSchema {
1586                    name: "row_id".into(),
1587                    logical_type: alopex_core::columnar::encoding::LogicalType::Int64,
1588                    nullable: false,
1589                    fixed_len: None,
1590                },
1591                alopex_core::columnar::segment_v2::ColumnSchema {
1592                    name: "val".into(),
1593                    logical_type: alopex_core::columnar::encoding::LogicalType::Int64,
1594                    nullable: false,
1595                    fixed_len: None,
1596                },
1597            ],
1598        };
1599        let batch = alopex_core::columnar::segment_v2::RecordBatch::new(
1600            schema.clone(),
1601            vec![
1602                alopex_core::columnar::encoding::Column::Int64(vec![999]),
1603                alopex_core::columnar::encoding::Column::Int64(vec![7]),
1604            ],
1605            vec![None, None],
1606        );
1607        let mut writer =
1608            alopex_core::columnar::segment_v2::SegmentWriterV2::new(Default::default());
1609        writer.write_batch(batch).unwrap();
1610        let segment = writer.finish().unwrap();
1611        let stats = vec![crate::columnar::statistics::compute_row_group_statistics(
1612            &[vec![SqlValue::BigInt(999), SqlValue::Integer(7)]],
1613        )];
1614
1615        persist_segment_for_test(&bridge, table.table_id, &segment, &stats);
1616
1617        let scan = ColumnarScan::new(table.table_id, vec![0, 1], None, None);
1618        let mut read_txn = bridge.begin_read().unwrap();
1619        let rows = execute_columnar_scan(&mut read_txn, &table, &scan).unwrap();
1620        assert_eq!(rows.len(), 1);
1621        assert_eq!(rows[0].row_id, 999);
1622        assert_eq!(rows[0].values[1], SqlValue::Integer(7));
1623    }
1624
1625    #[test]
1626    fn rowid_mode_none_uses_position() {
1627        let bridge = TxnBridge::new(Arc::new(MemoryKV::new()));
1628        let mut table = TableMetadata::new(
1629            "items",
1630            vec![ColumnMetadata::new("val", ResolvedType::Integer)],
1631        )
1632        .with_table_id(21);
1633        table.storage_options.storage_type = crate::catalog::StorageType::Columnar;
1634        table.storage_options.row_id_mode = RowIdMode::Direct;
1635
1636        let schema = alopex_core::columnar::segment_v2::Schema {
1637            columns: vec![alopex_core::columnar::segment_v2::ColumnSchema {
1638                name: "val".into(),
1639                logical_type: alopex_core::columnar::encoding::LogicalType::Int64,
1640                nullable: false,
1641                fixed_len: None,
1642            }],
1643        };
1644        let batch = alopex_core::columnar::segment_v2::RecordBatch::new(
1645            schema.clone(),
1646            vec![alopex_core::columnar::encoding::Column::Int64(vec![3, 4])],
1647            vec![None],
1648        );
1649        let mut writer =
1650            alopex_core::columnar::segment_v2::SegmentWriterV2::new(Default::default());
1651        writer.write_batch(batch).unwrap();
1652        let segment = writer.finish().unwrap();
1653        let stats = vec![crate::columnar::statistics::compute_row_group_statistics(
1654            &[vec![SqlValue::Integer(3)], vec![SqlValue::Integer(4)]],
1655        )];
1656
1657        persist_segment_for_test(&bridge, table.table_id, &segment, &stats);
1658
1659        let scan = ColumnarScan::new(table.table_id, vec![0], None, None);
1660        let mut read_txn = bridge.begin_read().unwrap();
1661        let rows = execute_columnar_scan(&mut read_txn, &table, &scan).unwrap();
1662        assert_eq!(rows.len(), 2);
1663        assert_eq!(rows[0].row_id, 0);
1664        assert_eq!(rows[1].row_id, 1);
1665    }
1666
1667    fn persist_segment_for_test(
1668        bridge: &TxnBridge<MemoryKV>,
1669        table_id: u32,
1670        segment: &alopex_core::columnar::segment_v2::ColumnSegmentV2,
1671        row_group_stats: &[crate::columnar::statistics::RowGroupStatistics],
1672    ) {
1673        let mut txn = bridge.begin_write().unwrap();
1674        let segment_bytes = alopex_core::storage::format::bincode_config()
1675            .serialize(segment)
1676            .unwrap();
1677        let meta_bytes = alopex_core::storage::format::bincode_config()
1678            .serialize(&segment.meta)
1679            .unwrap();
1680        let stats_bytes = alopex_core::storage::format::bincode_config()
1681            .serialize(row_group_stats)
1682            .unwrap();
1683        txn.inner_mut()
1684            .put(
1685                alopex_core::columnar::kvs_bridge::key_layout::column_segment_key(table_id, 0, 0),
1686                segment_bytes,
1687            )
1688            .unwrap();
1689        txn.inner_mut()
1690            .put(
1691                alopex_core::columnar::kvs_bridge::key_layout::statistics_key(table_id, 0),
1692                meta_bytes,
1693            )
1694            .unwrap();
1695        txn.inner_mut()
1696            .put(
1697                alopex_core::columnar::kvs_bridge::key_layout::row_group_stats_key(table_id, 0),
1698                stats_bytes,
1699            )
1700            .unwrap();
1701        let index_bytes = alopex_core::storage::format::bincode_config()
1702            .serialize(&vec![0u64])
1703            .unwrap();
1704        txn.inner_mut()
1705            .put(
1706                alopex_core::columnar::kvs_bridge::key_layout::segment_index_key(table_id),
1707                index_bytes,
1708            )
1709            .unwrap();
1710        txn.commit().unwrap();
1711    }
1712}