1use std::borrow::Cow;
61use std::collections::{BTreeSet, HashMap};
62use std::io::BufRead;
63
64use serde::{Deserialize, Serialize};
65use serde_json::Value as Json;
66
67use super::{
68 MissingBucket, MissingMap, Profile, ProfileOptions, Rules, describe, grouped, percent, plural,
69};
70use crate::infer::Inferrer;
71use crate::jsonl::Lines;
72use crate::record::cell;
73use crate::stats::{Stats, StatsOptions};
74use crate::{DataError, Rows, Value};
75
76pub const DEFAULT_DEPTH: usize = 4;
78pub const DEFAULT_MAX_PATHS: usize = 200;
80pub const MAX_MALFORMED_LINES: usize = 10;
82const MAX_DROPPED: usize = crate::record::MAX_UNKNOWN_KEYS;
84const NOTE_NAMES: usize = 10;
86
87fn is_zero(n: &u64) -> bool {
88 *n == 0
89}
90
91#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
94pub struct TypeCounts {
95 #[serde(default, skip_serializing_if = "is_zero")]
97 pub string: u64,
98 #[serde(default, skip_serializing_if = "is_zero")]
100 pub number: u64,
101 #[serde(default, skip_serializing_if = "is_zero")]
103 pub boolean: u64,
104 #[serde(default, skip_serializing_if = "is_zero")]
106 pub object: u64,
107 #[serde(default, skip_serializing_if = "is_zero")]
109 pub array: u64,
110 #[serde(default, skip_serializing_if = "is_zero")]
112 pub null: u64,
113}
114
115impl TypeCounts {
116 pub fn add(&mut self, value: &Json) {
118 let slot = match value {
119 Json::String(_) => &mut self.string,
120 Json::Number(_) => &mut self.number,
121 Json::Bool(_) => &mut self.boolean,
122 Json::Object(_) => &mut self.object,
123 Json::Array(_) => &mut self.array,
124 Json::Null => &mut self.null,
125 };
126 *slot += 1;
127 }
128
129 pub fn counts(&self) -> [(&'static str, &'static str, u64); 6] {
133 [
134 ("string", "str", self.string),
135 ("number", "num", self.number),
136 ("boolean", "bool", self.boolean),
137 ("object", "obj", self.object),
138 ("array", "arr", self.array),
139 ("null", "null", self.null),
140 ]
141 }
142
143 pub fn total(&self) -> u64 {
145 self.counts().iter().map(|(_, _, n)| n).sum()
146 }
147
148 pub fn kinds(&self) -> usize {
150 self.counts().iter().filter(|(_, _, n)| *n > 0).count()
151 }
152
153 pub fn mixed(&self) -> bool {
155 self.counts()
156 .iter()
157 .filter(|(name, _, n)| *n > 0 && *name != "null")
158 .count()
159 > 1
160 }
161
162 pub fn mix(&self) -> String {
166 let total = self.total();
167 let mut seen: Vec<(&str, u64)> = self
168 .counts()
169 .iter()
170 .filter(|(_, _, n)| *n > 0)
171 .map(|(_, short, n)| (*short, *n))
172 .collect();
173 if seen.len() == 1 {
174 return seen[0].0.to_string();
175 }
176 seen.sort_by_key(|(_, n)| std::cmp::Reverse(*n));
178 seen.iter()
179 .map(|(short, n)| format!("{short} {}", percent(*n as f64 / total as f64)))
180 .collect::<Vec<_>>()
181 .join(" · ")
182 }
183}
184
185#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
187pub struct PathSummary {
188 pub depth: usize,
190 pub max_paths: usize,
192 pub sampled: bool,
194 pub capped: bool,
196 #[serde(default, skip_serializing_if = "Vec::is_empty")]
198 pub dropped: Vec<String>,
199 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
201 pub more_dropped: bool,
202}
203
204pub fn path_key(key: &str) -> Cow<'_, str> {
218 let bare = !key.is_empty()
219 && key.trim() == key
220 && !key.contains(|c: char| matches!(c, '.' | '[' | ']' | '"' | '\\') || c.is_control());
221 if bare {
222 Cow::Borrowed(key)
223 } else {
224 let quoted = serde_json::to_string(key).unwrap_or_else(|_| format!("{key:?}"));
225 Cow::Owned(format!("[{quoted}]"))
226 }
227}
228
229#[derive(Clone, Copy)]
231enum Step<'a> {
232 Key(&'a str),
233 Item,
234}
235
236fn join(parent: Option<&str>, step: Step<'_>) -> String {
238 let parent = parent.unwrap_or("");
239 match step {
240 Step::Item => format!("{parent}[]"),
241 Step::Key(key) => match path_key(key) {
242 Cow::Borrowed(bare) if parent.is_empty() => bare.to_string(),
243 Cow::Borrowed(bare) => format!("{parent}.{bare}"),
244 Cow::Owned(quoted) => format!("{parent}{quoted}"),
245 },
246 }
247}
248
249#[derive(Clone, Debug)]
251struct Node {
252 name: String,
253 parent: Option<usize>,
254 keys: HashMap<String, usize>,
256 item: Option<usize>,
258 present: u64,
260 last_present: u64,
262 last_value: u64,
264 values: u64,
266 types: TypeCounts,
267 seen: u64,
269 reservoir: Vec<Value>,
270}
271
272impl Node {
273 fn new(name: String, parent: Option<usize>) -> Self {
274 Node {
275 name,
276 parent,
277 keys: HashMap::new(),
278 item: None,
279 present: 0,
280 last_present: 0,
281 last_value: 0,
282 values: 0,
283 types: TypeCounts::default(),
284 seen: 0,
285 reservoir: Vec::new(),
286 }
287 }
288}
289
290#[derive(Clone, Debug)]
308pub struct JsonProfiler {
309 options: ProfileOptions,
310 rows: u64,
311 nodes: Vec<Node>,
312 top: HashMap<String, usize>,
314 rng: u64,
315 bucket_rows: u64,
316 buckets: Vec<(u64, Vec<u64>)>,
318 dropped: BTreeSet<String>,
319 more_dropped: bool,
320 malformed: u64,
321 malformed_lines: Vec<usize>,
322 first_malformed: Option<(usize, String)>,
323}
324
325impl JsonProfiler {
326 pub fn new(options: ProfileOptions) -> Self {
329 JsonProfiler {
330 options,
331 rows: 0,
332 nodes: Vec::new(),
333 top: HashMap::new(),
334 rng: 0x9E37_79B9_7F4A_7C15,
335 bucket_rows: 1,
336 buckets: Vec::new(),
337 dropped: BTreeSet::new(),
338 more_dropped: false,
339 malformed: 0,
340 malformed_lines: Vec::new(),
341 first_malformed: None,
342 }
343 }
344
345 pub fn push(&mut self, record: &Json) {
347 self.rows += 1;
348 let depth = self.options.depth.max(1);
349 let mut stack: Vec<(Option<usize>, Step<'_>, &Json, usize)> = match record {
353 Json::Object(map) => map
354 .iter()
355 .rev()
356 .map(|(key, value)| (None, Step::Key(key.as_str()), value, 1))
357 .collect(),
358 other => vec![(None, Step::Key("value"), other, 1)],
359 };
360 while let Some((parent, step, value, level)) = stack.pop() {
361 let Some(id) = self.node(parent, step) else {
362 continue;
363 };
364 let expand = level < depth;
365 self.observe(id, value, expand);
366 if !expand {
367 continue;
368 }
369 match value {
370 Json::Object(map) => stack.extend(
371 map.iter()
372 .rev()
373 .map(|(key, value)| (Some(id), Step::Key(key.as_str()), value, level + 1)),
374 ),
375 Json::Array(items) => stack.extend(
376 items
377 .iter()
378 .rev()
379 .map(|item| (Some(id), Step::Item, item, level + 1)),
380 ),
381 _ => {}
382 }
383 }
384 self.end_record();
385 }
386
387 pub fn skip_malformed(&mut self, line: usize, message: impl Into<String>) {
390 self.malformed += 1;
391 if self.malformed_lines.len() < MAX_MALFORMED_LINES {
392 self.malformed_lines.push(line);
393 }
394 if self.first_malformed.is_none() {
395 self.first_malformed = Some((line, message.into()));
396 }
397 }
398
399 fn node(&mut self, parent: Option<usize>, step: Step<'_>) -> Option<usize> {
402 let found = match (parent, step) {
403 (None, Step::Key(key)) => self.top.get(key).copied(),
404 (Some(parent), Step::Key(key)) => self.nodes[parent].keys.get(key).copied(),
405 (Some(parent), Step::Item) => self.nodes[parent].item,
406 (None, Step::Item) => return None,
407 };
408 if found.is_some() {
409 return found;
410 }
411 let name = join(parent.map(|p| self.nodes[p].name.as_str()), step);
412 if self.nodes.len() >= self.options.max_paths.max(1) {
413 if !self.dropped.contains(&name) {
414 if self.dropped.len() < MAX_DROPPED {
415 self.dropped.insert(name);
416 } else {
417 self.more_dropped = true;
418 }
419 }
420 return None;
421 }
422 let id = self.nodes.len();
423 for (rows, missing) in &mut self.buckets {
425 missing.push(*rows);
426 }
427 self.nodes.push(Node::new(name, parent));
428 match (parent, step) {
429 (None, Step::Key(key)) => {
430 self.top.insert(key.to_string(), id);
431 }
432 (Some(parent), Step::Key(key)) => {
433 self.nodes[parent].keys.insert(key.to_string(), id);
434 }
435 (Some(parent), Step::Item) => self.nodes[parent].item = Some(id),
436 (None, Step::Item) => {}
437 }
438 Some(id)
439 }
440
441 fn observe(&mut self, id: usize, value: &Json, expand: bool) {
444 let record = self.rows;
445 let node = &mut self.nodes[id];
446 node.values += 1;
447 node.types.add(value);
448 if node.last_present != record {
449 node.last_present = record;
450 node.present += 1;
451 }
452 if value.is_null() {
453 return;
454 }
455 node.last_value = record;
456 if expand && matches!(value, Json::Object(_) | Json::Array(_)) {
457 return;
458 }
459 node.seen += 1;
462 let sample = self.options.sample.max(1);
463 let (seen, kept) = (node.seen, node.reservoir.len());
464 if kept < sample {
465 self.nodes[id].reservoir.push(cell(value));
466 return;
467 }
468 let slot = self.next_random() % seen;
469 if let Some(kept) = self.nodes[id].reservoir.get_mut(slot as usize) {
470 *kept = cell(value);
471 }
472 }
473
474 fn end_record(&mut self) {
477 let record = self.rows;
478 if self
479 .buckets
480 .last()
481 .is_none_or(|(rows, _)| *rows >= self.bucket_rows)
482 {
483 if self.buckets.len() >= self.options.buckets.max(1) {
484 self.merge_buckets();
485 }
486 if self
487 .buckets
488 .last()
489 .is_none_or(|(rows, _)| *rows >= self.bucket_rows)
490 {
491 self.buckets.push((0, vec![0; self.nodes.len()]));
492 }
493 }
494 let bucket = self.buckets.last_mut().expect("a bucket was just added");
495 bucket.0 += 1;
496 for (missing, node) in bucket.1.iter_mut().zip(&self.nodes) {
497 if node.last_value != record {
498 *missing += 1;
499 }
500 }
501 }
502
503 fn merge_buckets(&mut self) {
506 let width = self.nodes.len();
507 self.buckets = self
508 .buckets
509 .chunks(2)
510 .map(|pair| {
511 let mut rows = 0;
512 let mut missing = vec![0; width];
513 for (r, m) in pair {
514 rows += r;
515 for (total, n) in missing.iter_mut().zip(m) {
516 *total += n;
517 }
518 }
519 (rows, missing)
520 })
521 .collect();
522 self.bucket_rows *= 2;
523 }
524
525 fn next_random(&mut self) -> u64 {
527 self.rng = self.rng.wrapping_add(0x9E37_79B9_7F4A_7C15);
528 let mut z = self.rng;
529 z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
530 z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
531 z ^ (z >> 31)
532 }
533
534 fn tree_order(&self) -> Vec<usize> {
537 let mut children: Vec<Vec<usize>> = vec![Vec::new(); self.nodes.len()];
538 let mut stack: Vec<usize> = Vec::new();
539 for (id, node) in self.nodes.iter().enumerate() {
540 match node.parent {
541 Some(parent) => children[parent].push(id),
542 None => stack.push(id),
543 }
544 }
545 stack.reverse();
546 let mut order = Vec::with_capacity(self.nodes.len());
547 while let Some(id) = stack.pop() {
548 order.push(id);
549 stack.extend(children[id].iter().rev());
550 }
551 order
552 }
553
554 pub fn finish(mut self) -> Result<Profile, DataError> {
557 let order = match &self.options.columns {
558 None => self.tree_order(),
559 Some(wanted) => wanted
560 .iter()
561 .map(|name| {
562 self.nodes
563 .iter()
564 .position(|node| &node.name == name)
565 .ok_or_else(|| self.unknown_path(name))
566 })
567 .collect::<Result<_, _>>()?,
568 };
569 let rows = self.rows;
570 let sample = self.options.sample.max(1);
571 let sampled = self.nodes.iter().any(|n| n.seen > n.reservoir.len() as u64);
572 let stats_options = StatsOptions {
573 quantiles: vec![0.25, 0.75],
574 top: self.options.top,
575 };
576 let mut columns = Vec::with_capacity(order.len());
577 for &id in &order {
578 let node = &mut self.nodes[id];
579 let mut values = Rows::new([node.name.clone()]);
580 for value in std::mem::take(&mut node.reservoir) {
581 values.push([value]);
582 }
583 let inference = Inferrer::new()
585 .null_tokens(std::iter::empty::<String>())
586 .infer(&values);
587 inference.apply(&mut values);
588 let stats = Stats::with_options(&values, &stats_options);
589 let s = &stats.columns()[0];
590 let types = node.types;
591 let data_type = if s.count == 0 && types.object + types.array > 0 {
593 if types.object >= types.array {
594 "object"
595 } else {
596 "array"
597 }
598 } else {
599 inference.columns()[0].data_type().name()
600 };
601 let mut column = describe(&values, 0, s, data_type, self.options.bins);
602 column.nulls = types.null;
603 column.null_rate = if node.values == 0 {
604 0.0
605 } else {
606 types.null as f64 / node.values as f64
607 };
608 column.present = Some(node.present);
609 column.absent = Some(rows - node.present);
610 column.values = Some(node.values);
611 column.types = Some(types);
612 column.mixed = Some(types.mixed());
613 columns.push(column);
614 }
615 let mut first = 1;
616 let buckets = self
617 .buckets
618 .iter()
619 .map(|(records, missing)| {
620 let bucket = MissingBucket {
621 first,
622 rows: *records,
623 nulls: order.iter().map(|&id| missing[id]).collect(),
624 };
625 first += records;
626 bucket
627 })
628 .collect();
629 let mut notes = Vec::new();
630 let mixed: Vec<&str> = columns
631 .iter()
632 .filter(|c| c.mixed == Some(true))
633 .map(|c| c.name.as_str())
634 .collect();
635 if !mixed.is_empty() {
636 let count = mixed.len() as u64;
637 notes.push(format!(
638 "! marks {count} {} holding more than one JSON type besides null: {}",
639 plural(count, "path", "paths"),
640 listed(&mixed, count as usize)
641 ));
642 }
643 if !self.dropped.is_empty() {
644 let names: Vec<&str> = self.dropped.iter().map(String::as_str).collect();
645 let count = names.len() as u64;
646 let shown = if self.more_dropped {
647 format!("{}+", grouped(count))
648 } else {
649 grouped(count)
650 };
651 notes.push(format!(
652 "max_paths ({}) was reached: {shown} more {} not profiled: {}",
653 grouped(self.options.max_paths.max(1) as u64),
654 plural(count, "path is", "paths are"),
655 listed(
656 &names,
657 if self.more_dropped {
658 usize::MAX
659 } else {
660 names.len()
661 }
662 )
663 ));
664 }
665 if self.malformed > 0 {
666 let mut lines: Vec<String> =
667 self.malformed_lines.iter().map(usize::to_string).collect();
668 if self.malformed as usize > lines.len() {
669 lines.push("…".into());
670 }
671 let mut note = format!(
672 "{} {} not JSON and {} skipped: {} {}",
673 grouped(self.malformed),
674 plural(self.malformed, "line is", "lines are"),
675 plural(self.malformed, "was", "were"),
676 plural(self.malformed, "line", "lines"),
677 lines.join(", "),
678 );
679 if let Some((line, message)) = &self.first_malformed {
680 let why = message.strip_prefix("not JSON: ").unwrap_or(message);
683 let why = why.split(" at line ").next().unwrap_or(why);
684 if self.malformed == 1 {
685 note.push_str(&format!(" ({why})"));
686 } else {
687 note.push_str(&format!(" (line {line}: {why})"));
688 }
689 }
690 notes.push(note);
691 }
692 Ok(Profile {
693 name: None,
694 rows,
695 sample,
696 columns,
697 missing: MissingMap {
698 bucket_rows: self.bucket_rows,
699 buckets,
700 },
701 notes,
702 paths: Some(PathSummary {
703 depth: self.options.depth.max(1),
704 max_paths: self.options.max_paths.max(1),
705 sampled,
706 capped: !self.dropped.is_empty(),
707 dropped: self.dropped.into_iter().collect(),
708 more_dropped: self.more_dropped,
709 }),
710 malformed: self.malformed,
711 malformed_lines: self.malformed_lines,
712 rules: Rules::default(),
713 })
714 }
715
716 fn unknown_path(&self, name: &str) -> DataError {
718 let names: Vec<&str> = self.nodes.iter().map(|n| n.name.as_str()).collect();
719 let mut message = format!(
720 "no path {:?}; the paths are {}",
721 rich_ext::sanitize::sanitize_single_line(name),
722 listed(&names, names.len())
723 );
724 if !self.dropped.is_empty() {
725 message.push_str(&format!(
726 " (max_paths {} was reached; a path seen later is not profiled)",
727 self.options.max_paths.max(1)
728 ));
729 }
730 DataError::new(message)
731 }
732}
733
734fn listed(names: &[&str], total: usize) -> String {
736 let mut shown: Vec<String> = names
737 .iter()
738 .take(NOTE_NAMES)
739 .map(|n| rich_ext::sanitize::sanitize_single_line(n))
740 .collect();
741 if total > shown.len() {
742 shown.push("…".into());
743 }
744 shown.join(", ")
745}
746
747impl Profile {
748 pub fn from_json_lines(
754 reader: impl BufRead,
755 options: ProfileOptions,
756 ) -> Result<Self, DataError> {
757 let mut profiler = JsonProfiler::new(options);
758 let mut lines = Lines::new(reader);
759 while let Some(record) = lines.next() {
760 match record {
761 Ok(record) => profiler.push(&record),
762 Err(error) if !lines.stopped() => {
763 profiler.skip_malformed(error.line().unwrap_or(0), error.message())
764 }
765 Err(error) => return Err(error),
766 }
767 }
768 profiler.finish()
769 }
770}
771
772#[cfg(test)]
773mod tests {
774 use super::*;
775 use rich::Console;
776 use serde_json::json;
777
778 fn nested(text: &str, options: ProfileOptions) -> Profile {
779 Profile::from_json_lines(text.as_bytes(), options).unwrap()
780 }
781
782 fn names(profile: &Profile) -> Vec<&str> {
783 profile.columns().iter().map(|c| c.name.as_str()).collect()
784 }
785
786 #[test]
787 fn paths_follow_objects_and_arrays_in_tree_order() {
788 let p = nested(
789 "{\"id\": 1, \"items\": [{\"sku\": \"a\", \"qty\": 2}]}\n\
790 {\"id\": 2, \"user\": {\"name\": \"x\"}, \"items\": [{\"sku\": \"b\", \"note\": \"n\"}]}\n",
791 ProfileOptions::default(),
792 );
793 assert_eq!(
796 names(&p),
797 [
798 "id",
799 "items",
800 "items[]",
801 "items[].sku",
802 "items[].qty",
803 "items[].note",
804 "user",
805 "user.name"
806 ]
807 );
808 assert_eq!(p.heading(), "2 records, 8 paths");
809 let sku = p.column("items[].sku").unwrap();
810 assert_eq!(
811 (sku.present, sku.absent, sku.values),
812 (Some(2), Some(0), Some(2))
813 );
814 let items = p.column("items").unwrap();
815 assert_eq!(items.data_type, "array");
816 assert_eq!(p.column("items[]").unwrap().data_type, "object");
817 }
818
819 #[test]
820 fn array_items_count_every_value_and_presence_counts_records() {
821 let p = nested(
822 "{\"tags\": [\"a\", \"b\", \"a\"]}\n{\"tags\": []}\n{}\n",
823 ProfileOptions::default(),
824 );
825 let tags = p.column("tags[]").unwrap();
826 assert_eq!(
827 (tags.values, tags.present, tags.absent),
828 (Some(3), Some(1), Some(2))
829 );
830 assert_eq!((tags.count, tags.distinct), (3, 2));
831 let tags = p.column("tags").unwrap();
832 assert_eq!((tags.present, tags.absent), (Some(2), Some(1)));
833 }
834
835 #[test]
836 fn absent_keys_are_counted_apart_from_null() {
837 let p = nested(
838 "{\"a\": 1, \"b\": null}\n{\"a\": null}\n{\"a\": 2, \"b\": \"x\"}\n{}\n",
839 ProfileOptions::default(),
840 );
841 let a = p.column("a").unwrap();
842 assert_eq!((a.present, a.absent, a.nulls), (Some(3), Some(1), 1));
843 assert_eq!(a.null_rate, 1.0 / 3.0);
844 let b = p.column("b").unwrap();
845 assert_eq!((b.present, b.absent, b.nulls), (Some(2), Some(2), 1));
846 assert_eq!(b.types.unwrap().mix(), "str 50% · null 50%");
847 assert_eq!(b.mixed, Some(false));
848 let map = p.missing();
850 let b_missing: Vec<u64> = map.buckets.iter().map(|bucket| bucket.nulls[1]).collect();
851 assert_eq!(b_missing, [1, 1, 0, 1]);
852 let json = p.to_json();
853 assert_eq!(json["columns"][1]["absent"], 2);
854 assert_eq!(json["columns"][1]["types"], json!({"string": 1, "null": 1}));
855 }
856
857 #[test]
858 fn type_mixes_count_each_json_type() {
859 let p = nested(
860 "{\"v\": 1}\n{\"v\": \"1\"}\n{\"v\": true}\n{\"v\": [1]}\n{\"v\": {\"k\": 1}}\n\
861 {\"v\": null}\n{\"v\": 2.5}\n",
862 ProfileOptions::default(),
863 );
864 let v = p.column("v").unwrap();
865 let types = v.types.unwrap();
866 assert_eq!(
867 (
868 types.string,
869 types.number,
870 types.boolean,
871 types.object,
872 types.array,
873 types.null
874 ),
875 (1, 2, 1, 1, 1, 1)
876 );
877 assert_eq!(types.kinds(), 6);
878 assert!(types.mixed());
879 assert_eq!(
880 types.mix(),
881 "num 29% · str 14% · bool 14% · obj 14% · arr 14% · null 14%"
882 );
883 let only = TypeCounts {
885 number: 3,
886 ..Default::default()
887 };
888 assert_eq!((only.mix(), only.mixed()), ("num".to_string(), false));
889 assert_eq!(TypeCounts::default().mix(), "");
890 let table = Console::builder()
892 .width(160)
893 .color_system(None)
894 .build()
895 .render_export(&p.to_table());
896 assert!(table.contains("! num 29%"), "{table}");
897 assert!(p.notes()[0].starts_with("! marks 1 path holding more than one JSON type"));
898 }
899
900 #[test]
901 fn depth_bounds_the_walk_and_unwalked_values_are_json_text() {
902 let text = "{\"a\": {\"b\": {\"c\": {\"d\": {\"e\": 1}}}}}\n";
903 let p = nested(text, ProfileOptions::default());
904 assert_eq!(names(&p), ["a", "a.b", "a.b.c", "a.b.c.d"]);
905 let deepest = p.column("a.b.c.d").unwrap();
906 assert_eq!(deepest.max.as_deref(), Some("{\"e\":1}"));
907 let shallow = nested(
908 text,
909 ProfileOptions {
910 depth: 1,
911 ..Default::default()
912 },
913 );
914 assert_eq!(names(&shallow), ["a"]);
915 assert_eq!(shallow.paths().unwrap().depth, 1);
916 let mut deep = json!(1);
919 for _ in 0..100_000 {
920 deep = Json::Array(vec![deep]);
921 }
922 let mut profiler = JsonProfiler::new(ProfileOptions {
923 depth: usize::MAX,
924 max_paths: 10,
925 ..Default::default()
926 });
927 profiler.push(&deep);
928 let p = profiler.finish().unwrap();
929 assert_eq!(p.columns().len(), 10);
930 assert!(p.paths().unwrap().capped);
931 let mut value = deep;
933 while let Json::Array(mut items) = value {
934 value = items.pop().unwrap_or(Json::Null);
935 }
936 }
937
938 #[test]
939 fn the_path_cap_names_what_it_dropped_and_stays_bounded() {
940 let text: String = (0..1_000)
941 .map(|i| format!("{{\"id\": {i}, \"k{i:04}\": 1}}\n"))
942 .collect();
943 let p = nested(
944 &text,
945 ProfileOptions {
946 max_paths: 5,
947 ..Default::default()
948 },
949 );
950 assert_eq!(names(&p), ["id", "k0000", "k0001", "k0002", "k0003"]);
951 let paths = p.paths().unwrap();
952 assert!(paths.capped && paths.more_dropped);
953 assert_eq!(paths.dropped.len(), 100);
954 assert_eq!(paths.dropped[0], "k0004");
955 let note = p
956 .notes()
957 .iter()
958 .find(|n| n.starts_with("max_paths"))
959 .unwrap();
960 assert!(
961 note.starts_with(
962 "max_paths (5) was reached: 100+ more paths are not profiled: k0004, "
963 ),
964 "{note}"
965 );
966 assert!(note.ends_with(", …"), "{note}");
967 let table = Console::builder()
968 .width(160)
969 .color_system(None)
970 .build()
971 .render_export(&p.to_table());
972 assert!(
973 table.contains("100+ more paths not profiled (max_paths 5)"),
974 "{table}"
975 );
976 let json = p.to_json();
977 assert_eq!(json["paths"]["capped"], true);
978 assert_eq!(json["paths"]["max_paths"], 5);
979 let error = Profile::from_json_lines(
981 text.as_bytes(),
982 ProfileOptions {
983 max_paths: 5,
984 columns: Some(vec!["k0500".into()]),
985 ..Default::default()
986 },
987 )
988 .unwrap_err();
989 assert!(
990 error
991 .message()
992 .starts_with("no path \"k0500\"; the paths are id, k0000")
993 );
994 assert!(
995 error.message().contains("max_paths 5 was reached"),
996 "{error}"
997 );
998 }
999
1000 #[test]
1001 fn keys_that_look_like_paths_are_quoted() {
1002 let p = nested(
1003 "{\"a.b\": {\"c\": 1}, \"x[0]\": [true], \"\": 1, \" pad\": 2, \"q\\\"\": {\"k8s.io/n\": 3}}\n",
1004 ProfileOptions::default(),
1005 );
1006 assert_eq!(
1007 names(&p),
1008 [
1009 "[\"a.b\"]",
1010 "[\"a.b\"].c",
1011 "[\"x[0]\"]",
1012 "[\"x[0]\"][]",
1013 "[\"\"]",
1014 "[\" pad\"]",
1015 "[\"q\\\"\"]",
1016 "[\"q\\\"\"][\"k8s.io/n\"]"
1017 ]
1018 );
1019 let p = nested(
1021 "{\"a.b\": {\"c\": 1}}\n",
1022 ProfileOptions {
1023 columns: Some(vec!["[\"a.b\"].c".into()]),
1024 ..Default::default()
1025 },
1026 );
1027 assert_eq!(names(&p), ["[\"a.b\"].c"]);
1028 }
1029
1030 #[test]
1031 fn malformed_lines_are_counted_and_never_fatal() {
1032 let mut text = String::from("{\"a\": 1}\n{\"a\": \nnot json\n\u{0}\u{1}\n[1, 2\n");
1033 for i in 0..20 {
1034 text.push_str(&format!("{{oops {i}\n"));
1035 }
1036 text.push_str("{\"a\": 2}\n\"just a string\"\n7\n");
1037 let mut bytes = text.into_bytes();
1039 bytes.extend_from_slice(b"{\"a\": \"\xff\"}\n");
1040 let p = Profile::from_json_lines(&bytes[..], ProfileOptions::default()).unwrap();
1041 assert_eq!(p.rows(), 4);
1042 assert_eq!(p.malformed(), 25);
1043 assert_eq!(p.malformed_lines(), [2, 3, 4, 5, 6, 7, 8, 9, 10, 11]);
1044 let note = p.notes().last().unwrap();
1045 assert!(
1046 note.starts_with("25 lines are not JSON and were skipped: lines 2, 3,"),
1047 "{note}"
1048 );
1049 assert!(
1050 note.ends_with(", 11, … (line 2: EOF while parsing a value)"),
1051 "{note}"
1052 );
1053 assert_eq!(names(&p), ["a", "value"]);
1055 assert_eq!(
1056 p.column("value").unwrap().types.unwrap().mix(),
1057 "str 50% · num 50%"
1058 );
1059 let json = p.to_json();
1060 assert_eq!(json["malformed"], 25);
1061 assert_eq!(json["malformed_lines"][0], 2);
1062 let p = nested("nope\n{\n", ProfileOptions::default());
1064 assert_eq!((p.rows(), p.columns().len(), p.malformed()), (0, 0, 2));
1065 let console = Console::builder().width(40).color_system(None).build();
1066 console.render_export(&p);
1067 struct Broken(bool);
1069 impl std::io::Read for Broken {
1070 fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
1071 if std::mem::replace(&mut self.0, true) {
1072 return Err(std::io::Error::other("disk on fire"));
1073 }
1074 let line = b"{\"a\": 1}\n";
1075 buf[..line.len()].copy_from_slice(line);
1076 Ok(line.len())
1077 }
1078 }
1079 let error = Profile::from_json_lines(
1080 std::io::BufReader::new(Broken(false)),
1081 ProfileOptions::default(),
1082 )
1083 .unwrap_err();
1084 assert_eq!((error.line(), error.message()), (Some(2), "disk on fire"));
1085 }
1086
1087 #[test]
1088 fn a_long_stream_is_sampled_per_path_with_exact_counts() {
1089 let options = ProfileOptions {
1090 sample: 100,
1091 buckets: 8,
1092 ..Default::default()
1093 };
1094 let mut profiler = JsonProfiler::new(options);
1095 for i in 0..20_000i64 {
1096 let record = if i % 4 == 0 {
1097 json!({"n": i, "xs": [i, i + 1]})
1098 } else {
1099 json!({"n": i, "s": null})
1100 };
1101 profiler.push(&record);
1102 }
1103 let p = profiler.finish().unwrap();
1104 assert!(p.sampled());
1105 assert_eq!(p.sample_size(), 100);
1106 assert_eq!(
1107 p.heading(),
1108 "20,000 records, 4 paths, sampled to 100 values a path"
1109 );
1110 let n = p.column("n").unwrap();
1111 assert_eq!((n.values, n.count), (Some(20_000), 100));
1112 let xs = p.column("xs[]").unwrap();
1113 assert_eq!(
1114 (xs.values, xs.present, xs.absent),
1115 (Some(10_000), Some(5_000), Some(15_000))
1116 );
1117 let s = p.column("s").unwrap();
1118 assert_eq!((s.nulls, s.null_rate, s.absent), (15_000, 1.0, Some(5_000)));
1119 assert!(p.missing().buckets.len() <= 8);
1121 let records: u64 = p.missing().buckets.iter().map(|b| b.rows).sum();
1122 assert_eq!(records, 20_000);
1123 let s_missing: u64 = p.missing().buckets.iter().map(|b| b.nulls[3]).sum();
1124 assert_eq!(s_missing, 20_000);
1125 let again = {
1127 let mut profiler = JsonProfiler::new(ProfileOptions {
1128 sample: 100,
1129 buckets: 8,
1130 ..Default::default()
1131 });
1132 for i in 0..20_000i64 {
1133 profiler.push(&if i % 4 == 0 {
1134 json!({"n": i, "xs": [i, i + 1]})
1135 } else {
1136 json!({"n": i, "s": null})
1137 });
1138 }
1139 profiler.finish().unwrap()
1140 };
1141 assert_eq!(again, p);
1142 }
1143
1144 #[test]
1145 fn the_nested_model_round_trips_and_renders_narrow() {
1146 let p = nested(
1147 "{\"a\": {\"b\": [1, \"x\"]}, \"c\": null}\n{\"a\": 3}\nnope\n",
1148 ProfileOptions::default(),
1149 )
1150 .with_name("n.jsonl");
1151 let json = serde_json::to_string(&p).unwrap();
1152 let back: Profile = serde_json::from_str(&json).unwrap();
1153 assert_eq!(back, p);
1154 for width in [40, 50, 80] {
1155 let console = Console::builder().width(width).color_system(None).build();
1156 let segments = console.render(&p, None);
1157 for line in rich::Segment::split_lines(&segments) {
1158 let cells: usize = line.iter().map(rich::Segment::cell_length).sum();
1159 assert!(cells <= width, "{width}: {line:?}");
1160 }
1161 }
1162 }
1163}