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        (
1152            ResolvedType::BigInt
1153            | ResolvedType::Timestamp
1154            | ResolvedType::Date
1155            | ResolvedType::Time,
1156            Column::Int64(values),
1157        ) => {
1158            let v = *values
1159                .get(row_idx)
1160                .ok_or_else(|| ExecutorError::Columnar("row index out of bounds".into()))?;
1161            match ty {
1162                ResolvedType::Timestamp => Ok(SqlValue::Timestamp(v)),
1163                ResolvedType::Date => i32::try_from(v)
1164                    .map(SqlValue::Date)
1165                    .map_err(|_| ExecutorError::Columnar("date is out of range".into())),
1166                ResolvedType::Time => Ok(SqlValue::Time(v)),
1167                _ => Ok(SqlValue::BigInt(v)),
1168            }
1169        }
1170        (ResolvedType::Float, Column::Float32(values)) => {
1171            let v = *values
1172                .get(row_idx)
1173                .ok_or_else(|| ExecutorError::Columnar("row index out of bounds".into()))?;
1174            Ok(SqlValue::Float(v))
1175        }
1176        (ResolvedType::Double, Column::Float64(values)) => {
1177            let v = *values
1178                .get(row_idx)
1179                .ok_or_else(|| ExecutorError::Columnar("row index out of bounds".into()))?;
1180            Ok(SqlValue::Double(v))
1181        }
1182        (ResolvedType::Boolean, Column::Bool(values)) => {
1183            let v = *values
1184                .get(row_idx)
1185                .ok_or_else(|| ExecutorError::Columnar("row index out of bounds".into()))?;
1186            Ok(SqlValue::Boolean(v))
1187        }
1188        (ResolvedType::Text, Column::Binary(values)) => {
1189            let raw = values
1190                .get(row_idx)
1191                .ok_or_else(|| ExecutorError::Columnar("row index out of bounds".into()))?;
1192            String::from_utf8(raw.clone())
1193                .map(SqlValue::Text)
1194                .map_err(|e| ExecutorError::Columnar(e.to_string()))
1195        }
1196        (ResolvedType::Json, Column::Binary(values)) => {
1197            let raw = values
1198                .get(row_idx)
1199                .ok_or_else(|| ExecutorError::Columnar("row index out of bounds".into()))?;
1200            let text = std::str::from_utf8(raw)
1201                .map_err(|error| ExecutorError::Columnar(error.to_string()))?;
1202            crate::storage::JsonValue::parse(text)
1203                .map(SqlValue::Json)
1204                .map_err(|error| ExecutorError::Columnar(error.to_string()))
1205        }
1206        (ResolvedType::Blob, Column::Binary(values)) => {
1207            let raw = values
1208                .get(row_idx)
1209                .ok_or_else(|| ExecutorError::Columnar("row index out of bounds".into()))?;
1210            Ok(SqlValue::Blob(raw.clone()))
1211        }
1212        (
1213            expected
1214            @ (ResolvedType::Array(_) | ResolvedType::Map { .. } | ResolvedType::Struct(_)),
1215            Column::Binary(values),
1216        ) => {
1217            let raw = values
1218                .get(row_idx)
1219                .ok_or_else(|| ExecutorError::Columnar("row index out of bounds".into()))?;
1220            let mut decoded =
1221                crate::storage::RowCodec::decode_with_schema(raw, std::slice::from_ref(expected))
1222                    .map_err(|error| ExecutorError::Columnar(error.to_string()))?;
1223            Ok(decoded.remove(0))
1224        }
1225        (ResolvedType::Vector { .. }, Column::Fixed { values, .. }) => {
1226            let raw = values
1227                .get(row_idx)
1228                .ok_or_else(|| ExecutorError::Columnar("row index out of bounds".into()))?;
1229            if raw.len() % 4 != 0 {
1230                return Err(ExecutorError::Columnar(
1231                    "invalid vector byte length in columnar segment".into(),
1232                ));
1233            }
1234            let floats: Vec<f32> = raw
1235                .as_chunks::<4>()
1236                .0
1237                .iter()
1238                .map(|bytes| f32::from_le_bytes(*bytes))
1239                .collect();
1240            Ok(SqlValue::Vector(floats))
1241        }
1242        (ResolvedType::Interval, Column::Fixed { values, .. }) => {
1243            let raw = values
1244                .get(row_idx)
1245                .ok_or_else(|| ExecutorError::Columnar("row index out of bounds".into()))?;
1246            if raw.len() != 16 {
1247                return Err(ExecutorError::Columnar(
1248                    "invalid interval byte length".into(),
1249                ));
1250            }
1251            Ok(SqlValue::Interval {
1252                months: i32::from_le_bytes(raw[0..4].try_into().unwrap()),
1253                days: i32::from_le_bytes(raw[4..8].try_into().unwrap()),
1254                micros: i64::from_le_bytes(raw[8..16].try_into().unwrap()),
1255            })
1256        }
1257        (ResolvedType::Decimal { scale, .. }, Column::Fixed { values, .. }) => {
1258            let raw = values
1259                .get(row_idx)
1260                .ok_or_else(|| ExecutorError::Columnar("row index out of bounds".into()))?;
1261            let coefficient = i128::from_le_bytes(
1262                raw.as_slice()
1263                    .try_into()
1264                    .map_err(|_| ExecutorError::Columnar("invalid decimal byte length".into()))?,
1265            );
1266            Ok(SqlValue::Decimal(crate::storage::DecimalValue::new(
1267                coefficient,
1268                *scale,
1269            )))
1270        }
1271        (_, Column::Binary(values)) => {
1272            let raw = values
1273                .get(row_idx)
1274                .ok_or_else(|| ExecutorError::Columnar("row index out of bounds".into()))?;
1275            Ok(SqlValue::Blob(raw.clone()))
1276        }
1277        _ => Err(ExecutorError::Columnar(
1278            "unsupported column type for columnar read".into(),
1279        )),
1280    }
1281}
1282#[cfg(test)]
1283mod tests {
1284    use super::*;
1285    use crate::ast::expr::Literal;
1286    use crate::ast::span::Span;
1287    use crate::catalog::{ColumnMetadata, RowIdMode, TableMetadata};
1288    use crate::columnar::statistics::ColumnStatistics;
1289    use crate::planner::TypedCaseWhen;
1290    use crate::planner::typed_expr::TypedExpr;
1291    use crate::planner::typed_expr::TypedExprKind;
1292    use crate::planner::types::ResolvedType;
1293    use crate::storage::TxnBridge;
1294    use alopex_core::kv::memory::MemoryKV;
1295    use bincode::config::Options;
1296    use std::sync::Arc;
1297
1298    #[test]
1299    fn case_promotion_cast_keeps_column_in_projection() {
1300        let span = Span::default();
1301        let column = TypedExpr {
1302            kind: TypedExprKind::ColumnRef {
1303                table: "items".to_string(),
1304                column: "value".to_string(),
1305                column_index: 3,
1306            },
1307            resolved_type: ResolvedType::Integer,
1308            span,
1309        };
1310        let promoted_column = TypedExpr {
1311            kind: TypedExprKind::Cast {
1312                expr: Box::new(column),
1313                target_type: ResolvedType::Double,
1314            },
1315            resolved_type: ResolvedType::Double,
1316            span,
1317        };
1318        let case = TypedExpr {
1319            kind: TypedExprKind::Case {
1320                operand: None,
1321                branches: vec![TypedCaseWhen {
1322                    when: TypedExpr {
1323                        kind: TypedExprKind::Literal(Literal::Boolean(true)),
1324                        resolved_type: ResolvedType::Boolean,
1325                        span,
1326                    },
1327                    then: promoted_column,
1328                }],
1329                else_expr: None,
1330            },
1331            resolved_type: ResolvedType::Double,
1332            span,
1333        };
1334
1335        let mut columns = BTreeSet::new();
1336        collect_column_indices(&case, &mut columns);
1337        assert_eq!(columns, BTreeSet::from([3]));
1338    }
1339
1340    #[test]
1341    fn evaluate_pushdown_eq_prunes_out_of_range() {
1342        let stats = RowGroupStatistics {
1343            row_count: 3,
1344            columns: vec![ColumnStatistics {
1345                min: SqlValue::Integer(1),
1346                max: SqlValue::Integer(3),
1347                null_count: 0,
1348                total_count: 3,
1349                distinct_count: None,
1350            }],
1351            row_id_min: None,
1352            row_id_max: None,
1353        };
1354        let filter = PushdownFilter::Eq {
1355            column_idx: 0,
1356            value: SqlValue::Integer(10),
1357        };
1358        assert!(ColumnarScan::evaluate_pushdown(&filter, &stats));
1359    }
1360
1361    #[test]
1362    fn evaluate_pushdown_range_allows_overlap() {
1363        let stats = RowGroupStatistics {
1364            row_count: 3,
1365            columns: vec![ColumnStatistics {
1366                min: SqlValue::Integer(5),
1367                max: SqlValue::Integer(10),
1368                null_count: 0,
1369                total_count: 3,
1370                distinct_count: None,
1371            }],
1372            row_id_min: None,
1373            row_id_max: None,
1374        };
1375        let filter = PushdownFilter::Range {
1376            column_idx: 0,
1377            min: Some(SqlValue::Integer(8)),
1378            max: Some(SqlValue::Integer(12)),
1379        };
1380        assert!(!ColumnarScan::evaluate_pushdown(&filter, &stats));
1381    }
1382
1383    #[test]
1384    fn evaluate_pushdown_is_null_skips_when_no_nulls() {
1385        let stats = RowGroupStatistics {
1386            row_count: 2,
1387            columns: vec![ColumnStatistics {
1388                min: SqlValue::Integer(1),
1389                max: SqlValue::Integer(2),
1390                null_count: 0,
1391                total_count: 2,
1392                distinct_count: None,
1393            }],
1394            row_id_min: None,
1395            row_id_max: None,
1396        };
1397        let filter = PushdownFilter::IsNull {
1398            column_idx: 0,
1399            is_null: true,
1400        };
1401        assert!(ColumnarScan::evaluate_pushdown(&filter, &stats));
1402    }
1403
1404    #[test]
1405    fn evaluate_pushdown_is_not_null_skips_when_all_null() {
1406        let stats = RowGroupStatistics {
1407            row_count: 2,
1408            columns: vec![ColumnStatistics {
1409                min: SqlValue::Null,
1410                max: SqlValue::Null,
1411                null_count: 2,
1412                total_count: 2,
1413                distinct_count: None,
1414            }],
1415            row_id_min: None,
1416            row_id_max: None,
1417        };
1418        let filter = PushdownFilter::IsNull {
1419            column_idx: 0,
1420            is_null: false,
1421        };
1422        assert!(ColumnarScan::evaluate_pushdown(&filter, &stats));
1423    }
1424
1425    #[test]
1426    fn evaluate_pushdown_and_prunes_if_any_branch_skips() {
1427        let stats = RowGroupStatistics {
1428            row_count: 3,
1429            columns: vec![ColumnStatistics {
1430                min: SqlValue::Integer(1),
1431                max: SqlValue::Integer(3),
1432                null_count: 0,
1433                total_count: 3,
1434                distinct_count: None,
1435            }],
1436            row_id_min: None,
1437            row_id_max: None,
1438        };
1439        let filter = PushdownFilter::And(vec![
1440            PushdownFilter::Eq {
1441                column_idx: 0,
1442                value: SqlValue::Integer(10),
1443            },
1444            PushdownFilter::Eq {
1445                column_idx: 0,
1446                value: SqlValue::Integer(2),
1447            },
1448        ]);
1449        assert!(ColumnarScan::evaluate_pushdown(&filter, &stats));
1450    }
1451
1452    #[test]
1453    fn evaluate_pushdown_or_keeps_if_any_branch_may_match() {
1454        let stats = RowGroupStatistics {
1455            row_count: 3,
1456            columns: vec![ColumnStatistics {
1457                min: SqlValue::Integer(1),
1458                max: SqlValue::Integer(3),
1459                null_count: 0,
1460                total_count: 3,
1461                distinct_count: None,
1462            }],
1463            row_id_min: None,
1464            row_id_max: None,
1465        };
1466        let filter = PushdownFilter::Or(vec![
1467            PushdownFilter::Eq {
1468                column_idx: 0,
1469                value: SqlValue::Integer(10),
1470            },
1471            PushdownFilter::Eq {
1472                column_idx: 0,
1473                value: SqlValue::Integer(2),
1474            },
1475        ]);
1476        assert!(!ColumnarScan::evaluate_pushdown(&filter, &stats));
1477    }
1478
1479    #[test]
1480    fn expr_to_pushdown_converts_eq() {
1481        let expr = TypedExpr {
1482            kind: TypedExprKind::BinaryOp {
1483                left: Box::new(TypedExpr::column_ref(
1484                    "t".into(),
1485                    "c".into(),
1486                    0,
1487                    ResolvedType::Integer,
1488                    crate::Span::default(),
1489                )),
1490                op: BinaryOp::Eq,
1491                right: Box::new(TypedExpr::literal(
1492                    Literal::Number("1".into()),
1493                    ResolvedType::Integer,
1494                    crate::Span::default(),
1495                )),
1496            },
1497            resolved_type: ResolvedType::Boolean,
1498            span: crate::Span::default(),
1499        };
1500        let filter = expr_to_pushdown(&expr).unwrap();
1501        assert_eq!(
1502            filter,
1503            PushdownFilter::Eq {
1504                column_idx: 0,
1505                value: SqlValue::Integer(1)
1506            }
1507        );
1508    }
1509
1510    #[test]
1511    fn execute_columnar_scan_applies_residual_filter() {
1512        let bridge = TxnBridge::new(Arc::new(MemoryKV::new()));
1513        let mut table = TableMetadata::new(
1514            "users",
1515            vec![
1516                ColumnMetadata::new("id", ResolvedType::Integer),
1517                ColumnMetadata::new("name", ResolvedType::Text),
1518            ],
1519        )
1520        .with_table_id(1);
1521        table.storage_options.storage_type = crate::catalog::StorageType::Columnar;
1522
1523        // Columnar セグメントを直接書き込む。
1524        let schema = alopex_core::columnar::segment_v2::Schema {
1525            columns: vec![
1526                alopex_core::columnar::segment_v2::ColumnSchema {
1527                    name: "id".into(),
1528                    logical_type: alopex_core::columnar::encoding::LogicalType::Int64,
1529                    nullable: false,
1530                    fixed_len: None,
1531                },
1532                alopex_core::columnar::segment_v2::ColumnSchema {
1533                    name: "name".into(),
1534                    logical_type: alopex_core::columnar::encoding::LogicalType::Binary,
1535                    nullable: false,
1536                    fixed_len: None,
1537                },
1538            ],
1539        };
1540        let batch = alopex_core::columnar::segment_v2::RecordBatch::new(
1541            schema.clone(),
1542            vec![
1543                alopex_core::columnar::encoding::Column::Int64(vec![1]),
1544                alopex_core::columnar::encoding::Column::Binary(vec![b"alice".to_vec()]),
1545            ],
1546            vec![None, None],
1547        );
1548        let mut writer =
1549            alopex_core::columnar::segment_v2::SegmentWriterV2::new(Default::default());
1550        writer.write_batch(batch).unwrap();
1551        let segment = writer.finish().unwrap();
1552
1553        let stats = vec![crate::columnar::statistics::compute_row_group_statistics(
1554            &[vec![SqlValue::Integer(1), SqlValue::Text("alice".into())]],
1555        )];
1556
1557        let mut txn = bridge.begin_write().unwrap();
1558        let segment_bytes = alopex_core::storage::format::bincode_config()
1559            .serialize(&segment)
1560            .unwrap();
1561        let meta_bytes = alopex_core::storage::format::bincode_config()
1562            .serialize(&segment.meta)
1563            .unwrap();
1564        let stats_bytes = alopex_core::storage::format::bincode_config()
1565            .serialize(&stats)
1566            .unwrap();
1567        txn.inner_mut()
1568            .put(
1569                alopex_core::columnar::kvs_bridge::key_layout::column_segment_key(1, 0, 0),
1570                segment_bytes,
1571            )
1572            .unwrap();
1573        txn.inner_mut()
1574            .put(
1575                alopex_core::columnar::kvs_bridge::key_layout::statistics_key(1, 0),
1576                meta_bytes,
1577            )
1578            .unwrap();
1579        txn.inner_mut()
1580            .put(
1581                alopex_core::columnar::kvs_bridge::key_layout::row_group_stats_key(1, 0),
1582                stats_bytes,
1583            )
1584            .unwrap();
1585        let index_bytes = alopex_core::storage::format::bincode_config()
1586            .serialize(&vec![0u64])
1587            .unwrap();
1588        txn.inner_mut()
1589            .put(
1590                alopex_core::columnar::kvs_bridge::key_layout::segment_index_key(1),
1591                index_bytes,
1592            )
1593            .unwrap();
1594        txn.commit().unwrap();
1595
1596        let scan = ColumnarScan::new(
1597            table.table_id,
1598            vec![0, 1],
1599            Some(PushdownFilter::Eq {
1600                column_idx: 0,
1601                value: SqlValue::Integer(1),
1602            }),
1603            Some(TypedExpr {
1604                kind: TypedExprKind::BinaryOp {
1605                    left: Box::new(TypedExpr::column_ref(
1606                        "users".into(),
1607                        "id".into(),
1608                        0,
1609                        ResolvedType::Integer,
1610                        crate::Span::default(),
1611                    )),
1612                    op: BinaryOp::Eq,
1613                    right: Box::new(TypedExpr::literal(
1614                        Literal::Number("1".into()),
1615                        ResolvedType::Integer,
1616                        crate::Span::default(),
1617                    )),
1618                },
1619                resolved_type: ResolvedType::Boolean,
1620                span: crate::Span::default(),
1621            }),
1622        );
1623
1624        let mut read_txn = bridge.begin_read().unwrap();
1625        let rows = execute_columnar_scan(&mut read_txn, &table, &scan).unwrap();
1626        assert_eq!(rows.len(), 1);
1627        assert_eq!(rows[0].values[1], SqlValue::Text("alice".into()));
1628    }
1629
1630    #[test]
1631    fn rowid_mode_direct_prefers_rowid_column() {
1632        let bridge = TxnBridge::new(Arc::new(MemoryKV::new()));
1633        let mut table = TableMetadata::new(
1634            "items",
1635            vec![
1636                ColumnMetadata::new("row_id", ResolvedType::BigInt),
1637                ColumnMetadata::new("val", ResolvedType::Integer),
1638            ],
1639        )
1640        .with_table_id(20);
1641        table.storage_options.storage_type = crate::catalog::StorageType::Columnar;
1642        table.storage_options.row_id_mode = RowIdMode::Direct;
1643
1644        let schema = alopex_core::columnar::segment_v2::Schema {
1645            columns: vec![
1646                alopex_core::columnar::segment_v2::ColumnSchema {
1647                    name: "row_id".into(),
1648                    logical_type: alopex_core::columnar::encoding::LogicalType::Int64,
1649                    nullable: false,
1650                    fixed_len: None,
1651                },
1652                alopex_core::columnar::segment_v2::ColumnSchema {
1653                    name: "val".into(),
1654                    logical_type: alopex_core::columnar::encoding::LogicalType::Int64,
1655                    nullable: false,
1656                    fixed_len: None,
1657                },
1658            ],
1659        };
1660        let batch = alopex_core::columnar::segment_v2::RecordBatch::new(
1661            schema.clone(),
1662            vec![
1663                alopex_core::columnar::encoding::Column::Int64(vec![999]),
1664                alopex_core::columnar::encoding::Column::Int64(vec![7]),
1665            ],
1666            vec![None, None],
1667        );
1668        let mut writer =
1669            alopex_core::columnar::segment_v2::SegmentWriterV2::new(Default::default());
1670        writer.write_batch(batch).unwrap();
1671        let segment = writer.finish().unwrap();
1672        let stats = vec![crate::columnar::statistics::compute_row_group_statistics(
1673            &[vec![SqlValue::BigInt(999), SqlValue::Integer(7)]],
1674        )];
1675
1676        persist_segment_for_test(&bridge, table.table_id, &segment, &stats);
1677
1678        let scan = ColumnarScan::new(table.table_id, vec![0, 1], None, None);
1679        let mut read_txn = bridge.begin_read().unwrap();
1680        let rows = execute_columnar_scan(&mut read_txn, &table, &scan).unwrap();
1681        assert_eq!(rows.len(), 1);
1682        assert_eq!(rows[0].row_id, 999);
1683        assert_eq!(rows[0].values[1], SqlValue::Integer(7));
1684    }
1685
1686    #[test]
1687    fn rowid_mode_none_uses_position() {
1688        let bridge = TxnBridge::new(Arc::new(MemoryKV::new()));
1689        let mut table = TableMetadata::new(
1690            "items",
1691            vec![ColumnMetadata::new("val", ResolvedType::Integer)],
1692        )
1693        .with_table_id(21);
1694        table.storage_options.storage_type = crate::catalog::StorageType::Columnar;
1695        table.storage_options.row_id_mode = RowIdMode::Direct;
1696
1697        let schema = alopex_core::columnar::segment_v2::Schema {
1698            columns: vec![alopex_core::columnar::segment_v2::ColumnSchema {
1699                name: "val".into(),
1700                logical_type: alopex_core::columnar::encoding::LogicalType::Int64,
1701                nullable: false,
1702                fixed_len: None,
1703            }],
1704        };
1705        let batch = alopex_core::columnar::segment_v2::RecordBatch::new(
1706            schema.clone(),
1707            vec![alopex_core::columnar::encoding::Column::Int64(vec![3, 4])],
1708            vec![None],
1709        );
1710        let mut writer =
1711            alopex_core::columnar::segment_v2::SegmentWriterV2::new(Default::default());
1712        writer.write_batch(batch).unwrap();
1713        let segment = writer.finish().unwrap();
1714        let stats = vec![crate::columnar::statistics::compute_row_group_statistics(
1715            &[vec![SqlValue::Integer(3)], vec![SqlValue::Integer(4)]],
1716        )];
1717
1718        persist_segment_for_test(&bridge, table.table_id, &segment, &stats);
1719
1720        let scan = ColumnarScan::new(table.table_id, vec![0], None, None);
1721        let mut read_txn = bridge.begin_read().unwrap();
1722        let rows = execute_columnar_scan(&mut read_txn, &table, &scan).unwrap();
1723        assert_eq!(rows.len(), 2);
1724        assert_eq!(rows[0].row_id, 0);
1725        assert_eq!(rows[1].row_id, 1);
1726    }
1727
1728    fn persist_segment_for_test(
1729        bridge: &TxnBridge<MemoryKV>,
1730        table_id: u32,
1731        segment: &alopex_core::columnar::segment_v2::ColumnSegmentV2,
1732        row_group_stats: &[crate::columnar::statistics::RowGroupStatistics],
1733    ) {
1734        let mut txn = bridge.begin_write().unwrap();
1735        let segment_bytes = alopex_core::storage::format::bincode_config()
1736            .serialize(segment)
1737            .unwrap();
1738        let meta_bytes = alopex_core::storage::format::bincode_config()
1739            .serialize(&segment.meta)
1740            .unwrap();
1741        let stats_bytes = alopex_core::storage::format::bincode_config()
1742            .serialize(row_group_stats)
1743            .unwrap();
1744        txn.inner_mut()
1745            .put(
1746                alopex_core::columnar::kvs_bridge::key_layout::column_segment_key(table_id, 0, 0),
1747                segment_bytes,
1748            )
1749            .unwrap();
1750        txn.inner_mut()
1751            .put(
1752                alopex_core::columnar::kvs_bridge::key_layout::statistics_key(table_id, 0),
1753                meta_bytes,
1754            )
1755            .unwrap();
1756        txn.inner_mut()
1757            .put(
1758                alopex_core::columnar::kvs_bridge::key_layout::row_group_stats_key(table_id, 0),
1759                stats_bytes,
1760            )
1761            .unwrap();
1762        let index_bytes = alopex_core::storage::format::bincode_config()
1763            .serialize(&vec![0u64])
1764            .unwrap();
1765        txn.inner_mut()
1766            .put(
1767                alopex_core::columnar::kvs_bridge::key_layout::segment_index_key(table_id),
1768                index_bytes,
1769            )
1770            .unwrap();
1771        txn.commit().unwrap();
1772    }
1773}