Skip to main content

polars_io/ndjson/
core.rs

1use std::io::Cursor;
2use std::num::NonZeroUsize;
3
4use num_traits::pow::Pow;
5pub use polars_arrow::array::StructArray;
6use polars_core::prelude::*;
7use polars_core::runtime::RAYON;
8use polars_core::utils::accumulate_dataframes_vertical;
9use polars_json::json::ordered::TapeGuide;
10use rayon::prelude::*;
11
12use crate::RowIndex;
13use crate::mmap::ReaderBytes;
14use crate::ndjson::buffer::*;
15use crate::predicates::PhysicalIoExpr;
16use crate::prelude::*;
17const NEWLINE: u8 = b'\n';
18const CLOSING_BRACKET: u8 = b'}';
19
20pub(crate) struct CoreJsonReader<'a> {
21    reader_bytes: Option<ReaderBytes<'a>>,
22    n_rows: Option<usize>,
23    schema: SchemaRef,
24    n_threads: Option<usize>,
25    sample_size: usize,
26    chunk_size: NonZeroUsize,
27    low_memory: bool,
28    ignore_errors: bool,
29    row_index: Option<&'a mut RowIndex>,
30    predicate: Option<Arc<dyn PhysicalIoExpr>>,
31    projection: Option<Arc<[PlSmallStr]>>,
32}
33impl<'a> CoreJsonReader<'a> {
34    #[allow(clippy::too_many_arguments)]
35    pub(crate) fn new(
36        reader_bytes: ReaderBytes<'a>,
37        n_rows: Option<usize>,
38        schema: Option<SchemaRef>,
39        schema_overwrite: Option<&Schema>,
40        n_threads: Option<usize>,
41        sample_size: usize,
42        chunk_size: NonZeroUsize,
43        low_memory: bool,
44        infer_schema_len: Option<NonZeroUsize>,
45        ignore_errors: bool,
46        row_index: Option<&'a mut RowIndex>,
47        predicate: Option<Arc<dyn PhysicalIoExpr>>,
48        projection: Option<Arc<[PlSmallStr]>>,
49    ) -> PolarsResult<CoreJsonReader<'a>> {
50        let reader_bytes = reader_bytes;
51
52        let mut schema = match schema {
53            Some(schema) => schema,
54            None => {
55                let bytes: &[u8] = &reader_bytes;
56                let mut cursor = Cursor::new(bytes);
57                Arc::new(crate::ndjson::infer_schema(&mut cursor, infer_schema_len)?)
58            },
59        };
60        if let Some(overwriting_schema) = schema_overwrite {
61            let schema = Arc::make_mut(&mut schema);
62            overwrite_schema(schema, overwriting_schema)?;
63        }
64
65        Ok(CoreJsonReader {
66            reader_bytes: Some(reader_bytes),
67            schema,
68            sample_size,
69            n_rows,
70            n_threads,
71            chunk_size,
72            low_memory,
73            ignore_errors,
74            row_index,
75            predicate,
76            projection,
77        })
78    }
79
80    fn parse_json(&mut self, mut n_threads: usize, bytes: &[u8]) -> PolarsResult<DataFrame> {
81        let mut bytes = bytes;
82        let mut total_rows = 128;
83
84        if let Some((mean, std)) = get_line_stats_json(bytes, self.sample_size) {
85            let line_length_upper_bound = mean + 1.1 * std;
86
87            total_rows = (bytes.len() as f32 / (mean - 0.01 * std)) as usize;
88            if let Some(n_rows) = self.n_rows {
89                total_rows = std::cmp::min(n_rows, total_rows);
90                // the guessed upper bound of  the no. of bytes in the file
91                let n_bytes = (line_length_upper_bound * (n_rows as f32)) as usize;
92
93                if n_bytes < bytes.len() {
94                    if let Some(pos) = next_line_position_naive_json(&bytes[n_bytes..]) {
95                        bytes = &bytes[..n_bytes + pos]
96                    }
97                }
98            }
99        }
100
101        if total_rows <= 128 {
102            n_threads = 1;
103        }
104
105        let rows_per_thread = total_rows / n_threads;
106
107        let max_proxy = bytes.len() / n_threads / 2;
108        let capacity = if self.low_memory {
109            usize::from(self.chunk_size)
110        } else {
111            std::cmp::min(rows_per_thread, max_proxy)
112        };
113        let file_chunks = get_file_chunks_json(bytes, n_threads);
114
115        let row_index = self.row_index.as_ref().map(|ri| ri as &RowIndex);
116        let (mut dfs, prepredicate_heights) = RAYON.install(|| {
117            file_chunks
118                .into_par_iter()
119                .map(|(start_pos, stop_at_nbytes)| {
120                    let mut local_df = parse_ndjson(
121                        &bytes[start_pos..stop_at_nbytes],
122                        Some(capacity),
123                        &self.schema,
124                        self.ignore_errors,
125                    )?;
126
127                    let prepredicate_height = local_df.height() as IdxSize;
128                    if let Some(projection) = self.projection.as_deref() {
129                        local_df = local_df.select(projection.iter().cloned())?;
130                    }
131
132                    if let Some(row_index) = row_index {
133                        local_df = local_df
134                            .with_row_index(row_index.name.clone(), Some(row_index.offset))?;
135                    }
136
137                    if let Some(predicate) = &self.predicate {
138                        let s = predicate.evaluate_io(&local_df)?;
139                        let mask = s.bool()?;
140                        local_df = local_df.filter(mask)?;
141                    }
142
143                    Ok((local_df, prepredicate_height))
144                })
145                .collect::<PolarsResult<(Vec<_>, Vec<_>)>>()
146        })?;
147
148        if let Some(ref mut row_index) = self.row_index {
149            update_row_counts3(&mut dfs, &prepredicate_heights, 0);
150            row_index.offset += prepredicate_heights.iter().copied().sum::<IdxSize>();
151        }
152
153        accumulate_dataframes_vertical(dfs)
154    }
155
156    pub fn as_df(&mut self) -> PolarsResult<DataFrame> {
157        let n_threads = self
158            .n_threads
159            .unwrap_or_else(|| RAYON.current_num_threads());
160
161        let reader_bytes = self.reader_bytes.take().unwrap();
162
163        let mut df = self.parse_json(n_threads, &reader_bytes)?;
164
165        // if multi-threaded the n_rows was probabilistically determined.
166        // Let's slice to correct number of rows if possible.
167        if let Some(n_rows) = self.n_rows {
168            if n_rows < df.height() {
169                df = df.slice(0, n_rows)
170            }
171        }
172        Ok(df)
173    }
174}
175
176#[inline(always)]
177fn parse_impl(
178    bytes: &[u8],
179    buffers: &mut PlIndexMap<BufferKey, Buffer>,
180    scratch: &mut Scratch,
181    guide: Option<&TapeGuide>,
182    ignore_errors: bool,
183) -> PolarsResult<usize> {
184    scratch.json.clear();
185    scratch.json.extend_from_slice(bytes);
186    let n = scratch.json.len();
187    let parse_err = |e| polars_err!(ComputeError: "error parsing line: {}", e);
188    let value = match guide {
189        Some(guide) => {
190            let tape = simd_json::to_tape_with_buffers(&mut scratch.json, &mut scratch.buffers)
191                .map_err(parse_err)?;
192            polars_json::json::ordered::tape_to_value(&tape, guide, ignore_errors)?
193        },
194        None => simd_json::to_borrowed_value_with_buffers(&mut scratch.json, &mut scratch.buffers)
195            .map_err(parse_err)?,
196    };
197    match value {
198        simd_json::BorrowedValue::Object(value) => {
199            buffers.iter_mut().try_for_each(|(s, inner)| {
200                match s.0.map_lookup(&value) {
201                    Some(v) => inner.add(v)?,
202                    None => inner.add_null(),
203                }
204                PolarsResult::Ok(())
205            })?;
206        },
207        _ if ignore_errors => {
208            buffers.iter_mut().for_each(|(_, inner)| inner.add_null());
209        },
210        v => {
211            polars_bail!(ComputeError: "NDJSON line expected to contain JSON object: {v}");
212        },
213    };
214    Ok(n)
215}
216
217#[derive(Default)]
218struct Scratch {
219    json: Vec<u8>,
220    buffers: simd_json::Buffers,
221}
222
223pub fn json_lines(bytes: &[u8]) -> impl Iterator<Item = &[u8]> {
224    // This previously used `serde_json`'s `RawValue` to deserialize chunks without really deserializing them.
225    // However, this convenience comes at a cost. serde_json allocates and parses and does UTF-8 validation, all
226    // things we don't need since we use simd_json for them. Also, `serde_json::StreamDeserializer` has a more
227    // ambitious goal: it wants to parse potentially *non-delimited* sequences of JSON values, while we know
228    // our values are line-delimited. Turns out, custom splitting is very easy, and gives a very nice performance boost.
229    bytes
230        .split(|&byte| byte == b'\n')
231        .filter(|bytes| is_json_line(bytes))
232}
233
234#[inline]
235pub fn is_json_line(bytes: &[u8]) -> bool {
236    bytes
237        .iter()
238        .any(|byte| !matches!(*byte, b' ' | b'\t' | b'\r'))
239}
240
241fn parse_lines(
242    bytes: &[u8],
243    buffers: &mut PlIndexMap<BufferKey, Buffer>,
244    guide: Option<&TapeGuide>,
245    ignore_errors: bool,
246) -> PolarsResult<()> {
247    let mut scratch = Scratch::default();
248
249    let iter = json_lines(bytes);
250    for bytes in iter {
251        parse_impl(bytes, buffers, &mut scratch, guide, ignore_errors)?;
252    }
253    Ok(())
254}
255
256pub fn parse_ndjson(
257    bytes: &[u8],
258    n_rows_hint: Option<usize>,
259    schema: &Schema,
260    ignore_errors: bool,
261) -> PolarsResult<DataFrame> {
262    let capacity = n_rows_hint.unwrap_or_else(|| estimate_n_lines_in_chunk(bytes));
263
264    // Map columns are parsed in source order and decoded after deserialization.
265    let has_map = schema.iter_values().any(|dt| dt.contains_map());
266    let guide_dtype = has_map
267        .then(|| DataType::Struct(schema.iter_fields().collect()).to_arrow(CompatLevel::newest()));
268    let guide = guide_dtype.as_ref().map(TapeGuide::new);
269    let decode_schema: Option<Schema> = has_map.then(|| {
270        schema
271            .iter()
272            .map(|(name, dt)| {
273                let dt = if dt.contains_map() {
274                    dt.json_decode_dtype()
275                } else {
276                    dt.clone()
277                };
278                Field::new(name.clone(), dt)
279            })
280            .collect()
281    });
282
283    let mut buffers = init_buffers(
284        decode_schema.as_ref().unwrap_or(schema),
285        capacity,
286        ignore_errors,
287    )?;
288    parse_lines(bytes, &mut buffers, guide.as_ref(), ignore_errors)?;
289
290    DataFrame::new_infer_height(
291        buffers
292            .into_values()
293            .zip(schema.iter_values())
294            .map(|(buf, dt)| {
295                let s = buf.into_series()?;
296                let s = if dt.contains_map() {
297                    s.from_json_decoded(dt, ignore_errors)?
298                } else {
299                    s
300                };
301                Ok(s.into_column())
302            })
303            .collect::<PolarsResult<_>>()
304            .map_err(|e| match e {
305                // Nested types raise SchemaMismatch instead of ComputeError, we map it back here to
306                // be consistent.
307                PolarsError::ComputeError(..) => e,
308                PolarsError::SchemaMismatch(e) => PolarsError::ComputeError(e),
309                e => e,
310            })?,
311    )
312}
313
314/// Total len divided by max len of first and last non-empty lines.
315pub fn estimate_n_lines_in_chunk(chunk: &[u8]) -> usize {
316    chunk
317        .split(|&c| c == b'\n')
318        .find(|x| !x.is_empty())
319        .map_or(1, |x| {
320            chunk.len().div_ceil(
321                x.len().max(
322                    chunk
323                        .rsplit(|&c| c == b'\n')
324                        .find(|x| !x.is_empty())
325                        .unwrap()
326                        .len(),
327                ),
328            )
329        })
330}
331
332/// Find the nearest next line position.
333/// Does not check for new line characters embedded in String fields.
334/// This just looks for `}\n`
335pub(crate) fn next_line_position_naive_json(input: &[u8]) -> Option<usize> {
336    let pos = memchr::memchr(NEWLINE, input)?;
337    if pos == 0 {
338        return Some(1);
339    }
340
341    let is_closing_bracket = input.get(pos - 1) == Some(&CLOSING_BRACKET);
342    if is_closing_bracket {
343        Some(pos + 1)
344    } else {
345        None
346    }
347}
348
349/// Get the mean and standard deviation of length of lines in bytes
350pub(crate) fn get_line_stats_json(bytes: &[u8], n_lines: usize) -> Option<(f32, f32)> {
351    let mut lengths = Vec::with_capacity(n_lines);
352
353    let mut bytes_trunc;
354    let n_lines_per_iter = n_lines / 2;
355
356    let mut n_read = 0;
357
358    let bytes_len = bytes.len();
359
360    // sample from start and 75% in the file
361    for offset in [0, (bytes_len as f32 * 0.75) as usize] {
362        bytes_trunc = &bytes[offset..];
363        let pos = next_line_position_naive_json(bytes_trunc)?;
364        if pos >= bytes_len {
365            return None;
366        }
367        bytes_trunc = &bytes_trunc[pos + 1..];
368
369        for _ in offset..(offset + n_lines_per_iter) {
370            let pos = next_line_position_naive_json(bytes_trunc);
371            if let Some(pos) = pos {
372                lengths.push(pos);
373                let next_bytes = &bytes_trunc[pos..];
374                if next_bytes.is_empty() {
375                    return None;
376                }
377                bytes_trunc = next_bytes;
378                n_read += pos;
379            } else {
380                break;
381            }
382        }
383    }
384
385    let n_samples = lengths.len();
386    let mean = (n_read as f32) / (n_samples as f32);
387    let mut std = 0.0;
388    for &len in lengths.iter() {
389        std += (len as f32 - mean).pow(2.0)
390    }
391    std = (std / n_samples as f32).sqrt();
392    Some((mean, std))
393}
394
395pub(crate) fn get_file_chunks_json(bytes: &[u8], n_threads: usize) -> Vec<(usize, usize)> {
396    let mut last_pos = 0;
397    let total_len = bytes.len();
398    let chunk_size = total_len / n_threads;
399    let mut offsets = Vec::with_capacity(n_threads);
400    for _ in 0..n_threads {
401        let search_pos = last_pos + chunk_size;
402
403        if search_pos >= bytes.len() {
404            break;
405        }
406
407        let end_pos = match next_line_position_naive_json(&bytes[search_pos..]) {
408            Some(pos) => search_pos + pos,
409            None => {
410                break;
411            },
412        };
413        offsets.push((last_pos, end_pos));
414        last_pos = end_pos;
415    }
416    offsets.push((last_pos, total_len));
417    offsets
418}