use rayon::prelude::*;
use serde_json::Value;
use std::collections::HashMap;
#[derive(Debug, Clone, PartialEq)]
pub struct LookupEntry {
pub field: String,
pub key: String,
pub value: Value,
}
#[derive(Debug)]
pub struct Extracted {
pub rows: Vec<Option<String>>,
pub lookup: Vec<LookupEntry>,
}
#[derive(Default)]
struct Lookup {
entries: Vec<LookupEntry>,
index: HashMap<(String, String), usize>,
}
impl Lookup {
fn insert(&mut self, entry: LookupEntry, determinant: &str) -> Result<(), String> {
let id = (entry.field.clone(), entry.key.clone());
match self.index.get(&id) {
Some(&i) if self.entries[i].value != entry.value => Err(format!(
"extract_invariants: field '{}' is not invariant for its determinant '{}' \
('{}' has two different values)",
entry.field, determinant, entry.key
)),
Some(_) => Ok(()),
None => {
self.index.insert(id, self.entries.len());
self.entries.push(entry);
Ok(())
}
}
}
}
fn strip(node: &mut Value, spec: &[(String, String)], out: &mut Lookup) -> Result<(), String> {
match node {
Value::Object(obj) => {
for (field, key_field) in spec {
if !obj.contains_key(field) {
continue;
}
let key = match obj.get(key_field) {
Some(Value::String(s)) => s.clone(),
Some(Value::Null) | None => continue,
Some(other) => other.to_string(),
};
let value = obj.shift_remove(field).unwrap();
out.insert(
LookupEntry {
field: field.clone(),
key,
value,
},
key_field,
)?;
}
obj.values_mut().try_for_each(|v| strip(v, spec, out))
}
Value::Array(arr) => arr.iter_mut().try_for_each(|v| strip(v, spec, out)),
_ => Ok(()),
}
}
pub fn extract_invariants(
rows: Vec<Option<String>>,
spec: &[(String, String)],
) -> Result<Extracted, String> {
let chunk = rows.len().div_ceil(rayon::current_num_threads() * 4).max(1);
let chunks: Vec<(Vec<Option<String>>, Lookup)> = rows
.par_chunks(chunk)
.map(|rows| {
let mut lookup = Lookup::default();
let slim = rows
.iter()
.map(|row| {
let Some(s) = row else { return Ok(None) };
let Ok(mut v) = serde_json::from_str::<Value>(s) else {
return Ok(Some(s.clone()));
};
strip(&mut v, spec, &mut lookup)?;
Ok(Some(serde_json::to_string(&v).unwrap()))
})
.collect::<Result<Vec<_>, String>>()?;
Ok((slim, lookup))
})
.collect::<Result<_, String>>()?;
drop(rows);
let mut slim_rows = Vec::new();
let mut lookup = Lookup::default();
for (rows, part) in chunks {
slim_rows.extend(rows);
for entry in part.entries {
let determinant = spec
.iter()
.find(|(f, _)| *f == entry.field)
.map_or("", |(_, k)| k.as_str());
lookup.insert(entry, determinant)?;
}
}
Ok(Extracted {
rows: slim_rows,
lookup: lookup.entries,
})
}
#[cfg(test)]
mod tests {
include!("tests/extract.rs");
}