use std::io::Write;
use crate::error::{CliError, Result};
use crate::models::{Column, Row};
use crate::output::formatter::Formatter;
pub const DEFAULT_BUFFER_LIMIT: usize = 10 * 1024 * 1024;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum WriteStatus {
Continue,
LimitReached,
}
pub struct StreamingWriter<W> {
writer: W,
formatter: Box<dyn Formatter>,
columns: Vec<Column>,
limit: Option<usize>,
buffer_limit: usize,
buffer: Vec<Row>,
buffer_bytes: usize,
written_count: usize,
output_started: bool,
quiet: bool,
}
impl<W: Write> StreamingWriter<W> {
pub fn new(
writer: W,
formatter: Box<dyn Formatter>,
columns: Vec<Column>,
limit: Option<usize>,
) -> Self {
Self {
writer,
formatter,
columns,
limit,
buffer_limit: DEFAULT_BUFFER_LIMIT,
buffer: Vec::new(),
buffer_bytes: 0,
written_count: 0,
output_started: false,
quiet: false,
}
}
#[allow(dead_code)]
pub fn with_buffer_limit(mut self, buffer_limit: usize) -> Self {
self.buffer_limit = buffer_limit;
self
}
pub fn with_quiet(mut self, quiet: bool) -> Self {
self.quiet = quiet;
self
}
pub fn is_quiet(&self) -> bool {
self.quiet
}
pub fn prepare(&mut self, row_count_hint: Option<usize>) -> Result<()> {
let _ = row_count_hint;
if self.formatter.supports_streaming() {
self.formatter
.write_header(&mut self.writer, &self.columns)?;
self.output_started = true;
}
Ok(())
}
pub fn write_row(&mut self, row: Row) -> Result<WriteStatus> {
if let Some(limit) = self.limit {
if self.written_count >= limit {
return Ok(WriteStatus::LimitReached);
}
}
if self.formatter.supports_streaming() {
self.formatter.write_row(&mut self.writer, &row)?;
self.written_count += 1;
} else {
let row_bytes = estimate_row_bytes(&row);
self.buffer_bytes = self.buffer_bytes.saturating_add(row_bytes);
if self.buffer_bytes > self.buffer_limit {
return Err(CliError::InvalidArgument(
"Buffer limit exceeded (~10MB). \
Use --output json|csv|tsv or --limit to reduce results."
.into(),
));
}
self.buffer.push(row);
self.written_count += 1;
}
Ok(WriteStatus::Continue)
}
pub fn finish(&mut self) -> Result<()> {
if !self.output_started {
self.formatter
.write_header(&mut self.writer, &self.columns)?;
self.output_started = true;
for row in self.buffer.drain(..) {
self.formatter.write_row(&mut self.writer, &row)?;
}
}
self.formatter.write_footer(&mut self.writer)?;
Ok(())
}
#[allow(dead_code)]
pub fn written_count(&self) -> usize {
self.written_count
}
#[allow(dead_code)]
pub fn output_started(&self) -> bool {
self.output_started
}
#[allow(dead_code)]
pub fn buffered_bytes(&self) -> usize {
self.buffer_bytes
}
}
fn estimate_row_bytes(row: &Row) -> usize {
row.columns
.iter()
.map(estimate_value_bytes)
.sum::<usize>()
.saturating_add(row.columns.len() * 8)
}
fn estimate_value_bytes(value: &crate::models::Value) -> usize {
match value {
crate::models::Value::Null => 4,
crate::models::Value::Bool(_) => 1,
crate::models::Value::Int(_) => 8,
crate::models::Value::Float(_) => 8,
crate::models::Value::Text(text) => text.len(),
crate::models::Value::Bytes(bytes) => bytes.len(),
crate::models::Value::Vector(values) => values.len() * 4,
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::error::CliError;
use crate::models::{DataType, Value};
use crate::output::csv::CsvFormatter;
use crate::output::json::JsonFormatter;
use crate::output::jsonl::JsonlFormatter;
use crate::output::table::TableFormatter;
fn test_columns() -> Vec<Column> {
vec![
Column::new("id", DataType::Int),
Column::new("name", DataType::Text),
]
}
fn test_row(id: i64, name: &str) -> Row {
Row::new(vec![Value::Int(id), Value::Text(name.to_string())])
}
#[test]
fn test_streaming_format_immediate_output() {
let mut output = Vec::new();
let formatter = Box::new(JsonlFormatter::new());
let columns = test_columns();
let mut writer = StreamingWriter::new(&mut output, formatter, columns, None);
writer.prepare(None).unwrap();
assert!(writer.output_started());
let status = writer.write_row(test_row(1, "Alice")).unwrap();
assert_eq!(status, WriteStatus::Continue);
assert_eq!(writer.written_count(), 1);
writer.finish().unwrap();
let result = String::from_utf8(output).unwrap();
assert!(result.contains("\"id\":1"));
assert!(result.contains("\"name\":\"Alice\""));
}
#[test]
fn test_non_streaming_format_buffered_output() {
let mut output = Vec::new();
let formatter = Box::new(TableFormatter::new());
let columns = test_columns();
let mut writer = StreamingWriter::new(&mut output, formatter, columns, None);
writer.prepare(None).unwrap();
assert!(!writer.output_started());
let status = writer.write_row(test_row(1, "Alice")).unwrap();
assert_eq!(status, WriteStatus::Continue);
assert!(!writer.output_started());
writer.finish().unwrap();
assert!(writer.output_started());
let result = String::from_utf8(output).unwrap();
assert!(result.contains("id"));
assert!(result.contains("Alice"));
}
#[test]
fn test_limit_enforcement() {
let mut output = Vec::new();
let formatter = Box::new(CsvFormatter::new());
let columns = test_columns();
let mut writer = StreamingWriter::new(&mut output, formatter, columns, Some(2));
writer.prepare(None).unwrap();
assert_eq!(
writer.write_row(test_row(1, "Alice")).unwrap(),
WriteStatus::Continue
);
assert_eq!(
writer.write_row(test_row(2, "Bob")).unwrap(),
WriteStatus::Continue
);
assert_eq!(
writer.write_row(test_row(3, "Charlie")).unwrap(),
WriteStatus::LimitReached
);
assert_eq!(writer.written_count(), 2);
writer.finish().unwrap();
}
#[test]
fn test_buffer_overflow_errors() {
let mut output = Vec::new();
let formatter = Box::new(TableFormatter::new());
let columns = test_columns();
let mut writer =
StreamingWriter::new(&mut output, formatter, columns, None).with_buffer_limit(40);
writer.prepare(None).unwrap();
assert!(!writer.output_started());
assert_eq!(
writer.write_row(test_row(1, "Alice")).unwrap(),
WriteStatus::Continue
);
let err = writer.write_row(test_row(2, "Bob")).unwrap_err();
assert!(matches!(err, CliError::InvalidArgument(_)));
}
#[test]
fn test_empty_output() {
let mut output = Vec::new();
let formatter = Box::new(JsonFormatter::new());
let columns = test_columns();
let mut writer = StreamingWriter::new(&mut output, formatter, columns, None);
writer.prepare(None).unwrap();
writer.finish().unwrap();
let result = String::from_utf8(output).unwrap();
assert!(result.contains('['));
assert!(result.contains(']'));
}
#[test]
fn test_csv_streaming() {
let mut output = Vec::new();
let formatter = Box::new(CsvFormatter::new());
let columns = test_columns();
let mut writer = StreamingWriter::new(&mut output, formatter, columns, None);
writer.prepare(None).unwrap();
assert!(writer.output_started());
writer.write_row(test_row(1, "Alice")).unwrap();
writer.write_row(test_row(2, "Bob")).unwrap();
writer.finish().unwrap();
let result = String::from_utf8(output).unwrap();
assert_eq!(result, "id,name\n1,Alice\n2,Bob\n");
}
#[test]
fn test_written_count() {
let mut output = Vec::new();
let formatter = Box::new(CsvFormatter::new());
let columns = test_columns();
let mut writer = StreamingWriter::new(&mut output, formatter, columns, None);
writer.prepare(None).unwrap();
assert_eq!(writer.written_count(), 0);
writer.write_row(test_row(1, "Alice")).unwrap();
assert_eq!(writer.written_count(), 1);
writer.write_row(test_row(2, "Bob")).unwrap();
assert_eq!(writer.written_count(), 2);
writer.finish().unwrap();
}
#[test]
fn test_table_with_small_data() {
let mut output = Vec::new();
let formatter = Box::new(TableFormatter::new());
let columns = test_columns();
let mut writer = StreamingWriter::new(&mut output, formatter, columns, None);
writer.prepare(None).unwrap();
assert!(!writer.output_started());
writer.write_row(test_row(1, "Alice")).unwrap();
writer.write_row(test_row(2, "Bob")).unwrap();
writer.finish().unwrap();
let result = String::from_utf8(output).unwrap();
assert!(result.contains("id"));
assert!(result.contains("name"));
assert!(result.contains("Alice"));
assert!(result.contains("Bob"));
}
#[test]
fn test_streaming_large_row_count_does_not_buffer() {
let formatter = Box::new(JsonlFormatter::new());
let columns = test_columns();
let mut writer = StreamingWriter::new(std::io::sink(), formatter, columns, None);
writer.prepare(None).unwrap();
for i in 0..12_000 {
writer.write_row(test_row(i, "row")).unwrap();
}
assert_eq!(writer.written_count(), 12_000);
assert!(writer.buffer.is_empty());
writer.finish().unwrap();
}
}