1use polars_buffer::Buffer;
2use polars_core::prelude::*;
3#[cfg(feature = "polars-time")]
4use polars_time::chunkedarray::string::infer as date_infer;
5#[cfg(feature = "polars-time")]
6use polars_time::prelude::string::Pattern;
7use polars_utils::format_pl_smallstr;
8
9use super::splitfields::SplitFields;
10use super::{CsvParseOptions, NullValues};
11use crate::utils::{BOOLEAN_RE, FLOAT_RE, FLOAT_RE_DECIMAL, INTEGER_RE};
12
13#[allow(clippy::too_many_arguments)]
17pub(super) fn infer_file_schema_impl(
18 header_line: &Option<Buffer<u8>>,
19 content_lines: &[Buffer<u8>],
20 infer_all_as_str: bool,
21 parse_options: &CsvParseOptions,
22 column_names_overwrite: Option<&[PlSmallStr]>,
23 schema_overwrite: Option<&Schema>,
24 ignore_extra_columns: bool,
25 insert_missing_columns: bool,
26) -> PolarsResult<Schema> {
27 let mut headers = if let Some(header_line) = header_line {
28 infer_headers(header_line, parse_options)?
29 } else {
30 Vec::with_capacity(8)
31 };
32
33 let extend_header_with_unknown_column = header_line.is_none();
34
35 let mut column_types = vec![PlIndexSet::<DataType>::with_capacity(4); headers.len()];
36 let mut nulls = vec![false; headers.len()];
37
38 for content_line in content_lines {
39 infer_types_from_line(
40 content_line,
41 infer_all_as_str,
42 &mut headers,
43 extend_header_with_unknown_column,
44 parse_options,
45 &mut column_types,
46 &mut nulls,
47 );
48 }
49
50 if let Some(column_names_overwrite) = column_names_overwrite {
51 let mut err_hint: String = String::new();
52
53 if column_names_overwrite.len() < headers.len() && !ignore_extra_columns {
54 let n = headers.len() - column_names_overwrite.len();
55 err_hint = format!("pass extra_columns='ignore' to ignore ({n}) extra columns.")
56 }
57
58 if column_names_overwrite.len() > headers.len() && !insert_missing_columns {
59 let n = column_names_overwrite.len() - headers.len();
60 err_hint = format!(
61 "pass missing_columns='insert' to create ({n}) missing columns with all-NULL values."
62 );
63 }
64
65 if !err_hint.is_empty() {
66 polars_bail!(
67 SchemaMismatch:
68 "provided `new_columns` does not match number of columns in file ({} != {} in file). \
69 Ensure the number of names match, or {err_hint}",
70 column_names_overwrite.len(),
71 headers.len(),
72 )
73 }
74
75 headers.truncate(column_names_overwrite.len());
76 column_types.truncate(column_names_overwrite.len());
77
78 for (i, name) in column_names_overwrite.iter().cloned().enumerate() {
79 if i < headers.len() {
80 headers[i] = name
81 } else {
82 headers.push(name)
83 }
84
85 if i >= column_types.len() {
86 column_types.push(PlIndexSet::from_iter(Some(DataType::Null)))
87 }
88 }
89 }
90
91 Ok(build_schema(&headers, &column_types, schema_overwrite))
92}
93
94fn infer_headers(
95 mut header_line: &[u8],
96 parse_options: &CsvParseOptions,
97) -> PolarsResult<Vec<PlSmallStr>> {
98 let len = header_line.len();
99
100 if header_line.last().copied() == Some(b'\r') {
101 header_line = &header_line[..len - 1];
102 }
103
104 let byterecord = SplitFields::new(
105 header_line,
106 parse_options.separator,
107 parse_options.quote_char,
108 parse_options.eol_char,
109 );
110
111 let headers = byterecord
112 .map(|(slice, needs_escaping)| {
113 let slice_escaped = if needs_escaping && (slice.len() >= 2) {
114 &slice[1..(slice.len() - 1)]
115 } else {
116 slice
117 };
118 String::from_utf8_lossy(slice_escaped)
119 })
120 .collect::<Vec<_>>();
121
122 let mut deduplicated_headers = PlIndexSet::with_capacity(headers.len());
123 let mut header_names = PlHashMap::with_capacity(headers.len());
124
125 for name in &headers {
126 let count = header_names.entry(name.as_ref()).or_insert(0usize);
127 let duplicated = *count != 0;
128 let deduplicated_name = if duplicated {
129 format_pl_smallstr!("{}_duplicated_{}", name, *count - 1)
130 } else {
131 PlSmallStr::from_str(name)
132 };
133
134 if !deduplicated_headers.insert(deduplicated_name.clone()) {
135 let (deduplicated_from, nth_duplicated) = if duplicated {
136 (name.as_ref(), 1 + *count)
137 } else {
138 let i = deduplicated_name.rfind("_duplicated_").unwrap();
139 (
140 &deduplicated_name[..i],
141 2 + deduplicated_name[i + 12..].parse::<usize>().unwrap(),
142 )
143 };
144
145 polars_bail!(
146 Duplicate:
147 "de-duplication of occurrence #{nth_duplicated} of column name '{deduplicated_from}' \
148 failed; the name '{deduplicated_name}' also exists in the file."
149 )
150 }
151
152 *count += 1;
153 }
154
155 Ok(Vec::from_iter(deduplicated_headers))
156}
157
158fn infer_types_from_line(
159 mut line: &[u8],
160 infer_all_as_str: bool,
161 headers: &mut Vec<PlSmallStr>,
162 extend_header_with_unknown_column: bool,
163 parse_options: &CsvParseOptions,
164 column_types: &mut Vec<PlIndexSet<DataType>>,
165 nulls: &mut Vec<bool>,
166) {
167 let line_len = line.len();
168 if line.last().copied() == Some(b'\r') {
169 line = &line[..line_len - 1];
170 }
171
172 let record = SplitFields::new(
173 line,
174 parse_options.separator,
175 parse_options.quote_char,
176 parse_options.eol_char,
177 );
178
179 for (i, (slice, needs_escaping)) in record.enumerate() {
180 if i >= headers.len() {
181 if extend_header_with_unknown_column {
182 headers.push(column_name(i));
183 column_types.push(Default::default());
184 nulls.push(false);
185 } else {
186 break;
187 }
188 }
189
190 if infer_all_as_str {
191 column_types[i].insert(DataType::String);
192 continue;
193 }
194
195 if slice.is_empty() {
196 nulls[i] = true;
197 } else {
198 let slice_escaped = if needs_escaping && (slice.len() >= 2) {
199 &slice[1..(slice.len() - 1)]
200 } else {
201 slice
202 };
203 let s = String::from_utf8_lossy(slice_escaped);
204 let dtype = match &parse_options.null_values {
205 None => Some(infer_field_schema(
206 &s,
207 parse_options.try_parse_dates,
208 parse_options.decimal_comma,
209 )),
210 Some(NullValues::AllColumns(names)) => {
211 if !names.iter().any(|nv| nv == s.as_ref()) {
212 Some(infer_field_schema(
213 &s,
214 parse_options.try_parse_dates,
215 parse_options.decimal_comma,
216 ))
217 } else {
218 None
219 }
220 },
221 Some(NullValues::AllColumnsSingle(name)) => {
222 if s.as_ref() != name.as_str() {
223 Some(infer_field_schema(
224 &s,
225 parse_options.try_parse_dates,
226 parse_options.decimal_comma,
227 ))
228 } else {
229 None
230 }
231 },
232 Some(NullValues::Named(names)) => {
233 let current_name = &headers[i];
234 let null_name = &names.iter().find(|name| name.0 == current_name);
235
236 if let Some(null_name) = null_name {
237 if null_name.1.as_str() != s.as_ref() {
238 Some(infer_field_schema(
239 &s,
240 parse_options.try_parse_dates,
241 parse_options.decimal_comma,
242 ))
243 } else {
244 None
245 }
246 } else {
247 Some(infer_field_schema(
248 &s,
249 parse_options.try_parse_dates,
250 parse_options.decimal_comma,
251 ))
252 }
253 },
254 };
255 if let Some(dtype) = dtype {
256 column_types[i].insert(dtype);
257 }
258 }
259 }
260}
261
262fn build_schema(
263 headers: &[PlSmallStr],
264 column_types: &[PlIndexSet<DataType>],
265 schema_overwrite: Option<&Schema>,
266) -> Schema {
267 assert!(headers.len() == column_types.len());
268
269 let get_schema_overwrite = |field_name| {
270 if let Some(schema_overwrite) = schema_overwrite {
271 if let Some((_, name, dtype)) = schema_overwrite.get_full(field_name) {
274 return Some((name.clone(), dtype.clone()));
275 }
276 }
277
278 None
279 };
280
281 Schema::from_iter(
282 headers
283 .iter()
284 .zip(column_types)
285 .map(|(field_name, type_possibilities)| {
286 let (name, dtype) = get_schema_overwrite(field_name).unwrap_or_else(|| {
287 (
288 field_name.clone(),
289 finish_infer_field_schema(type_possibilities),
290 )
291 });
292
293 Field::new(name, dtype)
294 }),
295 )
296}
297
298pub fn finish_infer_field_schema(possibilities: &PlIndexSet<DataType>) -> DataType {
299 match possibilities.len() {
302 1 => possibilities.iter().next().unwrap().clone(),
303 2 if possibilities.contains(&DataType::Int64)
304 && possibilities.contains(&DataType::Float64) =>
305 {
306 DataType::Float64
308 },
309 #[cfg(feature = "dtype-i128")]
310 2 if possibilities.contains(&DataType::Int64)
311 && possibilities.contains(&DataType::Int128) =>
312 {
313 DataType::Int128
315 },
316 #[cfg(feature = "dtype-i128")]
317 2 if possibilities.contains(&DataType::Int128)
318 && possibilities.contains(&DataType::Float64) =>
319 {
320 DataType::Float64
322 },
323 _ => DataType::String,
325 }
326}
327
328pub fn infer_field_schema(string: &str, try_parse_dates: bool, decimal_comma: bool) -> DataType {
330 let bytes = string.as_bytes();
333 if bytes.len() >= 2 && *bytes.first().unwrap() == b'"' && *bytes.last().unwrap() == b'"' {
334 if try_parse_dates {
335 #[cfg(feature = "polars-time")]
336 {
337 match date_infer::infer_pattern_single(&string[1..string.len() - 1]) {
338 Some(pattern_with_offset) => match pattern_with_offset {
339 Pattern::DatetimeYMD | Pattern::DatetimeDMY => {
340 DataType::Datetime(TimeUnit::Microseconds, None)
341 },
342 Pattern::DateYMD | Pattern::DateDMY => DataType::Date,
343 Pattern::DatetimeYMDZ => {
344 DataType::Datetime(TimeUnit::Microseconds, Some(TimeZone::UTC))
345 },
346 Pattern::Time => DataType::Time,
347 },
348 None => DataType::String,
349 }
350 }
351 #[cfg(not(feature = "polars-time"))]
352 {
353 panic!("activate one of {{'dtype-date', 'dtype-datetime', dtype-time'}} features")
354 }
355 } else {
356 DataType::String
357 }
358 }
359 else if BOOLEAN_RE.is_match(string) {
361 DataType::Boolean
362 } else if !decimal_comma && FLOAT_RE.is_match(string)
363 || decimal_comma && FLOAT_RE_DECIMAL.is_match(string)
364 {
365 DataType::Float64
366 } else if INTEGER_RE.is_match(string) {
367 if string.parse::<i64>().is_ok() {
368 DataType::Int64
369 } else {
370 #[cfg(feature = "dtype-i128")]
371 {
372 DataType::Int128
373 }
374 #[cfg(not(feature = "dtype-i128"))]
375 {
376 DataType::Int64
377 }
378 }
379 } else if try_parse_dates {
380 #[cfg(feature = "polars-time")]
381 {
382 match date_infer::infer_pattern_single(string) {
383 Some(pattern_with_offset) => match pattern_with_offset {
384 Pattern::DatetimeYMD | Pattern::DatetimeDMY => {
385 DataType::Datetime(TimeUnit::Microseconds, None)
386 },
387 Pattern::DateYMD | Pattern::DateDMY => DataType::Date,
388 Pattern::DatetimeYMDZ => {
389 DataType::Datetime(TimeUnit::Microseconds, Some(TimeZone::UTC))
390 },
391 Pattern::Time => DataType::Time,
392 },
393 None => DataType::String,
394 }
395 }
396 #[cfg(not(feature = "polars-time"))]
397 {
398 panic!("activate one of {{'dtype-date', 'dtype-datetime', dtype-time'}} features")
399 }
400 } else {
401 DataType::String
402 }
403}
404
405fn column_name(i: usize) -> PlSmallStr {
406 format_pl_smallstr!("column_{}", i)
407}
408
409#[cfg(test)]
410mod tests {
411 use super::*;
412
413 #[test]
414 fn test_infer_field_schema_i64_overflow() {
415 assert_eq!(
417 infer_field_schema("9223372036854775807", false, false),
418 DataType::Int64,
419 );
420
421 let large = "12345678901234567890";
424 #[cfg(feature = "dtype-i128")]
425 assert_eq!(infer_field_schema(large, false, false), DataType::Int128,);
426 #[cfg(not(feature = "dtype-i128"))]
427 assert_eq!(infer_field_schema(large, false, false), DataType::Int64,);
428 }
429
430 #[test]
431 #[cfg(feature = "dtype-i128")]
432 fn test_finish_infer_field_schema_i64_and_i128() {
433 let mut possibilities = PlIndexSet::new();
434 possibilities.insert(DataType::Int64);
435 possibilities.insert(DataType::Int128);
436 assert_eq!(finish_infer_field_schema(&possibilities), DataType::Int128);
437 }
438}