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#[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#[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 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 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
157struct LoadedSegment {
163 reader: SegmentReaderV2,
165 row_group_stats: Option<Vec<RowGroupStatistics>>,
167 row_ids: Vec<u64>,
169 row_groups: Vec<alopex_core::columnar::segment_v2::RowGroupMeta>,
171}
172
173pub struct ColumnarScanIterator {
180 segments: Vec<LoadedSegment>,
182 segment_idx: usize,
184 row_group_idx: usize,
186 row_idx: usize,
188 current_batch: Option<RecordBatch>,
190 projected: Vec<usize>,
192 table_meta: TableMetadata,
194 schema: Vec<ColumnMetadata>,
196 scan: ColumnarScan,
198 row_id_col_idx: Option<usize>,
200 next_row_id: u64,
202}
203
204impl ColumnarScanIterator {
205 fn advance(&mut self) -> Option<Result<Row>> {
210 loop {
211 if self.current_batch.is_none() && !self.load_next_batch() {
213 return None; }
215
216 let row_count = match &self.current_batch {
218 Some(batch) => batch.num_rows(),
219 None => continue,
220 };
221
222 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 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 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 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 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 match segment
274 .reader
275 .read_row_group_by_index(&self.projected, self.row_group_idx)
276 {
277 Ok(mut batch) => {
278 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 self.row_group_idx += 1;
296 continue;
297 }
298 }
299 }
300
301 self.segment_idx += 1;
303 self.row_group_idx = 0;
304 }
305
306 false
307 }
308
309 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 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 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
400pub 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 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
470pub 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
552pub 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 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
703pub 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
830pub 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
853pub 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
877pub 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
903pub 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
909fn 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 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 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}