1use std::collections::hash_map::DefaultHasher;
4use std::hash::{Hash, Hasher};
5use std::path::{Path, PathBuf};
6use std::sync::Arc;
7
8use alopex_core::columnar::encoding::{Column, LogicalType};
9use alopex_core::columnar::encoding_v2::Bitmap;
10use alopex_core::columnar::kvs_bridge::{ColumnarKvsBridge, StreamingSegmentLayoutPreflightV08};
11use alopex_core::columnar::segment_v08::ChunkedSegmentAccessV08;
12use alopex_core::columnar::segment_v2::{
13 RecordBatch as CoreRecordBatch, Schema as CoreSchema, SegmentWriterV2,
14};
15use alopex_core::storage::format::AlopexFileWriter;
16use alopex_core::{StorageFactory, StorageMode as CoreStorageMode};
17use alopex_dataframe::io::ColumnarSegmentBatchSourceFactory;
18use alopex_dataframe::physical::{
19 ColumnarSegmentBatchPlan, ColumnarSegmentCursor, ColumnarSegmentProvider, DataFrameStream,
20 PlanSubject, SourceLimit,
21};
22use alopex_dataframe::{DataFrameError, Result as DataFrameResult};
23use arrow::array::{
24 ArrayRef, BinaryArray, BooleanArray, FixedSizeBinaryBuilder, Float32Array, Float64Array,
25 Int64Array,
26};
27use arrow::datatypes::{DataType, Field, Schema as ArrowSchema, SchemaRef};
28use arrow::record_batch::RecordBatch as ArrowRecordBatch;
29
30use crate::{Database, Error, Result, SegmentConfigV2, Transaction, TxnMode};
31
32#[cfg(test)]
33thread_local! {
34 static LAST_READ_COLUMN_INDICES: std::cell::RefCell<Option<Vec<usize>>> =
35 const { std::cell::RefCell::new(None) };
36}
37
38#[cfg(test)]
39fn record_read_column_indices(indices: &[usize]) {
40 LAST_READ_COLUMN_INDICES.with(|last| {
41 *last.borrow_mut() = Some(indices.to_vec());
42 });
43}
44
45#[derive(Debug, Clone)]
47pub struct ColumnarSegmentStats {
48 pub row_count: usize,
50 pub column_count: usize,
52 pub size_bytes: usize,
54}
55
56#[derive(Debug, Clone, Copy, PartialEq, Eq)]
58pub enum ColumnarIndexType {
59 Minmax,
61 Bloom,
63}
64
65impl ColumnarIndexType {
66 pub fn as_str(&self) -> &'static str {
68 match self {
69 Self::Minmax => "minmax",
70 Self::Bloom => "bloom",
71 }
72 }
73}
74
75#[derive(Debug, Clone)]
77pub struct ColumnarIndexInfo {
78 pub column: String,
80 pub index_type: ColumnarIndexType,
82}
83
84#[derive(Debug, Clone)]
86pub struct EmbeddedConfig {
87 pub path: Option<PathBuf>,
89 pub storage_mode: StorageMode,
91 pub memory_limit: Option<usize>,
93 pub segment_config: SegmentConfigV2,
95}
96
97impl EmbeddedConfig {
98 pub fn disk(path: PathBuf) -> Self {
100 Self {
101 path: Some(path),
102 storage_mode: StorageMode::Disk,
103 memory_limit: None,
104 segment_config: SegmentConfigV2::default(),
105 }
106 }
107
108 pub fn in_memory() -> Self {
110 Self {
111 path: None,
112 storage_mode: StorageMode::InMemory,
113 memory_limit: None,
114 segment_config: SegmentConfigV2::default(),
115 }
116 }
117
118 pub fn in_memory_with_limit(limit: usize) -> Self {
120 Self {
121 path: None,
122 storage_mode: StorageMode::InMemory,
123 memory_limit: Some(limit),
124 segment_config: SegmentConfigV2::default(),
125 }
126 }
127
128 pub fn with_segment_config(mut self, cfg: SegmentConfigV2) -> Self {
130 self.segment_config = cfg;
131 self
132 }
133}
134
135#[derive(Debug, Clone, Copy, PartialEq, Eq)]
137pub enum StorageMode {
138 Disk,
140 InMemory,
142}
143
144impl Database {
145 pub fn open_with_config(config: EmbeddedConfig) -> Result<Self> {
147 let store = match config.storage_mode {
148 StorageMode::Disk => {
149 let path = config.path.clone().ok_or_else(|| {
150 Error::Core(alopex_core::Error::InvalidFormat(
151 "disk mode requires a path".into(),
152 ))
153 })?;
154 let path = crate::disk_data_dir_path(&path);
155 StorageFactory::create(CoreStorageMode::Disk { path, config: None })
156 .map_err(Error::Core)?
157 }
158 StorageMode::InMemory => StorageFactory::create(CoreStorageMode::Memory {
159 max_size: config.memory_limit,
160 })
161 .map_err(Error::Core)?,
162 };
163
164 Ok(Self::init(
165 store,
166 config.storage_mode,
167 config.memory_limit,
168 config.segment_config,
169 ))
170 }
171
172 pub fn storage_mode(&self) -> StorageMode {
174 self.columnar_mode
175 }
176
177 pub fn write_columnar_segment(&self, table: &str, batch: CoreRecordBatch) -> Result<u64> {
179 let mut writer = SegmentWriterV2::new(self.segment_config.clone());
180 writer
181 .write_batch(batch)
182 .map_err(|e| Error::Core(e.into()))?;
183 let segment = writer.finish().map_err(|e| Error::Core(e.into()))?;
184 let table_id = table_id(table)?;
185
186 match self.columnar_mode {
187 StorageMode::Disk => self
188 .columnar_bridge
189 .write_segment(table_id, &segment)
190 .map_err(|e| Error::Core(e.into())),
191 StorageMode::InMemory => {
192 let store = self.columnar_memory.as_ref().ok_or_else(|| {
193 Error::Core(alopex_core::Error::InvalidFormat(
194 "in-memory columnar store is not initialized".into(),
195 ))
196 })?;
197 store
198 .write_segment(table_id, segment)
199 .map_err(|e| Error::Core(e.into()))
200 }
201 }
202 }
203
204 pub fn write_columnar_segment_with_config(
206 &self,
207 table: &str,
208 batch: CoreRecordBatch,
209 config: SegmentConfigV2,
210 ) -> Result<u64> {
211 let mut writer = SegmentWriterV2::new(config);
212 writer
213 .write_batch(batch)
214 .map_err(|e| Error::Core(e.into()))?;
215 let segment = writer.finish().map_err(|e| Error::Core(e.into()))?;
216 let table_id = table_id(table)?;
217
218 match self.columnar_mode {
219 StorageMode::Disk => self
220 .columnar_bridge
221 .write_segment(table_id, &segment)
222 .map_err(|e| Error::Core(e.into())),
223 StorageMode::InMemory => {
224 let store = self.columnar_memory.as_ref().ok_or_else(|| {
225 Error::Core(alopex_core::Error::InvalidFormat(
226 "in-memory columnar store is not initialized".into(),
227 ))
228 })?;
229 store
230 .write_segment(table_id, segment)
231 .map_err(|e| Error::Core(e.into()))
232 }
233 }
234 }
235
236 pub fn read_columnar_segment(
238 &self,
239 table: &str,
240 segment_id: u64,
241 columns: Option<&[&str]>,
242 ) -> Result<Vec<CoreRecordBatch>> {
243 let table_id = table_id(table)?;
244 match self.columnar_mode {
245 StorageMode::Disk => {
246 let read_indices: Vec<usize> = if let Some(names) = columns {
247 let segment = self
248 .columnar_bridge
249 .read_segment_raw(table_id, segment_id)
250 .map_err(|e| Error::Core(e.into()))?;
251 resolve_indices_from_schema(&segment.meta.schema, names)?
252 } else {
253 let column_count = self
254 .columnar_bridge
255 .column_count(table_id, segment_id)
256 .map_err(|e| Error::Core(e.into()))?;
257 (0..column_count).collect()
258 };
259
260 #[cfg(test)]
261 record_read_column_indices(&read_indices);
262
263 self.columnar_bridge
264 .read_segment(table_id, segment_id, &read_indices)
265 .map_err(|e| Error::Core(e.into()))
266 }
267 StorageMode::InMemory => {
268 let store = self.columnar_memory.as_ref().ok_or_else(|| {
269 Error::Core(alopex_core::Error::InvalidFormat(
270 "in-memory columnar store is not initialized".into(),
271 ))
272 })?;
273 let read_indices: Vec<usize> = if let Some(names) = columns {
274 let schema = store
275 .schema(table_id, segment_id)
276 .map_err(|e| Error::Core(e.into()))?;
277 resolve_indices_from_schema(&schema, names)?
278 } else {
279 let column_count = store
280 .column_count(table_id, segment_id)
281 .map_err(|e| Error::Core(e.into()))?;
282 (0..column_count).collect()
283 };
284
285 #[cfg(test)]
286 record_read_column_indices(&read_indices);
287
288 store
289 .read_segment(table_id, segment_id, &read_indices)
290 .map_err(|e| Error::Core(e.into()))
291 }
292 }
293 }
294
295 pub fn in_memory_usage(&self) -> Option<u64> {
297 if self.columnar_mode == StorageMode::InMemory {
298 self.columnar_memory.as_ref().map(|m| m.memory_usage())
299 } else {
300 None
301 }
302 }
303
304 pub fn open_in_memory_with_limit(limit: usize) -> Result<Self> {
306 Self::open_with_config(EmbeddedConfig::in_memory_with_limit(limit))
307 }
308
309 pub fn resolve_table_id(&self, table: &str) -> Result<u32> {
311 table_id(table)
312 }
313
314 pub fn columnar_segment_streaming_factory_v08(
320 &self,
321 table: &str,
322 segment_id: u64,
323 ) -> Result<ColumnarSegmentBatchSourceFactory> {
324 let table_id = table_id(table)?;
325 self.columnar_segment_streaming_factory_v08_by_ids(table_id, segment_id)
326 }
327
328 pub fn columnar_segment_streaming_factory_v08_by_id(
334 &self,
335 segment_id: &str,
336 ) -> Result<ColumnarSegmentBatchSourceFactory> {
337 let (table_id, segment_id) = parse_segment_id(segment_id)?;
338 self.columnar_segment_streaming_factory_v08_by_ids(table_id, segment_id)
339 }
340
341 fn columnar_segment_streaming_factory_v08_by_ids(
342 &self,
343 table_id: u32,
344 segment_id: u64,
345 ) -> Result<ColumnarSegmentBatchSourceFactory> {
346 match self.columnar_mode {
347 StorageMode::Disk => match self
348 .columnar_bridge
349 .streaming_layout_preflight_v08(table_id, segment_id)
350 .map_err(|error| Error::DataFrame(map_v08_error(error)))?
351 {
352 StreamingSegmentLayoutPreflightV08::Available => Ok(
353 ColumnarSegmentBatchSourceFactory::new(Arc::new(EmbeddedV08SegmentProvider {
354 bridge: self.columnar_bridge.clone(),
355 table_id,
356 segment_id,
357 })),
358 ),
359 StreamingSegmentLayoutPreflightV08::RequiresV08ChunkedLayout => {
360 Err(Error::DataFrame(DataFrameError::streaming_unsupported(
361 "columnar_segment_scan",
362 "requires_v08_chunked_layout",
363 )))
364 }
365 StreamingSegmentLayoutPreflightV08::NotFound => {
366 Err(Error::DataFrame(DataFrameError::streaming_unsupported(
367 "columnar_segment_scan",
368 "v08_segment_not_found",
369 )))
370 }
371 },
372 StorageMode::InMemory => Err(Error::DataFrame(DataFrameError::streaming_unsupported(
373 "columnar_segment_scan",
374 "requires_v08_chunked_layout",
375 ))),
376 }
377 }
378
379 pub fn stream_columnar_segment_v08(
381 &self,
382 table: &str,
383 segment_id: u64,
384 options: alopex_dataframe::physical::budget::StreamOptions,
385 ) -> Result<DataFrameStream> {
386 let factory = self.columnar_segment_streaming_factory_v08(table, segment_id)?;
387 DataFrameStream::from_factory(&factory, options).map_err(Error::DataFrame)
388 }
389
390 pub fn stream_columnar_segment_v08_by_id(
392 &self,
393 segment_id: &str,
394 options: alopex_dataframe::physical::budget::StreamOptions,
395 ) -> Result<DataFrameStream> {
396 let factory = self.columnar_segment_streaming_factory_v08_by_id(segment_id)?;
397 DataFrameStream::from_factory(&factory, options).map_err(Error::DataFrame)
398 }
399
400 pub fn scan_columnar_segment(
405 &self,
406 segment_id: &str,
407 ) -> Result<Vec<Vec<alopex_sql::SqlValue>>> {
408 let (table_id, seg_id) = parse_segment_id(segment_id)?;
409 let all_indices: Vec<usize> = match self.columnar_mode {
410 StorageMode::Disk => {
411 let count = self
412 .columnar_bridge
413 .column_count(table_id, seg_id)
414 .map_err(|e| Error::Core(e.into()))?;
415 (0..count).collect()
416 }
417 StorageMode::InMemory => {
418 let store = self.columnar_memory.as_ref().ok_or_else(|| {
419 Error::Core(alopex_core::Error::InvalidFormat(
420 "in-memory columnar store is not initialized".into(),
421 ))
422 })?;
423 let count = store
424 .column_count(table_id, seg_id)
425 .map_err(|e| Error::Core(e.into()))?;
426 (0..count).collect()
427 }
428 };
429
430 let batches = match self.columnar_mode {
431 StorageMode::Disk => self
432 .columnar_bridge
433 .read_segment(table_id, seg_id, &all_indices)
434 .map_err(|e| Error::Core(e.into()))?,
435 StorageMode::InMemory => self
436 .columnar_memory
437 .as_ref()
438 .ok_or_else(|| {
439 Error::Core(alopex_core::Error::InvalidFormat(
440 "in-memory columnar store is not initialized".into(),
441 ))
442 })?
443 .read_segment(table_id, seg_id, &all_indices)
444 .map_err(|e| Error::Core(e.into()))?,
445 };
446
447 let mut rows = Vec::new();
449 for batch in batches {
450 let num_rows = batch.num_rows();
451 for row_idx in 0..num_rows {
452 let mut row = Vec::with_capacity(batch.columns.len());
453 for col in &batch.columns {
454 let sql_val = column_value_to_sql_value(col, row_idx);
455 row.push(sql_val);
456 }
457 rows.push(row);
458 }
459 }
460 Ok(rows)
461 }
462
463 pub fn scan_columnar_segment_batches(&self, segment_id: &str) -> Result<Vec<CoreRecordBatch>> {
471 let (table_id, seg_id) = parse_segment_id(segment_id)?;
472 let all_indices: Vec<usize> = match self.columnar_mode {
473 StorageMode::Disk => {
474 let count = self
475 .columnar_bridge
476 .column_count(table_id, seg_id)
477 .map_err(|e| Error::Core(e.into()))?;
478 (0..count).collect()
479 }
480 StorageMode::InMemory => {
481 let store = self.columnar_memory.as_ref().ok_or_else(|| {
482 Error::Core(alopex_core::Error::InvalidFormat(
483 "in-memory columnar store is not initialized".into(),
484 ))
485 })?;
486 let count = store
487 .column_count(table_id, seg_id)
488 .map_err(|e| Error::Core(e.into()))?;
489 (0..count).collect()
490 }
491 };
492
493 match self.columnar_mode {
494 StorageMode::Disk => self
495 .columnar_bridge
496 .read_segment(table_id, seg_id, &all_indices)
497 .map_err(|e| Error::Core(e.into())),
498 StorageMode::InMemory => self
499 .columnar_memory
500 .as_ref()
501 .ok_or_else(|| {
502 Error::Core(alopex_core::Error::InvalidFormat(
503 "in-memory columnar store is not initialized".into(),
504 ))
505 })?
506 .read_segment(table_id, seg_id, &all_indices)
507 .map_err(|e| Error::Core(e.into())),
508 }
509 }
510
511 pub fn scan_columnar_segment_streaming(&self, segment_id: &str) -> Result<ColumnarRowIterator> {
518 let batches = self.scan_columnar_segment_batches(segment_id)?;
519 Ok(ColumnarRowIterator::new(batches))
520 }
521
522 pub fn get_columnar_segment_stats(&self, segment_id: &str) -> Result<ColumnarSegmentStats> {
526 let (table_id, seg_id) = parse_segment_id(segment_id)?;
527
528 match self.columnar_mode {
529 StorageMode::Disk => {
530 let column_count = self
531 .columnar_bridge
532 .column_count(table_id, seg_id)
533 .map_err(|e| Error::Core(e.into()))?;
534 let batches = self
535 .columnar_bridge
536 .read_segment(table_id, seg_id, &(0..column_count).collect::<Vec<_>>())
537 .map_err(|e| Error::Core(e.into()))?;
538 let row_count: usize = batches.iter().map(|b| b.num_rows()).sum();
539
540 Ok(ColumnarSegmentStats {
541 row_count,
542 column_count,
543 size_bytes: 0, })
545 }
546 StorageMode::InMemory => {
547 let store = self.columnar_memory.as_ref().ok_or_else(|| {
548 Error::Core(alopex_core::Error::InvalidFormat(
549 "in-memory columnar store is not initialized".into(),
550 ))
551 })?;
552 let column_count = store
553 .column_count(table_id, seg_id)
554 .map_err(|e| Error::Core(e.into()))?;
555 let batches = store
556 .read_segment(table_id, seg_id, &(0..column_count).collect::<Vec<_>>())
557 .map_err(|e| Error::Core(e.into()))?;
558 let row_count: usize = batches.iter().map(|b| b.num_rows()).sum();
559
560 Ok(ColumnarSegmentStats {
561 row_count,
562 column_count,
563 size_bytes: 0, })
565 }
566 }
567 }
568
569 pub fn list_columnar_segments(&self) -> Result<Vec<String>> {
573 match self.columnar_mode {
574 StorageMode::Disk => {
575 let segments = self
576 .columnar_bridge
577 .list_segments()
578 .map_err(|e| Error::Core(e.into()))?;
579 Ok(segments
580 .into_iter()
581 .map(|(table_id, seg_id)| format!("{}:{}", table_id, seg_id))
582 .collect())
583 }
584 StorageMode::InMemory => {
585 let store = self.columnar_memory.as_ref().ok_or_else(|| {
586 Error::Core(alopex_core::Error::InvalidFormat(
587 "in-memory columnar store is not initialized".into(),
588 ))
589 })?;
590 let segments = store.list_segments();
591 Ok(segments
592 .into_iter()
593 .map(|(table_id, seg_id)| format!("{}:{}", table_id, seg_id))
594 .collect())
595 }
596 }
597 }
598
599 pub fn create_columnar_index(
601 &self,
602 segment_id: &str,
603 column: &str,
604 index_type: ColumnarIndexType,
605 ) -> Result<()> {
606 let _ = self.get_columnar_segment_stats(segment_id)?;
607 let key = columnar_index_key(segment_id, column);
608 let value = index_type.as_str().as_bytes().to_vec();
609 let mut txn = self.begin(TxnMode::ReadWrite)?;
610 txn.put(&key, &value)?;
611 txn.commit()?;
612 Ok(())
613 }
614
615 pub fn list_columnar_indexes(&self, segment_id: &str) -> Result<Vec<ColumnarIndexInfo>> {
617 let _ = self.get_columnar_segment_stats(segment_id)?;
618 let prefix = columnar_index_prefix(segment_id);
619 let mut txn = self.begin(TxnMode::ReadOnly)?;
620 let mut entries = Vec::new();
621 for (key, value) in txn.scan_prefix(&prefix)? {
622 let column = parse_index_column(segment_id, &key)?;
623 let index_type = parse_index_type(&value)?;
624 entries.push(ColumnarIndexInfo { column, index_type });
625 }
626 txn.commit()?;
627 Ok(entries)
628 }
629
630 pub fn drop_columnar_index(&self, segment_id: &str, column: &str) -> Result<()> {
632 let _ = self.get_columnar_segment_stats(segment_id)?;
633 let key = columnar_index_key(segment_id, column);
634 let mut txn = self.begin(TxnMode::ReadWrite)?;
635 let exists = txn.get(&key)?.is_some();
636 if !exists {
637 txn.rollback()?;
638 return Err(Error::IndexNotFound(format!(
639 "columnar index {}:{}",
640 segment_id, column
641 )));
642 }
643 txn.delete(&key)?;
644 txn.commit()?;
645 Ok(())
646 }
647
648 pub fn flush_in_memory_segment_to_file(
650 &self,
651 table: &str,
652 segment_id: u64,
653 path: &Path,
654 ) -> Result<()> {
655 let store = self
656 .columnar_memory
657 .as_ref()
658 .ok_or(Error::NotInMemoryMode)?;
659 let table_id = table_id(table)?;
660 store
661 .flush_to_segment_file(table_id, segment_id, path)
662 .map_err(|e| Error::Core(e.into()))
663 }
664
665 pub fn flush_in_memory_segment_to_kvs(&self, table: &str, segment_id: u64) -> Result<u64> {
667 let store = self
668 .columnar_memory
669 .as_ref()
670 .ok_or(Error::NotInMemoryMode)?;
671 let table_id = table_id(table)?;
672 store
673 .flush_to_kvs(table_id, segment_id, &self.columnar_bridge)
674 .map_err(|e| Error::Core(e.into()))
675 }
676
677 pub fn flush_in_memory_segment_to_alopex(
679 &self,
680 table: &str,
681 segment_id: u64,
682 writer: &mut AlopexFileWriter,
683 ) -> Result<u32> {
684 let store = self
685 .columnar_memory
686 .as_ref()
687 .ok_or(Error::NotInMemoryMode)?;
688 let table_id = table_id(table)?;
689 store
690 .flush_to_alopex(table_id, segment_id, writer)
691 .map_err(|e| Error::Core(e.into()))
692 }
693}
694
695impl<'a> Transaction<'a> {
696 pub fn storage_mode(&self) -> StorageMode {
698 self.db.storage_mode()
699 }
700
701 pub fn write_columnar_segment(&self, table: &str, batch: CoreRecordBatch) -> Result<u64> {
703 self.db.write_columnar_segment(table, batch)
704 }
705
706 pub fn read_columnar_segment(
708 &self,
709 table: &str,
710 segment_id: u64,
711 columns: Option<&[&str]>,
712 ) -> Result<Vec<CoreRecordBatch>> {
713 self.db.read_columnar_segment(table, segment_id, columns)
714 }
715}
716
717fn table_id(table: &str) -> Result<u32> {
718 if table.is_empty() {
719 return Err(Error::TableNotFound("table name is empty".into()));
720 }
721 let mut hasher = DefaultHasher::new();
722 table.hash(&mut hasher);
723 Ok((hasher.finish() & 0xffff_ffff) as u32)
724}
725
726fn resolve_indices_from_schema(
727 schema: &alopex_core::columnar::segment_v2::Schema,
728 names: &[&str],
729) -> Result<Vec<usize>> {
730 let mut indices = Vec::with_capacity(names.len());
731 for name in names {
732 let pos = schema
733 .columns
734 .iter()
735 .position(|c| c.name == *name)
736 .ok_or_else(|| {
737 Error::Core(alopex_core::Error::InvalidFormat(format!(
738 "column not found: {name}"
739 )))
740 })?;
741 indices.push(pos);
742 }
743 Ok(indices)
744}
745
746const COLUMNAR_INDEX_PREFIX: &str = "__alopex_columnar_index__:";
747
748fn columnar_index_key(segment: &str, column: &str) -> Vec<u8> {
749 let mut key =
750 String::with_capacity(COLUMNAR_INDEX_PREFIX.len() + segment.len() + column.len() + 1);
751 key.push_str(COLUMNAR_INDEX_PREFIX);
752 key.push_str(segment);
753 key.push(':');
754 key.push_str(column);
755 key.into_bytes()
756}
757
758fn columnar_index_prefix(segment: &str) -> Vec<u8> {
759 let mut key = String::with_capacity(COLUMNAR_INDEX_PREFIX.len() + segment.len() + 1);
760 key.push_str(COLUMNAR_INDEX_PREFIX);
761 key.push_str(segment);
762 key.push(':');
763 key.into_bytes()
764}
765
766fn parse_index_column(segment: &str, key: &[u8]) -> Result<String> {
767 let prefix = columnar_index_prefix(segment);
768 if !key.starts_with(&prefix) {
769 return Err(Error::Core(alopex_core::Error::InvalidFormat(
770 "columnar index key is invalid".into(),
771 )));
772 }
773 let suffix = &key[prefix.len()..];
774 String::from_utf8(suffix.to_vec()).map_err(|_| {
775 Error::Core(alopex_core::Error::InvalidFormat(
776 "columnar index column is not valid UTF-8".into(),
777 ))
778 })
779}
780
781fn parse_index_type(raw: &[u8]) -> Result<ColumnarIndexType> {
782 let value = std::str::from_utf8(raw).map_err(|_| {
783 Error::Core(alopex_core::Error::InvalidFormat(
784 "columnar index type is not valid UTF-8".into(),
785 ))
786 })?;
787 match value {
788 "minmax" => Ok(ColumnarIndexType::Minmax),
789 "bloom" => Ok(ColumnarIndexType::Bloom),
790 other => Err(Error::Core(alopex_core::Error::InvalidFormat(format!(
791 "unknown columnar index type: {other}"
792 )))),
793 }
794}
795
796fn parse_segment_id(segment_id: &str) -> Result<(u32, u64)> {
800 let parts: Vec<&str> = segment_id.split(':').collect();
801 if parts.len() != 2 {
802 return Err(Error::Core(alopex_core::Error::InvalidFormat(format!(
803 "invalid segment ID format: expected 'table_id:segment_id', got '{}'",
804 segment_id
805 ))));
806 }
807
808 let table_id: u32 = parts[0].parse().map_err(|_| {
809 Error::Core(alopex_core::Error::InvalidFormat(format!(
810 "invalid table_id in segment ID: '{}'",
811 parts[0]
812 )))
813 })?;
814
815 let seg_id: u64 = parts[1].parse().map_err(|_| {
816 Error::Core(alopex_core::Error::InvalidFormat(format!(
817 "invalid segment_id in segment ID: '{}'",
818 parts[1]
819 )))
820 })?;
821
822 Ok((table_id, seg_id))
823}
824
825fn column_value_to_sql_value(col: &Column, row_idx: usize) -> alopex_sql::SqlValue {
827 match col {
828 Column::Int64(vals) => vals
829 .get(row_idx)
830 .map(|&v| alopex_sql::SqlValue::BigInt(v))
831 .unwrap_or(alopex_sql::SqlValue::Null),
832 Column::Float32(vals) => vals
833 .get(row_idx)
834 .map(|&v| alopex_sql::SqlValue::Float(v))
835 .unwrap_or(alopex_sql::SqlValue::Null),
836 Column::Float64(vals) => vals
837 .get(row_idx)
838 .map(|&v| alopex_sql::SqlValue::Double(v))
839 .unwrap_or(alopex_sql::SqlValue::Null),
840 Column::Bool(vals) => vals
841 .get(row_idx)
842 .map(|&v| alopex_sql::SqlValue::Boolean(v))
843 .unwrap_or(alopex_sql::SqlValue::Null),
844 Column::Binary(vals) => vals
845 .get(row_idx)
846 .map(|v| alopex_sql::SqlValue::Blob(v.clone()))
847 .unwrap_or(alopex_sql::SqlValue::Null),
848 Column::Fixed { values, .. } => values
849 .get(row_idx)
850 .map(|v| alopex_sql::SqlValue::Blob(v.clone()))
851 .unwrap_or(alopex_sql::SqlValue::Null),
852 }
853}
854
855struct EmbeddedV08SegmentProvider {
856 bridge: ColumnarKvsBridge,
857 table_id: u32,
858 segment_id: u64,
859}
860
861impl ColumnarSegmentProvider for EmbeddedV08SegmentProvider {
862 fn source_limits(&self) -> Vec<SourceLimit> {
863 vec![
864 SourceLimit {
865 source: PlanSubject::ColumnarSegmentScan,
866 code: "stable_row_group_order",
867 description: "V08 streaming preserves provisioned row-group and row order",
868 },
869 SourceLimit {
870 source: PlanSubject::ColumnarSegmentScan,
871 code: "requires_v08_chunked_layout",
872 description: "legacy V2 blobs and in-memory columnar storage are rejected before streaming reads",
873 },
874 SourceLimit {
875 source: PlanSubject::ColumnarSegmentScan,
876 code: "row_group_must_fit_resource_bound",
877 description: "each V08 row-group decoded and Arrow upper bound is reserved before chunk fetch",
878 },
879 ]
880 }
881
882 fn open(&self, metadata_limit_bytes: u64) -> DataFrameResult<Box<dyn ColumnarSegmentCursor>> {
883 let access = self
884 .bridge
885 .open_v08_segment_with_metadata_limit(
886 self.table_id,
887 self.segment_id,
888 metadata_limit_bytes,
889 )
890 .map_err(map_v08_error)?;
891 let schema = core_schema_to_arrow(access.schema())?;
892 let projection = (0..access.schema().column_count()).collect();
893 Ok(Box::new(EmbeddedV08SegmentCursor {
894 access,
895 schema,
896 projection,
897 next_row_group: 0,
898 closed: false,
899 }))
900 }
901}
902
903struct EmbeddedV08SegmentCursor {
904 access: ChunkedSegmentAccessV08,
905 schema: SchemaRef,
906 projection: Vec<usize>,
907 next_row_group: usize,
908 closed: bool,
909}
910
911impl ColumnarSegmentCursor for EmbeddedV08SegmentCursor {
912 fn schema(&self) -> SchemaRef {
913 self.schema.clone()
914 }
915
916 fn metadata_footprint_upper_bound(&self) -> u64 {
917 self.access.metadata_footprint_upper_bound()
918 }
919
920 fn next_batch_plan(&self) -> DataFrameResult<Option<ColumnarSegmentBatchPlan>> {
921 if self.closed || self.next_row_group == self.access.row_group_count() {
922 return Ok(None);
923 }
924 let row_group = self
925 .access
926 .row_group(self.next_row_group)
927 .map_err(map_v08_error)?;
928 Ok(Some(ColumnarSegmentBatchPlan {
929 decoded_bytes: row_group.decoded_bytes,
930 arrow_allocation_upper_bound: row_group.arrow_allocation_upper_bound,
931 }))
932 }
933
934 fn next_batch(&mut self) -> DataFrameResult<Option<ArrowRecordBatch>> {
935 if self.closed || self.next_row_group == self.access.row_group_count() {
936 return Ok(None);
937 }
938 let core_batch = self
939 .access
940 .read_row_group(self.next_row_group, &self.projection)
941 .map_err(map_v08_error)?;
942 let batch = core_batch_to_arrow(core_batch, self.schema.clone())?;
943 self.next_row_group = self.next_row_group.saturating_add(1);
944 Ok(Some(batch))
945 }
946
947 fn close(&mut self) -> DataFrameResult<()> {
948 self.closed = true;
949 Ok(())
950 }
951}
952
953fn core_schema_to_arrow(schema: &CoreSchema) -> DataFrameResult<SchemaRef> {
954 let fields = schema
955 .columns
956 .iter()
957 .map(|column| {
958 Ok(Field::new(
959 &column.name,
960 arrow_type(column.logical_type)?,
961 column.nullable,
962 ))
963 })
964 .collect::<DataFrameResult<Vec<_>>>()?;
965 Ok(Arc::new(ArrowSchema::new(fields)))
966}
967
968fn arrow_type(logical_type: LogicalType) -> DataFrameResult<DataType> {
969 match logical_type {
970 LogicalType::Int64 => Ok(DataType::Int64),
971 LogicalType::Float32 => Ok(DataType::Float32),
972 LogicalType::Float64 => Ok(DataType::Float64),
973 LogicalType::Bool => Ok(DataType::Boolean),
974 LogicalType::Binary => Ok(DataType::Binary),
975 LogicalType::Fixed(length) => Ok(DataType::FixedSizeBinary(i32::from(length))),
976 }
977}
978
979fn core_batch_to_arrow(
980 batch: CoreRecordBatch,
981 schema: SchemaRef,
982) -> DataFrameResult<ArrowRecordBatch> {
983 if batch.schema.columns.len() != schema.fields().len()
984 || batch.columns.len() != schema.fields().len()
985 || batch.null_bitmaps.len() != schema.fields().len()
986 {
987 return Err(DataFrameError::schema_mismatch(
988 "V08 decoded batch does not match its provisioned schema",
989 ));
990 }
991
992 let arrays = batch
993 .columns
994 .iter()
995 .zip(batch.null_bitmaps.iter())
996 .map(|(column, bitmap)| core_column_to_arrow(column, bitmap.as_ref()))
997 .collect::<DataFrameResult<Vec<_>>>()?;
998 ArrowRecordBatch::try_new(schema, arrays).map_err(|source| DataFrameError::Arrow { source })
999}
1000
1001fn core_column_to_arrow(column: &Column, bitmap: Option<&Bitmap>) -> DataFrameResult<ArrayRef> {
1002 let valid = |index| bitmap.is_none_or(|bitmap| bitmap.is_valid(index));
1003 match column {
1004 Column::Int64(values) => Ok(Arc::new(Int64Array::from(
1005 values
1006 .iter()
1007 .enumerate()
1008 .map(|(index, value)| valid(index).then_some(*value))
1009 .collect::<Vec<_>>(),
1010 )) as ArrayRef),
1011 Column::Float32(values) => Ok(Arc::new(Float32Array::from(
1012 values
1013 .iter()
1014 .enumerate()
1015 .map(|(index, value)| valid(index).then_some(*value))
1016 .collect::<Vec<_>>(),
1017 )) as ArrayRef),
1018 Column::Float64(values) => Ok(Arc::new(Float64Array::from(
1019 values
1020 .iter()
1021 .enumerate()
1022 .map(|(index, value)| valid(index).then_some(*value))
1023 .collect::<Vec<_>>(),
1024 )) as ArrayRef),
1025 Column::Bool(values) => Ok(Arc::new(BooleanArray::from(
1026 values
1027 .iter()
1028 .enumerate()
1029 .map(|(index, value)| valid(index).then_some(*value))
1030 .collect::<Vec<_>>(),
1031 )) as ArrayRef),
1032 Column::Binary(values) => Ok(Arc::new(BinaryArray::from(
1033 values
1034 .iter()
1035 .enumerate()
1036 .map(|(index, value)| valid(index).then_some(value.as_slice()))
1037 .collect::<Vec<_>>(),
1038 )) as ArrayRef),
1039 Column::Fixed { len, values } => {
1040 let byte_width = i32::try_from(*len).map_err(|_| {
1041 DataFrameError::schema_mismatch("V08 fixed binary length does not fit Arrow")
1042 })?;
1043 let mut builder = FixedSizeBinaryBuilder::with_capacity(values.len(), byte_width);
1044 for (index, value) in values.iter().enumerate() {
1045 if valid(index) {
1046 builder
1047 .append_value(value)
1048 .map_err(|source| DataFrameError::Arrow { source })?;
1049 } else {
1050 builder.append_null();
1051 }
1052 }
1053 Ok(Arc::new(builder.finish()) as ArrayRef)
1054 }
1055 }
1056}
1057
1058fn map_v08_error(error: alopex_core::columnar::ColumnarError) -> DataFrameError {
1059 match error {
1060 alopex_core::columnar::ColumnarError::RequiresV08ChunkedLayout => {
1061 DataFrameError::streaming_unsupported(
1062 "columnar_segment_scan",
1063 "requires_v08_chunked_layout",
1064 )
1065 }
1066 alopex_core::columnar::ColumnarError::NotFound => {
1067 DataFrameError::streaming_unsupported("columnar_segment_scan", "v08_segment_not_found")
1068 }
1069 error => DataFrameError::schema_mismatch(format!("V08 columnar segment error: {error}")),
1070 }
1071}
1072
1073pub struct ColumnarRowIterator {
1082 batches: Vec<CoreRecordBatch>,
1084 batch_idx: usize,
1086 row_idx: usize,
1088}
1089
1090impl ColumnarRowIterator {
1091 pub fn new(batches: Vec<CoreRecordBatch>) -> Self {
1093 Self {
1094 batches,
1095 batch_idx: 0,
1096 row_idx: 0,
1097 }
1098 }
1099
1100 pub fn batch_count(&self) -> usize {
1102 self.batches.len()
1103 }
1104
1105 pub fn current_batch(&self) -> Option<&CoreRecordBatch> {
1107 self.batches.get(self.batch_idx)
1108 }
1109}
1110
1111impl Iterator for ColumnarRowIterator {
1112 type Item = Vec<alopex_sql::SqlValue>;
1113
1114 fn next(&mut self) -> Option<Self::Item> {
1115 loop {
1116 if self.batch_idx >= self.batches.len() {
1118 return None;
1119 }
1120
1121 let batch = &self.batches[self.batch_idx];
1122 let row_count = batch.num_rows();
1123
1124 if self.row_idx >= row_count {
1126 self.batch_idx += 1;
1127 self.row_idx = 0;
1128 continue;
1129 }
1130
1131 let row_idx = self.row_idx;
1133 self.row_idx += 1;
1134
1135 let mut row = Vec::with_capacity(batch.columns.len());
1136 for col in &batch.columns {
1137 let sql_val = column_value_to_sql_value(col, row_idx);
1138 row.push(sql_val);
1139 }
1140 return Some(row);
1141 }
1142 }
1143}
1144
1145#[cfg(test)]
1146mod tests {
1147 use super::*;
1148 use alopex_core::columnar::encoding::{Column, LogicalType};
1149 use alopex_core::columnar::encoding_v2::{create_encoder, EncodingV2};
1150 use alopex_core::columnar::error::{ColumnarError, Result as ColumnarResult};
1151 use alopex_core::columnar::kvs_bridge::key_layout;
1152 use alopex_core::columnar::segment_v08::{
1153 ChecksummedMetadataV08, StreamingChunkMetaV08, StreamingRowGroupV08,
1154 StreamingSegmentDirectoryV08, StreamingSegmentHeaderV08,
1155 STREAMING_SEGMENT_LAYOUT_VERSION_V08, STREAMING_SEGMENT_MAGIC_V08,
1156 };
1157 use alopex_core::columnar::segment_v2::{ColumnSchema, Schema, SegmentReaderV2, SegmentSource};
1158 use alopex_core::kv::{KVStore, KVTransaction};
1159 use alopex_core::storage::compression::CompressionV2;
1160 use alopex_core::storage::format::bincode_config;
1161 use alopex_core::storage::format::{AlopexFileWriter, FileFlags, FileVersion};
1162 use alopex_core::TxnManager;
1163 use arrow::array::Array;
1164 use bincode::Options;
1165 use crc32fast::Hasher;
1166 use std::sync::atomic::{AtomicU64, AtomicUsize, Ordering};
1167 use std::sync::Arc;
1168 use tempfile::tempdir;
1169
1170 fn make_batch() -> CoreRecordBatch {
1171 let schema = Schema {
1172 columns: vec![
1173 ColumnSchema {
1174 name: "id".into(),
1175 logical_type: LogicalType::Int64,
1176 nullable: false,
1177 fixed_len: None,
1178 },
1179 ColumnSchema {
1180 name: "val".into(),
1181 logical_type: LogicalType::Int64,
1182 nullable: false,
1183 fixed_len: None,
1184 },
1185 ],
1186 };
1187 CoreRecordBatch::new(
1188 schema,
1189 vec![
1190 Column::Int64(vec![1, 2, 3]),
1191 Column::Int64(vec![10, 20, 30]),
1192 ],
1193 vec![None, None],
1194 )
1195 }
1196
1197 fn make_wide_batch(column_count: usize, row_count: usize) -> CoreRecordBatch {
1198 let schema = Schema {
1199 columns: (0..column_count)
1200 .map(|idx| ColumnSchema {
1201 name: format!("c{idx}"),
1202 logical_type: LogicalType::Int64,
1203 nullable: false,
1204 fixed_len: None,
1205 })
1206 .collect(),
1207 };
1208 let columns = (0..column_count)
1209 .map(|idx| {
1210 Column::Int64(
1211 (0..row_count)
1212 .map(|row| (idx as i64 * 1_000_000) + row as i64)
1213 .collect(),
1214 )
1215 })
1216 .collect();
1217 CoreRecordBatch::new(schema, columns, vec![None; column_count])
1218 }
1219
1220 fn decoded_payload_bytes(batches: &[CoreRecordBatch]) -> usize {
1221 batches
1222 .iter()
1223 .flat_map(|batch| batch.columns.iter())
1224 .map(|column| match column {
1225 Column::Int64(values) => values.len() * std::mem::size_of::<i64>(),
1226 Column::Float32(values) => values.len() * std::mem::size_of::<f32>(),
1227 Column::Float64(values) => values.len() * std::mem::size_of::<f64>(),
1228 Column::Bool(values) => values.len() * std::mem::size_of::<bool>(),
1229 Column::Binary(values) => values.iter().map(Vec::len).sum(),
1230 Column::Fixed { values, .. } => values.iter().map(Vec::len).sum(),
1231 })
1232 .sum()
1233 }
1234
1235 fn checksum(bytes: &[u8]) -> u32 {
1236 let mut hasher = Hasher::new();
1237 hasher.update(bytes);
1238 hasher.finalize()
1239 }
1240
1241 fn metadata_checksum<T: serde::Serialize>(value: &T) -> u32 {
1242 checksum(&bincode_config().serialize(value).unwrap())
1243 }
1244
1245 fn checksummed_metadata<T: serde::Serialize + Clone>(value: T) -> Vec<u8> {
1246 let checksum = metadata_checksum(&value);
1247 bincode_config()
1248 .serialize(&ChecksummedMetadataV08 { value, checksum })
1249 .unwrap()
1250 }
1251
1252 fn provision_v08_int64_fixture(db: &Database, table_id: u32, segment_id: u64) {
1253 let schema = Schema {
1254 columns: vec![ColumnSchema {
1255 name: "value".into(),
1256 logical_type: LogicalType::Int64,
1257 nullable: false,
1258 fixed_len: None,
1259 }],
1260 };
1261 let chunk_bytes = create_encoder(EncodingV2::Plain)
1262 .encode(&Column::Int64(vec![41, 42]), None)
1263 .unwrap();
1264 let chunk = StreamingChunkMetaV08 {
1265 column_index: 0,
1266 encoding: EncodingV2::Plain,
1267 compression: CompressionV2::None,
1268 encoded_bytes: chunk_bytes.len() as u64,
1269 decoded_bytes: chunk_bytes.len() as u64,
1270 checksum: checksum(&chunk_bytes),
1271 };
1272 let row_group = StreamingRowGroupV08 {
1273 row_start: 0,
1274 row_count: 2,
1275 encoded_bytes: chunk.encoded_bytes,
1276 decoded_bytes: chunk.decoded_bytes,
1277 arrow_allocation_upper_bound: 4096,
1278 chunks: vec![chunk],
1279 };
1280 let directory = StreamingSegmentDirectoryV08 {
1281 row_groups: vec![row_group],
1282 };
1283 let header = StreamingSegmentHeaderV08 {
1284 magic: STREAMING_SEGMENT_MAGIC_V08,
1285 format_version: STREAMING_SEGMENT_LAYOUT_VERSION_V08,
1286 row_count: 2,
1287 column_count: 1,
1288 row_group_count: 1,
1289 schema_checksum: metadata_checksum(&schema),
1290 directory_checksum: metadata_checksum(&directory),
1291 };
1292
1293 let manager = db.store.txn_manager();
1294 let mut txn = manager.begin(TxnMode::ReadWrite).unwrap();
1295 txn.put(
1296 key_layout::v08_header_key(table_id, segment_id),
1297 checksummed_metadata(header),
1298 )
1299 .unwrap();
1300 txn.put(
1301 key_layout::v08_schema_key(table_id, segment_id),
1302 checksummed_metadata(schema),
1303 )
1304 .unwrap();
1305 txn.put(
1306 key_layout::v08_directory_key(table_id, segment_id),
1307 checksummed_metadata(directory),
1308 )
1309 .unwrap();
1310 txn.put(
1311 key_layout::v08_chunk_key(table_id, segment_id, 0, 0),
1312 chunk_bytes,
1313 )
1314 .unwrap();
1315 manager.commit(txn).unwrap();
1316 }
1317
1318 #[test]
1319 fn v08_factory_rejects_legacy_v2_before_stream_open() {
1320 let dir = tempfile::tempdir().unwrap();
1321 let db =
1322 Database::open_with_config(EmbeddedConfig::disk(dir.path().join("wal.log"))).unwrap();
1323 let segment_id = db.write_columnar_segment("legacy", make_batch()).unwrap();
1324
1325 assert!(matches!(
1326 db.columnar_segment_streaming_factory_v08("legacy", segment_id),
1327 Err(Error::DataFrame(DataFrameError::StreamingUnsupported { reason, .. }))
1328 if reason == "requires_v08_chunked_layout"
1329 ));
1330 }
1331
1332 #[test]
1333 fn v08_adapter_converts_nullable_core_columns_to_arrow() {
1334 let schema = CoreSchema {
1335 columns: vec![
1336 ColumnSchema {
1337 name: "id".into(),
1338 logical_type: LogicalType::Int64,
1339 nullable: true,
1340 fixed_len: None,
1341 },
1342 ColumnSchema {
1343 name: "payload".into(),
1344 logical_type: LogicalType::Binary,
1345 nullable: false,
1346 fixed_len: None,
1347 },
1348 ],
1349 };
1350 let batch = CoreRecordBatch::new(
1351 schema.clone(),
1352 vec![
1353 Column::Int64(vec![10, 20]),
1354 Column::Binary(vec![b"a".to_vec(), b"b".to_vec()]),
1355 ],
1356 vec![Some(Bitmap::from_bools(&[true, false])), None],
1357 );
1358
1359 let arrow_schema = core_schema_to_arrow(&schema).unwrap();
1360 let arrow_batch = core_batch_to_arrow(batch, arrow_schema).unwrap();
1361 let ids = arrow_batch.columns()[0]
1362 .as_any()
1363 .downcast_ref::<Int64Array>()
1364 .unwrap();
1365 let payload = arrow_batch.columns()[1]
1366 .as_any()
1367 .downcast_ref::<BinaryArray>()
1368 .unwrap();
1369 assert_eq!(ids.value(0), 10);
1370 assert!(ids.is_null(1));
1371 assert_eq!(payload.value(1), b"b");
1372 }
1373
1374 #[test]
1375 fn v08_database_adapter_streams_a_provisioned_row_group() {
1376 let dir = tempfile::tempdir().unwrap();
1377 let db =
1378 Database::open_with_config(EmbeddedConfig::disk(dir.path().join("wal.log"))).unwrap();
1379 let table_id = db.resolve_table_id("v08").unwrap();
1380 provision_v08_int64_fixture(&db, table_id, 77);
1381 let options = alopex_dataframe::physical::budget::StreamOptions::new(
1382 64 * 1024,
1383 std::num::NonZeroUsize::new(1).unwrap(),
1384 std::num::NonZeroUsize::new(2).unwrap(),
1385 );
1386
1387 let mut stream = db.stream_columnar_segment_v08("v08", 77, options).unwrap();
1388 let batch = stream.next_batch().unwrap().unwrap();
1389 let values = batch.column("value").unwrap().to_arrow();
1390 let values = values[0].as_any().downcast_ref::<Int64Array>().unwrap();
1391 assert_eq!(values.value(0), 41);
1392 assert_eq!(values.value(1), 42);
1393 assert!(stream.next_batch().unwrap().is_none());
1394
1395 let mut by_id = db
1396 .stream_columnar_segment_v08_by_id(&format!("{table_id}:77"), options)
1397 .unwrap();
1398 let batch = by_id.next_batch().unwrap().unwrap();
1399 let values = batch.column("value").unwrap().to_arrow();
1400 let values = values[0].as_any().downcast_ref::<Int64Array>().unwrap();
1401 assert_eq!(values.value(0), 41);
1402 assert_eq!(values.value(1), 42);
1403 assert!(by_id.next_batch().unwrap().is_none());
1404 }
1405
1406 #[derive(Debug, Clone)]
1407 struct CountingSegmentSource {
1408 data: Arc<Vec<u8>>,
1409 bytes: Arc<AtomicU64>,
1410 calls: Arc<AtomicUsize>,
1411 }
1412
1413 impl CountingSegmentSource {
1414 fn new(data: Vec<u8>) -> Self {
1415 Self {
1416 data: Arc::new(data),
1417 bytes: Arc::new(AtomicU64::new(0)),
1418 calls: Arc::new(AtomicUsize::new(0)),
1419 }
1420 }
1421
1422 fn reset(&self) {
1423 self.bytes.store(0, Ordering::Relaxed);
1424 self.calls.store(0, Ordering::Relaxed);
1425 }
1426
1427 fn bytes(&self) -> u64 {
1428 self.bytes.load(Ordering::Relaxed)
1429 }
1430
1431 fn calls(&self) -> usize {
1432 self.calls.load(Ordering::Relaxed)
1433 }
1434 }
1435
1436 impl SegmentSource for CountingSegmentSource {
1437 fn read_range(&self, offset: u64, len: u64) -> ColumnarResult<Vec<u8>> {
1438 self.bytes.fetch_add(len, Ordering::Relaxed);
1439 self.calls.fetch_add(1, Ordering::Relaxed);
1440 let start = offset as usize;
1441 let end = start + len as usize;
1442 if end > self.data.len() {
1443 return Err(ColumnarError::InvalidFormat("range out of bounds".into()));
1444 }
1445 Ok(self.data[start..end].to_vec())
1446 }
1447
1448 fn total_size(&self) -> u64 {
1449 self.data.len() as u64
1450 }
1451 }
1452
1453 fn measured_segment_read(data: Vec<u8>, columns: &[usize]) -> ColumnarResult<(u64, usize)> {
1454 let source = CountingSegmentSource::new(data);
1455 let reader = SegmentReaderV2::open(Box::new(source.clone()))?;
1456 source.reset();
1457 let batches = reader.read_columns(columns)?;
1458 if batches.is_empty() {
1459 return Err(ColumnarError::InvalidFormat("segment is empty".into()));
1460 }
1461 Ok((source.bytes(), source.calls()))
1462 }
1463
1464 fn reset_last_read_column_indices() {
1465 LAST_READ_COLUMN_INDICES.with(|last| {
1466 *last.borrow_mut() = None;
1467 });
1468 }
1469
1470 fn last_read_column_indices() -> Vec<usize> {
1471 LAST_READ_COLUMN_INDICES.with(|last| {
1472 last.borrow()
1473 .clone()
1474 .expect("read_columnar_segment should record read indices")
1475 })
1476 }
1477
1478 #[test]
1479 fn write_read_disk_mode() {
1480 let dir = tempdir().unwrap();
1481 let wal = dir.path().join("wal.log");
1482 let cfg = EmbeddedConfig::disk(wal);
1483 let db = Database::open_with_config(cfg).unwrap();
1484 let seg_id = db.write_columnar_segment("tbl", make_batch()).unwrap();
1485 let batches = db.read_columnar_segment("tbl", seg_id, None).unwrap();
1486 assert_eq!(batches[0].num_rows(), 3);
1487 }
1488
1489 #[test]
1490 fn read_with_column_names() {
1491 let dir = tempdir().unwrap();
1492 let wal = dir.path().join("wal.log");
1493 let cfg = EmbeddedConfig::disk(wal);
1494 let db = Database::open_with_config(cfg).unwrap();
1495 let seg_id = db.write_columnar_segment("tbl", make_batch()).unwrap();
1496 let batches = db
1497 .read_columnar_segment("tbl", seg_id, Some(&["val"]))
1498 .unwrap();
1499 assert_eq!(batches[0].columns.len(), 1);
1500 if let Column::Int64(vals) = &batches[0].columns[0] {
1501 assert_eq!(vals, &vec![10, 20, 30]);
1502 } else {
1503 panic!("expected int64");
1504 }
1505 }
1506
1507 #[test]
1508 fn disk_projection_pushes_selected_columns_to_segment_reader() {
1509 let dir = tempdir().unwrap();
1510 let wal = dir.path().join("wal.log");
1511 let cfg = EmbeddedConfig::disk(wal);
1512 let db = Database::open_with_config(cfg).unwrap();
1513 let seg_id = db
1514 .write_columnar_segment("wide_tbl", make_wide_batch(12, 4096))
1515 .unwrap();
1516 let table_id = table_id("wide_tbl").unwrap();
1517 let raw = db
1518 .columnar_bridge
1519 .read_segment_raw(table_id, seg_id)
1520 .unwrap();
1521
1522 let all_indices: Vec<usize> = (0..12).collect();
1523 let projection = [2, 7, 10];
1524 let (full_read_bytes, full_read_calls) =
1525 measured_segment_read(raw.data.clone(), &all_indices).unwrap();
1526 let (projected_read_bytes, projected_read_calls) =
1527 measured_segment_read(raw.data, &projection).unwrap();
1528
1529 reset_last_read_column_indices();
1530 let full_batches = db.read_columnar_segment("wide_tbl", seg_id, None).unwrap();
1531 assert_eq!(last_read_column_indices(), all_indices);
1532
1533 reset_last_read_column_indices();
1534 let projected_batches = db
1535 .read_columnar_segment("wide_tbl", seg_id, Some(&["c2", "c7", "c10"]))
1536 .unwrap();
1537 let pushed_indices = last_read_column_indices();
1538 let full_payload_bytes = decoded_payload_bytes(&full_batches);
1539 let projected_payload_bytes = decoded_payload_bytes(&projected_batches);
1540
1541 eprintln!(
1542 "projection pushed_indices={pushed_indices:?} full_read_bytes={full_read_bytes} projected_read_bytes={projected_read_bytes} full_read_calls={full_read_calls} projected_read_calls={projected_read_calls} full_payload_bytes={full_payload_bytes} projected_payload_bytes={projected_payload_bytes}"
1543 );
1544
1545 assert_eq!(pushed_indices, projection);
1546 assert!(projected_read_bytes < full_read_bytes);
1547 assert!(projected_payload_bytes < full_payload_bytes);
1548 assert_eq!(projected_batches[0].schema.columns.len(), projection.len());
1549 }
1550
1551 #[test]
1552 fn in_memory_projection_pushes_selected_columns_to_segment_reader() {
1553 let db = Database::open_with_config(EmbeddedConfig::in_memory()).unwrap();
1554 let seg_id = db
1555 .write_columnar_segment("wide_mem_tbl", make_wide_batch(12, 4096))
1556 .unwrap();
1557
1558 let all_indices: Vec<usize> = (0..12).collect();
1559 let projection = [2, 7, 10];
1560
1561 reset_last_read_column_indices();
1562 let full_batches = db
1563 .read_columnar_segment("wide_mem_tbl", seg_id, None)
1564 .unwrap();
1565 assert_eq!(last_read_column_indices(), all_indices);
1566
1567 reset_last_read_column_indices();
1568 let projected_batches = db
1569 .read_columnar_segment("wide_mem_tbl", seg_id, Some(&["c2", "c7", "c10"]))
1570 .unwrap();
1571 let pushed_indices = last_read_column_indices();
1572 let full_payload_bytes = decoded_payload_bytes(&full_batches);
1573 let projected_payload_bytes = decoded_payload_bytes(&projected_batches);
1574
1575 eprintln!(
1576 "in_memory_projection pushed_indices={pushed_indices:?} full_payload_bytes={full_payload_bytes} projected_payload_bytes={projected_payload_bytes}"
1577 );
1578
1579 assert_eq!(pushed_indices, projection);
1580 assert!(projected_payload_bytes < full_payload_bytes);
1581 assert_eq!(projected_batches[0].schema.columns.len(), projection.len());
1582 }
1583
1584 #[test]
1585 fn in_memory_limit_rejects_large_segment() {
1586 let cfg = EmbeddedConfig::in_memory_with_limit(1);
1587 let db = Database::open_with_config(cfg).unwrap();
1588 let err = db
1589 .write_columnar_segment("tbl", make_batch())
1590 .expect_err("should exceed limit");
1591 assert!(format!("{err}").contains("memory limit exceeded"));
1592 }
1593
1594 #[test]
1595 fn storage_mode_flags() {
1596 let dir = tempdir().unwrap();
1597 let wal = dir.path().join("wal.log");
1598 let disk = Database::open_with_config(EmbeddedConfig::disk(wal)).unwrap();
1599 assert!(matches!(disk.storage_mode(), StorageMode::Disk));
1600
1601 let mem = Database::open_with_config(EmbeddedConfig::in_memory()).unwrap();
1602 assert!(matches!(mem.storage_mode(), StorageMode::InMemory));
1603 }
1604
1605 #[test]
1606 fn transaction_write_and_read() {
1607 let dir = tempdir().unwrap();
1608 let wal = dir.path().join("wal.log");
1609 let db = Database::open_with_config(EmbeddedConfig::disk(wal)).unwrap();
1610 let txn = db.begin(crate::TxnMode::ReadWrite).unwrap();
1611 let seg_id = txn.write_columnar_segment("tbl_txn", make_batch()).unwrap();
1612 txn.commit().unwrap();
1613
1614 let batches = db
1615 .read_columnar_segment("tbl_txn", seg_id, Some(&["id"]))
1616 .unwrap();
1617 assert_eq!(batches[0].num_rows(), 3);
1618 }
1619
1620 #[test]
1621 fn flush_in_memory_paths() {
1622 let dir = tempdir().unwrap();
1623 let db = Database::open_with_config(EmbeddedConfig::in_memory()).unwrap();
1624 let seg_id = db.write_columnar_segment("mem_tbl", make_batch()).unwrap();
1625
1626 let file_path = dir.path().join("seg.bin");
1628 db.flush_in_memory_segment_to_file("mem_tbl", seg_id, &file_path)
1629 .unwrap();
1630 let bytes = std::fs::read(&file_path).unwrap();
1631 assert!(!bytes.is_empty());
1632
1633 let kv_id = db
1635 .flush_in_memory_segment_to_kvs("mem_tbl", seg_id)
1636 .unwrap();
1637 assert_eq!(kv_id, 0);
1638
1639 let alo_path = dir.path().join("out.alopex");
1641 let mut writer =
1642 AlopexFileWriter::new(alo_path.clone(), FileVersion::CURRENT, FileFlags(0)).unwrap();
1643 db.flush_in_memory_segment_to_alopex("mem_tbl", seg_id, &mut writer)
1644 .unwrap();
1645 writer.finalize().unwrap();
1646 assert!(alo_path.exists());
1647 }
1648
1649 #[test]
1650 fn flush_not_in_memory_mode_errors() {
1651 let dir = tempdir().unwrap();
1652 let wal = dir.path().join("wal.log");
1653 let db = Database::open_with_config(EmbeddedConfig::disk(wal)).unwrap();
1654 let err = db
1655 .flush_in_memory_segment_to_kvs("tbl", 0)
1656 .expect_err("should error");
1657 assert!(matches!(err, Error::NotInMemoryMode));
1658 }
1659}