1use std::collections::hash_map::DefaultHasher;
4use std::hash::{Hash, Hasher};
5use std::path::{Path, PathBuf};
6
7use alopex_core::columnar::encoding::Column;
8use alopex_core::columnar::segment_v2::{RecordBatch, SegmentWriterV2};
9use alopex_core::storage::format::AlopexFileWriter;
10use alopex_core::{StorageFactory, StorageMode as CoreStorageMode};
11
12use crate::{Database, Error, Result, SegmentConfigV2, Transaction, TxnMode};
13
14#[cfg(test)]
15thread_local! {
16 static LAST_READ_COLUMN_INDICES: std::cell::RefCell<Option<Vec<usize>>> =
17 const { std::cell::RefCell::new(None) };
18}
19
20#[cfg(test)]
21fn record_read_column_indices(indices: &[usize]) {
22 LAST_READ_COLUMN_INDICES.with(|last| {
23 *last.borrow_mut() = Some(indices.to_vec());
24 });
25}
26
27#[derive(Debug, Clone)]
29pub struct ColumnarSegmentStats {
30 pub row_count: usize,
32 pub column_count: usize,
34 pub size_bytes: usize,
36}
37
38#[derive(Debug, Clone, Copy, PartialEq, Eq)]
40pub enum ColumnarIndexType {
41 Minmax,
43 Bloom,
45}
46
47impl ColumnarIndexType {
48 pub fn as_str(&self) -> &'static str {
50 match self {
51 Self::Minmax => "minmax",
52 Self::Bloom => "bloom",
53 }
54 }
55}
56
57#[derive(Debug, Clone)]
59pub struct ColumnarIndexInfo {
60 pub column: String,
62 pub index_type: ColumnarIndexType,
64}
65
66#[derive(Debug, Clone)]
68pub struct EmbeddedConfig {
69 pub path: Option<PathBuf>,
71 pub storage_mode: StorageMode,
73 pub memory_limit: Option<usize>,
75 pub segment_config: SegmentConfigV2,
77}
78
79impl EmbeddedConfig {
80 pub fn disk(path: PathBuf) -> Self {
82 Self {
83 path: Some(path),
84 storage_mode: StorageMode::Disk,
85 memory_limit: None,
86 segment_config: SegmentConfigV2::default(),
87 }
88 }
89
90 pub fn in_memory() -> Self {
92 Self {
93 path: None,
94 storage_mode: StorageMode::InMemory,
95 memory_limit: None,
96 segment_config: SegmentConfigV2::default(),
97 }
98 }
99
100 pub fn in_memory_with_limit(limit: usize) -> Self {
102 Self {
103 path: None,
104 storage_mode: StorageMode::InMemory,
105 memory_limit: Some(limit),
106 segment_config: SegmentConfigV2::default(),
107 }
108 }
109
110 pub fn with_segment_config(mut self, cfg: SegmentConfigV2) -> Self {
112 self.segment_config = cfg;
113 self
114 }
115}
116
117#[derive(Debug, Clone, Copy, PartialEq, Eq)]
119pub enum StorageMode {
120 Disk,
122 InMemory,
124}
125
126impl Database {
127 pub fn open_with_config(config: EmbeddedConfig) -> Result<Self> {
129 let store = match config.storage_mode {
130 StorageMode::Disk => {
131 let path = config.path.clone().ok_or_else(|| {
132 Error::Core(alopex_core::Error::InvalidFormat(
133 "disk mode requires a path".into(),
134 ))
135 })?;
136 let path = crate::disk_data_dir_path(&path);
137 StorageFactory::create(CoreStorageMode::Disk { path, config: None })
138 .map_err(Error::Core)?
139 }
140 StorageMode::InMemory => StorageFactory::create(CoreStorageMode::Memory {
141 max_size: config.memory_limit,
142 })
143 .map_err(Error::Core)?,
144 };
145
146 Ok(Self::init(
147 store,
148 config.storage_mode,
149 config.memory_limit,
150 config.segment_config,
151 ))
152 }
153
154 pub fn storage_mode(&self) -> StorageMode {
156 self.columnar_mode
157 }
158
159 pub fn write_columnar_segment(&self, table: &str, batch: RecordBatch) -> Result<u64> {
161 let mut writer = SegmentWriterV2::new(self.segment_config.clone());
162 writer
163 .write_batch(batch)
164 .map_err(|e| Error::Core(e.into()))?;
165 let segment = writer.finish().map_err(|e| Error::Core(e.into()))?;
166 let table_id = table_id(table)?;
167
168 match self.columnar_mode {
169 StorageMode::Disk => self
170 .columnar_bridge
171 .write_segment(table_id, &segment)
172 .map_err(|e| Error::Core(e.into())),
173 StorageMode::InMemory => {
174 let store = self.columnar_memory.as_ref().ok_or_else(|| {
175 Error::Core(alopex_core::Error::InvalidFormat(
176 "in-memory columnar store is not initialized".into(),
177 ))
178 })?;
179 store
180 .write_segment(table_id, segment)
181 .map_err(|e| Error::Core(e.into()))
182 }
183 }
184 }
185
186 pub fn write_columnar_segment_with_config(
188 &self,
189 table: &str,
190 batch: RecordBatch,
191 config: SegmentConfigV2,
192 ) -> Result<u64> {
193 let mut writer = SegmentWriterV2::new(config);
194 writer
195 .write_batch(batch)
196 .map_err(|e| Error::Core(e.into()))?;
197 let segment = writer.finish().map_err(|e| Error::Core(e.into()))?;
198 let table_id = table_id(table)?;
199
200 match self.columnar_mode {
201 StorageMode::Disk => self
202 .columnar_bridge
203 .write_segment(table_id, &segment)
204 .map_err(|e| Error::Core(e.into())),
205 StorageMode::InMemory => {
206 let store = self.columnar_memory.as_ref().ok_or_else(|| {
207 Error::Core(alopex_core::Error::InvalidFormat(
208 "in-memory columnar store is not initialized".into(),
209 ))
210 })?;
211 store
212 .write_segment(table_id, segment)
213 .map_err(|e| Error::Core(e.into()))
214 }
215 }
216 }
217
218 pub fn read_columnar_segment(
220 &self,
221 table: &str,
222 segment_id: u64,
223 columns: Option<&[&str]>,
224 ) -> Result<Vec<RecordBatch>> {
225 let table_id = table_id(table)?;
226 match self.columnar_mode {
227 StorageMode::Disk => {
228 let read_indices: Vec<usize> = if let Some(names) = columns {
229 let segment = self
230 .columnar_bridge
231 .read_segment_raw(table_id, segment_id)
232 .map_err(|e| Error::Core(e.into()))?;
233 resolve_indices_from_schema(&segment.meta.schema, names)?
234 } else {
235 let column_count = self
236 .columnar_bridge
237 .column_count(table_id, segment_id)
238 .map_err(|e| Error::Core(e.into()))?;
239 (0..column_count).collect()
240 };
241
242 #[cfg(test)]
243 record_read_column_indices(&read_indices);
244
245 self.columnar_bridge
246 .read_segment(table_id, segment_id, &read_indices)
247 .map_err(|e| Error::Core(e.into()))
248 }
249 StorageMode::InMemory => {
250 let store = self.columnar_memory.as_ref().ok_or_else(|| {
251 Error::Core(alopex_core::Error::InvalidFormat(
252 "in-memory columnar store is not initialized".into(),
253 ))
254 })?;
255 let read_indices: Vec<usize> = if let Some(names) = columns {
256 let schema = store
257 .schema(table_id, segment_id)
258 .map_err(|e| Error::Core(e.into()))?;
259 resolve_indices_from_schema(&schema, names)?
260 } else {
261 let column_count = store
262 .column_count(table_id, segment_id)
263 .map_err(|e| Error::Core(e.into()))?;
264 (0..column_count).collect()
265 };
266
267 #[cfg(test)]
268 record_read_column_indices(&read_indices);
269
270 store
271 .read_segment(table_id, segment_id, &read_indices)
272 .map_err(|e| Error::Core(e.into()))
273 }
274 }
275 }
276
277 pub fn in_memory_usage(&self) -> Option<u64> {
279 if self.columnar_mode == StorageMode::InMemory {
280 self.columnar_memory.as_ref().map(|m| m.memory_usage())
281 } else {
282 None
283 }
284 }
285
286 pub fn open_in_memory_with_limit(limit: usize) -> Result<Self> {
288 Self::open_with_config(EmbeddedConfig::in_memory_with_limit(limit))
289 }
290
291 pub fn resolve_table_id(&self, table: &str) -> Result<u32> {
293 table_id(table)
294 }
295
296 pub fn scan_columnar_segment(
301 &self,
302 segment_id: &str,
303 ) -> Result<Vec<Vec<alopex_sql::SqlValue>>> {
304 let (table_id, seg_id) = parse_segment_id(segment_id)?;
305 let all_indices: Vec<usize> = match self.columnar_mode {
306 StorageMode::Disk => {
307 let count = self
308 .columnar_bridge
309 .column_count(table_id, seg_id)
310 .map_err(|e| Error::Core(e.into()))?;
311 (0..count).collect()
312 }
313 StorageMode::InMemory => {
314 let store = self.columnar_memory.as_ref().ok_or_else(|| {
315 Error::Core(alopex_core::Error::InvalidFormat(
316 "in-memory columnar store is not initialized".into(),
317 ))
318 })?;
319 let count = store
320 .column_count(table_id, seg_id)
321 .map_err(|e| Error::Core(e.into()))?;
322 (0..count).collect()
323 }
324 };
325
326 let batches = match self.columnar_mode {
327 StorageMode::Disk => self
328 .columnar_bridge
329 .read_segment(table_id, seg_id, &all_indices)
330 .map_err(|e| Error::Core(e.into()))?,
331 StorageMode::InMemory => self
332 .columnar_memory
333 .as_ref()
334 .ok_or_else(|| {
335 Error::Core(alopex_core::Error::InvalidFormat(
336 "in-memory columnar store is not initialized".into(),
337 ))
338 })?
339 .read_segment(table_id, seg_id, &all_indices)
340 .map_err(|e| Error::Core(e.into()))?,
341 };
342
343 let mut rows = Vec::new();
345 for batch in batches {
346 let num_rows = batch.num_rows();
347 for row_idx in 0..num_rows {
348 let mut row = Vec::with_capacity(batch.columns.len());
349 for col in &batch.columns {
350 let sql_val = column_value_to_sql_value(col, row_idx);
351 row.push(sql_val);
352 }
353 rows.push(row);
354 }
355 }
356 Ok(rows)
357 }
358
359 pub fn scan_columnar_segment_batches(&self, segment_id: &str) -> Result<Vec<RecordBatch>> {
367 let (table_id, seg_id) = parse_segment_id(segment_id)?;
368 let all_indices: Vec<usize> = match self.columnar_mode {
369 StorageMode::Disk => {
370 let count = self
371 .columnar_bridge
372 .column_count(table_id, seg_id)
373 .map_err(|e| Error::Core(e.into()))?;
374 (0..count).collect()
375 }
376 StorageMode::InMemory => {
377 let store = self.columnar_memory.as_ref().ok_or_else(|| {
378 Error::Core(alopex_core::Error::InvalidFormat(
379 "in-memory columnar store is not initialized".into(),
380 ))
381 })?;
382 let count = store
383 .column_count(table_id, seg_id)
384 .map_err(|e| Error::Core(e.into()))?;
385 (0..count).collect()
386 }
387 };
388
389 match self.columnar_mode {
390 StorageMode::Disk => self
391 .columnar_bridge
392 .read_segment(table_id, seg_id, &all_indices)
393 .map_err(|e| Error::Core(e.into())),
394 StorageMode::InMemory => self
395 .columnar_memory
396 .as_ref()
397 .ok_or_else(|| {
398 Error::Core(alopex_core::Error::InvalidFormat(
399 "in-memory columnar store is not initialized".into(),
400 ))
401 })?
402 .read_segment(table_id, seg_id, &all_indices)
403 .map_err(|e| Error::Core(e.into())),
404 }
405 }
406
407 pub fn scan_columnar_segment_streaming(&self, segment_id: &str) -> Result<ColumnarRowIterator> {
414 let batches = self.scan_columnar_segment_batches(segment_id)?;
415 Ok(ColumnarRowIterator::new(batches))
416 }
417
418 pub fn get_columnar_segment_stats(&self, segment_id: &str) -> Result<ColumnarSegmentStats> {
422 let (table_id, seg_id) = parse_segment_id(segment_id)?;
423
424 match self.columnar_mode {
425 StorageMode::Disk => {
426 let column_count = self
427 .columnar_bridge
428 .column_count(table_id, seg_id)
429 .map_err(|e| Error::Core(e.into()))?;
430 let batches = self
431 .columnar_bridge
432 .read_segment(table_id, seg_id, &(0..column_count).collect::<Vec<_>>())
433 .map_err(|e| Error::Core(e.into()))?;
434 let row_count: usize = batches.iter().map(|b| b.num_rows()).sum();
435
436 Ok(ColumnarSegmentStats {
437 row_count,
438 column_count,
439 size_bytes: 0, })
441 }
442 StorageMode::InMemory => {
443 let store = self.columnar_memory.as_ref().ok_or_else(|| {
444 Error::Core(alopex_core::Error::InvalidFormat(
445 "in-memory columnar store is not initialized".into(),
446 ))
447 })?;
448 let column_count = store
449 .column_count(table_id, seg_id)
450 .map_err(|e| Error::Core(e.into()))?;
451 let batches = store
452 .read_segment(table_id, seg_id, &(0..column_count).collect::<Vec<_>>())
453 .map_err(|e| Error::Core(e.into()))?;
454 let row_count: usize = batches.iter().map(|b| b.num_rows()).sum();
455
456 Ok(ColumnarSegmentStats {
457 row_count,
458 column_count,
459 size_bytes: 0, })
461 }
462 }
463 }
464
465 pub fn list_columnar_segments(&self) -> Result<Vec<String>> {
469 match self.columnar_mode {
470 StorageMode::Disk => {
471 let segments = self
472 .columnar_bridge
473 .list_segments()
474 .map_err(|e| Error::Core(e.into()))?;
475 Ok(segments
476 .into_iter()
477 .map(|(table_id, seg_id)| format!("{}:{}", table_id, seg_id))
478 .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 segments = store.list_segments();
487 Ok(segments
488 .into_iter()
489 .map(|(table_id, seg_id)| format!("{}:{}", table_id, seg_id))
490 .collect())
491 }
492 }
493 }
494
495 pub fn create_columnar_index(
497 &self,
498 segment_id: &str,
499 column: &str,
500 index_type: ColumnarIndexType,
501 ) -> Result<()> {
502 let _ = self.get_columnar_segment_stats(segment_id)?;
503 let key = columnar_index_key(segment_id, column);
504 let value = index_type.as_str().as_bytes().to_vec();
505 let mut txn = self.begin(TxnMode::ReadWrite)?;
506 txn.put(&key, &value)?;
507 txn.commit()?;
508 Ok(())
509 }
510
511 pub fn list_columnar_indexes(&self, segment_id: &str) -> Result<Vec<ColumnarIndexInfo>> {
513 let _ = self.get_columnar_segment_stats(segment_id)?;
514 let prefix = columnar_index_prefix(segment_id);
515 let mut txn = self.begin(TxnMode::ReadOnly)?;
516 let mut entries = Vec::new();
517 for (key, value) in txn.scan_prefix(&prefix)? {
518 let column = parse_index_column(segment_id, &key)?;
519 let index_type = parse_index_type(&value)?;
520 entries.push(ColumnarIndexInfo { column, index_type });
521 }
522 txn.commit()?;
523 Ok(entries)
524 }
525
526 pub fn drop_columnar_index(&self, segment_id: &str, column: &str) -> Result<()> {
528 let _ = self.get_columnar_segment_stats(segment_id)?;
529 let key = columnar_index_key(segment_id, column);
530 let mut txn = self.begin(TxnMode::ReadWrite)?;
531 let exists = txn.get(&key)?.is_some();
532 if !exists {
533 txn.rollback()?;
534 return Err(Error::IndexNotFound(format!(
535 "columnar index {}:{}",
536 segment_id, column
537 )));
538 }
539 txn.delete(&key)?;
540 txn.commit()?;
541 Ok(())
542 }
543
544 pub fn flush_in_memory_segment_to_file(
546 &self,
547 table: &str,
548 segment_id: u64,
549 path: &Path,
550 ) -> Result<()> {
551 let store = self
552 .columnar_memory
553 .as_ref()
554 .ok_or(Error::NotInMemoryMode)?;
555 let table_id = table_id(table)?;
556 store
557 .flush_to_segment_file(table_id, segment_id, path)
558 .map_err(|e| Error::Core(e.into()))
559 }
560
561 pub fn flush_in_memory_segment_to_kvs(&self, table: &str, segment_id: u64) -> Result<u64> {
563 let store = self
564 .columnar_memory
565 .as_ref()
566 .ok_or(Error::NotInMemoryMode)?;
567 let table_id = table_id(table)?;
568 store
569 .flush_to_kvs(table_id, segment_id, &self.columnar_bridge)
570 .map_err(|e| Error::Core(e.into()))
571 }
572
573 pub fn flush_in_memory_segment_to_alopex(
575 &self,
576 table: &str,
577 segment_id: u64,
578 writer: &mut AlopexFileWriter,
579 ) -> Result<u32> {
580 let store = self
581 .columnar_memory
582 .as_ref()
583 .ok_or(Error::NotInMemoryMode)?;
584 let table_id = table_id(table)?;
585 store
586 .flush_to_alopex(table_id, segment_id, writer)
587 .map_err(|e| Error::Core(e.into()))
588 }
589}
590
591impl<'a> Transaction<'a> {
592 pub fn storage_mode(&self) -> StorageMode {
594 self.db.storage_mode()
595 }
596
597 pub fn write_columnar_segment(&self, table: &str, batch: RecordBatch) -> Result<u64> {
599 self.db.write_columnar_segment(table, batch)
600 }
601
602 pub fn read_columnar_segment(
604 &self,
605 table: &str,
606 segment_id: u64,
607 columns: Option<&[&str]>,
608 ) -> Result<Vec<RecordBatch>> {
609 self.db.read_columnar_segment(table, segment_id, columns)
610 }
611}
612
613fn table_id(table: &str) -> Result<u32> {
614 if table.is_empty() {
615 return Err(Error::TableNotFound("table name is empty".into()));
616 }
617 let mut hasher = DefaultHasher::new();
618 table.hash(&mut hasher);
619 Ok((hasher.finish() & 0xffff_ffff) as u32)
620}
621
622fn resolve_indices_from_schema(
623 schema: &alopex_core::columnar::segment_v2::Schema,
624 names: &[&str],
625) -> Result<Vec<usize>> {
626 let mut indices = Vec::with_capacity(names.len());
627 for name in names {
628 let pos = schema
629 .columns
630 .iter()
631 .position(|c| c.name == *name)
632 .ok_or_else(|| {
633 Error::Core(alopex_core::Error::InvalidFormat(format!(
634 "column not found: {name}"
635 )))
636 })?;
637 indices.push(pos);
638 }
639 Ok(indices)
640}
641
642const COLUMNAR_INDEX_PREFIX: &str = "__alopex_columnar_index__:";
643
644fn columnar_index_key(segment: &str, column: &str) -> Vec<u8> {
645 let mut key =
646 String::with_capacity(COLUMNAR_INDEX_PREFIX.len() + segment.len() + column.len() + 1);
647 key.push_str(COLUMNAR_INDEX_PREFIX);
648 key.push_str(segment);
649 key.push(':');
650 key.push_str(column);
651 key.into_bytes()
652}
653
654fn columnar_index_prefix(segment: &str) -> Vec<u8> {
655 let mut key = String::with_capacity(COLUMNAR_INDEX_PREFIX.len() + segment.len() + 1);
656 key.push_str(COLUMNAR_INDEX_PREFIX);
657 key.push_str(segment);
658 key.push(':');
659 key.into_bytes()
660}
661
662fn parse_index_column(segment: &str, key: &[u8]) -> Result<String> {
663 let prefix = columnar_index_prefix(segment);
664 if !key.starts_with(&prefix) {
665 return Err(Error::Core(alopex_core::Error::InvalidFormat(
666 "columnar index key is invalid".into(),
667 )));
668 }
669 let suffix = &key[prefix.len()..];
670 String::from_utf8(suffix.to_vec()).map_err(|_| {
671 Error::Core(alopex_core::Error::InvalidFormat(
672 "columnar index column is not valid UTF-8".into(),
673 ))
674 })
675}
676
677fn parse_index_type(raw: &[u8]) -> Result<ColumnarIndexType> {
678 let value = std::str::from_utf8(raw).map_err(|_| {
679 Error::Core(alopex_core::Error::InvalidFormat(
680 "columnar index type is not valid UTF-8".into(),
681 ))
682 })?;
683 match value {
684 "minmax" => Ok(ColumnarIndexType::Minmax),
685 "bloom" => Ok(ColumnarIndexType::Bloom),
686 other => Err(Error::Core(alopex_core::Error::InvalidFormat(format!(
687 "unknown columnar index type: {other}"
688 )))),
689 }
690}
691
692fn parse_segment_id(segment_id: &str) -> Result<(u32, u64)> {
696 let parts: Vec<&str> = segment_id.split(':').collect();
697 if parts.len() != 2 {
698 return Err(Error::Core(alopex_core::Error::InvalidFormat(format!(
699 "invalid segment ID format: expected 'table_id:segment_id', got '{}'",
700 segment_id
701 ))));
702 }
703
704 let table_id: u32 = parts[0].parse().map_err(|_| {
705 Error::Core(alopex_core::Error::InvalidFormat(format!(
706 "invalid table_id in segment ID: '{}'",
707 parts[0]
708 )))
709 })?;
710
711 let seg_id: u64 = parts[1].parse().map_err(|_| {
712 Error::Core(alopex_core::Error::InvalidFormat(format!(
713 "invalid segment_id in segment ID: '{}'",
714 parts[1]
715 )))
716 })?;
717
718 Ok((table_id, seg_id))
719}
720
721fn column_value_to_sql_value(col: &Column, row_idx: usize) -> alopex_sql::SqlValue {
723 match col {
724 Column::Int64(vals) => vals
725 .get(row_idx)
726 .map(|&v| alopex_sql::SqlValue::BigInt(v))
727 .unwrap_or(alopex_sql::SqlValue::Null),
728 Column::Float32(vals) => vals
729 .get(row_idx)
730 .map(|&v| alopex_sql::SqlValue::Float(v))
731 .unwrap_or(alopex_sql::SqlValue::Null),
732 Column::Float64(vals) => vals
733 .get(row_idx)
734 .map(|&v| alopex_sql::SqlValue::Double(v))
735 .unwrap_or(alopex_sql::SqlValue::Null),
736 Column::Bool(vals) => vals
737 .get(row_idx)
738 .map(|&v| alopex_sql::SqlValue::Boolean(v))
739 .unwrap_or(alopex_sql::SqlValue::Null),
740 Column::Binary(vals) => vals
741 .get(row_idx)
742 .map(|v| alopex_sql::SqlValue::Blob(v.clone()))
743 .unwrap_or(alopex_sql::SqlValue::Null),
744 Column::Fixed { values, .. } => values
745 .get(row_idx)
746 .map(|v| alopex_sql::SqlValue::Blob(v.clone()))
747 .unwrap_or(alopex_sql::SqlValue::Null),
748 }
749}
750
751pub struct ColumnarRowIterator {
760 batches: Vec<RecordBatch>,
762 batch_idx: usize,
764 row_idx: usize,
766}
767
768impl ColumnarRowIterator {
769 pub fn new(batches: Vec<RecordBatch>) -> Self {
771 Self {
772 batches,
773 batch_idx: 0,
774 row_idx: 0,
775 }
776 }
777
778 pub fn batch_count(&self) -> usize {
780 self.batches.len()
781 }
782
783 pub fn current_batch(&self) -> Option<&RecordBatch> {
785 self.batches.get(self.batch_idx)
786 }
787}
788
789impl Iterator for ColumnarRowIterator {
790 type Item = Vec<alopex_sql::SqlValue>;
791
792 fn next(&mut self) -> Option<Self::Item> {
793 loop {
794 if self.batch_idx >= self.batches.len() {
796 return None;
797 }
798
799 let batch = &self.batches[self.batch_idx];
800 let row_count = batch.num_rows();
801
802 if self.row_idx >= row_count {
804 self.batch_idx += 1;
805 self.row_idx = 0;
806 continue;
807 }
808
809 let row_idx = self.row_idx;
811 self.row_idx += 1;
812
813 let mut row = Vec::with_capacity(batch.columns.len());
814 for col in &batch.columns {
815 let sql_val = column_value_to_sql_value(col, row_idx);
816 row.push(sql_val);
817 }
818 return Some(row);
819 }
820 }
821}
822
823#[cfg(test)]
824mod tests {
825 use super::*;
826 use alopex_core::columnar::encoding::{Column, LogicalType};
827 use alopex_core::columnar::error::{ColumnarError, Result as ColumnarResult};
828 use alopex_core::columnar::segment_v2::{ColumnSchema, Schema, SegmentReaderV2, SegmentSource};
829 use alopex_core::storage::format::{AlopexFileWriter, FileFlags, FileVersion};
830 use std::sync::atomic::{AtomicU64, AtomicUsize, Ordering};
831 use std::sync::Arc;
832 use tempfile::tempdir;
833
834 fn make_batch() -> RecordBatch {
835 let schema = Schema {
836 columns: vec![
837 ColumnSchema {
838 name: "id".into(),
839 logical_type: LogicalType::Int64,
840 nullable: false,
841 fixed_len: None,
842 },
843 ColumnSchema {
844 name: "val".into(),
845 logical_type: LogicalType::Int64,
846 nullable: false,
847 fixed_len: None,
848 },
849 ],
850 };
851 RecordBatch::new(
852 schema,
853 vec![
854 Column::Int64(vec![1, 2, 3]),
855 Column::Int64(vec![10, 20, 30]),
856 ],
857 vec![None, None],
858 )
859 }
860
861 fn make_wide_batch(column_count: usize, row_count: usize) -> RecordBatch {
862 let schema = Schema {
863 columns: (0..column_count)
864 .map(|idx| ColumnSchema {
865 name: format!("c{idx}"),
866 logical_type: LogicalType::Int64,
867 nullable: false,
868 fixed_len: None,
869 })
870 .collect(),
871 };
872 let columns = (0..column_count)
873 .map(|idx| {
874 Column::Int64(
875 (0..row_count)
876 .map(|row| (idx as i64 * 1_000_000) + row as i64)
877 .collect(),
878 )
879 })
880 .collect();
881 RecordBatch::new(schema, columns, vec![None; column_count])
882 }
883
884 fn decoded_payload_bytes(batches: &[RecordBatch]) -> usize {
885 batches
886 .iter()
887 .flat_map(|batch| batch.columns.iter())
888 .map(|column| match column {
889 Column::Int64(values) => values.len() * std::mem::size_of::<i64>(),
890 Column::Float32(values) => values.len() * std::mem::size_of::<f32>(),
891 Column::Float64(values) => values.len() * std::mem::size_of::<f64>(),
892 Column::Bool(values) => values.len() * std::mem::size_of::<bool>(),
893 Column::Binary(values) => values.iter().map(Vec::len).sum(),
894 Column::Fixed { values, .. } => values.iter().map(Vec::len).sum(),
895 })
896 .sum()
897 }
898
899 #[derive(Debug, Clone)]
900 struct CountingSegmentSource {
901 data: Arc<Vec<u8>>,
902 bytes: Arc<AtomicU64>,
903 calls: Arc<AtomicUsize>,
904 }
905
906 impl CountingSegmentSource {
907 fn new(data: Vec<u8>) -> Self {
908 Self {
909 data: Arc::new(data),
910 bytes: Arc::new(AtomicU64::new(0)),
911 calls: Arc::new(AtomicUsize::new(0)),
912 }
913 }
914
915 fn reset(&self) {
916 self.bytes.store(0, Ordering::Relaxed);
917 self.calls.store(0, Ordering::Relaxed);
918 }
919
920 fn bytes(&self) -> u64 {
921 self.bytes.load(Ordering::Relaxed)
922 }
923
924 fn calls(&self) -> usize {
925 self.calls.load(Ordering::Relaxed)
926 }
927 }
928
929 impl SegmentSource for CountingSegmentSource {
930 fn read_range(&self, offset: u64, len: u64) -> ColumnarResult<Vec<u8>> {
931 self.bytes.fetch_add(len, Ordering::Relaxed);
932 self.calls.fetch_add(1, Ordering::Relaxed);
933 let start = offset as usize;
934 let end = start + len as usize;
935 if end > self.data.len() {
936 return Err(ColumnarError::InvalidFormat("range out of bounds".into()));
937 }
938 Ok(self.data[start..end].to_vec())
939 }
940
941 fn total_size(&self) -> u64 {
942 self.data.len() as u64
943 }
944 }
945
946 fn measured_segment_read(data: Vec<u8>, columns: &[usize]) -> ColumnarResult<(u64, usize)> {
947 let source = CountingSegmentSource::new(data);
948 let reader = SegmentReaderV2::open(Box::new(source.clone()))?;
949 source.reset();
950 let batches = reader.read_columns(columns)?;
951 if batches.is_empty() {
952 return Err(ColumnarError::InvalidFormat("segment is empty".into()));
953 }
954 Ok((source.bytes(), source.calls()))
955 }
956
957 fn reset_last_read_column_indices() {
958 LAST_READ_COLUMN_INDICES.with(|last| {
959 *last.borrow_mut() = None;
960 });
961 }
962
963 fn last_read_column_indices() -> Vec<usize> {
964 LAST_READ_COLUMN_INDICES.with(|last| {
965 last.borrow()
966 .clone()
967 .expect("read_columnar_segment should record read indices")
968 })
969 }
970
971 #[test]
972 fn write_read_disk_mode() {
973 let dir = tempdir().unwrap();
974 let wal = dir.path().join("wal.log");
975 let cfg = EmbeddedConfig::disk(wal);
976 let db = Database::open_with_config(cfg).unwrap();
977 let seg_id = db.write_columnar_segment("tbl", make_batch()).unwrap();
978 let batches = db.read_columnar_segment("tbl", seg_id, None).unwrap();
979 assert_eq!(batches[0].num_rows(), 3);
980 }
981
982 #[test]
983 fn read_with_column_names() {
984 let dir = tempdir().unwrap();
985 let wal = dir.path().join("wal.log");
986 let cfg = EmbeddedConfig::disk(wal);
987 let db = Database::open_with_config(cfg).unwrap();
988 let seg_id = db.write_columnar_segment("tbl", make_batch()).unwrap();
989 let batches = db
990 .read_columnar_segment("tbl", seg_id, Some(&["val"]))
991 .unwrap();
992 assert_eq!(batches[0].columns.len(), 1);
993 if let Column::Int64(vals) = &batches[0].columns[0] {
994 assert_eq!(vals, &vec![10, 20, 30]);
995 } else {
996 panic!("expected int64");
997 }
998 }
999
1000 #[test]
1001 fn disk_projection_pushes_selected_columns_to_segment_reader() {
1002 let dir = tempdir().unwrap();
1003 let wal = dir.path().join("wal.log");
1004 let cfg = EmbeddedConfig::disk(wal);
1005 let db = Database::open_with_config(cfg).unwrap();
1006 let seg_id = db
1007 .write_columnar_segment("wide_tbl", make_wide_batch(12, 4096))
1008 .unwrap();
1009 let table_id = table_id("wide_tbl").unwrap();
1010 let raw = db
1011 .columnar_bridge
1012 .read_segment_raw(table_id, seg_id)
1013 .unwrap();
1014
1015 let all_indices: Vec<usize> = (0..12).collect();
1016 let projection = [2, 7, 10];
1017 let (full_read_bytes, full_read_calls) =
1018 measured_segment_read(raw.data.clone(), &all_indices).unwrap();
1019 let (projected_read_bytes, projected_read_calls) =
1020 measured_segment_read(raw.data, &projection).unwrap();
1021
1022 reset_last_read_column_indices();
1023 let full_batches = db.read_columnar_segment("wide_tbl", seg_id, None).unwrap();
1024 assert_eq!(last_read_column_indices(), all_indices);
1025
1026 reset_last_read_column_indices();
1027 let projected_batches = db
1028 .read_columnar_segment("wide_tbl", seg_id, Some(&["c2", "c7", "c10"]))
1029 .unwrap();
1030 let pushed_indices = last_read_column_indices();
1031 let full_payload_bytes = decoded_payload_bytes(&full_batches);
1032 let projected_payload_bytes = decoded_payload_bytes(&projected_batches);
1033
1034 eprintln!(
1035 "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}"
1036 );
1037
1038 assert_eq!(pushed_indices, projection);
1039 assert!(projected_read_bytes < full_read_bytes);
1040 assert!(projected_payload_bytes < full_payload_bytes);
1041 assert_eq!(projected_batches[0].schema.columns.len(), projection.len());
1042 }
1043
1044 #[test]
1045 fn in_memory_projection_pushes_selected_columns_to_segment_reader() {
1046 let db = Database::open_with_config(EmbeddedConfig::in_memory()).unwrap();
1047 let seg_id = db
1048 .write_columnar_segment("wide_mem_tbl", make_wide_batch(12, 4096))
1049 .unwrap();
1050
1051 let all_indices: Vec<usize> = (0..12).collect();
1052 let projection = [2, 7, 10];
1053
1054 reset_last_read_column_indices();
1055 let full_batches = db
1056 .read_columnar_segment("wide_mem_tbl", seg_id, None)
1057 .unwrap();
1058 assert_eq!(last_read_column_indices(), all_indices);
1059
1060 reset_last_read_column_indices();
1061 let projected_batches = db
1062 .read_columnar_segment("wide_mem_tbl", seg_id, Some(&["c2", "c7", "c10"]))
1063 .unwrap();
1064 let pushed_indices = last_read_column_indices();
1065 let full_payload_bytes = decoded_payload_bytes(&full_batches);
1066 let projected_payload_bytes = decoded_payload_bytes(&projected_batches);
1067
1068 eprintln!(
1069 "in_memory_projection pushed_indices={pushed_indices:?} full_payload_bytes={full_payload_bytes} projected_payload_bytes={projected_payload_bytes}"
1070 );
1071
1072 assert_eq!(pushed_indices, projection);
1073 assert!(projected_payload_bytes < full_payload_bytes);
1074 assert_eq!(projected_batches[0].schema.columns.len(), projection.len());
1075 }
1076
1077 #[test]
1078 fn in_memory_limit_rejects_large_segment() {
1079 let cfg = EmbeddedConfig::in_memory_with_limit(1);
1080 let db = Database::open_with_config(cfg).unwrap();
1081 let err = db
1082 .write_columnar_segment("tbl", make_batch())
1083 .expect_err("should exceed limit");
1084 assert!(format!("{err}").contains("memory limit exceeded"));
1085 }
1086
1087 #[test]
1088 fn storage_mode_flags() {
1089 let dir = tempdir().unwrap();
1090 let wal = dir.path().join("wal.log");
1091 let disk = Database::open_with_config(EmbeddedConfig::disk(wal)).unwrap();
1092 assert!(matches!(disk.storage_mode(), StorageMode::Disk));
1093
1094 let mem = Database::open_with_config(EmbeddedConfig::in_memory()).unwrap();
1095 assert!(matches!(mem.storage_mode(), StorageMode::InMemory));
1096 }
1097
1098 #[test]
1099 fn transaction_write_and_read() {
1100 let dir = tempdir().unwrap();
1101 let wal = dir.path().join("wal.log");
1102 let db = Database::open_with_config(EmbeddedConfig::disk(wal)).unwrap();
1103 let txn = db.begin(crate::TxnMode::ReadWrite).unwrap();
1104 let seg_id = txn.write_columnar_segment("tbl_txn", make_batch()).unwrap();
1105 txn.commit().unwrap();
1106
1107 let batches = db
1108 .read_columnar_segment("tbl_txn", seg_id, Some(&["id"]))
1109 .unwrap();
1110 assert_eq!(batches[0].num_rows(), 3);
1111 }
1112
1113 #[test]
1114 fn flush_in_memory_paths() {
1115 let dir = tempdir().unwrap();
1116 let db = Database::open_with_config(EmbeddedConfig::in_memory()).unwrap();
1117 let seg_id = db.write_columnar_segment("mem_tbl", make_batch()).unwrap();
1118
1119 let file_path = dir.path().join("seg.bin");
1121 db.flush_in_memory_segment_to_file("mem_tbl", seg_id, &file_path)
1122 .unwrap();
1123 let bytes = std::fs::read(&file_path).unwrap();
1124 assert!(!bytes.is_empty());
1125
1126 let kv_id = db
1128 .flush_in_memory_segment_to_kvs("mem_tbl", seg_id)
1129 .unwrap();
1130 assert_eq!(kv_id, 0);
1131
1132 let alo_path = dir.path().join("out.alopex");
1134 let mut writer =
1135 AlopexFileWriter::new(alo_path.clone(), FileVersion::CURRENT, FileFlags(0)).unwrap();
1136 db.flush_in_memory_segment_to_alopex("mem_tbl", seg_id, &mut writer)
1137 .unwrap();
1138 writer.finalize().unwrap();
1139 assert!(alo_path.exists());
1140 }
1141
1142 #[test]
1143 fn flush_not_in_memory_mode_errors() {
1144 let dir = tempdir().unwrap();
1145 let wal = dir.path().join("wal.log");
1146 let db = Database::open_with_config(EmbeddedConfig::disk(wal)).unwrap();
1147 let err = db
1148 .flush_in_memory_segment_to_kvs("tbl", 0)
1149 .expect_err("should error");
1150 assert!(matches!(err, Error::NotInMemoryMode));
1151 }
1152}