Skip to main content

alopex_embedded/
columnar_api.rs

1//! カラムナーストレージの埋め込み API 拡張。
2
3use 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/// セグメント統計情報。
28#[derive(Debug, Clone)]
29pub struct ColumnarSegmentStats {
30    /// セグメント内の行数。
31    pub row_count: usize,
32    /// セグメント内のカラム数。
33    pub column_count: usize,
34    /// セグメントのサイズ(バイト)。
35    pub size_bytes: usize,
36}
37
38/// カラムナーインデックス種別。
39#[derive(Debug, Clone, Copy, PartialEq, Eq)]
40pub enum ColumnarIndexType {
41    /// 最小値/最大値インデックス。
42    Minmax,
43    /// Bloom フィルタインデックス。
44    Bloom,
45}
46
47impl ColumnarIndexType {
48    /// 文字列表現を返す。
49    pub fn as_str(&self) -> &'static str {
50        match self {
51            Self::Minmax => "minmax",
52            Self::Bloom => "bloom",
53        }
54    }
55}
56
57/// カラムナーインデックス情報。
58#[derive(Debug, Clone)]
59pub struct ColumnarIndexInfo {
60    /// 対象カラム名。
61    pub column: String,
62    /// インデックス種別。
63    pub index_type: ColumnarIndexType,
64}
65
66/// カラムナー関連設定。
67#[derive(Debug, Clone)]
68pub struct EmbeddedConfig {
69    /// データパス(Disk モード時に必須)。
70    pub path: Option<PathBuf>,
71    /// カラムナーストレージモード。
72    pub storage_mode: StorageMode,
73    /// InMemory モードのメモリ上限(バイト)。
74    pub memory_limit: Option<usize>,
75    /// セグメント設定。
76    pub segment_config: SegmentConfigV2,
77}
78
79impl EmbeddedConfig {
80    /// ディスクモードで初期化。
81    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    /// インメモリモードで初期化(無制限)。
91    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    /// インメモリモードでメモリ上限を設定。
101    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    /// セグメント設定を上書き。
111    pub fn with_segment_config(mut self, cfg: SegmentConfigV2) -> Self {
112        self.segment_config = cfg;
113        self
114    }
115}
116
117/// カラムナー用ストレージモード。
118#[derive(Debug, Clone, Copy, PartialEq, Eq)]
119pub enum StorageMode {
120    /// KVS 経由でディスク永続化。
121    Disk,
122    /// 完全インメモリ保持。
123    InMemory,
124}
125
126impl Database {
127    /// 構成付きでデータベースを開く(カラムナー機能を初期化)。
128    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    /// 現在のカラムナーストレージモードを返す。
155    pub fn storage_mode(&self) -> StorageMode {
156        self.columnar_mode
157    }
158
159    /// カラムナーセグメントを書き込む。
160    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    /// カラムナーセグメントを書き込む(構成上書き)。
187    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    /// カラムナーセグメントを読み取る(カラム名指定オプション付き)。
219    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    /// InMemory モード時のメモリ使用量を返す。Disk モードでは None。
278    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    /// メモリ上限付きでインメモリ DB を開く。
287    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    /// テーブル名から内部 ID を解決する。
292    pub fn resolve_table_id(&self, table: &str) -> Result<u32> {
293        table_id(table)
294    }
295
296    /// Scan a columnar segment by string ID.
297    ///
298    /// The segment ID format is `{table_id}:{segment_id}` (e.g., "12345:1").
299    /// Returns rows as a vector of SqlValue vectors.
300    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        // Convert RecordBatch to Vec<Vec<SqlValue>>
344        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    /// Scan a columnar segment by string ID, returning RecordBatches for streaming (FR-7).
360    ///
361    /// This method returns raw `RecordBatch` objects, allowing the caller to iterate
362    /// over rows without materializing all data upfront. Use this for large datasets
363    /// where streaming is required.
364    ///
365    /// The segment ID format is `{table_id}:{segment_id}` (e.g., "12345:1").
366    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    /// Create a streaming row iterator over a columnar segment (FR-7).
408    ///
409    /// This returns a `ColumnarRowIterator` that yields rows one at a time from
410    /// the underlying RecordBatches, without materializing all rows upfront.
411    ///
412    /// The segment ID format is `{table_id}:{segment_id}` (e.g., "12345:1").
413    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    /// Get statistics for a columnar segment by string ID.
419    ///
420    /// The segment ID format is `{table_id}:{segment_id}` (e.g., "12345:1").
421    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, // Size not available in current implementation
440                })
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, // Size not available in current implementation
460                })
461            }
462        }
463    }
464
465    /// List all columnar segments.
466    ///
467    /// Returns segment IDs in the format `{table_id}:{segment_id}`.
468    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    /// Create a columnar index for a segment/column.
496    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    /// List columnar indexes for a segment.
512    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    /// Drop a columnar index for a segment/column.
527    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    /// InMemory モードのセグメントをファイルへフラッシュする。
545    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    /// InMemory モードのセグメントを KVS へフラッシュする。
562    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    /// InMemory モードのセグメントを `.alopex` ファイルへフラッシュする。
574    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    /// 現在のカラムナーストレージモードを返す。
593    pub fn storage_mode(&self) -> StorageMode {
594        self.db.storage_mode()
595    }
596
597    /// カラムナーセグメントを書き込む(トランザクションコンテキスト利用)。
598    pub fn write_columnar_segment(&self, table: &str, batch: RecordBatch) -> Result<u64> {
599        self.db.write_columnar_segment(table, batch)
600    }
601
602    /// カラムナーセグメントを読み取る(トランザクションコンテキスト利用)。
603    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
692/// セグメントID文字列をパースする。
693///
694/// フォーマット: `{table_id}:{segment_id}` (例: "12345:1")
695fn 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
721/// カラム値を SqlValue に変換する。
722fn 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
751// ============================================================================
752// ColumnarRowIterator - FR-7 Streaming Row Iterator
753// ============================================================================
754
755/// Streaming row iterator for columnar segments (FR-7 compliant).
756///
757/// This iterator yields rows one at a time from pre-loaded RecordBatches,
758/// avoiding the need to materialize all rows into `Vec<Vec<SqlValue>>` upfront.
759pub struct ColumnarRowIterator {
760    /// Pre-loaded RecordBatches.
761    batches: Vec<RecordBatch>,
762    /// Current batch index.
763    batch_idx: usize,
764    /// Current row index within the batch.
765    row_idx: usize,
766}
767
768impl ColumnarRowIterator {
769    /// Create a new row iterator from RecordBatches.
770    pub fn new(batches: Vec<RecordBatch>) -> Self {
771        Self {
772            batches,
773            batch_idx: 0,
774            row_idx: 0,
775        }
776    }
777
778    /// Returns the total number of batches.
779    pub fn batch_count(&self) -> usize {
780        self.batches.len()
781    }
782
783    /// Returns the current batch being iterated, if any.
784    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            // Check if we've exhausted all batches
795            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            // Check if we've exhausted the current batch
803            if self.row_idx >= row_count {
804                self.batch_idx += 1;
805                self.row_idx = 0;
806                continue;
807            }
808
809            // Convert current row
810            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        // flush to file
1120        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        // flush to kvs
1127        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        // flush to .alopex
1133        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}