Skip to main content

alopex_sql/executor/bulk/
mod.rs

1//! COPY / Bulk Load 実装。
2//!
3//! 現段階では CSV/Parquet を簡易的に読み込み、テーブルスキーマに従って
4//! `SqlValue` へ変換する。Columnar ストレージも Row ストレージと同じ経路で
5//! 取り込み、将来の columnar エンジン実装で差し替え可能な構造にしている。
6
7use std::fs;
8use std::path::{Path, PathBuf};
9
10use alopex_core::columnar::encoding::{Column, LogicalType};
11use alopex_core::columnar::encoding_v2::Bitmap;
12use alopex_core::columnar::kvs_bridge::key_layout;
13use alopex_core::columnar::segment_v2::{
14    ColumnSchema, ColumnSegmentV2, RecordBatch, Schema, SegmentConfigV2, SegmentWriterV2,
15};
16use alopex_core::kv::{KVStore, KVTransaction};
17use alopex_core::storage::compression::CompressionV2;
18use alopex_core::storage::format::bincode_config;
19use bincode::config::Options;
20
21use crate::ast::ddl::IndexMethod;
22use crate::catalog::{
23    Catalog, ColumnMetadata, Compression, IndexMetadata, RowIdMode, TableMetadata,
24};
25use crate::columnar::statistics::compute_row_group_statistics;
26use crate::executor::fts_bridge::FtsBridge;
27use crate::executor::hnsw_bridge::HnswBridge;
28use crate::executor::{ExecutionResult, ExecutorError, Result};
29use crate::planner::types::ResolvedType;
30use crate::storage::{SqlTransaction, SqlValue, StorageError};
31
32mod csv;
33mod parquet;
34
35pub use csv::CsvReader;
36pub use parquet::ParquetReader;
37
38/// ファイル形式。
39#[derive(Debug, Clone, Copy, PartialEq, Eq)]
40pub enum FileFormat {
41    Csv,
42    Parquet,
43}
44
45/// COPY オプション。
46#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
47pub struct CopyOptions {
48    /// CSV ヘッダ行の有無。
49    pub header: bool,
50}
51
52/// COPY セキュリティ設定。
53#[derive(Debug, Clone, PartialEq, Eq, Default)]
54pub struct CopySecurityConfig {
55    /// 許可するベースディレクトリ一覧(None なら無制限)。
56    pub allowed_base_dirs: Option<Vec<PathBuf>>,
57    /// シンボリックリンクを許可するか。
58    pub allow_symlinks: bool,
59}
60
61/// 入力スキーマのフィールド。
62#[derive(Debug, Clone, PartialEq, Eq)]
63pub struct CopyField {
64    pub name: Option<String>,
65    pub data_type: Option<ResolvedType>,
66}
67
68/// 入力スキーマ。
69#[derive(Debug, Clone, PartialEq, Eq)]
70pub struct CopySchema {
71    pub fields: Vec<CopyField>,
72}
73
74impl CopySchema {
75    pub fn from_table(table: &TableMetadata) -> Self {
76        let fields = table
77            .columns
78            .iter()
79            .map(|c| CopyField {
80                name: Some(c.name.clone()),
81                data_type: Some(c.data_type.clone()),
82            })
83            .collect();
84        Self { fields }
85    }
86}
87
88/// バッチリーダー。
89pub trait BulkReader {
90    /// 入力スキーマを返す。
91    fn schema(&self) -> &CopySchema;
92    /// 最大 `max_rows` 行のバッチを返す。終端で None。
93    fn next_batch(&mut self, max_rows: usize) -> Result<Option<Vec<Vec<SqlValue>>>>;
94}
95
96/// COPY 文を実行する。
97pub fn execute_copy<S: KVStore, C: Catalog + ?Sized>(
98    txn: &mut SqlTransaction<'_, S>,
99    catalog: &C,
100    table_name: &str,
101    file_path: &str,
102    format: FileFormat,
103    options: CopyOptions,
104    config: &CopySecurityConfig,
105) -> Result<ExecutionResult> {
106    let table_meta = catalog
107        .get_table(table_name)
108        .cloned()
109        .ok_or_else(|| ExecutorError::TableNotFound(table_name.to_string()))?;
110
111    validate_file_path(file_path, config)?;
112
113    if !Path::new(file_path).exists() {
114        return Err(ExecutorError::FileNotFound(file_path.to_string()));
115    }
116
117    let reader: Box<dyn BulkReader> = match format {
118        FileFormat::Parquet => {
119            Box::new(ParquetReader::open(file_path, &table_meta, options.header)?)
120        }
121        FileFormat::Csv => Box::new(CsvReader::open(file_path, &table_meta, options.header)?),
122    };
123
124    validate_schema(reader.schema(), &table_meta)?;
125
126    let rows_loaded = match table_meta.storage_options.storage_type {
127        crate::catalog::StorageType::Columnar => {
128            bulk_load_columnar(txn, catalog, &table_meta, reader)?
129        }
130        crate::catalog::StorageType::Row => bulk_load_row(txn, catalog, &table_meta, reader)?,
131    };
132
133    Ok(ExecutionResult::RowsAffected(rows_loaded))
134}
135
136/// パスセキュリティ検証。
137pub fn validate_file_path(file_path: &str, config: &CopySecurityConfig) -> Result<()> {
138    let path = Path::new(file_path);
139
140    // 先に存在確認を行い、設計どおり FileNotFound を優先する。
141    if !path.exists() {
142        return Err(ExecutorError::FileNotFound(file_path.into()));
143    }
144
145    let canonical = path
146        .canonicalize()
147        .map_err(|e| ExecutorError::PathValidationFailed {
148            path: file_path.into(),
149            reason: format!("failed to canonicalize: {e}"),
150        })?;
151
152    if let Some(base_dirs) = &config.allowed_base_dirs {
153        let allowed = base_dirs.iter().any(|base| canonical.starts_with(base));
154        if !allowed {
155            return Err(ExecutorError::PathValidationFailed {
156                path: file_path.into(),
157                reason: format!("path not in allowed directories: {:?}", base_dirs),
158            });
159        }
160    }
161
162    if !config.allow_symlinks && path.is_symlink() {
163        return Err(ExecutorError::PathValidationFailed {
164            path: file_path.into(),
165            reason: "symbolic links not allowed".into(),
166        });
167    }
168
169    let metadata = fs::metadata(&canonical).map_err(|e| ExecutorError::PathValidationFailed {
170        path: file_path.into(),
171        reason: format!("cannot access file: {e}"),
172    })?;
173
174    if !metadata.is_file() {
175        return Err(ExecutorError::PathValidationFailed {
176            path: file_path.into(),
177            reason: "path is not a regular file".into(),
178        });
179    }
180
181    #[cfg(unix)]
182    {
183        use std::os::unix::fs::PermissionsExt;
184        if metadata.permissions().mode() & 0o444 == 0 {
185            return Err(ExecutorError::PathValidationFailed {
186                path: file_path.into(),
187                reason: "file is not readable".into(),
188            });
189        }
190    }
191
192    Ok(())
193}
194
195/// スキーマ整合性検証。
196pub fn validate_schema(schema: &CopySchema, table_meta: &TableMetadata) -> Result<()> {
197    if schema.fields.len() != table_meta.columns.len() {
198        return Err(ExecutorError::SchemaMismatch {
199            expected: table_meta.columns.len(),
200            actual: schema.fields.len(),
201            reason: "column count mismatch".into(),
202        });
203    }
204
205    for (idx, (field, col)) in schema
206        .fields
207        .iter()
208        .zip(table_meta.columns.iter())
209        .enumerate()
210    {
211        if let Some(dt) = &field.data_type
212            && !is_type_compatible(dt, &col.data_type)
213        {
214            return Err(ExecutorError::SchemaMismatch {
215                expected: table_meta.columns.len(),
216                actual: schema.fields.len(),
217                reason: format!(
218                    "type mismatch for column '{}': expected {:?}, got {:?}",
219                    col.name, col.data_type, dt
220                ),
221            });
222        }
223        if let Some(name) = &field.name
224            && name != &col.name
225        {
226            return Err(ExecutorError::SchemaMismatch {
227                expected: table_meta.columns.len(),
228                actual: schema.fields.len(),
229                reason: format!(
230                    "column name mismatch at position {}: expected '{}', got '{}'",
231                    idx, col.name, name
232                ),
233            });
234        }
235    }
236
237    Ok(())
238}
239
240/// Row ストレージへの書き込み。
241fn bulk_load_row<S: KVStore, C: Catalog + ?Sized>(
242    txn: &mut SqlTransaction<'_, S>,
243    catalog: &C,
244    table: &TableMetadata,
245    mut reader: Box<dyn BulkReader>,
246) -> Result<u64> {
247    let indexes: Vec<IndexMetadata> = catalog
248        .get_indexes_for_table(&table.name)
249        .into_iter()
250        .cloned()
251        .collect();
252    let (hnsw_indexes, indexes): (Vec<_>, Vec<_>) = indexes
253        .into_iter()
254        .partition(|idx| matches!(idx.method, Some(IndexMethod::Hnsw)));
255    let (fts_indexes, btree_indexes): (Vec<_>, Vec<_>) = indexes
256        .into_iter()
257        .partition(|idx| matches!(idx.method, Some(IndexMethod::Fts)));
258
259    let mut staged: Vec<(u64, Vec<SqlValue>)> = Vec::new();
260    {
261        let mut storage = txn.table_storage(table);
262        while let Some(batch) = reader.next_batch(1024)? {
263            for row in batch {
264                if row.len() != table.column_count() {
265                    return Err(ExecutorError::BulkLoad(format!(
266                        "row has {} columns, expected {}",
267                        row.len(),
268                        table.column_count()
269                    )));
270                }
271                let row_id = storage
272                    .next_row_id()
273                    .map_err(|e| map_storage_error(table, e))?;
274                storage
275                    .insert(row_id, &row)
276                    .map_err(|e| map_storage_error(table, e))?;
277                staged.push((row_id, row));
278            }
279        }
280    }
281
282    populate_indexes(txn, &btree_indexes, &staged)?;
283    populate_fts_indexes(txn, &fts_indexes, &staged)?;
284    populate_hnsw_indexes(txn, table, &hnsw_indexes, &staged)?;
285
286    Ok(staged.len() as u64)
287}
288
289fn populate_fts_indexes<S: KVStore>(
290    txn: &mut SqlTransaction<'_, S>,
291    indexes: &[IndexMetadata],
292    rows: &[(u64, Vec<SqlValue>)],
293) -> Result<()> {
294    for index in indexes {
295        for (row_id, row) in rows {
296            FtsBridge::on_insert(txn, index, *row_id, row)?;
297        }
298    }
299    Ok(())
300}
301
302/// Columnar ストレージへの書き込み(現状は Row と同経路で処理)。
303fn bulk_load_columnar<S: KVStore, C: Catalog + ?Sized>(
304    txn: &mut SqlTransaction<'_, S>,
305    catalog: &C,
306    table: &TableMetadata,
307    mut reader: Box<dyn BulkReader>,
308) -> Result<u64> {
309    let _ = catalog; // reserved for future index integration
310
311    let row_group_size = table.storage_options.row_group_size.max(1) as usize;
312    let compression = map_compression(table.storage_options.compression);
313    let mut writer = SegmentWriterV2::new(SegmentConfigV2 {
314        row_group_size: row_group_size as u64,
315        compression,
316        ..Default::default()
317    });
318    let schema = build_segment_schema(table)?;
319
320    let mut row_group_stats = Vec::new();
321    let mut total_rows = 0u64;
322    while let Some(batch) = reader.next_batch(row_group_size)? {
323        if batch.is_empty() {
324            continue;
325        }
326        let stats = compute_row_group_statistics(&batch);
327        let record_batch = build_record_batch(&schema, table, &batch)?;
328        writer
329            .write_batch(record_batch)
330            .map_err(|e| ExecutorError::Columnar(e.to_string()))?;
331        row_group_stats.push(stats);
332        total_rows += batch.len() as u64;
333    }
334
335    if total_rows == 0 {
336        return Ok(0);
337    }
338
339    let segment = writer
340        .finish()
341        .map_err(|e| ExecutorError::Columnar(e.to_string()))?;
342    let _segment_id = persist_segment(txn, table, segment, &row_group_stats)?;
343
344    Ok(total_rows)
345}
346
347fn map_compression(compression: Compression) -> CompressionV2 {
348    let desired = match compression {
349        Compression::None => CompressionV2::None,
350        Compression::Lz4 => CompressionV2::Lz4,
351        Compression::Zstd => CompressionV2::Zstd { level: 3 },
352    };
353
354    if desired.is_available() {
355        desired
356    } else {
357        CompressionV2::None
358    }
359}
360
361fn build_segment_schema(table: &TableMetadata) -> Result<Schema> {
362    let mut columns = Vec::with_capacity(table.column_count());
363    for col in &table.columns {
364        let logical_type = logical_type_for(&col.data_type)?;
365        columns.push(ColumnSchema {
366            name: col.name.clone(),
367            logical_type,
368            nullable: !col.not_null,
369            fixed_len: fixed_len_for(&col.data_type),
370        });
371    }
372    Ok(Schema { columns })
373}
374
375fn logical_type_for(ty: &ResolvedType) -> Result<LogicalType> {
376    match ty {
377        ResolvedType::Integer
378        | ResolvedType::BigInt
379        | ResolvedType::Timestamp
380        | ResolvedType::Date
381        | ResolvedType::Time => Ok(LogicalType::Int64),
382        ResolvedType::Interval => Ok(LogicalType::Fixed(16)),
383        ResolvedType::Decimal { .. } => Ok(LogicalType::Fixed(16)),
384        ResolvedType::Vector { dimension, .. } => {
385            Ok(LogicalType::Fixed(dimension.checked_mul(4).ok_or_else(|| {
386                ExecutorError::Columnar("vector dimension overflow when computing fixed len".into())
387            })? as u16))
388        }
389        ResolvedType::Float => Ok(LogicalType::Float32),
390        ResolvedType::Double => Ok(LogicalType::Float64),
391        ResolvedType::Boolean => Ok(LogicalType::Bool),
392        ResolvedType::Text
393        | ResolvedType::Blob
394        | ResolvedType::Json
395        | ResolvedType::Array(_)
396        | ResolvedType::Map { .. }
397        | ResolvedType::Struct(_) => Ok(LogicalType::Binary),
398        ResolvedType::Null => Err(ExecutorError::Columnar(
399            "NULL column type is not supported for columnar storage".into(),
400        )),
401    }
402}
403
404fn fixed_len_for(ty: &ResolvedType) -> Option<u32> {
405    match ty {
406        ResolvedType::Vector { dimension, .. } => Some(dimension.saturating_mul(4)),
407        ResolvedType::Decimal { .. } => Some(16),
408        _ => None,
409    }
410}
411
412fn build_record_batch(
413    schema: &Schema,
414    table: &TableMetadata,
415    rows: &[Vec<SqlValue>],
416) -> Result<RecordBatch> {
417    for row in rows {
418        if row.len() != table.column_count() {
419            return Err(ExecutorError::BulkLoad(format!(
420                "row has {} columns, expected {}",
421                row.len(),
422                table.column_count()
423            )));
424        }
425    }
426
427    let mut columns = Vec::with_capacity(table.column_count());
428    let mut bitmaps = Vec::with_capacity(table.column_count());
429    for (idx, col_meta) in table.columns.iter().enumerate() {
430        let (col, bitmap) = build_column(idx, col_meta, rows)?;
431        columns.push(col);
432        bitmaps.push(bitmap);
433    }
434
435    Ok(RecordBatch::new(schema.clone(), columns, bitmaps))
436}
437
438fn validity_bitmap(validity: &[bool]) -> Option<Bitmap> {
439    if validity.iter().all(|v| *v) {
440        None
441    } else {
442        Some(Bitmap::from_bools(validity))
443    }
444}
445
446fn build_column(
447    col_idx: usize,
448    col_meta: &ColumnMetadata,
449    rows: &[Vec<SqlValue>],
450) -> Result<(Column, Option<Bitmap>)> {
451    match &col_meta.data_type {
452        ResolvedType::Integer => {
453            let mut validity = Vec::with_capacity(rows.len());
454            let mut values = Vec::with_capacity(rows.len());
455            for row in rows {
456                match row
457                    .get(col_idx)
458                    .ok_or_else(|| ExecutorError::BulkLoad("row too short".into()))?
459                {
460                    SqlValue::Null => {
461                        validity.push(false);
462                        values.push(0);
463                    }
464                    SqlValue::Integer(v) => {
465                        validity.push(true);
466                        values.push(*v as i64);
467                    }
468                    SqlValue::BigInt(v) => {
469                        validity.push(true);
470                        values.push(*v);
471                    }
472                    other => {
473                        return Err(ExecutorError::BulkLoad(format!(
474                            "type mismatch for column '{}': expected Integer, got {}",
475                            col_meta.name,
476                            other.type_name()
477                        )));
478                    }
479                }
480            }
481            Ok((Column::Int64(values), validity_bitmap(&validity)))
482        }
483        ResolvedType::BigInt
484        | ResolvedType::Timestamp
485        | ResolvedType::Date
486        | ResolvedType::Time => {
487            let mut validity = Vec::with_capacity(rows.len());
488            let mut values = Vec::with_capacity(rows.len());
489            for row in rows {
490                let value = row
491                    .get(col_idx)
492                    .ok_or_else(|| ExecutorError::BulkLoad("row too short".into()))?;
493                match (&col_meta.data_type, value) {
494                    (_, SqlValue::Null) => {
495                        validity.push(false);
496                        values.push(0);
497                    }
498                    (
499                        ResolvedType::BigInt | ResolvedType::Timestamp,
500                        SqlValue::BigInt(v) | SqlValue::Timestamp(v),
501                    )
502                    | (ResolvedType::Time, SqlValue::Time(v)) => {
503                        validity.push(true);
504                        values.push(*v);
505                    }
506                    (ResolvedType::BigInt | ResolvedType::Timestamp, SqlValue::Integer(v))
507                    | (ResolvedType::Date, SqlValue::Date(v)) => {
508                        validity.push(true);
509                        values.push(*v as i64);
510                    }
511                    (_, other) => {
512                        return Err(ExecutorError::BulkLoad(format!(
513                            "type mismatch for column '{}': expected {}, got {}",
514                            col_meta.name,
515                            col_meta.data_type.type_name(),
516                            other.type_name()
517                        )));
518                    }
519                }
520            }
521            Ok((Column::Int64(values), validity_bitmap(&validity)))
522        }
523        ResolvedType::Float => {
524            let mut validity = Vec::with_capacity(rows.len());
525            let mut values = Vec::with_capacity(rows.len());
526            for row in rows {
527                match row
528                    .get(col_idx)
529                    .ok_or_else(|| ExecutorError::BulkLoad("row too short".into()))?
530                {
531                    SqlValue::Null => {
532                        validity.push(false);
533                        values.push(0.0);
534                    }
535                    SqlValue::Float(v) => {
536                        validity.push(true);
537                        values.push(*v);
538                    }
539                    other => {
540                        return Err(ExecutorError::BulkLoad(format!(
541                            "type mismatch for column '{}': expected Float, got {}",
542                            col_meta.name,
543                            other.type_name()
544                        )));
545                    }
546                }
547            }
548            Ok((Column::Float32(values), validity_bitmap(&validity)))
549        }
550        ResolvedType::Double => {
551            let mut validity = Vec::with_capacity(rows.len());
552            let mut values = Vec::with_capacity(rows.len());
553            for row in rows {
554                match row
555                    .get(col_idx)
556                    .ok_or_else(|| ExecutorError::BulkLoad("row too short".into()))?
557                {
558                    SqlValue::Null => {
559                        validity.push(false);
560                        values.push(0.0);
561                    }
562                    SqlValue::Double(v) => {
563                        validity.push(true);
564                        values.push(*v);
565                    }
566                    other => {
567                        return Err(ExecutorError::BulkLoad(format!(
568                            "type mismatch for column '{}': expected Double, got {}",
569                            col_meta.name,
570                            other.type_name()
571                        )));
572                    }
573                }
574            }
575            Ok((Column::Float64(values), validity_bitmap(&validity)))
576        }
577        ResolvedType::Boolean => {
578            let mut validity = Vec::with_capacity(rows.len());
579            let mut values = Vec::with_capacity(rows.len());
580            for row in rows {
581                match row
582                    .get(col_idx)
583                    .ok_or_else(|| ExecutorError::BulkLoad("row too short".into()))?
584                {
585                    SqlValue::Null => {
586                        validity.push(false);
587                        values.push(false);
588                    }
589                    SqlValue::Boolean(v) => {
590                        validity.push(true);
591                        values.push(*v);
592                    }
593                    other => {
594                        return Err(ExecutorError::BulkLoad(format!(
595                            "type mismatch for column '{}': expected Boolean, got {}",
596                            col_meta.name,
597                            other.type_name()
598                        )));
599                    }
600                }
601            }
602            Ok((Column::Bool(values), validity_bitmap(&validity)))
603        }
604        ResolvedType::Text => {
605            let mut validity = Vec::with_capacity(rows.len());
606            let mut values = Vec::with_capacity(rows.len());
607            for row in rows {
608                match row
609                    .get(col_idx)
610                    .ok_or_else(|| ExecutorError::BulkLoad("row too short".into()))?
611                {
612                    SqlValue::Null => {
613                        validity.push(false);
614                        values.push(Vec::new());
615                    }
616                    SqlValue::Text(v) => {
617                        validity.push(true);
618                        values.push(v.as_bytes().to_vec());
619                    }
620                    other => {
621                        return Err(ExecutorError::BulkLoad(format!(
622                            "type mismatch for column '{}': expected Text, got {}",
623                            col_meta.name,
624                            other.type_name()
625                        )));
626                    }
627                }
628            }
629            Ok((Column::Binary(values), validity_bitmap(&validity)))
630        }
631        ResolvedType::Json => {
632            let mut validity = Vec::with_capacity(rows.len());
633            let mut values = Vec::with_capacity(rows.len());
634            for row in rows {
635                match row
636                    .get(col_idx)
637                    .ok_or_else(|| ExecutorError::BulkLoad("row too short".into()))?
638                {
639                    SqlValue::Null => {
640                        validity.push(false);
641                        values.push(Vec::new());
642                    }
643                    SqlValue::Json(value) => {
644                        validity.push(true);
645                        values.push(value.as_str().as_bytes().to_vec());
646                    }
647                    other => {
648                        return Err(ExecutorError::BulkLoad(format!(
649                            "type mismatch for column '{}': expected Json, got {}",
650                            col_meta.name,
651                            other.type_name()
652                        )));
653                    }
654                }
655            }
656            Ok((Column::Binary(values), validity_bitmap(&validity)))
657        }
658        ResolvedType::Array(_) | ResolvedType::Map { .. } | ResolvedType::Struct(_) => {
659            let mut validity = Vec::with_capacity(rows.len());
660            let mut values = Vec::with_capacity(rows.len());
661            for row in rows {
662                match row
663                    .get(col_idx)
664                    .ok_or_else(|| ExecutorError::BulkLoad("row too short".into()))?
665                {
666                    SqlValue::Null => {
667                        validity.push(false);
668                        values.push(Vec::new());
669                    }
670                    value
671                        if matches!(
672                            value,
673                            SqlValue::Array(_) | SqlValue::Map(_) | SqlValue::Struct(_)
674                        ) =>
675                    {
676                        validity.push(true);
677                        values.push(crate::storage::RowCodec::encode(std::slice::from_ref(
678                            value,
679                        )));
680                    }
681                    other => {
682                        return Err(ExecutorError::BulkLoad(format!(
683                            "type mismatch for column '{}': expected {}, got {}",
684                            col_meta.name,
685                            col_meta.data_type.type_name(),
686                            other.type_name()
687                        )));
688                    }
689                }
690            }
691            Ok((Column::Binary(values), validity_bitmap(&validity)))
692        }
693        ResolvedType::Blob => {
694            let mut validity = Vec::with_capacity(rows.len());
695            let mut values = Vec::with_capacity(rows.len());
696            for row in rows {
697                match row
698                    .get(col_idx)
699                    .ok_or_else(|| ExecutorError::BulkLoad("row too short".into()))?
700                {
701                    SqlValue::Null => {
702                        validity.push(false);
703                        values.push(Vec::new());
704                    }
705                    SqlValue::Blob(v) => {
706                        validity.push(true);
707                        values.push(v.clone());
708                    }
709                    other => {
710                        return Err(ExecutorError::BulkLoad(format!(
711                            "type mismatch for column '{}': expected Blob, got {}",
712                            col_meta.name,
713                            other.type_name()
714                        )));
715                    }
716                }
717            }
718            Ok((Column::Binary(values), validity_bitmap(&validity)))
719        }
720        ResolvedType::Vector { dimension, .. } => {
721            let fixed_len = dimension.saturating_mul(4) as usize;
722            let mut validity = Vec::with_capacity(rows.len());
723            let mut values = Vec::with_capacity(rows.len());
724            for row in rows {
725                match row
726                    .get(col_idx)
727                    .ok_or_else(|| ExecutorError::BulkLoad("row too short".into()))?
728                {
729                    SqlValue::Null => {
730                        validity.push(false);
731                        values.push(vec![0u8; fixed_len]);
732                    }
733                    SqlValue::Vector(v) => {
734                        if v.len() as u32 != *dimension {
735                            return Err(ExecutorError::BulkLoad(format!(
736                                "vector dimension mismatch for column '{}': expected {}, got {}",
737                                col_meta.name,
738                                dimension,
739                                v.len()
740                            )));
741                        }
742                        validity.push(true);
743                        let mut buf = Vec::with_capacity(fixed_len);
744                        for f in v {
745                            buf.extend_from_slice(&f.to_le_bytes());
746                        }
747                        values.push(buf);
748                    }
749                    other => {
750                        return Err(ExecutorError::BulkLoad(format!(
751                            "type mismatch for column '{}': expected Vector, got {}",
752                            col_meta.name,
753                            other.type_name()
754                        )));
755                    }
756                }
757            }
758            Ok((
759                Column::Fixed {
760                    len: fixed_len,
761                    values,
762                },
763                validity_bitmap(&validity),
764            ))
765        }
766        ResolvedType::Interval => {
767            let mut validity = Vec::with_capacity(rows.len());
768            let mut values = Vec::with_capacity(rows.len());
769            for row in rows {
770                match row
771                    .get(col_idx)
772                    .ok_or_else(|| ExecutorError::BulkLoad("row too short".into()))?
773                {
774                    SqlValue::Null => {
775                        validity.push(false);
776                        values.push(vec![0; 16]);
777                    }
778                    SqlValue::Interval {
779                        months,
780                        days,
781                        micros,
782                    } => {
783                        validity.push(true);
784                        let mut value = Vec::with_capacity(16);
785                        value.extend_from_slice(&months.to_le_bytes());
786                        value.extend_from_slice(&days.to_le_bytes());
787                        value.extend_from_slice(&micros.to_le_bytes());
788                        values.push(value);
789                    }
790                    other => {
791                        return Err(ExecutorError::BulkLoad(format!(
792                            "type mismatch for column '{}': expected Interval, got {}",
793                            col_meta.name,
794                            other.type_name()
795                        )));
796                    }
797                }
798            }
799            Ok((
800                Column::Fixed { len: 16, values },
801                validity_bitmap(&validity),
802            ))
803        }
804        ResolvedType::Decimal { precision, scale } => {
805            let mut validity = Vec::with_capacity(rows.len());
806            let mut values = Vec::with_capacity(rows.len());
807            for row in rows {
808                match row
809                    .get(col_idx)
810                    .ok_or_else(|| ExecutorError::BulkLoad("row too short".into()))?
811                {
812                    SqlValue::Null => {
813                        validity.push(false);
814                        values.push(vec![0; 16]);
815                    }
816                    SqlValue::Decimal(value)
817                        if value.scale == *scale && value.fits_precision(*precision) =>
818                    {
819                        validity.push(true);
820                        values.push(value.coefficient.to_le_bytes().to_vec());
821                    }
822                    other => {
823                        return Err(ExecutorError::BulkLoad(format!(
824                            "type mismatch for column '{}': expected Decimal({precision},{scale}), got {}",
825                            col_meta.name,
826                            other.type_name()
827                        )));
828                    }
829                }
830            }
831            Ok((
832                Column::Fixed { len: 16, values },
833                validity_bitmap(&validity),
834            ))
835        }
836        ResolvedType::Null => Err(ExecutorError::Columnar(
837            "NULL column type is not supported for columnar storage".into(),
838        )),
839    }
840}
841
842fn persist_segment<S: KVStore>(
843    txn: &mut SqlTransaction<'_, S>,
844    table: &TableMetadata,
845    mut segment: ColumnSegmentV2,
846    row_group_stats: &[crate::columnar::statistics::RowGroupStatistics],
847) -> Result<u64> {
848    if row_group_stats.len() != segment.meta.row_groups.len() {
849        return Err(ExecutorError::Columnar(
850            "row group statistics length mismatch".into(),
851        ));
852    }
853
854    let table_id = table.table_id;
855    let index_key = key_layout::segment_index_key(table_id);
856    let existing = txn.inner_mut().get(&index_key)?;
857    let mut index: Vec<u64> = if let Some(bytes) = existing {
858        bincode_config()
859            .deserialize(&bytes)
860            .map_err(|e| ExecutorError::Columnar(e.to_string()))?
861    } else {
862        Vec::new()
863    };
864    let segment_id = index
865        .last()
866        .copied()
867        .map(|id| id.saturating_add(1))
868        .unwrap_or(0);
869
870    let mut row_group_stats = row_group_stats.to_vec();
871    if table.storage_options.row_id_mode == RowIdMode::Direct {
872        let total_rows = usize::try_from(segment.meta.num_rows)
873            .map_err(|_| ExecutorError::Columnar("segment row count exceeds usize::MAX".into()))?;
874        segment.row_ids = (0..total_rows)
875            .map(|idx| {
876                alopex_core::columnar::segment_v2::encode_row_id(segment_id, idx as u64)
877                    .map_err(|e| ExecutorError::Columnar(e.to_string()))
878            })
879            .collect::<Result<Vec<u64>>>()?;
880
881        for (idx, meta) in segment.meta.row_groups.iter().enumerate() {
882            let start = usize::try_from(meta.row_start)
883                .map_err(|_| ExecutorError::Columnar("row_start exceeds usize::MAX".into()))?;
884            let count = usize::try_from(meta.row_count)
885                .map_err(|_| ExecutorError::Columnar("row_count exceeds usize::MAX".into()))?;
886            if count == 0 {
887                continue;
888            }
889            let end = start
890                .checked_add(count)
891                .ok_or_else(|| ExecutorError::Columnar("row_id range overflow".into()))?;
892            if end > segment.row_ids.len() {
893                return Err(ExecutorError::Columnar(
894                    "row_ids length is smaller than row_group range".into(),
895                ));
896            }
897            row_group_stats[idx].row_id_min = segment.row_ids.get(start).copied();
898            row_group_stats[idx].row_id_max = segment.row_ids.get(end - 1).copied();
899        }
900    } else {
901        segment.row_ids.clear();
902    }
903
904    let segment_bytes = bincode_config()
905        .serialize(&segment)
906        .map_err(|e| ExecutorError::Columnar(e.to_string()))?;
907    txn.inner_mut().put(
908        key_layout::column_segment_key(table_id, segment_id, 0),
909        segment_bytes,
910    )?;
911
912    let meta_bytes = bincode_config()
913        .serialize(&segment.meta)
914        .map_err(|e| ExecutorError::Columnar(e.to_string()))?;
915    txn.inner_mut()
916        .put(key_layout::statistics_key(table_id, segment_id), meta_bytes)?;
917
918    let rg_bytes = bincode_config()
919        .serialize(&row_group_stats)
920        .map_err(|e| ExecutorError::Columnar(e.to_string()))?;
921    txn.inner_mut().put(
922        key_layout::row_group_stats_key(table_id, segment_id),
923        rg_bytes,
924    )?;
925
926    index.push(segment_id);
927    let index_bytes = bincode_config()
928        .serialize(&index)
929        .map_err(|e| ExecutorError::Columnar(e.to_string()))?;
930    txn.inner_mut().put(index_key, index_bytes)?;
931    Ok(segment_id)
932}
933
934/// テキストをテーブル型に合わせて `SqlValue` へ変換する。
935pub(crate) fn parse_value(raw: &str, ty: &ResolvedType) -> Result<SqlValue> {
936    let trimmed = raw.trim();
937    if trimmed.eq_ignore_ascii_case("null") {
938        return Ok(SqlValue::Null);
939    }
940
941    match ty {
942        ResolvedType::Integer => trimmed
943            .parse::<i32>()
944            .map(SqlValue::Integer)
945            .map_err(|e| parse_error(trimmed, ty, e)),
946        ResolvedType::BigInt => trimmed
947            .parse::<i64>()
948            .map(SqlValue::BigInt)
949            .map_err(|e| parse_error(trimmed, ty, e)),
950        ResolvedType::Float => trimmed
951            .parse::<f32>()
952            .map(SqlValue::Float)
953            .map_err(|e| parse_error(trimmed, ty, e)),
954        ResolvedType::Double => trimmed
955            .parse::<f64>()
956            .map(SqlValue::Double)
957            .map_err(|e| parse_error(trimmed, ty, e)),
958        ResolvedType::Boolean => {
959            let parsed = trimmed
960                .parse::<bool>()
961                .or(match trimmed {
962                    "1" => Ok(true),
963                    "0" => Ok(false),
964                    _ => Err(()),
965                })
966                .map_err(|_| {
967                    ExecutorError::BulkLoad(format!(
968                        "failed to parse value '{trimmed}' as {}: invalid boolean",
969                        ty.type_name()
970                    ))
971                })?;
972            Ok(SqlValue::Boolean(parsed))
973        }
974        ResolvedType::Timestamp => trimmed
975            .parse::<i64>()
976            .map(SqlValue::Timestamp)
977            .map_err(|e| parse_error(trimmed, ty, e)),
978        ResolvedType::Date
979        | ResolvedType::Time
980        | ResolvedType::Interval
981        | ResolvedType::Decimal { .. }
982        | ResolvedType::Json => {
983            crate::executor::evaluator::coerce_value(SqlValue::Text(trimmed.to_string()), ty)
984        }
985        ResolvedType::Array(_) | ResolvedType::Map { .. } | ResolvedType::Struct(_) => {
986            crate::executor::evaluator::nested::parse_typed_json(trimmed, ty)
987        }
988        ResolvedType::Text => Ok(SqlValue::Text(trimmed.to_string())),
989        ResolvedType::Blob => Ok(SqlValue::Blob(trimmed.as_bytes().to_vec())),
990        ResolvedType::Vector { dimension, .. } => {
991            let body = trimmed.trim_matches(['[', ']']);
992            if body.is_empty() {
993                return Err(ExecutorError::BulkLoad(
994                    "vector literal cannot be empty".into(),
995                ));
996            }
997            let mut values = Vec::new();
998            for part in body.split(',') {
999                let v = part
1000                    .trim()
1001                    .parse::<f32>()
1002                    .map_err(|e| ExecutorError::BulkLoad(format!("invalid vector value: {e}")))?;
1003                values.push(v);
1004            }
1005            if values.len() as u32 != *dimension {
1006                return Err(ExecutorError::BulkLoad(format!(
1007                    "vector dimension mismatch: expected {}, got {}",
1008                    dimension,
1009                    values.len()
1010                )));
1011            }
1012            Ok(SqlValue::Vector(values))
1013        }
1014        ResolvedType::Null => Ok(SqlValue::Null),
1015    }
1016}
1017
1018fn parse_error(trimmed: &str, ty: &ResolvedType, err: impl std::fmt::Display) -> ExecutorError {
1019    ExecutorError::BulkLoad(format!(
1020        "failed to parse value '{trimmed}' as {}: {err}",
1021        ty.type_name()
1022    ))
1023}
1024
1025fn is_type_compatible(file_type: &ResolvedType, table_type: &ResolvedType) -> bool {
1026    match (file_type, table_type) {
1027        (
1028            ResolvedType::Vector {
1029                dimension: f_dim,
1030                metric: f_metric,
1031            },
1032            ResolvedType::Vector {
1033                dimension: t_dim,
1034                metric: t_metric,
1035            },
1036        ) => f_dim == t_dim && f_metric == t_metric,
1037        (ft, tt) => ft == tt || ft.can_cast_to(tt),
1038    }
1039}
1040
1041fn map_storage_error(table: &TableMetadata, err: StorageError) -> ExecutorError {
1042    match err {
1043        StorageError::NullConstraintViolation { column } => {
1044            ExecutorError::ConstraintViolation(crate::executor::ConstraintViolation::NotNull {
1045                column,
1046            })
1047        }
1048        StorageError::PrimaryKeyViolation { .. } => {
1049            ExecutorError::ConstraintViolation(crate::executor::ConstraintViolation::PrimaryKey {
1050                columns: table.primary_key.clone().unwrap_or_default(),
1051                value: None,
1052            })
1053        }
1054        StorageError::TransactionConflict => ExecutorError::TransactionConflict,
1055        other => ExecutorError::Storage(other),
1056    }
1057}
1058
1059fn map_index_error(index: &IndexMetadata, err: StorageError) -> ExecutorError {
1060    match err {
1061        StorageError::UniqueViolation { .. } => {
1062            if index.name.starts_with("__pk_") {
1063                ExecutorError::ConstraintViolation(
1064                    crate::executor::ConstraintViolation::PrimaryKey {
1065                        columns: index.columns.clone(),
1066                        value: None,
1067                    },
1068                )
1069            } else {
1070                ExecutorError::ConstraintViolation(crate::executor::ConstraintViolation::Unique {
1071                    index_name: index.name.clone(),
1072                    columns: index.columns.clone(),
1073                    value: None,
1074                })
1075            }
1076        }
1077        StorageError::NullConstraintViolation { column } => {
1078            ExecutorError::ConstraintViolation(crate::executor::ConstraintViolation::NotNull {
1079                column,
1080            })
1081        }
1082        StorageError::TransactionConflict => ExecutorError::TransactionConflict,
1083        other => ExecutorError::Storage(other),
1084    }
1085}
1086
1087fn populate_indexes<S: KVStore>(
1088    txn: &mut SqlTransaction<'_, S>,
1089    indexes: &[IndexMetadata],
1090    rows: &[(u64, Vec<SqlValue>)],
1091) -> Result<()> {
1092    for index in indexes {
1093        let mut storage =
1094            txn.index_storage(index.index_id, index.unique, index.column_indices.clone());
1095        for (row_id, row) in rows {
1096            if should_skip_unique_index_for_null(index, row) {
1097                continue;
1098            }
1099            storage
1100                .insert(row, *row_id)
1101                .map_err(|e| map_index_error(index, e))?;
1102        }
1103    }
1104    Ok(())
1105}
1106
1107fn populate_hnsw_indexes<S: KVStore>(
1108    txn: &mut SqlTransaction<'_, S>,
1109    table: &TableMetadata,
1110    indexes: &[IndexMetadata],
1111    rows: &[(u64, Vec<SqlValue>)],
1112) -> Result<()> {
1113    for index in indexes {
1114        for (row_id, row) in rows {
1115            HnswBridge::on_insert(txn, table, index, *row_id, row)?;
1116        }
1117    }
1118    Ok(())
1119}
1120
1121fn should_skip_unique_index_for_null(index: &IndexMetadata, row: &[SqlValue]) -> bool {
1122    index.unique
1123        && index
1124            .column_indices
1125            .iter()
1126            .any(|&idx| row.get(idx).is_none_or(SqlValue::is_null))
1127}
1128
1129#[cfg(test)]
1130mod tests {
1131    use super::*;
1132    use crate::catalog::{ColumnMetadata, MemoryCatalog, StorageType};
1133    use crate::executor::ddl::create_table::execute_create_table;
1134    use crate::planner::types::ResolvedType;
1135    use crate::storage::TxnBridge;
1136    use ::parquet::arrow::ArrowWriter;
1137    use alopex_core::kv::memory::MemoryKV;
1138    use arrow_array::{Int32Array, RecordBatch, StringArray};
1139    use arrow_schema::{DataType as ArrowDataType, Field as ArrowField, Schema as ArrowSchema};
1140    use std::fs::File;
1141    use std::io::Write;
1142    use std::path::Path;
1143    use std::sync::Arc;
1144
1145    fn bridge() -> (TxnBridge<MemoryKV>, MemoryCatalog) {
1146        (
1147            TxnBridge::new(Arc::new(MemoryKV::new())),
1148            MemoryCatalog::new(),
1149        )
1150    }
1151
1152    fn create_table(
1153        bridge: &TxnBridge<MemoryKV>,
1154        catalog: &mut MemoryCatalog,
1155        storage: StorageType,
1156    ) {
1157        let mut table = TableMetadata::new(
1158            "users",
1159            vec![
1160                ColumnMetadata::new("id", ResolvedType::Integer).with_primary_key(true),
1161                ColumnMetadata::new("name", ResolvedType::Text),
1162            ],
1163        )
1164        .with_primary_key(vec!["id".into()]);
1165        table.storage_options.storage_type = storage;
1166
1167        let mut txn = bridge.begin_write().unwrap();
1168        execute_create_table(&mut txn, catalog, table, vec![], false).unwrap();
1169        txn.commit().unwrap();
1170    }
1171
1172    #[test]
1173    fn validate_file_path_rejects_symlink_and_directory() {
1174        let dir = std::env::temp_dir();
1175        let dir_path = dir.join("alopex_copy_dir");
1176        std::fs::create_dir_all(&dir_path).unwrap();
1177
1178        let config = CopySecurityConfig {
1179            allowed_base_dirs: Some(vec![dir.clone()]),
1180            allow_symlinks: false,
1181        };
1182
1183        // Directory is rejected.
1184        let err = validate_file_path(dir_path.to_str().unwrap(), &config).unwrap_err();
1185        assert!(matches!(err, ExecutorError::PathValidationFailed { .. }));
1186
1187        // Symlink is rejected on unix.
1188        #[cfg(unix)]
1189        {
1190            use std::os::unix::fs::symlink;
1191            let file_path = dir.join("alopex_copy_file.txt");
1192            fs::write(&file_path, "1,alice\n").unwrap();
1193            let link = dir.join("alopex_copy_link.txt");
1194            let _ = fs::remove_file(&link);
1195            symlink(&file_path, &link).unwrap();
1196            let err = validate_file_path(link.to_str().unwrap(), &config).unwrap_err();
1197            assert!(matches!(err, ExecutorError::PathValidationFailed { .. }));
1198        }
1199    }
1200
1201    #[test]
1202    fn validate_schema_checks_names_and_types() {
1203        let (bridge, mut catalog) = bridge();
1204        create_table(&bridge, &mut catalog, StorageType::Row);
1205        let table = catalog.get_table("users").unwrap();
1206
1207        let schema = CopySchema {
1208            fields: vec![
1209                CopyField {
1210                    name: Some("users".into()),
1211                    data_type: Some(ResolvedType::Integer),
1212                },
1213                CopyField {
1214                    name: Some("name".into()),
1215                    data_type: Some(ResolvedType::Text),
1216                },
1217            ],
1218        };
1219
1220        let err = validate_schema(&schema, table).unwrap_err();
1221        assert!(matches!(err, ExecutorError::SchemaMismatch { .. }));
1222    }
1223
1224    #[test]
1225    fn temporal_columnar_builder_does_not_accept_sibling_integer_variants() {
1226        let table = TableMetadata::new(
1227            "events",
1228            vec![ColumnMetadata::new("day", ResolvedType::Date)],
1229        );
1230        let schema = build_segment_schema(&table).unwrap();
1231
1232        build_record_batch(&schema, &table, &[vec![SqlValue::Date(19_782)]]).unwrap();
1233        assert!(build_record_batch(&schema, &table, &[vec![SqlValue::Timestamp(19_782)]]).is_err());
1234    }
1235
1236    #[test]
1237    fn execute_copy_csv_inserts_rows() {
1238        let dir = std::env::temp_dir();
1239        let file_path = dir.join("alopex_copy_test.csv");
1240        let mut file = File::create(&file_path).unwrap();
1241        writeln!(file, "id,name").unwrap();
1242        writeln!(file, "1,alice").unwrap();
1243        writeln!(file, "2,bob").unwrap();
1244
1245        let (bridge, mut catalog) = bridge();
1246        create_table(&bridge, &mut catalog, StorageType::Row);
1247
1248        let mut txn = bridge.begin_write().unwrap();
1249        let result = execute_copy(
1250            &mut txn,
1251            &catalog,
1252            "users",
1253            file_path.to_str().unwrap(),
1254            FileFormat::Csv,
1255            CopyOptions { header: true },
1256            &CopySecurityConfig::default(),
1257        )
1258        .unwrap();
1259        txn.commit().unwrap();
1260        assert_eq!(result, ExecutionResult::RowsAffected(2));
1261
1262        // Verify rows inserted.
1263        let table = catalog.get_table("users").unwrap().clone();
1264        let mut read_txn = bridge.begin_read().unwrap();
1265        let mut storage = read_txn.table_storage(&table);
1266        let rows: Vec<_> = storage.scan().unwrap().map(|r| r.unwrap().1).collect();
1267        assert_eq!(rows.len(), 2);
1268        assert!(rows.contains(&vec![SqlValue::Integer(1), SqlValue::Text("alice".into())]));
1269    }
1270
1271    #[test]
1272    fn execute_copy_parquet_reads_schema_and_rows() {
1273        let dir = std::env::temp_dir();
1274        let file_path = dir.join("alopex_copy_test.parquet");
1275        write_parquet_sample(&file_path, 2);
1276
1277        let (bridge, mut catalog) = bridge();
1278        create_table(&bridge, &mut catalog, StorageType::Row);
1279
1280        let mut txn = bridge.begin_write().unwrap();
1281        let result = execute_copy(
1282            &mut txn,
1283            &catalog,
1284            "users",
1285            file_path.to_str().unwrap(),
1286            FileFormat::Parquet,
1287            CopyOptions::default(),
1288            &CopySecurityConfig::default(),
1289        )
1290        .unwrap();
1291        txn.commit().unwrap();
1292        assert_eq!(result, ExecutionResult::RowsAffected(2));
1293
1294        // スキーマは Parquet から取得するため、テーブル側と不一致なら validate_schema が弾く。
1295        let table = catalog.get_table("users").unwrap().clone();
1296        let mut read_txn = bridge.begin_read().unwrap();
1297        let mut storage = read_txn.table_storage(&table);
1298        let rows: Vec<_> = storage.scan().unwrap().map(|r| r.unwrap().1).collect();
1299        assert_eq!(rows.len(), 2);
1300        assert!(rows.contains(&vec![SqlValue::Integer(1), SqlValue::Text("user0".into())]));
1301    }
1302
1303    #[test]
1304    fn parquet_reader_streams_batches() {
1305        let dir = std::env::temp_dir();
1306        let file_path = dir.join("alopex_copy_stream.parquet");
1307        write_parquet_sample(&file_path, 1500);
1308
1309        let (bridge, mut catalog) = bridge();
1310        create_table(&bridge, &mut catalog, StorageType::Row);
1311        let table = catalog.get_table("users").unwrap().clone();
1312
1313        let mut reader = ParquetReader::open(file_path.to_str().unwrap(), &table, false).unwrap();
1314        let mut batches = 0;
1315        let mut total = 0;
1316        while let Some(batch) = reader.next_batch(512).unwrap() {
1317            total += batch.len();
1318            batches += 1;
1319        }
1320        assert!(
1321            batches >= 2,
1322            "複数バッチを期待しましたが {batches} バッチでした"
1323        );
1324        assert_eq!(total, 1500);
1325    }
1326
1327    fn write_parquet_sample(path: &Path, count: usize) {
1328        let schema = Arc::new(ArrowSchema::new(vec![
1329            ArrowField::new("id", ArrowDataType::Int32, false),
1330            ArrowField::new("name", ArrowDataType::Utf8, false),
1331        ]));
1332
1333        let file = File::create(path).unwrap();
1334        let mut writer = ArrowWriter::try_new(file, schema.clone(), None).unwrap();
1335
1336        let chunk_size = 700;
1337        let mut start = 0;
1338        while start < count {
1339            let end = (start + chunk_size).min(count);
1340            let ids: Vec<i32> = ((start + 1) as i32..=end as i32).collect();
1341            let names: Vec<String> = (start..end).map(|i| format!("user{i}")).collect();
1342
1343            let batch = RecordBatch::try_new(
1344                schema.clone(),
1345                vec![
1346                    Arc::new(Int32Array::from(ids)) as Arc<_>,
1347                    Arc::new(StringArray::from(names)) as Arc<_>,
1348                ],
1349            )
1350            .unwrap();
1351            writer.write(&batch).unwrap();
1352            start = end;
1353        }
1354
1355        writer.close().unwrap();
1356    }
1357}