pub fn emit_shaped_line(
out: &mut impl Write,
value: Value,
shape: &OutputShape,
) -> std::io::Result<bool> {
let Some(mut shaped) = shape_value(value, shape) else {
return Ok(false);
};
scrub_envelope_keys(&mut shaped);
serde_json::to_writer(&mut *out, &shaped)?;
out.write_all(b"\n")?;
out.flush()?;
Ok(true)
}
fn stamp_suppressed(value: &mut Value, dropped: u64) {
if dropped == 0 {
return;
}
if let Some(obj) = value.as_object_mut() {
obj.entry("records_suppressed")
.or_insert(Value::from(dropped));
}
}
fn emit_terminal_line(
out: &mut impl Write,
value: Value,
shape: &OutputShape,
outcome: &mut FlushOutcome,
) -> std::io::Result<()> {
let original = value.clone();
let mut value = keep_terminal_keys(project_select(value, shape), &original);
scrub_envelope_keys(&mut value);
serde_json::to_writer(&mut *out, &value)?;
out.write_all(b"\n")?;
out.flush()?;
outcome.emitted += 1;
Ok(())
}
fn group_blocks(events: Vec<PositionedValue>) -> Vec<Vec<PositionedValue>> {
let mut blocks: Vec<Vec<PositionedValue>> = Vec::new();
let mut open: Option<(String, usize)> = None;
for event in events {
let key = event
.value
.as_object()
.and_then(|o| o.get("path"))
.and_then(|p| p.as_str())
.map(str::to_string);
match (&key, &open) {
(Some(path), Some((current, index))) if path == current => {
blocks[*index].push(event);
}
_ => {
if let Some(path) = key {
open = Some((path, blocks.len()));
} else {
open = None;
}
blocks.push(vec![event]);
}
}
}
blocks
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct FlushOutcome {
pub emitted: u64,
pub dropped: u64,
pub aggregated: u64,
pub diff: Option<StreamDiff>,
pub limit_hit: bool,
}
pub fn flush_positioned(
out: &mut impl Write,
events: Vec<PositionedValue>,
shape: &OutputShape,
join_tables: &[JoinTable],
) -> anyhow::Result<FlushOutcome> {
use std::collections::{HashMap, HashSet};
let mut outcome = FlushOutcome::default();
let (terminals, mut events): (Vec<PositionedValue>, Vec<PositionedValue>) =
events.into_iter().partition(|event| event.terminal);
if shape.sort.is_some() {
events.sort_by(|a, b| compare_positioned(a, b, shape));
}
for event in events.iter_mut() {
for table in join_tables {
apply_join(&mut event.value, table);
}
}
if !shape.dedupe_keys.is_empty() {
let mut seen: HashSet<String> = HashSet::new();
let before = events.len();
events.retain(|event| {
event.framing || seen.insert(dedupe_identity(&event.value, &shape.dedupe_keys))
});
outcome.dropped += (before - events.len()) as u64;
}
if let Some(first) = shape.aggregates.first() {
let group_key = first.group.clone();
let mut index_of: HashMap<String, usize> = HashMap::new();
let mut grouped: Vec<(Value, Vec<PositionedValue>)> = Vec::new();
for event in events.drain(..) {
let Some(key) = event
.value
.as_object()
.and_then(|o| o.get(&group_key))
.cloned()
else {
outcome.dropped += 1;
continue;
};
let identity = key.to_string();
match index_of.get(&identity) {
Some(index) => grouped[*index].1.push(event),
None => {
index_of.insert(identity, grouped.len());
grouped.push((key, vec![event]));
}
}
}
outcome.aggregated = grouped
.iter()
.map(|(_, members)| members.len() as u64)
.sum();
events = grouped
.into_iter()
.enumerate()
.map(|(index, (key, members))| PositionedValue {
value: aggregate_group(&group_key, key, &members, &shape.aggregates),
line_index: index as u64,
terminal: false,
framing: false,
})
.collect();
}
if let Some(path) = &shape.diff_against {
let baseline = load_baseline(path)?;
let diff = diff_streams(&baseline, &events);
let envelope = serde_json::to_value(StreamDiffOutput::from_diff(&diff))?;
if emit_shaped_line(out, envelope, shape)? {
outcome.emitted += 1;
}
outcome.diff = Some(diff);
for mut terminal in terminals {
stamp_suppressed(&mut terminal.value, outcome.dropped);
emit_terminal_line(out, terminal.value, shape, &mut outcome)?;
}
return Ok(outcome);
}
let mut counted: u64 = 0;
for block in group_blocks(events) {
let room = shape
.limit
.map_or(u64::MAX, |max| max.saturating_sub(counted));
if shape.limit.is_some() && room == 0 {
if block.iter().any(|event| !event.framing) {
outcome.limit_hit = true;
}
outcome.dropped += block.len() as u64;
continue;
}
for event in block {
if emit_shaped_line(out, event.value, shape)? {
outcome.emitted += 1;
if !event.framing {
counted = counted.saturating_add(1);
}
} else {
outcome.dropped += 1;
}
}
}
for mut terminal in terminals {
stamp_suppressed(&mut terminal.value, outcome.dropped);
emit_terminal_line(out, terminal.value, shape, &mut outcome)?;
}
Ok(outcome)
}