Skip to main content

genson_core/
extract.rs

1//! Move fields that are invariant per key out of JSON rows into a lookup table.
2//!
3//! With `spec = [("labels", "id")]`, `labels` is declared invariant for its determinant
4//! `id`: every object holding both has `labels` removed, and each distinct `(labels, <id>)`
5//! value is kept once in the lookup table, in first-seen order.
6
7use rayon::prelude::*;
8use rustc_hash::FxHashMap;
9use serde_json::Value;
10
11pub(crate) mod scan;
12
13/// One distinct subtree removed from the rows.
14#[derive(Debug, Clone, PartialEq)]
15pub struct LookupEntry {
16    /// The removed field's name, e.g. `labels`.
17    pub field: String,
18    /// The determinant's value, e.g. `Q5`.
19    pub key: String,
20    /// The removed subtree.
21    pub value: Value,
22}
23
24/// Slim rows (one per input row, `None` kept as `None`) and their lookup table.
25#[derive(Debug)]
26pub struct Extracted {
27    pub rows: Vec<Option<String>>,
28    pub lookup: Vec<LookupEntry>,
29}
30
31/// A chunk of rows extracted: its slim rows and its lookup entries.
32type Chunk = (Vec<Option<String>>, Lookup);
33
34/// A removed field's value as a lookup holds it: its JSON text where the row was scanned,
35/// and its `Value`, parsed from the text only once it is needed.
36struct Held {
37    text: Option<String>,
38    value: Option<Value>,
39    /// How deeply the text nests, where it was scanned.
40    nesting: usize,
41}
42
43impl Held {
44    fn value(&mut self) -> &Value {
45        let Held { text, value, .. } = self;
46        value.get_or_insert_with(|| {
47            let text = text
48                .as_deref()
49                .expect("a held value has its text or its value");
50            serde_json::from_str(text).expect("a field's text was validated by the scan")
51        })
52    }
53
54    /// Whether two held values are equal: by their texts when both have one and they are
55    /// the same, else by their values.
56    fn same(&mut self, other: &mut Held) -> bool {
57        match (&self.text, &other.text) {
58            (Some(a), Some(b)) if a == b => true,
59            _ => self.value() == other.value(),
60        }
61    }
62}
63
64/// Distinct entries in first-seen order.
65#[derive(Default)]
66struct Lookup {
67    ids: Vec<(String, String)>,
68    held: Vec<Held>,
69    /// Each entry's index, by field and then key.
70    index: FxHashMap<String, FxHashMap<String, usize>>,
71}
72
73fn not_invariant(field: &str, determinant: &str, key: &str) -> String {
74    format!(
75        "extract_invariants: field '{}' is not invariant for its determinant '{}' \
76         ('{}' has two different values)",
77        field, determinant, key
78    )
79}
80
81impl Lookup {
82    fn position(&self, field: &str, key: &str) -> Option<usize> {
83        self.index.get(field)?.get(key).copied()
84    }
85
86    fn insert_held(
87        &mut self,
88        field: &str,
89        key: &str,
90        mut held: Held,
91        determinant: &str,
92    ) -> Result<(), String> {
93        match self.position(field, key) {
94            Some(i) if !self.held[i].same(&mut held) => Err(not_invariant(field, determinant, key)),
95            Some(_) => Ok(()),
96            None => {
97                let at = self.held.len();
98                self.index
99                    .entry(field.to_string())
100                    .or_default()
101                    .insert(key.to_string(), at);
102                self.ids.push((field.to_string(), key.to_string()));
103                self.held.push(held);
104                Ok(())
105            }
106        }
107    }
108
109    fn insert(&mut self, entry: LookupEntry, determinant: &str) -> Result<(), String> {
110        let held = Held {
111            text: None,
112            value: Some(entry.value),
113            nesting: 0,
114        };
115        self.insert_held(&entry.field, &entry.key, held, determinant)
116    }
117
118    /// `insert` of a field's value given as its JSON text, held as text: nothing is copied
119    /// when the entry already holds this text.
120    fn insert_text(
121        &mut self,
122        field: &str,
123        key: &str,
124        text: &str,
125        nesting: usize,
126        determinant: &str,
127    ) -> Result<(), String> {
128        if let Some(i) = self.position(field, key) {
129            if self.held[i].text.as_deref() == Some(text) {
130                return Ok(());
131            }
132        }
133        let held = Held {
134            text: Some(text.to_string()),
135            value: None,
136            nesting,
137        };
138        self.insert_held(field, key, held, determinant)
139    }
140
141    fn into_entries(self) -> Vec<LookupEntry> {
142        self.ids
143            .into_iter()
144            .zip(self.held)
145            .map(|((field, key), mut held)| {
146                held.value();
147                LookupEntry {
148                    field,
149                    key,
150                    value: held.value.unwrap(),
151                }
152            })
153            .collect()
154    }
155}
156
157/// Remove each `field` whose determinant (its sibling key field) is present, at any depth.
158fn strip(node: &mut Value, spec: &[(String, String)], out: &mut Lookup) -> Result<(), String> {
159    match node {
160        Value::Object(obj) => {
161            for (field, key_field) in spec {
162                if !obj.contains_key(field) {
163                    continue;
164                }
165                let key = match obj.get(key_field) {
166                    Some(Value::String(s)) => s.clone(),
167                    Some(Value::Null) | None => continue,
168                    Some(other) => other.to_string(),
169                };
170                // shift_remove keeps the order of the remaining keys
171                let value = obj.shift_remove(field).unwrap();
172                out.insert(
173                    LookupEntry {
174                        field: field.clone(),
175                        key,
176                        value,
177                    },
178                    key_field,
179                )?;
180            }
181            obj.values_mut().try_for_each(|v| strip(v, spec, out))
182        }
183        Value::Array(arr) => arr.iter_mut().try_for_each(|v| strip(v, spec, out)),
184        _ => Ok(()),
185    }
186}
187
188/// Extract the `spec` fields, given as `(field, determinant)` pairs, from every row. Fields
189/// found in the same object are recorded in `spec` order. Rows that are not valid JSON
190/// pass through unchanged. Errors if a field is not invariant for its determinant, i.e. one
191/// determinant value is seen with two different values of the field.
192pub fn extract_invariants(
193    rows: Vec<Option<String>>,
194    spec: &[(String, String)],
195) -> Result<Extracted, String> {
196    extract_invariants_from(&rows, spec)
197}
198
199/// [`extract_invariants`] on borrowed rows, such as those of a
200/// [`crate::parquet::StringColumn`].
201pub fn extract_invariants_from<S: AsRef<str> + Sync>(
202    rows: &[Option<S>],
203    spec: &[(String, String)],
204) -> Result<Extracted, String> {
205    extract(rows, spec, true)
206}
207
208/// [`extract_invariants_from`] with every row read as a `Value`, never by scanning its
209/// text: the reference the scan is tested against. The slim rows are written as
210/// `serde_json` writes them, where the scan keeps the row's own text.
211#[doc(hidden)]
212pub fn extract_invariants_by_value<S: AsRef<str> + Sync>(
213    rows: &[Option<S>],
214    spec: &[(String, String)],
215) -> Result<Extracted, String> {
216    extract(rows, spec, false)
217}
218
219fn extract<S: AsRef<str> + Sync>(
220    rows: &[Option<S>],
221    spec: &[(String, String)],
222    scan_rows: bool,
223) -> Result<Extracted, String> {
224    let chunk = rows.len().div_ceil(rayon::current_num_threads() * 4).max(1);
225    // Each chunk keeps its own distinct entries, so repeats are dropped as they are seen
226    // Each chunk's result is kept, so that the error reported is the first in row order
227    // rather than whichever chunk failed first
228    let chunks: Vec<Result<Chunk, String>> = rows
229        .par_chunks(chunk)
230        .map(|rows| {
231            let mut lookup = Lookup::default();
232            let slim = rows
233                .iter()
234                .map(|row| {
235                    let Some(s) = row else { return Ok(None) };
236                    let s = s.as_ref();
237                    // One pass over the row's text when it can be read exactly so, else
238                    // the row as a `Value`
239                    // A value this chunk's lookup holds as scanned text is validated
240                    let known = |field: usize, key: &str| {
241                        let held = &lookup.held[lookup.position(&spec[field].0, key)?];
242                        Some((held.text.as_deref()?, held.nesting))
243                    };
244                    if let Some(Ok(scanned)) = scan_rows.then(|| scan::scan_known(s, spec, &known))
245                    {
246                        for removed in &scanned.removed {
247                            let (field, key_field) = &spec[removed.field];
248                            let text = &s[removed.value.clone()];
249                            let nesting = removed.nesting;
250                            lookup.insert_text(field, &removed.key, text, nesting, key_field)?;
251                        }
252                        return Ok(Some(scanned.slim));
253                    }
254                    let Ok(mut v) = serde_json::from_str::<Value>(s) else {
255                        return Ok(Some(s.to_string()));
256                    };
257                    strip(&mut v, spec, &mut lookup)?;
258                    Ok(Some(serde_json::to_string(&v).unwrap()))
259                })
260                .collect::<Result<Vec<_>, String>>()?;
261            Ok((slim, lookup))
262        })
263        .collect();
264
265    let mut slim_rows = Vec::new();
266    let mut lookup = Lookup::default();
267    for chunk in chunks {
268        let (rows, part) = chunk?;
269        slim_rows.extend(rows);
270        for ((field, key), held) in part.ids.into_iter().zip(part.held) {
271            let determinant = spec
272                .iter()
273                .find(|(f, _)| *f == field)
274                .map_or("", |(_, k)| k.as_str());
275            lookup.insert_held(&field, &key, held, determinant)?;
276        }
277    }
278    Ok(Extracted {
279        rows: slim_rows,
280        lookup: lookup.into_entries(),
281    })
282}
283
284#[cfg(test)]
285mod tests {
286    include!("tests/extract.rs");
287}