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