Skip to main content

operator/statement/
copy_table_from.rs

1// Copyright 2023 Greptime Team
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use 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    /// Returns the [SchemaRef]
87    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                    // The projection is already applied in the CSV reader when we created the stream,
257                    // so we pass None here to avoid double projection which would cause schema mismatch errors.
258                    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                    // The projection is already applied in the JSON reader when we created the stream,
293                    // so we pass None here to avoid double projection which would cause schema mismatch errors.
294                    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            // TODO(hl): make this configurable through options.
440            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
550/// Executes all pending inserts all at once, drain pending requests and reset pending bytes.
551async 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
566/// Custom type compatibility check for GreptimeDB that handles Map -> Binary (JSON) conversion
567fn can_cast_types_for_greptime(from: &ArrowDataType, to: &ArrowDataType) -> bool {
568    // Handle Map -> Binary conversion for JSON types
569    if let ArrowDataType::Map(_, _) = from
570        && let ArrowDataType::Binary = to
571    {
572        return true;
573    }
574
575    // For all other cases, use Arrow's built-in can_cast_types
576    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
702/// Generates a maybe compatible schema of the file schema.
703///
704/// If there is a field is found in table schema,
705/// copy the field data type to maybe compatible schema(`compatible_fields`).
706fn 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            // Safety: the compatible_fields has same length as file schema
719            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        // Test Map -> Binary conversion for JSON types
927        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), // intersection
1008            (&file_schema1, &table_schema, true), // subset
1009            (&file_schema2, &table_schema, true), // full-eq
1010            (&file_schema3, &table_schema, true), // non-intersection
1011        ];
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}