1use std::cmp;
2use std::iter::Iterator;
3use std::num::NonZeroUsize;
4
5use polars_buffer::Buffer;
6use polars_core::prelude::Schema;
7use polars_core::schema::SchemaRef;
8use polars_error::{PolarsResult, polars_bail, polars_ensure};
9
10use crate::csv::read::schema_inference::infer_file_schema_impl;
11use crate::prelude::_csv_read_internal::{SplitLines, is_comment_line};
12use crate::prelude::{CsvParseOptions, CsvReadOptions};
13use crate::utils::compression::{ByteSourceReader, CompressedReader};
14use crate::utils::stream_buf_reader::ReaderSource;
15
16pub type InspectContentFn<'a> = Box<dyn FnMut(&[u8]) + 'a>;
17
18#[inline(never)]
31pub fn read_until_start_and_infer_schema_from_compressed_reader(
32 options: &CsvReadOptions,
33 projected_schema: Option<SchemaRef>,
34 ignore_extra_columns: bool,
35 insert_missing_columns: bool,
36 mut inspect_first_content_row_fn: Option<InspectContentFn<'_>>,
37 reader: &mut CompressedReader,
38) -> PolarsResult<(Schema, Buffer<u8>)> {
39 const ESTIMATED_BYTES_PER_ROW: usize = 200;
41
42 #[derive(Copy, Clone)]
43 enum State {
44 SkipEmpty,
46 SkipRowsBeforeHeader(usize),
47 SkipHeader(bool),
48 SkipRowsAfterHeader(usize),
49 ContentInspect,
50 InferCollect,
51 Done,
52 }
53
54 polars_ensure!(
55 !(options.skip_lines != 0 && options.skip_rows != 0),
56 InvalidOperation: "only one of 'skip_rows'/'skip_lines' may be set"
57 );
58
59 let prev_leftover = skip_lines_naive_from_compressed_reader(
62 options.parse_options.eol_char,
63 options.skip_lines,
64 options.raise_if_empty,
65 reader,
66 )?;
67
68 let mut state = if options.has_header {
69 State::SkipEmpty
70 } else if options.skip_lines != 0 {
71 State::SkipHeader(false)
74 } else {
75 State::SkipRowsBeforeHeader(options.skip_rows)
76 };
77
78 let comment_prefix = options.parse_options.comment_prefix.as_ref();
79 let infer_schema_length = if options.schema.is_some() {
80 Some(0)
82 } else {
83 options.infer_schema_length
84 };
85
86 let mut header_line = None;
87 let mut content_lines = Vec::with_capacity(infer_schema_length.unwrap_or_else(|| {
88 reader
89 .total_len_estimate()
90 .saturating_div(ESTIMATED_BYTES_PER_ROW)
91 }));
92
93 let initial_read_size = infer_schema_length
99 .map(|isl| {
100 cmp::max(
101 CompressedReader::initial_read_size(),
102 isl.saturating_mul(ESTIMATED_BYTES_PER_ROW),
103 )
104 })
105 .unwrap_or(usize::MAX);
106
107 let leftover = for_each_line_from_reader_from_compressed_reader(
108 &options.parse_options,
109 true,
110 prev_leftover,
111 initial_read_size,
112 reader,
113 |mem_slice_line| {
114 let line = &*mem_slice_line;
115
116 let done = loop {
117 match &mut state {
118 State::SkipEmpty => {
119 if line.is_empty() || line == b"\r" {
120 break LineUse::ConsumeDiscard;
121 }
122
123 state = State::SkipRowsBeforeHeader(options.skip_rows);
124 },
125 State::SkipRowsBeforeHeader(remaining) => {
126 let is_comment = is_comment_line(line, comment_prefix);
127
128 if *remaining == 0 && !is_comment {
129 state = State::SkipHeader(false);
130 continue;
131 }
132
133 *remaining -= !is_comment as usize;
134 break LineUse::ConsumeDiscard;
135 },
136 State::SkipHeader(did_skip) => {
137 if !options.has_header || *did_skip {
138 state = State::SkipRowsAfterHeader(options.skip_rows_after_header);
139 continue;
140 }
141
142 header_line = Some(mem_slice_line.clone());
143 *did_skip = true;
144 break LineUse::ConsumeDiscard;
145 },
146 State::SkipRowsAfterHeader(remaining) => {
147 let is_comment = is_comment_line(line, comment_prefix);
148
149 if *remaining == 0 && !is_comment {
150 state = State::ContentInspect;
151 continue;
152 }
153
154 *remaining -= !is_comment as usize;
155 break LineUse::ConsumeDiscard;
156 },
157 State::ContentInspect => {
158 if let Some(func) = &mut inspect_first_content_row_fn {
159 func(line);
160 }
161
162 state = State::InferCollect;
163 },
164 State::InferCollect => {
165 if !is_comment_line(line, comment_prefix) {
166 content_lines.push(mem_slice_line.clone());
167 if content_lines.len() >= infer_schema_length.unwrap_or(usize::MAX) {
168 state = State::Done;
169 continue;
170 }
171 }
172
173 break LineUse::ConsumeKeep;
174 },
175 State::Done => {
176 break LineUse::Done;
177 },
178 }
179 };
180
181 Ok(done)
182 },
183 )?;
184
185 let infer_all_as_str = infer_schema_length == Some(0);
186
187 let inferred_schema = infer_schema(
188 &header_line,
189 &content_lines,
190 infer_all_as_str,
191 options,
192 projected_schema,
193 ignore_extra_columns,
194 insert_missing_columns,
195 )?;
196
197 Ok((inferred_schema, leftover))
198}
199
200#[inline(never)]
213pub fn read_until_start_and_infer_schema(
214 options: &CsvReadOptions,
215 projected_schema: Option<SchemaRef>,
216 ignore_extra_columns: bool,
217 insert_missing_columns: bool,
218 decompressed_file_size_hint: Option<usize>,
219 mut inspect_first_content_row_fn: Option<InspectContentFn<'_>>,
220 reader: &mut ByteSourceReader<ReaderSource>,
221) -> PolarsResult<(Schema, Buffer<u8>)> {
222 const ESTIMATED_BYTES_PER_ROW: usize = 200;
224
225 #[derive(Copy, Clone)]
226 enum State {
227 SkipEmpty,
229 SkipRowsBeforeHeader(usize),
230 SkipHeader(bool),
231 SkipRowsAfterHeader(usize),
232 ContentInspect,
233 InferCollect,
234 Done,
235 }
236
237 polars_ensure!(
238 !(options.skip_lines != 0 && options.skip_rows != 0),
239 InvalidOperation: "only one of 'skip_rows'/'skip_lines' may be set"
240 );
241
242 let prev_leftover = skip_lines_naive(
245 options.parse_options.eol_char,
246 options.skip_lines,
247 options.raise_if_empty,
248 decompressed_file_size_hint,
249 reader,
250 )?;
251
252 let mut state = if options.has_header {
253 State::SkipEmpty
254 } else if options.skip_lines != 0 {
255 State::SkipHeader(false)
258 } else {
259 State::SkipRowsBeforeHeader(options.skip_rows)
260 };
261
262 let comment_prefix = options.parse_options.comment_prefix.as_ref();
263 let infer_schema_length = if options.schema.is_some() {
264 Some(0)
266 } else {
267 options.infer_schema_length
268 };
269
270 let mut header_line = None;
271 let mut content_lines = Vec::with_capacity(infer_schema_length.unwrap_or_else(|| {
272 decompressed_file_size_hint
273 .map(|size| size.saturating_div(ESTIMATED_BYTES_PER_ROW))
274 .unwrap_or(100)
275 }));
276
277 let initial_read_size = infer_schema_length
283 .map(|isl| {
284 cmp::max(
285 CompressedReader::initial_read_size(),
286 isl.saturating_mul(ESTIMATED_BYTES_PER_ROW),
287 )
288 })
289 .unwrap_or(usize::MAX);
290
291 let leftover = for_each_line_from_reader(
292 &options.parse_options,
293 true,
294 prev_leftover,
295 initial_read_size,
296 decompressed_file_size_hint,
297 reader,
298 |mem_slice_line| {
299 let line = &*mem_slice_line;
300
301 let done = loop {
302 match &mut state {
303 State::SkipEmpty => {
304 if line.is_empty() || line == b"\r" {
305 break LineUse::ConsumeDiscard;
306 }
307
308 state = State::SkipRowsBeforeHeader(options.skip_rows);
309 },
310 State::SkipRowsBeforeHeader(remaining) => {
311 let is_comment = is_comment_line(line, comment_prefix);
312
313 if *remaining == 0 && !is_comment {
314 state = State::SkipHeader(false);
315 continue;
316 }
317
318 *remaining -= !is_comment as usize;
319 break LineUse::ConsumeDiscard;
320 },
321 State::SkipHeader(did_skip) => {
322 if !options.has_header || *did_skip {
323 state = State::SkipRowsAfterHeader(options.skip_rows_after_header);
324 continue;
325 }
326
327 header_line = Some(mem_slice_line.clone());
328 *did_skip = true;
329 break LineUse::ConsumeDiscard;
330 },
331 State::SkipRowsAfterHeader(remaining) => {
332 let is_comment = is_comment_line(line, comment_prefix);
333
334 if *remaining == 0 && !is_comment {
335 state = State::ContentInspect;
336 continue;
337 }
338
339 *remaining -= !is_comment as usize;
340 break LineUse::ConsumeDiscard;
341 },
342 State::ContentInspect => {
343 if let Some(func) = &mut inspect_first_content_row_fn {
344 func(line);
345 }
346
347 state = State::InferCollect;
348 },
349 State::InferCollect => {
350 if !is_comment_line(line, comment_prefix) {
351 content_lines.push(mem_slice_line.clone());
352 if content_lines.len() >= infer_schema_length.unwrap_or(usize::MAX) {
353 state = State::Done;
354 continue;
355 }
356 }
357
358 break LineUse::ConsumeKeep;
359 },
360 State::Done => {
361 break LineUse::Done;
362 },
363 }
364 };
365
366 Ok(done)
367 },
368 )?;
369
370 let infer_all_as_str = infer_schema_length == Some(0);
371
372 let inferred_schema = infer_schema(
373 &header_line,
374 &content_lines,
375 infer_all_as_str,
376 options,
377 projected_schema,
378 ignore_extra_columns,
379 insert_missing_columns,
380 )?;
381
382 Ok((inferred_schema, leftover))
383}
384
385enum LineUse {
386 ConsumeDiscard,
387 ConsumeKeep,
388 Done,
389}
390
391fn for_each_line_from_reader_from_compressed_reader(
396 parse_options: &CsvParseOptions,
397 is_file_start: bool,
398 mut prev_leftover: Buffer<u8>,
399 initial_read_size: usize,
400 reader: &mut CompressedReader,
401 mut line_fn: impl FnMut(Buffer<u8>) -> PolarsResult<LineUse>,
402) -> PolarsResult<Buffer<u8>> {
403 let mut is_first_line = is_file_start;
404
405 let fixed_read_size = std::env::var("POLARS_FORCE_CSV_INFER_READ_SIZE")
406 .map(|x| {
407 x.parse::<NonZeroUsize>()
408 .unwrap_or_else(|_| {
409 panic!("invalid value for POLARS_FORCE_CSV_INFER_READ_SIZE: {x}")
410 })
411 .get()
412 })
413 .ok();
414
415 let mut read_size = fixed_read_size.unwrap_or(initial_read_size);
416 let mut retain_offset = None;
417
418 loop {
419 let (mut slice, bytes_read) = reader.read_next_slice(&prev_leftover, read_size)?;
420 if slice.is_empty() {
421 return Ok(Buffer::new());
422 }
423
424 if is_first_line {
425 is_first_line = false;
426 const UTF8_BOM_MARKER: Option<&[u8]> = Some(b"\xef\xbb\xbf");
427 if slice.get(0..3) == UTF8_BOM_MARKER {
428 slice = slice.sliced(3..);
429 }
430 }
431
432 let line_to_sub_slice = |line: &[u8]| {
433 let start = line.as_ptr() as usize - slice.as_ptr() as usize;
434 slice.clone().sliced(start..(start + line.len()))
435 };
436
437 let effective_slice = if let Some(offset) = retain_offset {
439 slice.clone().sliced(offset..)
440 } else {
441 slice.clone()
442 };
443
444 let mut lines = SplitLines::new(
445 &effective_slice,
446 parse_options.quote_char,
447 parse_options.eol_char,
448 parse_options.comment_prefix.as_ref(),
449 );
450 let Some(mut prev_line) = lines.next() else {
451 read_size = read_size.saturating_mul(2);
452 prev_leftover = slice;
453 continue;
454 };
455
456 let mut should_ret = false;
457
458 for next_line in lines {
461 match line_fn(line_to_sub_slice(prev_line))? {
462 LineUse::ConsumeDiscard => debug_assert!(retain_offset.is_none()),
463 LineUse::ConsumeKeep => {
464 if retain_offset.is_none() {
465 let retain_start_offset =
466 prev_line.as_ptr() as usize - slice.as_ptr() as usize;
467 prev_leftover = slice.clone().sliced(retain_start_offset..);
468 retain_offset = Some(0);
469 }
470 },
471 LineUse::Done => {
472 should_ret = true;
473 break;
474 },
475 }
476 prev_line = next_line;
477 }
478
479 let mut unconsumed_offset = prev_line.as_ptr() as usize - effective_slice.as_ptr() as usize;
480
481 if bytes_read < read_size {
484 match line_fn(line_to_sub_slice(prev_line))? {
485 LineUse::ConsumeDiscard => {
486 debug_assert!(retain_offset.is_none());
487 unconsumed_offset += prev_line.len();
488 if effective_slice.get(unconsumed_offset) == Some(&parse_options.eol_char) {
489 unconsumed_offset += 1;
490 }
491 },
492 LineUse::ConsumeKeep | LineUse::Done => (),
493 }
494 should_ret = true;
495 }
496
497 if let Some(offset) = &mut retain_offset {
498 if *offset == 0 {
499 *offset = unconsumed_offset - (slice.len() - prev_leftover.len());
502 } else {
503 prev_leftover = slice;
504 *offset += unconsumed_offset;
505 }
506 } else {
507 prev_leftover = slice.sliced(unconsumed_offset..);
510 }
511
512 if should_ret {
513 return Ok(prev_leftover);
514 }
515
516 if read_size < CompressedReader::ideal_read_size() && fixed_read_size.is_none() {
517 read_size *= 4;
518 }
519 }
520}
521
522fn for_each_line_from_reader(
527 parse_options: &CsvParseOptions,
528 is_file_start: bool,
529 mut prev_leftover: Buffer<u8>,
530 initial_read_size: usize,
531 decompressed_file_size_hint: Option<usize>,
532 reader: &mut ByteSourceReader<ReaderSource>,
533 mut line_fn: impl FnMut(Buffer<u8>) -> PolarsResult<LineUse>,
534) -> PolarsResult<Buffer<u8>> {
535 let mut is_first_line = is_file_start;
536
537 let fixed_read_size = std::env::var("POLARS_FORCE_CSV_INFER_READ_SIZE")
538 .map(|x| {
539 x.parse::<NonZeroUsize>()
540 .unwrap_or_else(|_| {
541 panic!("invalid value for POLARS_FORCE_CSV_INFER_READ_SIZE: {x}")
542 })
543 .get()
544 })
545 .ok();
546
547 let mut read_size = fixed_read_size.unwrap_or(initial_read_size);
548 let mut retain_offset = None;
549
550 loop {
551 let (mut slice, bytes_read) =
552 reader.read_next_slice(&prev_leftover, read_size, decompressed_file_size_hint)?;
553 if slice.is_empty() {
554 return Ok(Buffer::new());
555 }
556
557 if is_first_line {
558 is_first_line = false;
559 const UTF8_BOM_MARKER: Option<&[u8]> = Some(b"\xef\xbb\xbf");
560 if slice.get(0..3) == UTF8_BOM_MARKER {
561 slice = slice.sliced(3..);
562 }
563 }
564
565 let line_to_sub_slice = |line: &[u8]| {
566 let start = line.as_ptr() as usize - slice.as_ptr() as usize;
567 slice.clone().sliced(start..(start + line.len()))
568 };
569
570 let effective_slice = if let Some(offset) = retain_offset {
572 slice.clone().sliced(offset..)
573 } else {
574 slice.clone()
575 };
576
577 let mut lines = SplitLines::new(
578 &effective_slice,
579 parse_options.quote_char,
580 parse_options.eol_char,
581 parse_options.comment_prefix.as_ref(),
582 );
583 let Some(mut prev_line) = lines.next() else {
584 read_size = read_size.saturating_mul(2);
585 prev_leftover = slice;
586 continue;
587 };
588
589 let mut should_ret = false;
590
591 for next_line in lines {
594 match line_fn(line_to_sub_slice(prev_line))? {
595 LineUse::ConsumeDiscard => debug_assert!(retain_offset.is_none()),
596 LineUse::ConsumeKeep => {
597 if retain_offset.is_none() {
598 let retain_start_offset =
599 prev_line.as_ptr() as usize - slice.as_ptr() as usize;
600 prev_leftover = slice.clone().sliced(retain_start_offset..);
601 retain_offset = Some(0);
602 }
603 },
604 LineUse::Done => {
605 should_ret = true;
606 break;
607 },
608 }
609 prev_line = next_line;
610 }
611
612 let mut unconsumed_offset = prev_line.as_ptr() as usize - effective_slice.as_ptr() as usize;
613
614 if bytes_read < read_size {
617 match line_fn(line_to_sub_slice(prev_line))? {
618 LineUse::ConsumeDiscard => {
619 debug_assert!(retain_offset.is_none());
620 unconsumed_offset += prev_line.len();
621 if effective_slice.get(unconsumed_offset) == Some(&parse_options.eol_char) {
622 unconsumed_offset += 1;
623 }
624 },
625 LineUse::ConsumeKeep | LineUse::Done => (),
626 }
627 should_ret = true;
628 }
629
630 if let Some(offset) = &mut retain_offset {
631 if *offset == 0 {
632 *offset = unconsumed_offset - (slice.len() - prev_leftover.len());
635 } else {
636 prev_leftover = slice;
637 *offset += unconsumed_offset;
638 }
639 } else {
640 prev_leftover = slice.sliced(unconsumed_offset..);
643 }
644
645 if should_ret {
646 return Ok(prev_leftover);
647 }
648
649 if read_size < ByteSourceReader::<ReaderSource>::ideal_read_size()
650 && fixed_read_size.is_none()
651 {
652 read_size *= 4;
653 }
654 }
655}
656
657fn skip_lines_naive_from_compressed_reader(
658 eol_char: u8,
659 skip_lines: usize,
660 raise_if_empty: bool,
661 reader: &mut CompressedReader,
662) -> PolarsResult<Buffer<u8>> {
663 let mut prev_leftover = Buffer::new();
664
665 if skip_lines == 0 {
666 return Ok(prev_leftover);
667 }
668
669 let mut remaining = skip_lines;
670 let mut read_size = CompressedReader::initial_read_size();
671
672 loop {
673 let (slice, bytes_read) = reader.read_next_slice(&prev_leftover, read_size)?;
674 let mut bytes: &[u8] = &slice;
675
676 'inner: loop {
677 let Some(mut pos) = memchr::memchr(eol_char, bytes) else {
678 read_size = read_size.saturating_mul(2);
679 break 'inner;
680 };
681 pos = cmp::min(pos + 1, bytes.len());
682
683 bytes = &bytes[pos..];
684 remaining -= 1;
685
686 if remaining == 0 {
687 let unconsumed_offset = bytes.as_ptr() as usize - slice.as_ptr() as usize;
688 prev_leftover = slice.sliced(unconsumed_offset..);
689 return Ok(prev_leftover);
690 }
691 }
692
693 if bytes_read == 0 {
694 if raise_if_empty {
695 polars_bail!(NoData: "specified skip_lines is larger than total number of lines.");
696 } else {
697 return Ok(Buffer::new());
698 }
699 }
700
701 prev_leftover = Buffer::new();
703
704 if read_size < CompressedReader::ideal_read_size() {
705 read_size *= 4;
706 }
707 }
708}
709
710fn skip_lines_naive(
711 eol_char: u8,
712 skip_lines: usize,
713 raise_if_empty: bool,
714 decompressed_file_size_hint: Option<usize>,
715 reader: &mut ByteSourceReader<ReaderSource>,
716) -> PolarsResult<Buffer<u8>> {
717 let mut prev_leftover = Buffer::new();
718
719 if skip_lines == 0 {
720 return Ok(prev_leftover);
721 }
722
723 let mut remaining = skip_lines;
724 let mut read_size = CompressedReader::initial_read_size();
725
726 loop {
727 let (slice, bytes_read) =
728 reader.read_next_slice(&prev_leftover, read_size, decompressed_file_size_hint)?;
729 let mut bytes: &[u8] = &slice;
730
731 'inner: loop {
732 let Some(mut pos) = memchr::memchr(eol_char, bytes) else {
733 read_size = read_size.saturating_mul(2);
734 break 'inner;
735 };
736 pos = cmp::min(pos + 1, bytes.len());
737
738 bytes = &bytes[pos..];
739 remaining -= 1;
740
741 if remaining == 0 {
742 let unconsumed_offset = bytes.as_ptr() as usize - slice.as_ptr() as usize;
743 prev_leftover = slice.sliced(unconsumed_offset..);
744 return Ok(prev_leftover);
745 }
746 }
747
748 if bytes_read == 0 {
749 if raise_if_empty {
750 polars_bail!(NoData: "specified skip_lines is larger than total number of lines.");
751 } else {
752 return Ok(Buffer::new());
753 }
754 }
755
756 prev_leftover = Buffer::new();
758
759 if read_size < CompressedReader::ideal_read_size() {
760 read_size *= 4;
761 }
762 }
763}
764
765fn infer_schema(
766 header_line: &Option<Buffer<u8>>,
767 content_lines: &[Buffer<u8>],
768 infer_all_as_str: bool,
769 options: &CsvReadOptions,
770 projected_schema: Option<SchemaRef>,
771 ignore_extra_columns: bool,
772 insert_missing_columns: bool,
773) -> PolarsResult<Schema> {
774 let has_no_inference_data = if options.has_header {
775 header_line.is_none()
776 } else {
777 content_lines.is_empty()
778 };
779
780 if options.raise_if_empty && has_no_inference_data {
781 polars_bail!(NoData: "empty CSV");
782 }
783
784 let mut inferred_schema = if has_no_inference_data {
785 Schema::default()
786 } else {
787 infer_file_schema_impl(
788 header_line,
789 content_lines,
790 infer_all_as_str,
791 &options.parse_options,
792 options.column_names_overwrite.as_deref(),
793 options.schema_overwrite.as_deref(),
794 ignore_extra_columns,
795 insert_missing_columns,
796 )?
797 };
798
799 if let Some(schema) = &options.schema {
800 if !has_no_inference_data {
801 if !ignore_extra_columns {
802 let mut extra_names = vec![];
803
804 let num_extra_names = if options.has_header {
805 extra_names.extend(
806 inferred_schema
807 .iter_names()
808 .filter(|name| !schema.contains(name))
809 .collect::<Vec<_>>(),
810 );
811 extra_names.len()
812 } else {
813 inferred_schema.len().saturating_sub(schema.len())
814 };
815
816 if num_extra_names != 0 {
817 let mut names = String::new();
818
819 if !extra_names.is_empty() {
820 names = format!(" (extra names: {extra_names:?})");
821 }
822
823 polars_bail!(
824 SchemaMismatch:
825 "CSV file contained column names not specified in schema (n_extra = {num_extra_names}). \
826 Specify these names in the schema, or pass `extra_columns='ignore'` to \
827 ignore these columns.{names}"
828 )
829 }
830 }
831
832 if !insert_missing_columns {
833 let mut missing_names = vec![];
834
835 let num_missing_names = if options.has_header {
836 missing_names.extend(
837 schema
838 .iter_names()
839 .filter(|name| !inferred_schema.contains(name))
840 .collect::<Vec<_>>(),
841 );
842 missing_names.len()
843 } else {
844 schema.len().saturating_sub(inferred_schema.len())
845 };
846
847 if num_missing_names != 0 {
848 let mut names = String::new();
849
850 if !missing_names.is_empty() {
851 names = format!(" (missing names: {missing_names:?})");
852 }
853
854 polars_bail!(
855 SchemaMismatch:
856 "column names specified in schema not found in CSV file (n_missing = {num_missing_names}). \
857 Remove these names from the schema, or pass `missing_columns='insert'` to \
858 insert these columns with NULL row values.{names}"
859 )
860 }
861 }
862 }
863
864 if !options.has_header {
865 inferred_schema = schema.as_ref().clone();
866 } else {
867 for (name, dtype) in schema.iter() {
868 inferred_schema.insert(name.clone(), dtype.clone());
869 }
870 }
871 }
872
873 if let Some(dtypes) = options.dtype_overwrite.as_deref() {
874 polars_ensure!(
875 dtypes.len() == inferred_schema.len(),
876 SchemaMismatch:
877 "The number of dtypes in schema override must be equal to the number \
878 of fields in the file ({} != {}).",
879 dtypes.len(), inferred_schema.len()
880 );
881 for (i, dtype) in dtypes.iter().enumerate() {
882 inferred_schema.set_dtype_at_index(i, dtype.clone());
883 }
884 }
885
886 if let Some(projected_schema) = projected_schema {
889 for (name, inferred_dtype) in inferred_schema.iter_mut() {
890 if let Some(projected_dtype) = projected_schema.get(name) {
891 *inferred_dtype = projected_dtype.clone();
892 }
893 }
894 }
895
896 Ok(inferred_schema)
897}