1use serde_json::Value;
27
28#[derive(Debug)]
34pub struct ObjectAccumulator {
35 buf: Vec<u8>,
36 rows: usize,
37 parted_bytes: usize,
40 max_rows: Option<usize>,
41 max_bytes: Option<usize>,
42 part_bytes: Option<usize>,
43}
44
45#[derive(Debug, PartialEq, Eq)]
47pub struct CompletedObject {
48 pub body: Vec<u8>,
50 pub rows: usize,
53}
54
55#[derive(Debug, PartialEq, Eq)]
61pub enum Emit {
62 Nothing,
64 Part(Vec<u8>),
68 Object(CompletedObject),
70}
71
72impl ObjectAccumulator {
73 pub fn new(max_rows: Option<usize>, max_bytes: Option<usize>) -> Self {
77 Self {
78 buf: Vec::new(),
79 rows: 0,
80 parted_bytes: 0,
81 max_rows: max_rows.filter(|n| *n > 0),
82 max_bytes: max_bytes.filter(|n| *n > 0),
83 part_bytes: None,
84 }
85 }
86
87 pub fn with_part_size(mut self, bytes: usize) -> Self {
94 self.part_bytes = (bytes > 0).then_some(bytes);
95 self
96 }
97
98 pub fn parted_bytes(&self) -> usize {
100 self.parted_bytes
101 }
102
103 pub fn has_parts(&self) -> bool {
107 self.parted_bytes > 0
108 }
109
110 pub fn rows(&self) -> usize {
112 self.rows
113 }
114
115 pub fn len(&self) -> usize {
117 self.buf.len()
118 }
119
120 pub fn is_empty(&self) -> bool {
122 self.rows == 0
123 }
124
125 pub fn push_encoded(&mut self, encoded: &[u8]) -> Emit {
133 self.buf.extend_from_slice(encoded);
134 self.rows += 1;
135 if self.max_rows.is_some_and(|m| self.rows >= m)
138 || self
139 .max_bytes
140 .is_some_and(|m| self.parted_bytes + self.buf.len() >= m)
141 {
142 return Emit::Object(self.take());
143 }
144 if self.part_bytes.is_some_and(|m| self.buf.len() >= m) {
145 let part = std::mem::take(&mut self.buf);
146 self.parted_bytes += part.len();
147 return Emit::Part(part);
148 }
149 Emit::Nothing
150 }
151
152 pub fn push_record(&mut self, record: &Value) -> Result<Emit, crate::FaucetError> {
154 let mut line = serde_json::to_vec(record)
155 .map_err(|e| crate::FaucetError::Sink(format!("JSON serialization failed: {e}")))?;
156 line.push(b'\n');
157 Ok(self.push_encoded(&line))
158 }
159
160 pub fn finish(&mut self) -> Option<CompletedObject> {
168 (!self.is_empty()).then(|| self.take())
169 }
170
171 fn take(&mut self) -> CompletedObject {
172 self.parted_bytes = 0;
173 CompletedObject {
174 body: std::mem::take(&mut self.buf),
175 rows: std::mem::replace(&mut self.rows, 0),
176 }
177 }
178}
179
180#[cfg(test)]
181mod tests {
182 use super::*;
183 use serde_json::json;
184
185 fn rec(i: u64) -> Value {
186 json!({ "id": i })
187 }
188
189 fn push_all(acc: &mut ObjectAccumulator, n: u64) -> (Vec<CompletedObject>, Vec<Vec<u8>>) {
191 let (mut objects, mut parts) = (Vec::new(), Vec::new());
192 for i in 0..n {
193 match acc.push_record(&rec(i)).unwrap() {
194 Emit::Nothing => {}
195 Emit::Part(p) => parts.push(p),
196 Emit::Object(o) => objects.push(o),
197 }
198 }
199 (objects, parts)
200 }
201
202 #[test]
203 fn nothing_rolls_without_a_threshold() {
204 let mut acc = ObjectAccumulator::new(None, None);
207 let (objects, parts) = push_all(&mut acc, 1000);
208 assert!(objects.is_empty() && parts.is_empty());
209 let done = acc.finish().expect("the remainder must be written");
210 assert_eq!(done.rows, 1000);
211 assert!(acc.finish().is_none(), "finish must not double-emit");
212 }
213
214 #[test]
215 fn rows_across_several_pages_land_in_one_object() {
216 let mut acc = ObjectAccumulator::new(Some(10), None);
220 let mut completed = Vec::new();
221 for page in 0..3 {
222 for i in 0..4 {
223 if let Emit::Object(o) = acc.push_record(&rec(page * 4 + i)).unwrap() {
224 completed.push(o);
225 }
226 }
227 }
228 assert_eq!(completed.len(), 1, "one rollover at 10 rows");
229 assert_eq!(completed[0].rows, 10);
230 let rest = acc.finish().expect("2 rows remain");
231 assert_eq!(rest.rows, 2);
232 assert_eq!(
233 completed[0].rows + rest.rows,
234 12,
235 "no row may be lost across a rollover"
236 );
237 }
238
239 #[test]
240 fn the_byte_threshold_rolls_independently_of_rows() {
241 let mut acc = ObjectAccumulator::new(None, Some(32));
244 let (objects, _) = push_all(&mut acc, 20);
245 assert!(!objects.is_empty(), "a byte cap must roll");
246 for obj in &objects {
247 assert!(
248 obj.body.len() >= 32,
249 "an object rolls at or past the threshold, not before: {}",
250 obj.body.len()
251 );
252 assert!(obj.body.len() < 32 + 64, "overshoot bounded by one record");
253 }
254 }
255
256 #[test]
257 fn whichever_threshold_hits_first_wins() {
258 let mut acc = ObjectAccumulator::new(Some(1000), Some(24));
259 let (objects, _) = push_all(&mut acc, 10);
260 assert!(
261 !objects.is_empty(),
262 "the byte cap must roll even though the row cap is far away"
263 );
264 }
265
266 #[test]
267 fn a_single_oversized_record_gets_its_own_object() {
268 let mut acc = ObjectAccumulator::new(None, Some(8));
272 let big = json!({ "blob": "x".repeat(500) });
273 let Emit::Object(obj) = acc.push_record(&big).unwrap() else {
274 panic!("an oversized record must complete an object immediately");
275 };
276 assert_eq!(obj.rows, 1);
277 assert!(obj.body.len() > 8);
278 }
279
280 #[test]
281 fn a_zero_threshold_means_no_limit_not_roll_every_record() {
282 let mut acc = ObjectAccumulator::new(Some(0), Some(0));
285 let (objects, parts) = push_all(&mut acc, 50);
286 assert!(objects.is_empty() && parts.is_empty());
287 assert_eq!(acc.finish().expect("remainder").rows, 50);
288 }
289
290 #[test]
291 fn bodies_are_ndjson_with_a_trailing_newline() {
292 let mut acc = ObjectAccumulator::new(Some(2), None);
293 acc.push_record(&rec(1)).unwrap();
294 let Emit::Object(obj) = acc.push_record(&rec(2)).unwrap() else {
295 panic!("rolled");
296 };
297 let text = String::from_utf8(obj.body).unwrap();
298 assert_eq!(text, "{\"id\":1}\n{\"id\":2}\n");
299 }
300
301 #[test]
302 fn counters_track_the_open_object() {
303 let mut acc = ObjectAccumulator::new(Some(10), None);
304 assert!(acc.is_empty());
305 acc.push_record(&rec(1)).unwrap();
306 assert_eq!(acc.rows(), 1);
307 assert!(!acc.is_empty());
308 acc.finish();
309 assert!(acc.is_empty(), "finish resets the open object");
310 assert_eq!(acc.rows(), 0);
311 assert_eq!(acc.len(), 0);
312 }
313
314 #[test]
315 fn parts_bound_peak_memory_for_an_uncapped_object() {
316 let mut acc = ObjectAccumulator::new(None, None).with_part_size(64);
320 let (objects, parts) = push_all(&mut acc, 200);
321 assert!(objects.is_empty(), "no object cap, so nothing rolls");
322 assert!(!parts.is_empty(), "parts must be emitted");
323 for p in &parts {
324 assert!(p.len() >= 64, "a part fills before it is emitted");
325 assert!(p.len() < 64 + 64, "and is not held far past the threshold");
326 }
327 assert!(
328 acc.len() < 64,
329 "the unflushed tail stays under one part: {}",
330 acc.len()
331 );
332 assert!(acc.has_parts());
333 let tail = acc.finish().expect("tail");
335 assert_eq!(tail.rows, 200, "rows count the whole object, not the tail");
336 }
337
338 #[test]
339 fn the_byte_cap_measures_the_whole_object_not_just_the_tail() {
340 let mut acc = ObjectAccumulator::new(None, Some(200)).with_part_size(64);
344 let (objects, parts) = push_all(&mut acc, 200);
345 assert!(!parts.is_empty(), "parts still stream");
346 assert!(
347 !objects.is_empty(),
348 "the object cap must still be reached once parts are counted"
349 );
350 }
351
352 #[test]
353 fn taking_an_object_resets_the_part_counter() {
354 let mut acc = ObjectAccumulator::new(Some(2), None).with_part_size(8);
358 acc.push_record(&rec(1)).unwrap();
359 let Emit::Object(_) = acc.push_record(&rec(2)).unwrap() else {
360 panic!("rolled at 2 rows");
361 };
362 assert_eq!(acc.parted_bytes(), 0);
363 assert!(!acc.has_parts());
364 }
365
366 #[test]
367 fn an_object_cap_closes_rather_than_parting_on_the_same_push() {
368 let mut acc = ObjectAccumulator::new(Some(1), None).with_part_size(1);
372 assert!(matches!(acc.push_record(&rec(1)).unwrap(), Emit::Object(_)));
373 }
374}
375
376#[derive(Debug)]
399pub struct PageAccumulator {
400 rows: Vec<Value>,
401 bytes: usize,
402 max_rows: Option<usize>,
403 max_bytes: Option<usize>,
404}
405
406impl PageAccumulator {
407 pub const DEFAULT_ROWS: usize = 100_000;
410 pub const DEFAULT_BYTES: usize = 256 * 1024 * 1024;
413
414 pub fn new(max_rows: Option<usize>, max_bytes: Option<usize>) -> Self {
417 Self {
418 rows: Vec::new(),
419 bytes: 0,
420 max_rows: max_rows.filter(|n| *n > 0),
421 max_bytes: max_bytes.filter(|n| *n > 0),
422 }
423 }
424
425 pub fn bounded(max_rows: Option<usize>, max_bytes: Option<usize>) -> Self {
430 if max_rows.is_none() && max_bytes.is_none() {
431 Self::new(Some(Self::DEFAULT_ROWS), Some(Self::DEFAULT_BYTES))
432 } else {
433 Self::new(max_rows, max_bytes)
434 }
435 }
436
437 pub fn restore(&mut self, rows: Vec<Value>) {
440 if rows.is_empty() {
441 return;
442 }
443 self.bytes += rows.iter().map(estimate_size).sum::<usize>();
444 let mut restored = rows;
445 restored.append(&mut self.rows);
446 self.rows = restored;
447 }
448
449 pub fn len(&self) -> usize {
451 self.rows.len()
452 }
453
454 pub fn is_empty(&self) -> bool {
456 self.rows.is_empty()
457 }
458
459 pub fn bytes(&self) -> usize {
461 self.bytes
462 }
463
464 pub fn push_page(&mut self, page: &[Value]) -> Option<Vec<Value>> {
471 for r in page {
472 self.bytes += estimate_size(r);
476 self.rows.push(r.clone());
477 }
478 let full = self.max_rows.is_some_and(|m| self.rows.len() >= m)
479 || self.max_bytes.is_some_and(|m| self.bytes >= m);
480 full.then(|| self.take())
481 }
482
483 pub fn finish(&mut self) -> Option<Vec<Value>> {
485 (!self.is_empty()).then(|| self.take())
486 }
487
488 fn take(&mut self) -> Vec<Value> {
489 self.bytes = 0;
490 std::mem::take(&mut self.rows)
491 }
492}
493
494fn estimate_size(v: &Value) -> usize {
500 match v {
501 Value::Null => 4,
502 Value::Bool(_) => 5,
503 Value::Number(_) => 8,
504 Value::String(s) => s.len() + 2,
505 Value::Array(a) => 2 + a.iter().map(estimate_size).sum::<usize>() + a.len(),
506 Value::Object(m) => {
507 2 + m
508 .iter()
509 .map(|(k, v)| k.len() + 3 + estimate_size(v))
510 .sum::<usize>()
511 }
512 }
513}
514
515#[cfg(test)]
516mod page_accumulator_tests {
517 use super::*;
518 use serde_json::json;
519
520 fn page(n: usize) -> Vec<Value> {
521 (0..n).map(|i| json!({ "id": i })).collect()
522 }
523
524 #[test]
525 fn small_pages_merge_into_one_commit_group() {
526 let mut acc = PageAccumulator::new(Some(100), None);
529 let mut commits = Vec::new();
530 for _ in 0..10 {
531 if let Some(g) = acc.push_page(&page(10)) {
532 commits.push(g);
533 }
534 }
535 assert_eq!(commits.len(), 1, "ten small pages → one commit");
536 assert_eq!(commits[0].len(), 100);
537 assert!(acc.finish().is_none(), "nothing left over");
538 }
539
540 #[test]
541 fn a_group_overshoots_by_at_most_one_page() {
542 let mut acc = PageAccumulator::new(Some(10), None);
545 let g = acc
546 .push_page(&page(25))
547 .expect("one page over the cap commits");
548 assert_eq!(g.len(), 25);
549 }
550
551 #[test]
552 fn the_byte_cap_rolls_independently_of_rows() {
553 let mut acc = PageAccumulator::new(None, Some(200));
554 let mut commits = 0;
555 for _ in 0..20 {
556 if acc.push_page(&page(5)).is_some() {
557 commits += 1;
558 }
559 }
560 assert!(commits > 0, "a byte cap must roll");
561 }
562
563 #[test]
564 fn no_cap_means_one_commit_for_the_whole_run() {
565 let mut acc = PageAccumulator::new(None, None);
566 for _ in 0..50 {
567 assert!(acc.push_page(&page(10)).is_none());
568 }
569 assert_eq!(acc.finish().expect("flush commits").len(), 500);
570 }
571
572 #[test]
573 fn a_zero_cap_means_no_limit() {
574 let mut acc = PageAccumulator::new(Some(0), Some(0));
577 assert!(acc.push_page(&page(10)).is_none());
578 assert_eq!(acc.finish().expect("remainder").len(), 10);
579 }
580
581 #[test]
582 fn counters_track_the_open_group() {
583 let mut acc = PageAccumulator::new(Some(100), None);
584 assert!(acc.is_empty());
585 acc.push_page(&page(3));
586 assert_eq!(acc.len(), 3);
587 assert!(acc.bytes() > 0);
588 acc.finish();
589 assert!(acc.is_empty());
590 assert_eq!(acc.bytes(), 0, "finish resets the size estimate too");
591 }
592
593 #[test]
594 fn restored_rows_commit_on_the_next_flush_before_newer_ones() {
595 let mut acc = PageAccumulator::new(Some(100), None);
596 acc.push_page(&page(3));
597 let group = acc.finish().expect("group");
598 acc.push_page(&[json!({ "id": 99 })]);
599 acc.restore(group);
600 assert_eq!(acc.len(), 4);
601 assert!(acc.bytes() > 0);
602 let again = acc.finish().expect("restored group");
603 assert_eq!(again[0], json!({ "id": 0 }));
604 assert_eq!(again[3], json!({ "id": 99 }));
605 acc.restore(Vec::new());
606 assert!(acc.is_empty());
607 }
608
609 #[test]
610 fn bounded_applies_finite_defaults_only_when_nothing_is_set() {
611 let mut acc = PageAccumulator::bounded(None, None);
612 assert_eq!(acc.max_rows, Some(PageAccumulator::DEFAULT_ROWS));
613 assert_eq!(acc.max_bytes, Some(PageAccumulator::DEFAULT_BYTES));
614 assert!(acc.push_page(&page(10)).is_none());
615 let unlimited = PageAccumulator::bounded(Some(0), None);
616 assert_eq!((unlimited.max_rows, unlimited.max_bytes), (None, None));
617 let rows_only = PageAccumulator::bounded(Some(5), None);
618 assert_eq!((rows_only.max_rows, rows_only.max_bytes), (Some(5), None));
619 }
620
621 #[test]
622 fn size_estimate_grows_with_real_content() {
623 let small = estimate_size(&json!({ "a": 1 }));
625 let big = estimate_size(&json!({ "a": 1, "b": "x".repeat(1000) }));
626 assert!(big > small + 900, "{small} vs {big}");
627 assert!(estimate_size(&json!([1, 2, 3])) > estimate_size(&json!([])));
628 }
629}