1use 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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
40pub enum FileFormat {
41 Csv,
42 Parquet,
43}
44
45#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
47pub struct CopyOptions {
48 pub header: bool,
50}
51
52#[derive(Debug, Clone, PartialEq, Eq, Default)]
54pub struct CopySecurityConfig {
55 pub allowed_base_dirs: Option<Vec<PathBuf>>,
57 pub allow_symlinks: bool,
59}
60
61#[derive(Debug, Clone, PartialEq, Eq)]
63pub struct CopyField {
64 pub name: Option<String>,
65 pub data_type: Option<ResolvedType>,
66}
67
68#[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
88pub trait BulkReader {
90 fn schema(&self) -> &CopySchema;
92 fn next_batch(&mut self, max_rows: usize) -> Result<Option<Vec<Vec<SqlValue>>>>;
94}
95
96pub 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
136pub fn validate_file_path(file_path: &str, config: &CopySecurityConfig) -> Result<()> {
138 let path = Path::new(file_path);
139
140 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
195pub 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
240fn 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
302fn 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; 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(µs.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
934pub(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 let err = validate_file_path(dir_path.to_str().unwrap(), &config).unwrap_err();
1185 assert!(matches!(err, ExecutorError::PathValidationFailed { .. }));
1186
1187 #[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 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 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}