Skip to main content

alopex_core/columnar/
segment_v2.rs

1//! Segment V2 のデータモデル・ヘッダ定義と簡易Writer/Reader。
2//!
3//! セグメントのフォーマットメタデータ、RowGroup/Column チャンクの
4//! オフセットテーブル、チェックサム設定を扱い、メモリバッファベースで
5//! 読み書きできる最小限の実装を提供する。
6
7use std::collections::HashSet;
8use std::convert::TryFrom;
9use std::io::{Cursor, Read};
10
11use crc32fast::Hasher;
12use serde::{Deserialize, Serialize};
13
14use crate::columnar::encoding::{Column, LogicalType};
15use crate::columnar::encoding_v2::{
16    create_decoder, create_encoder, select_encoding, Bitmap, Decoder, Encoder, EncodingHints,
17    EncodingV2,
18};
19use crate::columnar::error::{ColumnarError, Result};
20use crate::storage::compression::{create_compressor, CompressionV2};
21
22/// Segment V2 のマジックバイト。
23pub const SEGMENT_MAGIC: &[u8; 4] = b"ALXC";
24/// Segment V2 のフォーマットバージョン。
25pub const SEGMENT_FORMAT_VERSION_V2: u16 = 2;
26/// ヘッダの固定長(24バイト)。
27pub const SEGMENT_HEADER_SIZE: usize = 24;
28/// RowID のセグメントID割り当てビット数(上位 20bit)。
29pub const ROW_ID_SEGMENT_BITS: u8 = 20;
30/// RowID のセグメント内オフセットビット数(下位 44bit)。
31pub const ROW_ID_OFFSET_BITS: u8 = 44;
32const ROW_ID_OFFSET_MASK: u64 = (1u64 << ROW_ID_OFFSET_BITS) - 1;
33const ROW_ID_SEGMENT_MASK: u64 = (1u64 << ROW_ID_SEGMENT_BITS) - 1;
34
35/// RowID をエンコードする(segment_id << ROW_ID_OFFSET_BITS | local_offset)。
36pub fn encode_row_id(segment_id: u64, local_offset: u64) -> Result<u64> {
37    if segment_id > ROW_ID_SEGMENT_MASK {
38        return Err(ColumnarError::InvalidFormat(format!(
39            "segment_id overflow for RowID: {segment_id} > {ROW_ID_SEGMENT_MASK}"
40        )));
41    }
42    if local_offset > ROW_ID_OFFSET_MASK {
43        return Err(ColumnarError::InvalidFormat(format!(
44            "row offset overflow for RowID: {local_offset} > {ROW_ID_OFFSET_MASK}"
45        )));
46    }
47    Ok((segment_id << ROW_ID_OFFSET_BITS) | local_offset)
48}
49
50/// RowID から (segment_id, local_offset) をデコードする。
51pub fn decode_row_id(row_id: u64) -> (u64, u64) {
52    let segment_id = row_id >> ROW_ID_OFFSET_BITS;
53    let local_offset = row_id & ROW_ID_OFFSET_MASK;
54    (segment_id, local_offset)
55}
56
57/// チェックサムの適用範囲。
58#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
59pub enum ChecksumScope {
60    /// チェックサムなし。
61    None = 0,
62    /// フッターのみチェックサム付与。
63    Footer = 1,
64    /// 各チャンクにチェックサム付与。
65    Chunk = 2,
66}
67
68impl From<ChecksumScope> for u8 {
69    fn from(scope: ChecksumScope) -> Self {
70        scope as u8
71    }
72}
73
74impl TryFrom<u8> for ChecksumScope {
75    type Error = ColumnarError;
76
77    fn try_from(value: u8) -> std::result::Result<Self, Self::Error> {
78        match value {
79            0 => Ok(ChecksumScope::None),
80            1 => Ok(ChecksumScope::Footer),
81            2 => Ok(ChecksumScope::Chunk),
82            other => Err(ColumnarError::InvalidFormat(format!(
83                "unknown checksum scope: {other}"
84            ))),
85        }
86    }
87}
88
89/// セグメントヘッダ(固定長 24 バイト)。
90#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
91pub struct SegmentHeader {
92    /// マジック "ALXC"。
93    pub magic: [u8; 4],
94    /// フォーマットバージョン (u16)。
95    pub format_version: u16,
96    /// カラム数 (u16)。
97    pub column_count: u16,
98    /// 総行数 (u64)。
99    pub row_count: u64,
100    /// RowGroup のターゲット行数 (u32)。
101    pub row_group_size: u32,
102    /// チェックサムスコープ。
103    pub checksum_scope: ChecksumScope,
104    /// デフォルト圧縮形式。
105    pub compression: CompressionV2,
106    /// 予約フィールド(24 バイト揃えのためのパディング)。
107    pub reserved: [u8; 2],
108}
109
110impl SegmentHeader {
111    /// V2 デフォルト値を埋めたヘッダを生成する。
112    pub fn new(
113        column_count: u16,
114        row_count: u64,
115        row_group_size: u32,
116        checksum_scope: ChecksumScope,
117        compression: CompressionV2,
118    ) -> Self {
119        Self {
120            magic: *SEGMENT_MAGIC,
121            format_version: SEGMENT_FORMAT_VERSION_V2,
122            column_count,
123            row_count,
124            row_group_size,
125            checksum_scope,
126            compression,
127            reserved: [0u8; 2],
128        }
129    }
130}
131
132/// シリアライズヘッダをバイト配列へ書き込む。
133fn write_header(header: &SegmentHeader, buf: &mut Vec<u8>) {
134    buf.extend_from_slice(&header.magic);
135    buf.extend_from_slice(&header.format_version.to_le_bytes());
136    buf.extend_from_slice(&header.column_count.to_le_bytes());
137    buf.extend_from_slice(&header.row_count.to_le_bytes());
138    buf.extend_from_slice(&header.row_group_size.to_le_bytes());
139    buf.push(header.checksum_scope as u8);
140    buf.push(match header.compression {
141        CompressionV2::None => 0,
142        CompressionV2::Lz4 => 1,
143        CompressionV2::Zstd { .. } => 2,
144    });
145    buf.extend_from_slice(&header.reserved);
146}
147
148/// Column 長を返すヘルパー。
149fn column_len(column: &Column) -> usize {
150    match column {
151        Column::Int64(v) => v.len(),
152        Column::Float32(v) => v.len(),
153        Column::Float64(v) => v.len(),
154        Column::Bool(v) => v.len(),
155        Column::Binary(v) => v.len(),
156        Column::Fixed { values, .. } => values.len(),
157    }
158}
159
160/// Column をスライスするヘルパー。
161fn slice_column(column: &Column, start: usize, len: usize) -> Result<Column> {
162    match column {
163        Column::Int64(v) => Ok(Column::Int64(v[start..start + len].to_vec())),
164        Column::Float32(v) => Ok(Column::Float32(v[start..start + len].to_vec())),
165        Column::Float64(v) => Ok(Column::Float64(v[start..start + len].to_vec())),
166        Column::Bool(v) => Ok(Column::Bool(v[start..start + len].to_vec())),
167        Column::Binary(v) => Ok(Column::Binary(v[start..start + len].to_vec())),
168        Column::Fixed { values, len: fixed } => Ok(Column::Fixed {
169            len: *fixed,
170            values: values[start..start + len].to_vec(),
171        }),
172    }
173}
174
175fn slice_bitmap(bitmap: &Bitmap, start: usize, len: usize) -> Bitmap {
176    let mut v = Vec::with_capacity(len);
177    for i in start..start + len {
178        v.push(bitmap.get(i));
179    }
180    Bitmap::from_bools(&v)
181}
182
183/// エンコーディング選択用の簡易ヒントを構築する。
184fn build_encoding_hints(column: &Column, bitmap: Option<&Bitmap>) -> EncodingHints {
185    let mut hints = EncodingHints::default();
186    let mut last: Option<i64> = None;
187    let mut distinct_int = HashSet::new();
188    let mut distinct_bool = HashSet::new();
189
190    match column {
191        Column::Int64(values) => {
192            let mut min = i64::MAX;
193            let mut max = i64::MIN;
194            for (i, v) in values.iter().enumerate() {
195                if let Some(bm) = bitmap {
196                    if !bm.get(i) {
197                        continue;
198                    }
199                }
200                min = min.min(*v);
201                max = max.max(*v);
202                hints.total_count += 1;
203                distinct_int.insert(*v);
204                if let Some(prev) = last {
205                    if prev > *v {
206                        hints.is_sorted = false;
207                    }
208                } else {
209                    hints.is_sorted = true;
210                }
211                last = Some(*v);
212            }
213            hints.distinct_count = distinct_int.len();
214            if hints.total_count > 0 {
215                hints.value_range = Some((max - min) as u64);
216            }
217        }
218        Column::Float64(values) => {
219            hints.total_count = values.len();
220            hints.distinct_count = values.len(); // 粗い推定
221            hints.is_sorted = values.windows(2).all(|w| {
222                w[0].partial_cmp(&w[1])
223                    .map(|o| o != std::cmp::Ordering::Greater)
224                    .unwrap_or(true)
225            });
226        }
227        Column::Float32(values) => {
228            hints.total_count = values.len();
229            hints.distinct_count = values.len();
230            hints.is_sorted = values.windows(2).all(|w| {
231                w[0].partial_cmp(&w[1])
232                    .map(|o| o != std::cmp::Ordering::Greater)
233                    .unwrap_or(true)
234            });
235        }
236        Column::Bool(values) => {
237            for (i, v) in values.iter().enumerate() {
238                if let Some(bm) = bitmap {
239                    if !bm.get(i) {
240                        continue;
241                    }
242                }
243                hints.total_count += 1;
244                distinct_bool.insert(*v);
245            }
246            hints.distinct_count = distinct_bool.len();
247            hints.is_sorted = true;
248        }
249        Column::Binary(values) => {
250            hints.total_count = values.len();
251            hints.distinct_count = values.len(); // 粗い推定
252            hints.is_sorted = values.windows(2).all(|w| w[0] <= w[1]);
253        }
254        Column::Fixed { values, .. } => {
255            hints.total_count = values.len();
256            hints.distinct_count = values.len();
257            hints.is_sorted = values.windows(2).all(|w| w[0] <= w[1]);
258        }
259    }
260
261    hints
262}
263
264/// バイト列からヘッダを復元する。
265fn read_header(bytes: &[u8]) -> Result<SegmentHeader> {
266    if bytes.len() < SEGMENT_HEADER_SIZE {
267        return Err(ColumnarError::InvalidFormat("header too short".into()));
268    }
269    let mut cur = Cursor::new(bytes);
270    let mut magic = [0u8; 4];
271    cur.read_exact(&mut magic)
272        .map_err(|e| ColumnarError::InvalidFormat(format!("read magic failed: {e}")))?;
273    if &magic != SEGMENT_MAGIC {
274        return Err(ColumnarError::InvalidFormat("invalid segment magic".into()));
275    }
276    let mut u16buf = [0u8; 2];
277    cur.read_exact(&mut u16buf)
278        .map_err(|e| ColumnarError::InvalidFormat(format!("read version failed: {e}")))?;
279    let format_version = u16::from_le_bytes(u16buf);
280
281    let mut u16buf = [0u8; 2];
282    cur.read_exact(&mut u16buf)
283        .map_err(|e| ColumnarError::InvalidFormat(format!("read column_count failed: {e}")))?;
284    let column_count = u16::from_le_bytes(u16buf);
285
286    let mut u64buf = [0u8; 8];
287    cur.read_exact(&mut u64buf)
288        .map_err(|e| ColumnarError::InvalidFormat(format!("read row_count failed: {e}")))?;
289    let row_count = u64::from_le_bytes(u64buf);
290
291    let mut u32buf = [0u8; 4];
292    cur.read_exact(&mut u32buf)
293        .map_err(|e| ColumnarError::InvalidFormat(format!("read row_group_size failed: {e}")))?;
294    let row_group_size = u32::from_le_bytes(u32buf);
295
296    let mut scope_byte = [0u8; 1];
297    cur.read_exact(&mut scope_byte)
298        .map_err(|e| ColumnarError::InvalidFormat(format!("read checksum_scope failed: {e}")))?;
299    let checksum_scope = ChecksumScope::try_from(scope_byte[0])?;
300
301    let mut comp_byte = [0u8; 1];
302    cur.read_exact(&mut comp_byte)
303        .map_err(|e| ColumnarError::InvalidFormat(format!("read compression failed: {e}")))?;
304    let compression = match comp_byte[0] {
305        0 => CompressionV2::None,
306        1 => CompressionV2::Lz4,
307        2 => CompressionV2::Zstd { level: 3 },
308        other => {
309            return Err(ColumnarError::InvalidFormat(format!(
310                "unknown compression id: {other}"
311            )))
312        }
313    };
314
315    let mut reserved = [0u8; 2];
316    cur.read_exact(&mut reserved)
317        .map_err(|e| ColumnarError::InvalidFormat(format!("read reserved failed: {e}")))?;
318
319    Ok(SegmentHeader {
320        magic,
321        format_version,
322        column_count,
323        row_count,
324        row_group_size,
325        checksum_scope,
326        compression,
327        reserved,
328    })
329}
330
331/// セグメント内のカラムスキーマ。
332#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
333pub struct ColumnSchema {
334    /// カラム名。
335    pub name: String,
336    /// 論理型。
337    pub logical_type: LogicalType,
338    /// NULL 許容フラグ。
339    pub nullable: bool,
340    /// 固定長型の場合のバイト長。
341    pub fixed_len: Option<u32>,
342}
343
344/// セグメント全体のスキーマ。
345#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
346pub struct Schema {
347    /// カラム定義一覧。
348    pub columns: Vec<ColumnSchema>,
349}
350
351impl Schema {
352    /// カラム数を返す。
353    pub fn column_count(&self) -> usize {
354        self.columns.len()
355    }
356}
357
358/// セグメントメタデータ (V2)。
359#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
360pub struct SegmentMetaV2 {
361    /// フォーマットバージョン (u16)。
362    pub format_version: u16,
363    /// セグメントのスキーマ。
364    pub schema: Schema,
365    /// 総行数。
366    pub num_rows: u64,
367    /// 作成タイムスタンプ (Unix epoch millis)。
368    pub created_at: u64,
369    /// 非圧縮サイズ。
370    pub uncompressed_size: u64,
371    /// 圧縮後サイズ。
372    pub compressed_size: u64,
373    /// RowGroup メタデータ一覧。
374    pub row_groups: Vec<RowGroupMeta>,
375}
376
377/// RowGroup 単位のメタデータ。
378#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
379pub struct RowGroupMeta {
380    /// RowGroup の開始行インデックス。
381    pub row_start: u64,
382    /// RowGroup 内の行数。
383    pub row_count: u64,
384    /// RowGroup 圧縮サイズ。
385    pub compressed_size: u64,
386    /// カラムチャンクメタデータ。
387    pub column_chunks: Vec<ColumnChunkMeta>,
388    /// checksum_scope = Chunk の場合のチェックサム。
389    pub checksum: Option<u32>,
390}
391
392/// カラムチャンクメタデータ。
393#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
394pub struct ColumnChunkMeta {
395    /// カラムインデックス (schema 基準)。
396    pub column_index: u16,
397    /// 使用したエンコーディング。
398    pub encoding: EncodingV2,
399    /// 使用した圧縮。
400    pub compression: CompressionV2,
401    /// セグメント先頭からのオフセット。
402    pub offset: u64,
403    /// 圧縮後サイズ。
404    pub compressed_size: u64,
405    /// 非圧縮サイズ。
406    pub uncompressed_size: u64,
407    /// NULL の件数。
408    pub null_count: u64,
409    /// 辞書ページオフセット(辞書利用時のみ)。
410    pub dictionary_offset: Option<u64>,
411}
412
413/// RowGroup テーブル(ランダムアクセス用オフセットテーブル)。
414#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
415pub struct RowGroupTable {
416    /// RowGroup テーブルのエントリ一覧。
417    pub entries: Vec<RowGroupTableEntry>,
418}
419
420/// RowGroup テーブルの 1 エントリ。
421#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
422pub struct RowGroupTableEntry {
423    /// RowGroup の開始行。
424    pub row_start: u64,
425    /// RowGroup 行数。
426    pub row_count: u64,
427    /// セグメント先頭からの RowGroup データ開始オフセット。
428    pub data_offset: u64,
429    /// RowGroup の圧縮サイズ合計。
430    pub compressed_size: u64,
431    /// RowGroup 内の各カラムチャンクの相対オフセット。
432    pub column_chunk_offsets: Vec<ColumnChunkOffset>,
433    /// checksum_scope = Chunk の場合のチェックサム。
434    pub checksum: Option<u32>,
435}
436
437/// RowGroup 内のカラムチャンク位置。
438#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
439pub struct ColumnChunkOffset {
440    /// カラムインデックス。
441    pub column_idx: u16,
442    /// RowGroup データ先頭からのオフセット。
443    pub offset: u64,
444    /// カラムチャンクの圧縮サイズ。
445    pub length: u64,
446    /// カラムチャンクの非圧縮サイズ。
447    pub uncompressed_length: u64,
448    /// チャンクチェックサム (checksum_scope = Chunk の場合)。
449    pub checksum: Option<u32>,
450}
451
452/// カラムディスクリプタ集合(列単位メタデータ)。
453#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
454pub struct ColumnDescriptors {
455    /// カラムディスクリプタ一覧。
456    pub columns: Vec<ColumnDescriptor>,
457}
458
459/// 単一カラムのメタデータ。
460#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
461pub struct ColumnDescriptor {
462    /// カラムインデックス。
463    pub column_idx: u16,
464    /// 論理型。
465    pub logical_type: LogicalType,
466    /// 使用エンコーディング。
467    pub encoding: EncodingV2,
468    /// 使用圧縮。
469    pub compression: CompressionV2,
470    /// NULL 許容フラグ。
471    pub nullable: bool,
472    /// 固定長型のバイト長。
473    pub fixed_len: Option<u32>,
474    /// 辞書オフセット(辞書利用時)。
475    pub dictionary_offset: Option<u64>,
476    /// セグメント内でのデータ開始オフセット(最初の RowGroup)。
477    pub data_offset: u64,
478    /// 全 RowGroup 合計のデータ長。
479    pub data_length: u64,
480}
481
482/// フッター(RowGroupTable と ColumnDescriptors をまとめたもの)。
483#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
484pub struct SegmentFooter {
485    /// RowGroup テーブル。
486    pub row_group_table: RowGroupTable,
487    /// カラムディスクリプタ。
488    pub column_descriptors: ColumnDescriptors,
489}
490
491/// レコードバッチ(単純な Schema + Column + null bitmap の組)。
492#[derive(Clone, Debug)]
493pub struct RecordBatch {
494    /// スキーマ。
495    pub schema: Schema,
496    /// カラムデータ。
497    pub columns: Vec<Column>,
498    /// NULL ビットマップ。
499    pub null_bitmaps: Vec<Option<Bitmap>>,
500    /// 行ID(RowIDモードで利用)。
501    pub row_ids: Option<Vec<u64>>,
502}
503
504impl RecordBatch {
505    /// 新規作成。カラム数とビットマップ数がスキーマと一致することを前提にする。
506    pub fn new(schema: Schema, columns: Vec<Column>, null_bitmaps: Vec<Option<Bitmap>>) -> Self {
507        Self {
508            schema,
509            columns,
510            null_bitmaps,
511            row_ids: None,
512        }
513    }
514
515    /// 行数を返す(先頭カラム長で代表)。
516    pub fn num_rows(&self) -> usize {
517        self.columns.first().map(column_len).unwrap_or_default()
518    }
519
520    /// RowID を埋め込んだ新しいバッチを返す。
521    pub fn with_row_ids(mut self, row_ids: Option<Vec<u64>>) -> Self {
522        self.row_ids = row_ids;
523        self
524    }
525}
526
527/// SegmentWriterV2 の構成。
528#[derive(Clone, Debug)]
529pub struct SegmentConfigV2 {
530    /// RowGroup のターゲット行数。
531    pub row_group_size: u64,
532    /// デフォルト圧縮。
533    pub compression: CompressionV2,
534    /// チェックサムスコープ。
535    pub checksum_scope: ChecksumScope,
536    /// 圧縮後サイズの上限(デフォルト 16MiB)。
537    pub max_row_group_bytes: u64,
538}
539
540impl Default for SegmentConfigV2 {
541    fn default() -> Self {
542        Self {
543            row_group_size: 100_000,
544            compression: CompressionV2::None,
545            checksum_scope: ChecksumScope::Footer,
546            max_row_group_bytes: 16 * 1024 * 1024,
547        }
548    }
549}
550
551/// セグメント出力(メモリバッファ)。
552#[derive(Clone, Debug, Serialize, Deserialize)]
553pub struct ColumnSegmentV2 {
554    /// ヘッダ。
555    pub header: SegmentHeader,
556    /// メタデータ。
557    pub meta: SegmentMetaV2,
558    /// セグメント全体のバイト列。
559    pub data: Vec<u8>,
560    /// RowID(セグメント内オフセットまたはエンコード済み)を保持する。無い場合は空。
561    #[serde(default)]
562    pub row_ids: Vec<u64>,
563}
564
565/// メモリバッファを入力とする SegmentSource 実装。
566#[derive(Debug)]
567pub struct InMemorySegmentSource {
568    data: Vec<u8>,
569}
570
571impl InMemorySegmentSource {
572    /// 新しいメモリソースを作成。
573    pub fn new(data: Vec<u8>) -> Self {
574        Self { data }
575    }
576}
577
578/// セグメント読み込み元を抽象化する。
579pub trait SegmentSource: Send + Sync + std::fmt::Debug {
580    /// 指定範囲を読み取る。
581    fn read_range(&self, offset: u64, len: u64) -> Result<Vec<u8>>;
582    /// 全体サイズを返す。
583    fn total_size(&self) -> u64;
584}
585
586impl SegmentSource for InMemorySegmentSource {
587    fn read_range(&self, offset: u64, len: u64) -> Result<Vec<u8>> {
588        let start = offset as usize;
589        let end = start + len as usize;
590        if end > self.data.len() {
591            return Err(ColumnarError::InvalidFormat("range out of bounds".into()));
592        }
593        Ok(self.data[start..end].to_vec())
594    }
595
596    fn total_size(&self) -> u64 {
597        self.data.len() as u64
598    }
599}
600
601/// SegmentWriterV2 実装。
602#[derive(Debug)]
603pub struct SegmentWriterV2 {
604    config: SegmentConfigV2,
605    buffer: Vec<RecordBatch>,
606}
607
608impl SegmentWriterV2 {
609    /// 新しい Writer を生成。
610    pub fn new(config: SegmentConfigV2) -> Self {
611        Self {
612            config,
613            buffer: Vec::new(),
614        }
615    }
616
617    /// バッチを追加する。行数はスキーマと整合している前提。
618    pub fn write_batch(&mut self, batch: RecordBatch) -> Result<()> {
619        self.buffer.push(batch);
620        Ok(())
621    }
622
623    /// セグメントを書き出し、ColumnSegmentV2 を返す。
624    pub fn finish(mut self) -> Result<ColumnSegmentV2> {
625        // すべて同じスキーマであることを前提に整合性チェック。
626        let schema = self
627            .buffer
628            .first()
629            .ok_or_else(|| ColumnarError::InvalidFormat("no batches".into()))?
630            .schema
631            .clone();
632        for b in &self.buffer {
633            if b.schema.column_count() != schema.column_count() {
634                return Err(ColumnarError::InvalidFormat("schema mismatch".into()));
635            }
636        }
637
638        // RowGroup 分割
639        let row_group_size = self.config.row_group_size as usize;
640        let mut row_groups: Vec<RecordBatch> = Vec::new();
641        for batch in self.buffer.drain(..) {
642            let rows = batch.num_rows();
643            let mut offset = 0;
644            while offset < rows {
645                let end = usize::min(offset + row_group_size, rows);
646                let mut cols = Vec::new();
647                for col in &batch.columns {
648                    cols.push(slice_column(col, offset, end - offset)?);
649                }
650                let mut bitmaps = Vec::new();
651                for bm in &batch.null_bitmaps {
652                    let sliced = bm.as_ref().map(|b| slice_bitmap(b, offset, end - offset));
653                    bitmaps.push(sliced);
654                }
655                row_groups.push(RecordBatch::new(batch.schema.clone(), cols, bitmaps));
656                offset = end;
657            }
658        }
659
660        let total_rows: u64 = row_groups.iter().map(|rg| rg.num_rows() as u64).sum();
661
662        // ヘッダとスキーマ
663        let header = SegmentHeader::new(
664            schema.column_count() as u16,
665            total_rows,
666            self.config.row_group_size as u32,
667            self.config.checksum_scope,
668            self.config.compression,
669        );
670
671        let schema_bytes =
672            bincode::serialize(&schema).map_err(|e| ColumnarError::InvalidFormat(e.to_string()))?;
673        let schema_len = schema_bytes.len() as u32;
674
675        // データ部とメタ
676        let mut data = Vec::new();
677        write_header(&header, &mut data);
678        data.extend_from_slice(&schema_len.to_le_bytes());
679        data.extend_from_slice(&schema_bytes);
680
681        let mut row_group_table_entries = Vec::new();
682        let mut column_descriptors = Vec::new();
683        column_descriptors.resize(
684            schema.column_count(),
685            ColumnDescriptor {
686                column_idx: 0,
687                logical_type: LogicalType::Int64,
688                encoding: EncodingV2::Plain,
689                compression: self.config.compression,
690                nullable: false,
691                fixed_len: None,
692                dictionary_offset: None,
693                data_offset: 0,
694                data_length: 0,
695            },
696        );
697
698        let mut current_offset = data.len() as u64;
699        let mut row_start = 0u64;
700        let mut total_uncompressed = 0u64;
701        let mut queue: std::collections::VecDeque<RecordBatch> = row_groups.into_iter().collect();
702
703        let mut meta_row_groups = Vec::new();
704        while let Some(rg) = queue.pop_front() {
705            let mut rg_buffer = Vec::new();
706            let mut rg_uncompressed_size = 0u64;
707            let mut pending_chunks = Vec::new();
708
709            for (col_idx, col) in rg.columns.iter().enumerate() {
710                let null_bitmap = rg.null_bitmaps.get(col_idx).and_then(|b| b.as_ref());
711                let hints = build_encoding_hints(col, null_bitmap);
712                let encoding = select_encoding(schema.columns[col_idx].logical_type, &hints);
713                let encoder: Box<dyn Encoder> = create_encoder(encoding);
714                let encoded = encoder
715                    .encode(col, null_bitmap)
716                    .map_err(|e| ColumnarError::InvalidFormat(e.to_string()))?;
717                let uncompressed_len = encoded.len() as u64;
718                rg_uncompressed_size += uncompressed_len;
719
720                let compressed = if let CompressionV2::None = self.config.compression {
721                    encoded
722                } else {
723                    let compressor = create_compressor(self.config.compression)
724                        .map_err(|e| ColumnarError::InvalidFormat(e.to_string()))?;
725                    compressor
726                        .compress(&encoded)
727                        .map_err(|e| ColumnarError::InvalidFormat(e.to_string()))?
728                };
729
730                let chunk_offset = rg_buffer.len() as u64;
731                let chunk_checksum = if self.config.checksum_scope == ChecksumScope::Chunk {
732                    let mut hasher = Hasher::new();
733                    hasher.update(&compressed);
734                    Some(hasher.finalize())
735                } else {
736                    None
737                };
738
739                let chunk_len = compressed.len() as u64;
740                rg_buffer.extend_from_slice(&compressed);
741
742                pending_chunks.push((
743                    col_idx,
744                    encoding,
745                    chunk_offset,
746                    chunk_len,
747                    uncompressed_len,
748                    chunk_checksum,
749                    null_bitmap.map(|b| b.null_count() as u64).unwrap_or(0),
750                    compressed,
751                ));
752            }
753
754            let rg_compressed_size = rg_buffer.len() as u64;
755
756            if self.config.max_row_group_bytes > 0
757                && rg_compressed_size > self.config.max_row_group_bytes
758            {
759                // 圧縮後サイズが上限を超えたら分割を試みる。1 行なら分割不可なのでエラー。
760                if rg.num_rows() <= 1 {
761                    return Err(ColumnarError::RowGroupTooLarge {
762                        size: rg_compressed_size,
763                        max: self.config.max_row_group_bytes,
764                    });
765                }
766                let mid = rg.num_rows() / 2;
767                let mut left_cols = Vec::new();
768                let mut right_cols = Vec::new();
769                for col in &rg.columns {
770                    left_cols.push(slice_column(col, 0, mid)?);
771                    right_cols.push(slice_column(col, mid, rg.num_rows() - mid)?);
772                }
773                let mut left_bm = Vec::new();
774                let mut right_bm = Vec::new();
775                for bm in &rg.null_bitmaps {
776                    match bm {
777                        Some(b) => {
778                            left_bm.push(Some(slice_bitmap(b, 0, mid)));
779                            right_bm.push(Some(slice_bitmap(b, mid, rg.num_rows() - mid)));
780                        }
781                        None => {
782                            left_bm.push(None);
783                            right_bm.push(None);
784                        }
785                    }
786                }
787                queue.push_front(RecordBatch::new(rg.schema.clone(), right_cols, right_bm));
788                queue.push_front(RecordBatch::new(rg.schema.clone(), left_cols, left_bm));
789                continue;
790            }
791
792            let rg_data_offset = current_offset;
793            let mut column_chunk_offsets = Vec::new();
794            let mut rg_column_chunks = Vec::new();
795            let mut written = 0u64;
796            for (
797                col_idx,
798                encoding,
799                chunk_relative,
800                chunk_len,
801                uncompressed_len,
802                chunk_checksum,
803                null_count,
804                compressed,
805            ) in pending_chunks
806            {
807                let chunk_offset_abs = rg_data_offset + chunk_relative;
808                column_chunk_offsets.push(ColumnChunkOffset {
809                    column_idx: col_idx as u16,
810                    offset: chunk_relative,
811                    length: chunk_len,
812                    uncompressed_length: uncompressed_len,
813                    checksum: chunk_checksum,
814                });
815
816                rg_column_chunks.push(ColumnChunkMeta {
817                    column_index: col_idx as u16,
818                    encoding,
819                    compression: self.config.compression,
820                    offset: chunk_offset_abs,
821                    compressed_size: chunk_len,
822                    uncompressed_size: uncompressed_len,
823                    null_count,
824                    dictionary_offset: if encoding == EncodingV2::Dictionary {
825                        Some(chunk_offset_abs)
826                    } else {
827                        None
828                    },
829                });
830
831                // ColumnDescriptors の初期値設定
832                let desc = &mut column_descriptors[col_idx];
833                desc.column_idx = col_idx as u16;
834                desc.logical_type = schema.columns[col_idx].logical_type;
835                desc.encoding = encoding;
836                desc.compression = self.config.compression;
837                desc.nullable = schema.columns[col_idx].nullable;
838                desc.fixed_len = schema.columns[col_idx].fixed_len;
839                if meta_row_groups.is_empty() {
840                    desc.data_offset = chunk_offset_abs;
841                }
842                desc.data_length += chunk_len;
843                if encoding == EncodingV2::Dictionary {
844                    desc.dictionary_offset = Some(chunk_offset_abs);
845                }
846
847                data.extend_from_slice(&compressed);
848                written += chunk_len;
849            }
850
851            debug_assert_eq!(written, rg_compressed_size);
852
853            // Chunk スコープでは ColumnChunk 側でチェックサムを保持するため、
854            // RowGroup 単位のチェックサムは計算しない。
855            let row_group_checksum = None;
856
857            row_group_table_entries.push(RowGroupTableEntry {
858                row_start,
859                row_count: rg.num_rows() as u64,
860                data_offset: rg_data_offset,
861                compressed_size: rg_compressed_size,
862                column_chunk_offsets,
863                checksum: row_group_checksum,
864            });
865
866            meta_row_groups.push(RowGroupMeta {
867                row_start,
868                row_count: rg.num_rows() as u64,
869                compressed_size: rg_compressed_size,
870                column_chunks: rg_column_chunks,
871                checksum: row_group_checksum,
872            });
873
874            current_offset += rg_compressed_size;
875            row_start += rg.num_rows() as u64;
876            total_uncompressed += rg_uncompressed_size;
877        }
878
879        let footer = SegmentFooter {
880            row_group_table: RowGroupTable {
881                entries: row_group_table_entries,
882            },
883            column_descriptors: ColumnDescriptors {
884                columns: column_descriptors,
885            },
886        };
887
888        let footer_payload =
889            bincode::serialize(&footer).map_err(|e| ColumnarError::InvalidFormat(e.to_string()))?;
890        let footer_size = footer_payload.len() as u32;
891        let mut hasher = Hasher::new();
892        hasher.update(&footer_payload);
893        let footer_checksum = hasher.finalize();
894
895        data.extend_from_slice(&footer_payload);
896        data.extend_from_slice(&footer_size.to_le_bytes());
897        data.extend_from_slice(&footer_checksum.to_le_bytes());
898
899        let meta = SegmentMetaV2 {
900            format_version: header.format_version,
901            schema: schema.clone(),
902            num_rows: header.row_count,
903            created_at: 0,
904            uncompressed_size: total_uncompressed,
905            compressed_size: data.len() as u64,
906            row_groups: meta_row_groups,
907        };
908
909        Ok(ColumnSegmentV2 {
910            header,
911            meta,
912            data,
913            row_ids: Vec::new(),
914        })
915    }
916}
917
918/// SegmentReaderV2 実装。
919#[derive(Debug)]
920pub struct SegmentReaderV2 {
921    header: SegmentHeader,
922    schema: Schema,
923    footer: SegmentFooter,
924    source: Box<dyn SegmentSource>,
925}
926
927impl SegmentReaderV2 {
928    /// セグメントをオープンしてヘッダ/フッター検証を行う。
929    pub fn open(source: Box<dyn SegmentSource>) -> Result<Self> {
930        let total_size = source.total_size();
931        if total_size < (SEGMENT_HEADER_SIZE + 8) as u64 {
932            return Err(ColumnarError::InvalidFormat("segment too small".into()));
933        }
934
935        // フッターサイズとチェックサムを取得
936        let trailer = source.read_range(total_size - 8, 8)?;
937        let footer_size = u32::from_le_bytes(trailer[0..4].try_into().unwrap()) as u64;
938        let footer_checksum = u32::from_le_bytes(trailer[4..8].try_into().unwrap());
939
940        if footer_size + 8 > total_size {
941            return Err(ColumnarError::InvalidFormat("invalid footer size".into()));
942        }
943
944        let footer_start = total_size - 8 - footer_size;
945        let footer_bytes = source.read_range(footer_start, footer_size)?;
946        let mut hasher = Hasher::new();
947        hasher.update(&footer_bytes);
948        let computed = hasher.finalize();
949        if computed != footer_checksum {
950            return Err(ColumnarError::ChecksumMismatch);
951        }
952
953        let footer: SegmentFooter = bincode::deserialize(&footer_bytes)
954            .map_err(|e| ColumnarError::InvalidFormat(e.to_string()))?;
955
956        // ヘッダとスキーマ読み込み
957        let header_bytes = source.read_range(0, SEGMENT_HEADER_SIZE as u64)?;
958        let header = read_header(&header_bytes)?;
959        if header.format_version != SEGMENT_FORMAT_VERSION_V2 {
960            return Err(ColumnarError::UnsupportedFormatVersion {
961                found: header.format_version,
962                expected: SEGMENT_FORMAT_VERSION_V2,
963            });
964        }
965
966        let schema_len_bytes = source.read_range(SEGMENT_HEADER_SIZE as u64, 4)?;
967        let schema_len = u32::from_le_bytes(schema_len_bytes.try_into().unwrap()) as u64;
968        let schema_bytes = source.read_range(SEGMENT_HEADER_SIZE as u64 + 4, schema_len)?;
969        let schema: Schema = bincode::deserialize(&schema_bytes)
970            .map_err(|e| ColumnarError::InvalidFormat(e.to_string()))?;
971
972        Ok(Self {
973            header,
974            schema,
975            footer,
976            source,
977        })
978    }
979
980    /// カラムの一部だけを読み取る(カラムプルーニング)。
981    pub fn read_columns(&self, columns: &[usize]) -> Result<Vec<RecordBatch>> {
982        let mut batches = Vec::new();
983        for idx in 0..self.footer.row_group_table.entries.len() {
984            batches.push(self.read_row_group_by_index(columns, idx)?);
985        }
986        Ok(batches)
987    }
988
989    /// 指定RowGroupインデックスの指定カラムを読み取る。
990    pub fn read_row_group_by_index(
991        &self,
992        columns: &[usize],
993        rg_index: usize,
994    ) -> Result<RecordBatch> {
995        let entry = self
996            .footer
997            .row_group_table
998            .entries
999            .get(rg_index)
1000            .ok_or_else(|| ColumnarError::InvalidFormat("row group index out of bounds".into()))?;
1001
1002        let mut cols = Vec::new();
1003        let mut bitmaps = Vec::new();
1004        for &col_idx in columns {
1005            let desc = self
1006                .footer
1007                .column_descriptors
1008                .columns
1009                .get(col_idx)
1010                .ok_or_else(|| ColumnarError::InvalidFormat("column index out of bounds".into()))?;
1011            let chunk_meta = entry
1012                .column_chunk_offsets
1013                .iter()
1014                .find(|c| c.column_idx as usize == col_idx)
1015                .ok_or_else(|| ColumnarError::InvalidFormat("missing chunk offset".into()))?;
1016            let chunk_bytes = self
1017                .source
1018                .read_range(entry.data_offset + chunk_meta.offset, chunk_meta.length)?;
1019
1020            if self.header.checksum_scope == ChecksumScope::Chunk {
1021                if let Some(expected) = chunk_meta.checksum {
1022                    let mut hasher = Hasher::new();
1023                    hasher.update(&chunk_bytes);
1024                    let computed = hasher.finalize();
1025                    if expected != computed {
1026                        return Err(ColumnarError::ChecksumMismatch);
1027                    }
1028                }
1029                if let Some(expected_rg) = entry.checksum {
1030                    let mut hasher = Hasher::new();
1031                    let rg_bytes = self
1032                        .source
1033                        .read_range(entry.data_offset, entry.compressed_size)?;
1034                    hasher.update(&rg_bytes);
1035                    if hasher.finalize() != expected_rg {
1036                        return Err(ColumnarError::ChecksumMismatch);
1037                    }
1038                }
1039            }
1040
1041            let decoder: Box<dyn Decoder> = create_decoder(desc.encoding);
1042            let decompressed = if let CompressionV2::None = desc.compression {
1043                chunk_bytes
1044            } else {
1045                let compressor = create_compressor(desc.compression)
1046                    .map_err(|e| ColumnarError::InvalidFormat(e.to_string()))?;
1047                compressor
1048                    .decompress(&chunk_bytes, chunk_meta.uncompressed_length as usize)
1049                    .map_err(|e| ColumnarError::InvalidFormat(e.to_string()))?
1050            };
1051            let (col, bitmap) =
1052                decoder.decode(&decompressed, entry.row_count as usize, desc.logical_type)?;
1053            cols.push(col);
1054            bitmaps.push(bitmap);
1055        }
1056        let projected_schema = Schema {
1057            columns: columns
1058                .iter()
1059                .map(|&i| self.schema.columns[i].clone())
1060                .collect(),
1061        };
1062        Ok(RecordBatch::new(projected_schema, cols, bitmaps))
1063    }
1064
1065    /// RowGroup 単位で RecordBatch を順次読み取るイテレータ。
1066    pub fn iter_row_groups(&self) -> RowGroupIter<'_> {
1067        RowGroupIter {
1068            reader: self,
1069            index: 0,
1070        }
1071    }
1072
1073    /// RowGroup の総数を返す。
1074    pub fn row_group_count(&self) -> usize {
1075        self.footer.row_group_table.entries.len()
1076    }
1077}
1078
1079/// RowGroup イテレータ。
1080#[derive(Debug)]
1081pub struct RowGroupIter<'a> {
1082    reader: &'a SegmentReaderV2,
1083    index: usize,
1084}
1085
1086impl<'a> Iterator for RowGroupIter<'a> {
1087    type Item = Result<RecordBatch>;
1088
1089    fn next(&mut self) -> Option<Self::Item> {
1090        if self.index >= self.reader.footer.row_group_table.entries.len() {
1091            return None;
1092        }
1093        let idx = self.index;
1094        self.index += 1;
1095        let cols: Vec<usize> = (0..self.reader.schema.column_count()).collect();
1096        Some(self.reader.read_row_group_by_index(&cols, idx))
1097    }
1098}
1099
1100#[cfg(all(test, not(target_arch = "wasm32")))]
1101mod tests {
1102    use super::*;
1103
1104    fn simple_schema() -> Schema {
1105        Schema {
1106            columns: vec![
1107                ColumnSchema {
1108                    name: "id".into(),
1109                    logical_type: LogicalType::Int64,
1110                    nullable: false,
1111                    fixed_len: None,
1112                },
1113                ColumnSchema {
1114                    name: "val".into(),
1115                    logical_type: LogicalType::Int64,
1116                    nullable: false,
1117                    fixed_len: None,
1118                },
1119            ],
1120        }
1121    }
1122
1123    fn make_batch(rows: &[(i64, i64)]) -> RecordBatch {
1124        let ids: Vec<i64> = rows.iter().map(|(a, _)| *a).collect();
1125        let vals: Vec<i64> = rows.iter().map(|(_, b)| *b).collect();
1126        RecordBatch::new(
1127            simple_schema(),
1128            vec![Column::Int64(ids), Column::Int64(vals)],
1129            vec![None, None],
1130        )
1131    }
1132
1133    fn write_and_read(
1134        config: SegmentConfigV2,
1135        batches: Vec<RecordBatch>,
1136    ) -> Result<SegmentReaderV2> {
1137        let mut writer = SegmentWriterV2::new(config);
1138        for b in batches {
1139            writer.write_batch(b)?;
1140        }
1141        let segment = writer.finish()?;
1142        SegmentReaderV2::open(Box::new(InMemorySegmentSource::new(segment.data)))
1143    }
1144
1145    #[test]
1146    fn test_row_group_boundary() {
1147        let cfg = SegmentConfigV2 {
1148            row_group_size: 2,
1149            ..Default::default()
1150        };
1151        let reader = write_and_read(cfg, vec![make_batch(&[(1, 10), (2, 20), (3, 30)])]).unwrap();
1152        let batches = reader.read_columns(&[0, 1]).unwrap();
1153        assert_eq!(batches.len(), 2);
1154        assert_eq!(batches[0].num_rows(), 2);
1155        assert_eq!(batches[1].num_rows(), 1);
1156    }
1157
1158    #[test]
1159    fn test_16mib_row_group_guard() {
1160        let cfg = SegmentConfigV2 {
1161            row_group_size: 10,
1162            max_row_group_bytes: 16,
1163            ..Default::default()
1164        };
1165        let oversized = RecordBatch::new(
1166            simple_schema(),
1167            vec![Column::Int64(vec![1, 2, 3]), Column::Int64(vec![4, 5, 6])],
1168            vec![None, None],
1169        );
1170        let mut writer = SegmentWriterV2::new(cfg);
1171        writer.write_batch(oversized).unwrap();
1172        let err = writer.finish().unwrap_err();
1173        assert!(matches!(err, ColumnarError::RowGroupTooLarge { .. }));
1174    }
1175
1176    #[test]
1177    fn test_single_batch_too_large() {
1178        let cfg = SegmentConfigV2 {
1179            row_group_size: 100,
1180            max_row_group_bytes: 20,
1181            ..Default::default()
1182        };
1183        let batch = RecordBatch::new(
1184            simple_schema(),
1185            vec![
1186                Column::Int64((0..10).collect()),
1187                Column::Int64((0..10).collect()),
1188            ],
1189            vec![None, None],
1190        );
1191        let mut writer = SegmentWriterV2::new(cfg);
1192        writer.write_batch(batch).unwrap();
1193        let err = writer.finish().unwrap_err();
1194        assert!(matches!(err, ColumnarError::RowGroupTooLarge { .. }));
1195    }
1196
1197    #[test]
1198    fn test_footer_checksum_validation() {
1199        let segment = {
1200            let mut writer = SegmentWriterV2::new(Default::default());
1201            writer.write_batch(make_batch(&[(1, 10)])).unwrap();
1202            writer.finish().unwrap()
1203        };
1204        let mut data = segment.data.clone();
1205        let len = data.len();
1206        data[len - 1] ^= 0xFF; // フッターのチェックサムを壊す
1207        let err = SegmentReaderV2::open(Box::new(InMemorySegmentSource::new(data))).unwrap_err();
1208        assert!(matches!(err, ColumnarError::ChecksumMismatch));
1209    }
1210
1211    #[test]
1212    fn test_multi_column_roundtrip() {
1213        let reader = write_and_read(
1214            Default::default(),
1215            vec![make_batch(&[(1, 10), (2, 20), (3, 30)])],
1216        )
1217        .unwrap();
1218        let batches = reader.read_columns(&[0, 1]).unwrap();
1219        assert_eq!(batches.len(), 1);
1220        if let Column::Int64(ids) = &batches[0].columns[0] {
1221            assert_eq!(ids, &vec![1, 2, 3]);
1222        } else {
1223            panic!("expected int64");
1224        }
1225    }
1226
1227    #[test]
1228    fn test_offset_table_random_access() {
1229        let cfg = SegmentConfigV2 {
1230            row_group_size: 2,
1231            ..Default::default()
1232        };
1233        let reader =
1234            write_and_read(cfg, vec![make_batch(&[(1, 10), (2, 20), (3, 30), (4, 40)])]).unwrap();
1235        let batch = reader.read_row_group_by_index(&[1], 1).unwrap();
1236        assert_eq!(batch.num_rows(), 2);
1237        if let Column::Int64(vals) = &batch.columns[0] {
1238            assert_eq!(vals, &vec![30, 40]);
1239        } else {
1240            panic!("expected int64");
1241        }
1242    }
1243
1244    #[test]
1245    fn test_format_version_rejection() {
1246        let segment = {
1247            let mut writer = SegmentWriterV2::new(Default::default());
1248            writer.write_batch(make_batch(&[(1, 1)])).unwrap();
1249            writer.finish().unwrap()
1250        };
1251        let mut bytes = segment.data.clone();
1252        // バージョンを 1 にする
1253        bytes[4..6].copy_from_slice(&1u16.to_le_bytes());
1254        let err = SegmentReaderV2::open(Box::new(InMemorySegmentSource::new(bytes))).unwrap_err();
1255        assert!(matches!(
1256            err,
1257            ColumnarError::UnsupportedFormatVersion { .. }
1258        ));
1259    }
1260
1261    #[test]
1262    fn test_column_pruning() {
1263        let reader =
1264            write_and_read(Default::default(), vec![make_batch(&[(1, 10), (2, 20)])]).unwrap();
1265        let batches = reader.read_columns(&[1]).unwrap();
1266        assert_eq!(batches[0].columns.len(), 1);
1267        if let Column::Int64(vals) = &batches[0].columns[0] {
1268            assert_eq!(vals, &vec![10, 20]);
1269        } else {
1270            panic!("expected int64");
1271        }
1272    }
1273
1274    #[test]
1275    fn test_chunk_checksum_validation() {
1276        let cfg = SegmentConfigV2 {
1277            checksum_scope: ChecksumScope::Chunk,
1278            ..Default::default()
1279        };
1280        let segment = {
1281            let mut writer = SegmentWriterV2::new(cfg);
1282            writer
1283                .write_batch(make_batch(&[(1, 10), (2, 20), (3, 30)]))
1284                .unwrap();
1285            writer.finish().unwrap()
1286        };
1287        let reader_ok =
1288            SegmentReaderV2::open(Box::new(InMemorySegmentSource::new(segment.data.clone())))
1289                .unwrap();
1290        let entry = &reader_ok.footer.row_group_table.entries[0];
1291        let chunk = &entry.column_chunk_offsets[0];
1292        let mut corrupted = segment.data.clone();
1293        let chunk_pos = (entry.data_offset + chunk.offset) as usize;
1294        corrupted[chunk_pos] ^= 0xAA;
1295        let reader =
1296            SegmentReaderV2::open(Box::new(InMemorySegmentSource::new(corrupted))).unwrap();
1297        let err = reader.read_columns(&[0]).unwrap_err();
1298        assert!(matches!(err, ColumnarError::ChecksumMismatch));
1299    }
1300
1301    #[test]
1302    fn row_id_encode_decode_roundtrip() {
1303        let segment_id = 123u64;
1304        let offset = 456u64;
1305        let encoded = encode_row_id(segment_id, offset).expect("encode");
1306        let (decoded_seg, decoded_off) = decode_row_id(encoded);
1307        assert_eq!(decoded_seg, segment_id);
1308        assert_eq!(decoded_off, offset);
1309    }
1310
1311    #[test]
1312    fn row_id_encode_rejects_overflow() {
1313        let overflow_segment = (1u64 << ROW_ID_SEGMENT_BITS) + 1;
1314        assert!(matches!(
1315            encode_row_id(overflow_segment, 0),
1316            Err(ColumnarError::InvalidFormat(_))
1317        ));
1318
1319        let overflow_offset = (1u64 << ROW_ID_OFFSET_BITS) + 1;
1320        assert!(matches!(
1321            encode_row_id(0, overflow_offset),
1322            Err(ColumnarError::InvalidFormat(_))
1323        ));
1324    }
1325}