use std::collections::{BTreeSet, HashMap, HashSet, VecDeque};
use serde_json::{Map, Value as Json};
use crate::{DataError, DataType, Field, Row, RowSource, Schema, Value};
pub const MAX_DEPTH: usize = 32;
pub const DEFAULT_SAMPLE: usize = 1000;
pub const MAX_UNKNOWN_KEYS: usize = 100;
#[derive(Debug)]
pub struct RecordSource<I> {
records: I,
buffer: VecDeque<Map<String, Json>>,
columns: Vec<String>,
names: HashSet<String>,
schema: Schema,
unknown: BTreeSet<String>,
more_unknown: bool,
}
impl<I> RecordSource<I>
where
I: Iterator<Item = Result<Json, DataError>>,
{
pub(crate) fn new(mut records: I, sample: usize) -> Result<Self, DataError> {
let mut buffer = VecDeque::new();
let mut fields: Vec<Field> = Vec::new();
let mut index: HashMap<String, usize> = HashMap::new();
let mut first: Vec<usize> = Vec::new();
let mut held: Vec<usize> = Vec::new();
for number in 0..sample.max(1) {
let Some(record) = records.next() else {
break;
};
let record = object(record?);
for (key, value) in &record {
let seen = field_of(key, value, 0);
match index.get(key) {
Some(&at) => {
fields[at] = merge(fields[at].clone(), seen);
held[at] += 1;
}
None => {
index.insert(key.clone(), fields.len());
fields.push(seen.nullable(number > 0 || value.is_null()));
first.push(number);
held.push(1);
}
}
}
buffer.push_back(record);
}
let read = buffer.len();
for ((field, first), held) in fields.iter_mut().zip(first).zip(held) {
if held < read - first {
*field = field.clone().nullable(true);
}
}
Ok(RecordSource {
records,
buffer,
names: index.into_keys().collect(),
columns: fields.iter().map(|f| f.name().to_string()).collect(),
schema: Schema::new(fields),
unknown: BTreeSet::new(),
more_unknown: false,
})
}
pub fn unknown_keys(&self) -> impl Iterator<Item = &str> {
self.unknown.iter().map(String::as_str)
}
pub fn more_unknown_keys(&self) -> bool {
self.more_unknown
}
fn row(&mut self, record: Map<String, Json>) -> Row {
for key in record.keys() {
if !self.names.contains(key) && !self.unknown.contains(key) {
if self.unknown.len() < MAX_UNKNOWN_KEYS {
self.unknown.insert(key.clone());
} else {
self.more_unknown = true;
}
}
}
self.columns
.iter()
.zip(self.schema.fields())
.map(
|(column, field)| match record.get(column).map_or(Value::Null, cell) {
Value::Int(n) if field.data_type() == &DataType::Float => {
Value::Float(n as f64)
}
other => other,
},
)
.collect()
}
}
impl<I> RowSource for RecordSource<I>
where
I: Iterator<Item = Result<Json, DataError>>,
{
fn columns(&self) -> &[String] {
&self.columns
}
fn schema(&self) -> Option<&Schema> {
Some(&self.schema)
}
fn next_row(&mut self) -> Option<Result<Row, DataError>> {
let record = match self.buffer.pop_front() {
Some(record) => record,
None => match self.records.next()? {
Ok(record) => object(record),
Err(error) => return Some(Err(error)),
},
};
Some(Ok(self.row(record)))
}
}
fn object(record: Json) -> Map<String, Json> {
match record {
Json::Object(map) => map,
other => Map::from_iter([("value".to_string(), other)]),
}
}
pub(crate) fn cell(value: &Json) -> Value {
match value {
Json::Null => Value::Null,
Json::Bool(b) => Value::Str(b.to_string()),
Json::Number(n) => match n.as_i64() {
Some(i) => Value::Int(i),
None => n.as_f64().map_or(Value::Null, Value::Float),
},
Json::String(s) => Value::Str(s.clone()),
other => Value::Str(other.to_string()),
}
}
fn field_of(name: &str, value: &Json, depth: usize) -> Field {
let data_type = match value {
Json::Null => return Field::new(name, DataType::Unknown),
_ if depth >= MAX_DEPTH => DataType::Any,
Json::Bool(_) => DataType::Boolean,
Json::Number(n) if n.is_f64() => DataType::Float,
Json::Number(_) => DataType::Integer,
Json::String(_) => DataType::String,
Json::Array(items) => {
let item = items
.iter()
.map(|item| field_of("item", item, depth + 1))
.reduce(merge)
.unwrap_or_else(|| Field::new("item", DataType::Unknown));
DataType::List(Box::new(item))
}
Json::Object(map) => {
let fields = map.iter().map(|(k, v)| field_of(k, v, depth + 1)).collect();
DataType::Struct(fields)
}
};
Field::new(name, data_type).nullable(false)
}
fn merge(a: Field, b: Field) -> Field {
let nullable = a.is_nullable() || b.is_nullable();
let data_type = match (a.data_type().clone(), b.data_type().clone()) {
(DataType::Unknown, other) | (other, DataType::Unknown) => other,
(x, y) if x == y => x,
(DataType::Integer, DataType::Float) | (DataType::Float, DataType::Integer) => {
DataType::Float
}
(DataType::List(x), DataType::List(y)) => DataType::List(Box::new(merge(*x, *y))),
(DataType::Struct(xs), DataType::Struct(ys)) => {
let at: HashMap<String, usize> = ys
.iter()
.enumerate()
.map(|(i, y)| (y.name().to_string(), i))
.collect();
let mut ys: Vec<Option<Field>> = ys.into_iter().map(Some).collect();
let mut fields: Vec<Field> = xs
.into_iter()
.map(|x| match at.get(x.name()).and_then(|&i| ys[i].take()) {
Some(y) => merge(x, y),
None => x.nullable(true),
})
.collect();
fields.extend(ys.into_iter().flatten().map(|y| y.nullable(true)));
DataType::Struct(fields)
}
_ => DataType::Any,
};
a.with_type(data_type).nullable(nullable)
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
fn source(
records: Vec<Json>,
sample: usize,
) -> RecordSource<impl Iterator<Item = Result<Json, DataError>>> {
RecordSource::new(records.into_iter().map(Ok), sample).unwrap()
}
#[test]
fn the_sample_decides_columns_and_types() {
let source = source(
vec![
json!({"id": 1, "score": 2, "tags": ["a"], "meta": {"x": 1}}),
json!({"id": 2, "score": 2.5, "tags": [], "meta": {"y": "z"}, "extra": null}),
],
10,
);
assert_eq!(source.columns(), ["id", "score", "tags", "meta", "extra"]);
let schema = source.schema().unwrap();
let types: Vec<String> = schema
.fields()
.iter()
.map(|f| f.data_type().to_string())
.collect();
assert_eq!(
types,
["integer", "float", "list<string>", "struct", "unknown"]
);
assert!(!schema.fields()[0].is_nullable());
assert!(schema.fields()[4].is_nullable());
let meta: Vec<(&str, bool)> = schema.fields()[3]
.children()
.map(|f| (f.name(), f.is_nullable()))
.collect();
assert_eq!(meta, [("x", true), ("y", true)]);
}
#[test]
fn keys_after_the_sample_are_reported_not_invented() {
let mut source = source(
vec![json!({"a": 1}), json!({"a": "x", "b": true}), json!(7)],
1,
);
assert_eq!(source.columns(), ["a"]);
assert_eq!(source.next_row().unwrap().unwrap(), [Value::Int(1)]);
assert_eq!(source.next_row().unwrap().unwrap(), [Value::from("x")]);
assert_eq!(source.next_row().unwrap().unwrap(), [Value::Null]);
assert!(source.next_row().is_none());
assert_eq!(source.unknown_keys().collect::<Vec<_>>(), ["b", "value"]);
}
#[test]
fn cells_keep_scalars_and_write_nested_values_as_json() {
assert_eq!(cell(&json!(true)), Value::from("true"));
assert_eq!(cell(&json!(1.5)), Value::Float(1.5));
assert_eq!(cell(&json!(u64::MAX)), Value::Float(u64::MAX as f64));
assert_eq!(cell(&json!({"a": [1, 2]})), Value::from(r#"{"a":[1,2]}"#));
}
}