1use std::collections::{BTreeSet, HashMap, HashSet};
16use std::future::Future;
17use std::path::Path;
18use std::pin::Pin;
19use std::sync::Arc;
20use std::task::{Context, Poll};
21
22use client::{Output, OutputData, OutputMeta};
23use common_base::readable_size::ReadableSize;
24use common_datasource::file_format::csv::{
25 CsvFormat, is_skippable_arrow_error, tolerant_csv_stream,
26};
27use common_datasource::file_format::json::JsonFormat;
28use common_datasource::file_format::orc::{ReaderAdapter, infer_orc_schema, new_orc_stream_reader};
29use common_datasource::file_format::{FileFormat, Format, file_to_stream};
30use common_datasource::lister::{Lister, Source};
31use common_datasource::object_store::{FS_SCHEMA, build_backend, parse_url};
32use common_datasource::util::find_dir_and_filename;
33use common_query::{OutputCost, OutputRows};
34use common_recordbatch::DfSendableRecordBatchStream;
35use common_recordbatch::adapter::RecordBatchStreamTypeAdapter;
36use common_telemetry::{debug, tracing};
37use datafusion::datasource::physical_plan::{CsvSource, FileSource, JsonSource};
38use datafusion::parquet::arrow::ParquetRecordBatchStreamBuilder;
39use datafusion::parquet::arrow::arrow_reader::ArrowReaderMetadata;
40use datafusion_common::DataFusionError;
41use datafusion_common::arrow::error::ArrowError;
42use datafusion_common::config::CsvOptions;
43use datafusion_expr::Expr;
44use datatypes::arrow::compute::can_cast_types;
45use datatypes::arrow::datatypes::{DataType as ArrowDataType, Schema, SchemaRef};
46use datatypes::arrow::record_batch::RecordBatch;
47use datatypes::vectors::Helper;
48use futures_util::StreamExt;
49use object_store::{Entry, EntryMode, ObjectStore};
50use regex::Regex;
51use session::context::QueryContextRef;
52use snafu::{ResultExt, ensure};
53use table::requests::{CopyTableRequest, InsertRequest};
54use table::table_reference::TableReference;
55use tokio_util::compat::FuturesAsyncReadCompatExt;
56
57use crate::error::{self, IntoVectorsSnafu, PathNotFoundSnafu, Result};
58use crate::statement::StatementExecutor;
59
60const DEFAULT_BATCH_SIZE: usize = 8192;
61const DEFAULT_READ_BUFFER: usize = 256 * 1024;
62
63enum FileMetadata {
64 Parquet {
65 schema: SchemaRef,
66 metadata: ArrowReaderMetadata,
67 path: String,
68 },
69 Orc {
70 schema: SchemaRef,
71 path: String,
72 },
73 Json {
74 schema: SchemaRef,
75 format: JsonFormat,
76 path: String,
77 },
78 Csv {
79 schema: SchemaRef,
80 format: CsvFormat,
81 path: String,
82 },
83}
84
85impl FileMetadata {
86 pub fn schema(&self) -> &SchemaRef {
88 match self {
89 FileMetadata::Parquet { schema, .. } => schema,
90 FileMetadata::Orc { schema, .. } => schema,
91 FileMetadata::Json { schema, .. } => schema,
92 FileMetadata::Csv { schema, .. } => schema,
93 }
94 }
95}
96
97impl StatementExecutor {
98 async fn list_copy_from_entries(
99 &self,
100 req: &CopyTableRequest,
101 ) -> Result<(ObjectStore, Vec<Entry>)> {
102 let (schema, _host, path) = parse_url(&req.location).context(error::ParseUrlSnafu)?;
103
104 if schema.to_uppercase() == FS_SCHEMA {
105 ensure!(Path::new(&path).exists(), PathNotFoundSnafu { path });
106 }
107
108 let object_store =
109 build_backend(&req.location, &req.connection).context(error::BuildBackendSnafu)?;
110
111 let (dir, filename) = find_dir_and_filename(&path);
112 let regex = req
113 .pattern
114 .as_ref()
115 .map(|x| Regex::new(x))
116 .transpose()
117 .context(error::BuildRegexSnafu)?;
118
119 let source = if let Some(filename) = filename {
120 Source::Filename(filename)
121 } else {
122 Source::Dir
123 };
124
125 let lister = Lister::new(object_store.clone(), source.clone(), dir.clone(), regex);
126
127 let entries = lister.list().await.context(error::ListObjectsSnafu)?;
128 debug!("Copy from dir: {dir:?}, {source:?}, entries: {entries:?}");
129 Ok((object_store, entries))
130 }
131
132 async fn collect_metadata(
133 &self,
134 object_store: &ObjectStore,
135 format: Format,
136 path: String,
137 ) -> Result<FileMetadata> {
138 match format {
139 Format::Csv(format) => Ok(FileMetadata::Csv {
140 schema: Arc::new(
141 format
142 .infer_schema(object_store, &path)
143 .await
144 .context(error::InferSchemaSnafu { path: &path })?,
145 ),
146 format,
147 path,
148 }),
149 Format::Json(format) => Ok(FileMetadata::Json {
150 schema: Arc::new(
151 format
152 .infer_schema(object_store, &path)
153 .await
154 .context(error::InferSchemaSnafu { path: &path })?,
155 ),
156 format,
157 path,
158 }),
159 Format::Parquet(_) => {
160 let meta = object_store
161 .stat(&path)
162 .await
163 .context(error::ReadObjectSnafu { path: &path })?;
164 let mut reader = object_store
165 .reader(&path)
166 .await
167 .context(error::ReadObjectSnafu { path: &path })?
168 .into_futures_async_read(0..meta.content_length())
169 .await
170 .context(error::ReadObjectSnafu { path: &path })?
171 .compat();
172 let metadata = ArrowReaderMetadata::load_async(&mut reader, Default::default())
173 .await
174 .context(error::ReadParquetMetadataSnafu)?;
175
176 Ok(FileMetadata::Parquet {
177 schema: metadata.schema().clone(),
178 metadata,
179 path,
180 })
181 }
182 Format::Orc(_) => {
183 let meta = object_store
184 .stat(&path)
185 .await
186 .context(error::ReadObjectSnafu { path: &path })?;
187
188 let reader = object_store
189 .reader(&path)
190 .await
191 .context(error::ReadObjectSnafu { path: &path })?;
192
193 let schema = infer_orc_schema(ReaderAdapter::new(reader, meta.content_length()))
194 .await
195 .context(error::ReadOrcSnafu)?;
196
197 Ok(FileMetadata::Orc {
198 schema: Arc::new(schema),
199 path,
200 })
201 }
202 }
203 }
204
205 async fn build_read_stream(
206 &self,
207 compat_schema: SchemaRef,
208 object_store: &ObjectStore,
209 file_metadata: &FileMetadata,
210 projection: Vec<usize>,
211 filters: Vec<Expr>,
212 ) -> Result<DfSendableRecordBatchStream> {
213 match file_metadata {
214 FileMetadata::Csv {
215 format,
216 path,
217 schema,
218 } => {
219 let output_schema = Arc::new(
220 compat_schema
221 .project(&projection)
222 .context(error::ProjectSchemaSnafu)?,
223 );
224
225 let options = CsvOptions::default()
226 .with_has_header(format.has_header)
227 .with_delimiter(format.delimiter);
228 let csv_source = CsvSource::new(schema.clone())
229 .with_csv_options(options)
230 .with_batch_size(DEFAULT_BATCH_SIZE);
231 let stream = if format.skip_bad_records {
232 let reader_schema =
233 csv_reader_schema_for_skip_bad_records(schema, &compat_schema);
234 tolerant_csv_stream(
235 object_store,
236 path,
237 Arc::new(reader_schema),
238 projection.clone(),
239 format,
240 )
241 .await
242 .context(error::BuildFileStreamSnafu)?
243 } else {
244 file_to_stream(
245 object_store,
246 path,
247 csv_source,
248 Some(projection),
249 format.compression_type,
250 )
251 .await
252 .context(error::BuildFileStreamSnafu)?
253 };
254
255 let stream = Box::pin(
256 RecordBatchStreamTypeAdapter::new(output_schema, stream, None)
259 .with_filter(filters)
260 .context(error::PhysicalExprSnafu)?,
261 );
262 if format.skip_bad_records {
263 Ok(Box::pin(SkipBadRecordsStream::new(stream, path)))
264 } else {
265 Ok(stream)
266 }
267 }
268 FileMetadata::Json {
269 path,
270 format,
271 schema,
272 } => {
273 let output_schema = Arc::new(
274 compat_schema
275 .project(&projection)
276 .context(error::ProjectSchemaSnafu)?,
277 );
278
279 let json_source =
280 JsonSource::new(schema.clone()).with_batch_size(DEFAULT_BATCH_SIZE);
281 let stream = file_to_stream(
282 object_store,
283 path,
284 json_source,
285 Some(projection),
286 format.compression_type,
287 )
288 .await
289 .context(error::BuildFileStreamSnafu)?;
290
291 Ok(Box::pin(
292 RecordBatchStreamTypeAdapter::new(output_schema, stream, None)
295 .with_filter(filters)
296 .context(error::PhysicalExprSnafu)?,
297 ))
298 }
299 FileMetadata::Parquet { metadata, path, .. } => {
300 let meta = object_store
301 .stat(path)
302 .await
303 .context(error::ReadObjectSnafu { path })?;
304 let reader = object_store
305 .reader_with(path)
306 .chunk(DEFAULT_READ_BUFFER)
307 .await
308 .context(error::ReadObjectSnafu { path })?
309 .into_futures_async_read(0..meta.content_length())
310 .await
311 .context(error::ReadObjectSnafu { path })?
312 .compat();
313 let builder =
314 ParquetRecordBatchStreamBuilder::new_with_metadata(reader, metadata.clone());
315 let stream = builder
316 .build()
317 .context(error::BuildParquetRecordBatchStreamSnafu)?;
318
319 let output_schema = Arc::new(
320 compat_schema
321 .project(&projection)
322 .context(error::ProjectSchemaSnafu)?,
323 );
324 Ok(Box::pin(
325 RecordBatchStreamTypeAdapter::new(output_schema, stream, Some(projection))
326 .with_filter(filters)
327 .context(error::PhysicalExprSnafu)?,
328 ))
329 }
330 FileMetadata::Orc { path, .. } => {
331 let meta = object_store
332 .stat(path)
333 .await
334 .context(error::ReadObjectSnafu { path })?;
335
336 let reader = object_store
337 .reader_with(path)
338 .chunk(DEFAULT_READ_BUFFER)
339 .await
340 .context(error::ReadObjectSnafu { path })?;
341 let stream =
342 new_orc_stream_reader(ReaderAdapter::new(reader, meta.content_length()))
343 .await
344 .context(error::ReadOrcSnafu)?;
345
346 let output_schema = Arc::new(
347 compat_schema
348 .project(&projection)
349 .context(error::ProjectSchemaSnafu)?,
350 );
351
352 Ok(Box::pin(
353 RecordBatchStreamTypeAdapter::new(output_schema, stream, Some(projection))
354 .with_filter(filters)
355 .context(error::PhysicalExprSnafu)?,
356 ))
357 }
358 }
359 }
360
361 #[tracing::instrument(skip_all)]
362 pub async fn copy_table_from(
363 &self,
364 req: CopyTableRequest,
365 query_ctx: QueryContextRef,
366 ) -> Result<Output> {
367 let table_ref = TableReference {
368 catalog: &req.catalog_name,
369 schema: &req.schema_name,
370 table: &req.table_name,
371 };
372 let table = self.get_table(&table_ref).await?;
373 let format = Format::try_from(&req.with).context(error::ParseFileFormatSnafu)?;
374 let (object_store, entries) = self.list_copy_from_entries(&req).await?;
375 let mut files = Vec::with_capacity(entries.len());
376 let table_schema = table.schema().arrow_schema().clone();
377 let filters = table
378 .schema()
379 .timestamp_column()
380 .and_then(|c| {
381 common_query::logical_plan::build_same_type_ts_filter(c, req.timestamp_range)
382 })
383 .into_iter()
384 .collect::<Vec<_>>();
385
386 for entry in entries.iter() {
387 if entry.metadata().mode() != EntryMode::FILE {
388 continue;
389 }
390 let path = entry.path();
391 let file_metadata = self
392 .collect_metadata(&object_store, format.clone(), path.to_string())
393 .await?;
394
395 validate_csv_headers_if_required(&file_metadata, &table_schema)?;
396 let schema_mapping = copy_from_schema_mapping(&file_metadata, &table_schema);
397 let projected_file_schema = Arc::new(
398 file_metadata
399 .schema()
400 .project(&schema_mapping.file_projection)
401 .context(error::ProjectSchemaSnafu)?,
402 );
403 let projected_table_schema = Arc::new(
404 table_schema
405 .project(&schema_mapping.table_projection)
406 .context(error::ProjectSchemaSnafu)?,
407 );
408 ensure_schema_compatible(&projected_file_schema, &projected_table_schema)?;
409
410 files.push((
411 Arc::new(schema_mapping.compat_file_schema),
412 schema_mapping.file_projection,
413 projected_table_schema,
414 file_metadata,
415 ))
416 }
417
418 let mut rows_inserted = 0;
419 let mut insert_cost = 0;
420 let max_insert_rows = req.limit.map(|n| n as usize);
421 for (compat_schema, file_schema_projection, projected_table_schema, file_metadata) in files
422 {
423 let mut stream = self
424 .build_read_stream(
425 compat_schema,
426 &object_store,
427 &file_metadata,
428 file_schema_projection,
429 filters.clone(),
430 )
431 .await?;
432
433 let fields = projected_table_schema
434 .fields()
435 .iter()
436 .map(|f| f.name().clone())
437 .collect::<Vec<_>>();
438
439 let pending_mem_threshold = ReadableSize::mb(32).as_bytes();
441 let mut pending_mem_size = 0;
442 let mut pending = vec![];
443
444 while let Some(r) = stream.next().await {
445 let record_batch = r.context(error::ReadDfRecordBatchSnafu)?;
446 let vectors =
447 Helper::try_into_vectors(record_batch.columns()).context(IntoVectorsSnafu)?;
448
449 pending_mem_size += vectors.iter().map(|v| v.memory_size()).sum::<usize>();
450
451 let columns_values = fields
452 .iter()
453 .cloned()
454 .zip(vectors)
455 .collect::<HashMap<_, _>>();
456
457 pending.push(self.inserter.handle_table_insert(
458 InsertRequest {
459 catalog_name: req.catalog_name.clone(),
460 schema_name: req.schema_name.clone(),
461 table_name: req.table_name.clone(),
462 columns_values,
463 },
464 query_ctx.clone(),
465 ));
466
467 if pending_mem_size as u64 >= pending_mem_threshold {
468 let (rows, cost) = batch_insert(&mut pending, &mut pending_mem_size).await?;
469 rows_inserted += rows;
470 insert_cost += cost;
471 }
472
473 if let Some(max_insert_rows) = max_insert_rows
474 && rows_inserted >= max_insert_rows
475 {
476 return Ok(gen_insert_output(rows_inserted, insert_cost));
477 }
478 }
479
480 if !pending.is_empty() {
481 let (rows, cost) = batch_insert(&mut pending, &mut pending_mem_size).await?;
482 rows_inserted += rows;
483 insert_cost += cost;
484 }
485 }
486
487 Ok(gen_insert_output(rows_inserted, insert_cost))
488 }
489}
490
491fn gen_insert_output(rows_inserted: usize, insert_cost: usize) -> Output {
492 Output::new(
493 OutputData::AffectedRows(rows_inserted),
494 OutputMeta::new_with_cost(insert_cost),
495 )
496}
497
498struct SkipBadRecordsStream {
499 inner: DfSendableRecordBatchStream,
500 path: String,
501}
502
503impl SkipBadRecordsStream {
504 fn new(inner: DfSendableRecordBatchStream, path: impl Into<String>) -> Self {
505 Self {
506 inner,
507 path: path.into(),
508 }
509 }
510}
511
512impl datafusion::physical_plan::RecordBatchStream for SkipBadRecordsStream {
513 fn schema(&self) -> SchemaRef {
514 self.inner.schema()
515 }
516}
517
518impl futures::Stream for SkipBadRecordsStream {
519 type Item = datafusion_common::Result<RecordBatch>;
520
521 fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
522 let this = self.get_mut();
523 loop {
524 match this.inner.as_mut().poll_next(cx) {
525 Poll::Ready(Some(Err(error))) if is_skippable_record_error(&error) => {
526 common_telemetry::warn!(
527 "Skipping bad record while copying from {}: {}",
528 this.path,
529 error
530 );
531 continue;
532 }
533 other => return other,
534 }
535 }
536 }
537}
538
539fn is_skippable_record_error(error: &DataFusionError) -> bool {
540 match error {
541 DataFusionError::ArrowError(error, _) => is_skippable_arrow_error(error),
542 DataFusionError::External(error) => error
543 .downcast_ref::<ArrowError>()
544 .is_some_and(is_skippable_arrow_error),
545 DataFusionError::Context(_, error) => is_skippable_record_error(error),
546 _ => false,
547 }
548}
549
550async fn batch_insert(
552 pending: &mut Vec<impl Future<Output = Result<Output>>>,
553 pending_bytes: &mut usize,
554) -> Result<(OutputRows, OutputCost)> {
555 let batch = pending.drain(..);
556 let result = futures::future::try_join_all(batch)
557 .await?
558 .iter()
559 .map(|o| o.extract_rows_and_cost())
560 .reduce(|(a, b), (c, d)| (a + c, b + d))
561 .unwrap_or((0, 0));
562 *pending_bytes = 0;
563 Ok(result)
564}
565
566fn can_cast_types_for_greptime(from: &ArrowDataType, to: &ArrowDataType) -> bool {
568 if let ArrowDataType::Map(_, _) = from
570 && let ArrowDataType::Binary = to
571 {
572 return true;
573 }
574
575 can_cast_types(from, to)
577}
578
579fn csv_reader_schema_for_skip_bad_records(file: &SchemaRef, compat: &SchemaRef) -> Schema {
580 let fields = file
581 .fields()
582 .iter()
583 .enumerate()
584 .map(|(idx, file_field)| match compat.fields().get(idx) {
585 Some(compat_field) if can_csv_reader_parse_type(compat_field.data_type()) => {
586 compat_field.clone()
587 }
588 _ => file_field.clone(),
589 })
590 .collect::<Vec<_>>();
591
592 Schema::new_with_metadata(fields, file.metadata().clone())
593}
594
595fn can_csv_reader_parse_type(data_type: &ArrowDataType) -> bool {
596 match data_type {
597 ArrowDataType::Boolean
598 | ArrowDataType::Decimal32(_, _)
599 | ArrowDataType::Decimal64(_, _)
600 | ArrowDataType::Decimal128(_, _)
601 | ArrowDataType::Decimal256(_, _)
602 | ArrowDataType::Int8
603 | ArrowDataType::Int16
604 | ArrowDataType::Int32
605 | ArrowDataType::Int64
606 | ArrowDataType::UInt8
607 | ArrowDataType::UInt16
608 | ArrowDataType::UInt32
609 | ArrowDataType::UInt64
610 | ArrowDataType::Float32
611 | ArrowDataType::Float64
612 | ArrowDataType::Date32
613 | ArrowDataType::Date64
614 | ArrowDataType::Time32(_)
615 | ArrowDataType::Time64(_)
616 | ArrowDataType::Timestamp(_, _)
617 | ArrowDataType::Null
618 | ArrowDataType::Utf8
619 | ArrowDataType::Utf8View => true,
620 ArrowDataType::Dictionary(_, value_type) => value_type.as_ref() == &ArrowDataType::Utf8,
621 _ => false,
622 }
623}
624
625fn ensure_schema_compatible(from: &SchemaRef, to: &SchemaRef) -> Result<()> {
626 let not_match = from
627 .fields
628 .iter()
629 .zip(to.fields.iter())
630 .map(|(l, r)| (l.data_type(), r.data_type()))
631 .enumerate()
632 .find(|(_, (l, r))| !can_cast_types_for_greptime(l, r));
633
634 if let Some((index, _)) = not_match {
635 error::InvalidSchemaSnafu {
636 index,
637 table_schema: to.to_string(),
638 file_schema: from.to_string(),
639 }
640 .fail()
641 } else {
642 Ok(())
643 }
644}
645
646fn validate_csv_headers_if_required(file_metadata: &FileMetadata, table: &SchemaRef) -> Result<()> {
647 let FileMetadata::Csv {
648 schema,
649 format,
650 path,
651 } = file_metadata
652 else {
653 return Ok(());
654 };
655
656 if !format.strict_headers {
657 return Ok(());
658 }
659
660 let mut seen_file_columns = HashSet::with_capacity(schema.fields().len());
661 let duplicate_columns = schema
662 .fields()
663 .iter()
664 .filter_map(|field| {
665 if seen_file_columns.insert(field.name().clone()) {
666 None
667 } else {
668 Some(field.name().clone())
669 }
670 })
671 .collect::<BTreeSet<_>>()
672 .into_iter()
673 .collect::<Vec<_>>();
674 let file_columns = seen_file_columns.into_iter().collect::<BTreeSet<_>>();
675 let table_columns = table
676 .fields()
677 .iter()
678 .map(|field| field.name().clone())
679 .collect::<BTreeSet<_>>();
680 let unknown_columns = file_columns
681 .difference(&table_columns)
682 .cloned()
683 .collect::<Vec<_>>();
684 let missing_columns = table_columns
685 .difference(&file_columns)
686 .cloned()
687 .collect::<Vec<_>>();
688
689 ensure!(
690 unknown_columns.is_empty() && missing_columns.is_empty() && duplicate_columns.is_empty(),
691 error::CsvHeaderMismatchSnafu {
692 path,
693 unknown_columns,
694 missing_columns,
695 duplicate_columns,
696 }
697 );
698
699 Ok(())
700}
701
702fn generated_schema_projection_and_compatible_file_schema(
707 file: &SchemaRef,
708 table: &SchemaRef,
709) -> (Vec<usize>, Vec<usize>, Schema) {
710 let mut file_projection = Vec::with_capacity(file.fields.len());
711 let mut table_projection = Vec::with_capacity(file.fields.len());
712 let mut compatible_fields = file.fields.iter().cloned().collect::<Vec<_>>();
713 for (file_idx, file_field) in file.fields.iter().enumerate() {
714 if let Some((table_idx, table_field)) = table.fields.find(file_field.name()) {
715 file_projection.push(file_idx);
716 table_projection.push(table_idx);
717
718 compatible_fields[file_idx] = table_field.clone();
720 }
721 }
722
723 (
724 file_projection,
725 table_projection,
726 Schema::new(compatible_fields),
727 )
728}
729
730struct CopyFromSchemaMapping {
731 file_projection: Vec<usize>,
732 table_projection: Vec<usize>,
733 compat_file_schema: Schema,
734}
735
736fn copy_from_schema_mapping(
737 file_metadata: &FileMetadata,
738 table: &SchemaRef,
739) -> CopyFromSchemaMapping {
740 match file_metadata {
741 FileMetadata::Csv { schema, format, .. } if !format.has_header => {
742 generated_positional_schema_projection_and_compatible_file_schema(schema, table)
743 }
744 _ => {
745 let (file_projection, table_projection, compat_file_schema) =
746 generated_schema_projection_and_compatible_file_schema(
747 file_metadata.schema(),
748 table,
749 );
750 CopyFromSchemaMapping {
751 file_projection,
752 table_projection,
753 compat_file_schema,
754 }
755 }
756 }
757}
758
759fn generated_positional_schema_projection_and_compatible_file_schema(
760 file: &SchemaRef,
761 table: &SchemaRef,
762) -> CopyFromSchemaMapping {
763 let len = file.fields.len().min(table.fields.len());
764 let file_projection = (0..len).collect::<Vec<_>>();
765 let table_projection = (0..len).collect::<Vec<_>>();
766 let compatible_fields = file
767 .fields
768 .iter()
769 .enumerate()
770 .map(|(idx, file_field)| {
771 if idx < len {
772 table.fields[idx].clone()
773 } else {
774 file_field.clone()
775 }
776 })
777 .collect::<Vec<_>>();
778
779 CopyFromSchemaMapping {
780 file_projection,
781 table_projection,
782 compat_file_schema: Schema::new(compatible_fields),
783 }
784}
785
786#[cfg(test)]
787mod tests {
788 use std::sync::Arc;
789
790 use datatypes::arrow::datatypes::{DataType, Field, Schema};
791
792 use super::*;
793
794 fn test_schema_matches(from: (DataType, bool), to: (DataType, bool), matches: bool) {
795 let s1 = Arc::new(Schema::new(vec![Field::new("col", from.0.clone(), from.1)]));
796 let s2 = Arc::new(Schema::new(vec![Field::new("col", to.0.clone(), to.1)]));
797 let res = ensure_schema_compatible(&s1, &s2);
798 assert_eq!(
799 matches,
800 res.is_ok(),
801 "from data type: {}, to data type: {}, expected: {}, but got: {}",
802 from.0,
803 to.0,
804 matches,
805 res.is_ok()
806 )
807 }
808
809 #[test]
810 fn test_ensure_datatype_matches_ignore_timezone() {
811 test_schema_matches(
812 (
813 DataType::Timestamp(datatypes::arrow::datatypes::TimeUnit::Second, None),
814 true,
815 ),
816 (
817 DataType::Timestamp(datatypes::arrow::datatypes::TimeUnit::Second, None),
818 true,
819 ),
820 true,
821 );
822
823 test_schema_matches(
824 (
825 DataType::Timestamp(
826 datatypes::arrow::datatypes::TimeUnit::Second,
827 Some("UTC".into()),
828 ),
829 true,
830 ),
831 (
832 DataType::Timestamp(datatypes::arrow::datatypes::TimeUnit::Second, None),
833 true,
834 ),
835 true,
836 );
837
838 test_schema_matches(
839 (
840 DataType::Timestamp(
841 datatypes::arrow::datatypes::TimeUnit::Second,
842 Some("UTC".into()),
843 ),
844 true,
845 ),
846 (
847 DataType::Timestamp(
848 datatypes::arrow::datatypes::TimeUnit::Second,
849 Some("PDT".into()),
850 ),
851 true,
852 ),
853 true,
854 );
855
856 test_schema_matches(
857 (
858 DataType::Timestamp(
859 datatypes::arrow::datatypes::TimeUnit::Second,
860 Some("UTC".into()),
861 ),
862 true,
863 ),
864 (
865 DataType::Timestamp(
866 datatypes::arrow::datatypes::TimeUnit::Millisecond,
867 Some("UTC".into()),
868 ),
869 true,
870 ),
871 true,
872 );
873
874 test_schema_matches((DataType::Int8, true), (DataType::Int8, true), true);
875
876 test_schema_matches((DataType::Int8, true), (DataType::Int16, true), true);
877 }
878
879 #[test]
880 fn test_data_type_equals_ignore_timezone_with_options() {
881 test_schema_matches(
882 (
883 DataType::Timestamp(
884 datatypes::arrow::datatypes::TimeUnit::Microsecond,
885 Some("UTC".into()),
886 ),
887 true,
888 ),
889 (
890 DataType::Timestamp(
891 datatypes::arrow::datatypes::TimeUnit::Millisecond,
892 Some("PDT".into()),
893 ),
894 true,
895 ),
896 true,
897 );
898
899 test_schema_matches(
900 (DataType::Utf8, true),
901 (
902 DataType::Timestamp(
903 datatypes::arrow::datatypes::TimeUnit::Millisecond,
904 Some("PDT".into()),
905 ),
906 true,
907 ),
908 true,
909 );
910
911 test_schema_matches(
912 (
913 DataType::Timestamp(
914 datatypes::arrow::datatypes::TimeUnit::Millisecond,
915 Some("PDT".into()),
916 ),
917 true,
918 ),
919 (DataType::Utf8, true),
920 true,
921 );
922 }
923
924 #[test]
925 fn test_map_to_binary_json_compatibility() {
926 let map_type = DataType::Map(
928 Arc::new(Field::new(
929 "key_value",
930 DataType::Struct(
931 vec![
932 Field::new("key", DataType::Utf8, false),
933 Field::new("value", DataType::Utf8, false),
934 ]
935 .into(),
936 ),
937 false,
938 )),
939 false,
940 );
941
942 test_schema_matches((map_type, false), (DataType::Binary, true), true);
943
944 test_schema_matches((DataType::Int8, true), (DataType::Int16, true), true);
945 test_schema_matches((DataType::Utf8, true), (DataType::Binary, true), true);
946 }
947
948 fn make_test_schema(v: &[Field]) -> Arc<Schema> {
949 Arc::new(Schema::new(v.to_vec()))
950 }
951
952 #[test]
953 fn test_compatible_file_schema() {
954 let file_schema0 = make_test_schema(&[
955 Field::new("c1", DataType::UInt8, true),
956 Field::new("c2", DataType::UInt8, true),
957 ]);
958
959 let table_schema = make_test_schema(&[
960 Field::new("c1", DataType::Int16, true),
961 Field::new("c2", DataType::Int16, true),
962 Field::new("c3", DataType::Int16, true),
963 ]);
964
965 let compat_schema = make_test_schema(&[
966 Field::new("c1", DataType::Int16, true),
967 Field::new("c2", DataType::Int16, true),
968 ]);
969
970 let (_, tp, _) =
971 generated_schema_projection_and_compatible_file_schema(&file_schema0, &table_schema);
972
973 assert_eq!(table_schema.project(&tp).unwrap(), *compat_schema);
974 }
975
976 #[test]
977 fn test_schema_projection() {
978 let file_schema0 = make_test_schema(&[
979 Field::new("c1", DataType::UInt8, true),
980 Field::new("c2", DataType::UInt8, true),
981 Field::new("c3", DataType::UInt8, true),
982 ]);
983
984 let file_schema1 = make_test_schema(&[
985 Field::new("c3", DataType::UInt8, true),
986 Field::new("c4", DataType::UInt8, true),
987 ]);
988
989 let file_schema2 = make_test_schema(&[
990 Field::new("c3", DataType::UInt8, true),
991 Field::new("c4", DataType::UInt8, true),
992 Field::new("c5", DataType::UInt8, true),
993 ]);
994
995 let file_schema3 = make_test_schema(&[
996 Field::new("c1", DataType::UInt8, true),
997 Field::new("c2", DataType::UInt8, true),
998 ]);
999
1000 let table_schema = make_test_schema(&[
1001 Field::new("c3", DataType::UInt8, true),
1002 Field::new("c4", DataType::UInt8, true),
1003 Field::new("c5", DataType::UInt8, true),
1004 ]);
1005
1006 let tests = [
1007 (&file_schema0, &table_schema, true), (&file_schema1, &table_schema, true), (&file_schema2, &table_schema, true), (&file_schema3, &table_schema, true), ];
1012
1013 for test in tests {
1014 let (fp, tp, _) =
1015 generated_schema_projection_and_compatible_file_schema(test.0, test.1);
1016 assert_eq!(test.0.project(&fp).unwrap(), test.1.project(&tp).unwrap());
1017 }
1018 }
1019
1020 #[test]
1021 fn test_csv_reader_schema_for_skip_bad_records() {
1022 let file_schema = make_test_schema(&[
1023 Field::new("id", DataType::Utf8, true),
1024 Field::new("jsons", DataType::Utf8, true),
1025 Field::new("ts", DataType::Utf8, true),
1026 ]);
1027 let compat_schema = make_test_schema(&[
1028 Field::new("id", DataType::UInt32, true),
1029 Field::new("jsons", DataType::Binary, true),
1030 Field::new(
1031 "ts",
1032 DataType::Timestamp(datatypes::arrow::datatypes::TimeUnit::Millisecond, None),
1033 true,
1034 ),
1035 ]);
1036
1037 let reader_schema = csv_reader_schema_for_skip_bad_records(&file_schema, &compat_schema);
1038
1039 assert_eq!(reader_schema.field(0).data_type(), &DataType::UInt32);
1040 assert_eq!(reader_schema.field(1).data_type(), &DataType::Utf8);
1041 assert_eq!(
1042 reader_schema.field(2).data_type(),
1043 compat_schema.field(2).data_type()
1044 );
1045 }
1046
1047 fn make_csv_metadata(schema: Arc<Schema>, has_header: bool) -> FileMetadata {
1048 FileMetadata::Csv {
1049 schema,
1050 format: CsvFormat {
1051 has_header,
1052 ..CsvFormat::default()
1053 },
1054 path: "test.csv".to_string(),
1055 }
1056 }
1057
1058 fn make_strict_csv_metadata(schema: Arc<Schema>) -> FileMetadata {
1059 FileMetadata::Csv {
1060 schema,
1061 format: CsvFormat {
1062 strict_headers: true,
1063 ..CsvFormat::default()
1064 },
1065 path: "test.csv".to_string(),
1066 }
1067 }
1068
1069 fn assert_field(schema: &Schema, idx: usize, name: &str, data_type: &DataType) {
1070 let field = schema.field(idx);
1071 assert_eq!(field.name(), name);
1072 assert_eq!(field.data_type(), data_type);
1073 }
1074
1075 #[test]
1076 fn test_strict_csv_headers_allows_reordered_columns() {
1077 let file_schema = make_test_schema(&[
1078 Field::new("ts", DataType::Utf8, true),
1079 Field::new("host_id", DataType::UInt8, true),
1080 Field::new("reading_value", DataType::Float64, true),
1081 ]);
1082 let table_schema = make_test_schema(&[
1083 Field::new("host_id", DataType::UInt32, true),
1084 Field::new("reading_value", DataType::Float64, true),
1085 Field::new("ts", DataType::Utf8, true),
1086 ]);
1087
1088 validate_csv_headers_if_required(&make_strict_csv_metadata(file_schema), &table_schema)
1089 .unwrap();
1090 }
1091
1092 #[test]
1093 fn test_strict_csv_headers_rejects_unknown_columns() {
1094 let file_schema = make_test_schema(&[
1095 Field::new("host_id", DataType::UInt8, true),
1096 Field::new("reading_value", DataType::Float64, true),
1097 Field::new("ts", DataType::Utf8, true),
1098 Field::new("extra", DataType::Utf8, true),
1099 ]);
1100 let table_schema = make_test_schema(&[
1101 Field::new("host_id", DataType::UInt32, true),
1102 Field::new("reading_value", DataType::Float64, true),
1103 Field::new("ts", DataType::Utf8, true),
1104 ]);
1105
1106 let err =
1107 validate_csv_headers_if_required(&make_strict_csv_metadata(file_schema), &table_schema)
1108 .unwrap_err();
1109
1110 assert!(matches!(
1111 err,
1112 error::Error::CsvHeaderMismatch {
1113 unknown_columns,
1114 missing_columns,
1115 duplicate_columns,
1116 ..
1117 } if unknown_columns == vec!["extra".to_string()]
1118 && missing_columns.is_empty()
1119 && duplicate_columns.is_empty()
1120 ));
1121 }
1122
1123 #[test]
1124 fn test_strict_csv_headers_rejects_missing_columns() {
1125 let file_schema = make_test_schema(&[
1126 Field::new("host_id", DataType::UInt8, true),
1127 Field::new("ts", DataType::Utf8, true),
1128 ]);
1129 let table_schema = make_test_schema(&[
1130 Field::new("host_id", DataType::UInt32, true),
1131 Field::new("reading_value", DataType::Float64, true),
1132 Field::new("ts", DataType::Utf8, true),
1133 ]);
1134
1135 let err =
1136 validate_csv_headers_if_required(&make_strict_csv_metadata(file_schema), &table_schema)
1137 .unwrap_err();
1138
1139 assert!(matches!(
1140 err,
1141 error::Error::CsvHeaderMismatch {
1142 unknown_columns,
1143 missing_columns,
1144 duplicate_columns,
1145 ..
1146 } if unknown_columns.is_empty()
1147 && missing_columns == vec!["reading_value".to_string()]
1148 && duplicate_columns.is_empty()
1149 ));
1150 }
1151
1152 #[test]
1153 fn test_strict_csv_headers_rejects_duplicate_columns() {
1154 let file_schema = make_test_schema(&[
1155 Field::new("host_id", DataType::UInt8, true),
1156 Field::new("reading_value", DataType::Float64, true),
1157 Field::new("ts", DataType::Utf8, true),
1158 Field::new("host_id", DataType::UInt16, true),
1159 ]);
1160 let table_schema = make_test_schema(&[
1161 Field::new("host_id", DataType::UInt32, true),
1162 Field::new("reading_value", DataType::Float64, true),
1163 Field::new("ts", DataType::Utf8, true),
1164 ]);
1165
1166 let err =
1167 validate_csv_headers_if_required(&make_strict_csv_metadata(file_schema), &table_schema)
1168 .unwrap_err();
1169
1170 assert!(matches!(
1171 err,
1172 error::Error::CsvHeaderMismatch {
1173 unknown_columns,
1174 missing_columns,
1175 duplicate_columns,
1176 ..
1177 } if unknown_columns.is_empty()
1178 && missing_columns.is_empty()
1179 && duplicate_columns == vec!["host_id".to_string()]
1180 ));
1181 }
1182
1183 #[test]
1184 fn test_headerless_csv_schema_projection_is_positional() {
1185 let file_schema = make_test_schema(&[
1186 Field::new("column_1", DataType::UInt8, true),
1187 Field::new("column_2", DataType::Float64, true),
1188 Field::new("column_3", DataType::Utf8, true),
1189 ]);
1190 let table_schema = make_test_schema(&[
1191 Field::new("host_id", DataType::UInt32, true),
1192 Field::new("reading_value", DataType::Float64, true),
1193 Field::new(
1194 "ts",
1195 DataType::Timestamp(datatypes::arrow::datatypes::TimeUnit::Millisecond, None),
1196 true,
1197 ),
1198 ]);
1199
1200 let mapping =
1201 copy_from_schema_mapping(&make_csv_metadata(file_schema, false), &table_schema);
1202
1203 assert_eq!(mapping.file_projection, vec![0, 1, 2]);
1204 assert_eq!(mapping.table_projection, vec![0, 1, 2]);
1205 assert_field(&mapping.compat_file_schema, 0, "host_id", &DataType::UInt32);
1206 assert_field(
1207 &mapping.compat_file_schema,
1208 1,
1209 "reading_value",
1210 &DataType::Float64,
1211 );
1212 assert_field(
1213 &mapping.compat_file_schema,
1214 2,
1215 "ts",
1216 table_schema.field(2).data_type(),
1217 );
1218 assert_eq!(
1219 mapping
1220 .compat_file_schema
1221 .project(&mapping.file_projection)
1222 .unwrap(),
1223 table_schema.project(&mapping.table_projection).unwrap()
1224 );
1225 }
1226
1227 #[test]
1228 fn test_headerless_csv_schema_projection_ignores_extra_file_columns() {
1229 let file_schema = make_test_schema(&[
1230 Field::new("column_1", DataType::UInt8, true),
1231 Field::new("column_2", DataType::Float64, true),
1232 Field::new("column_3", DataType::Utf8, true),
1233 Field::new("column_4", DataType::Utf8, true),
1234 ]);
1235 let table_schema = make_test_schema(&[
1236 Field::new("host_id", DataType::UInt32, true),
1237 Field::new("reading_value", DataType::Float64, true),
1238 Field::new("ts", DataType::Utf8, true),
1239 ]);
1240
1241 let mapping =
1242 copy_from_schema_mapping(&make_csv_metadata(file_schema, false), &table_schema);
1243
1244 assert_eq!(mapping.file_projection, vec![0, 1, 2]);
1245 assert_eq!(mapping.table_projection, vec![0, 1, 2]);
1246 assert_eq!(mapping.compat_file_schema.fields().len(), 4);
1247 assert_field(&mapping.compat_file_schema, 0, "host_id", &DataType::UInt32);
1248 assert_field(
1249 &mapping.compat_file_schema,
1250 1,
1251 "reading_value",
1252 &DataType::Float64,
1253 );
1254 assert_field(&mapping.compat_file_schema, 2, "ts", &DataType::Utf8);
1255 assert_field(&mapping.compat_file_schema, 3, "column_4", &DataType::Utf8);
1256 }
1257
1258 #[test]
1259 fn test_headerless_csv_schema_projection_supports_prefix_import() {
1260 let file_schema = make_test_schema(&[
1261 Field::new("column_1", DataType::UInt8, true),
1262 Field::new("column_2", DataType::Float64, true),
1263 ]);
1264 let table_schema = make_test_schema(&[
1265 Field::new("host_id", DataType::UInt32, true),
1266 Field::new("reading_value", DataType::Float64, true),
1267 Field::new("ts", DataType::Utf8, true),
1268 ]);
1269
1270 let mapping =
1271 copy_from_schema_mapping(&make_csv_metadata(file_schema, false), &table_schema);
1272
1273 assert_eq!(mapping.file_projection, vec![0, 1]);
1274 assert_eq!(mapping.table_projection, vec![0, 1]);
1275 assert_field(&mapping.compat_file_schema, 0, "host_id", &DataType::UInt32);
1276 assert_field(
1277 &mapping.compat_file_schema,
1278 1,
1279 "reading_value",
1280 &DataType::Float64,
1281 );
1282 assert_eq!(
1283 mapping
1284 .compat_file_schema
1285 .project(&mapping.file_projection)
1286 .unwrap(),
1287 table_schema.project(&mapping.table_projection).unwrap()
1288 );
1289 }
1290
1291 #[test]
1292 fn test_csv_reader_schema_for_skip_bad_records_uses_positional_mapping() {
1293 let file_schema = make_test_schema(&[
1294 Field::new("column_1", DataType::Utf8, true),
1295 Field::new("column_2", DataType::Utf8, true),
1296 Field::new("column_3", DataType::Utf8, true),
1297 ]);
1298 let table_schema = make_test_schema(&[
1299 Field::new("host_id", DataType::UInt32, true),
1300 Field::new("jsons", DataType::Binary, true),
1301 Field::new(
1302 "ts",
1303 DataType::Timestamp(datatypes::arrow::datatypes::TimeUnit::Millisecond, None),
1304 true,
1305 ),
1306 ]);
1307 let mapping = copy_from_schema_mapping(
1308 &make_csv_metadata(file_schema.clone(), false),
1309 &table_schema,
1310 );
1311 let compat_schema = Arc::new(mapping.compat_file_schema);
1312
1313 let reader_schema = csv_reader_schema_for_skip_bad_records(&file_schema, &compat_schema);
1314
1315 assert_eq!(reader_schema.field(0).data_type(), &DataType::UInt32);
1316 assert_eq!(reader_schema.field(1).data_type(), &DataType::Utf8);
1317 assert_eq!(
1318 reader_schema.field(2).data_type(),
1319 table_schema.field(2).data_type()
1320 );
1321 }
1322}