1use std::sync::Arc;
2
3use crate::{
4 LadduDataError, LadduDataResult,
5 data::{EventBatch, OwnedEvent},
6 io::{
7 DataFragment, EventSink, EventSource, FragmentedSource, ReadPlan, SinkState,
8 SourceCapabilities, WritePlan, fragmented_batches,
9 },
10 schema::Schema,
11};
12
13#[derive(Clone, Debug)]
15pub struct MemorySource {
16 schema: Arc<Schema>,
17 batches: Arc<[EventBatch]>,
18}
19
20#[derive(Clone, Copy, Debug)]
22pub struct MemoryFragmentKey {
23 batch_index: usize,
24}
25
26impl MemorySource {
27 pub fn new(batch: EventBatch) -> Self {
29 Self {
30 schema: Arc::clone(batch.schema()),
31 batches: Arc::from([batch]),
32 }
33 }
34
35 pub fn from_batches(batches: Vec<EventBatch>) -> LadduDataResult<Self> {
42 if batches.is_empty() {
43 return Err(LadduDataError::InvalidArgument(
44 "memory source requires at least one batch",
45 ));
46 }
47
48 let schema = Arc::clone(batches[0].schema());
49
50 for batch in &batches {
51 if batch.schema().as_ref() != schema.as_ref() {
52 return Err(LadduDataError::Schema(
53 "memory source batches have different schemas".into(),
54 ));
55 }
56 }
57
58 Ok(Self {
59 schema,
60 batches: batches.into(),
61 })
62 }
63
64 pub fn from_events<I>(schema: Arc<Schema>, events: I) -> LadduDataResult<Self>
71 where
72 I: IntoIterator<Item = OwnedEvent>,
73 {
74 let batch = EventBatch::from_events(schema, events)?;
75 Ok(Self::new(batch))
76 }
77
78 pub fn schema_arc(&self) -> &Arc<Schema> {
80 &self.schema
81 }
82
83 pub fn batches_slice(&self) -> &[EventBatch] {
85 &self.batches
86 }
87
88 pub fn into_batches(self) -> Arc<[EventBatch]> {
90 self.batches
91 }
92
93 pub fn into_batch(self) -> LadduDataResult<EventBatch> {
100 EventBatch::concat(&self.batches)
101 }
102}
103
104impl EventSource for MemorySource {
105 fn schema(&self) -> LadduDataResult<Arc<Schema>> {
106 Ok(Arc::clone(&self.schema))
107 }
108
109 fn capabilities(&self) -> SourceCapabilities {
110 SourceCapabilities {
111 exact_len: true,
112 exact_weighted_total: true,
113 random_access: true,
114 deterministic_partitioning: true,
115 predicate_pushdown: false,
116 projection_pushdown: false,
117 streaming: false,
118 }
119 }
120
121 fn num_events(&self) -> LadduDataResult<Option<u64>> {
122 Ok(Some(self.batches.iter().map(|b| b.len() as u64).sum()))
123 }
124
125 fn weighted_total(&self) -> LadduDataResult<Option<f64>> {
126 let total = self
127 .batches
128 .iter()
129 .map(|batch| (0..batch.len()).map(|i| batch.weights_at(i)).sum::<f64>())
130 .sum();
131
132 Ok(Some(total))
133 }
134
135 fn batches(
136 &self,
137 plan: ReadPlan,
138 ) -> LadduDataResult<Box<dyn Iterator<Item = LadduDataResult<EventBatch>> + Send>> {
139 fragmented_batches(Arc::new(self.clone()), plan)
140 }
141}
142
143impl FragmentedSource for MemorySource {
144 type Key = MemoryFragmentKey;
145
146 fn fragments(&self) -> LadduDataResult<Vec<DataFragment<Self::Key>>> {
147 let mut fragments = Vec::with_capacity(self.batches.len());
148 let mut global_start = 0_u64;
149
150 for (batch_index, batch) in self.batches.iter().enumerate() {
151 let rows = batch.len() as u64;
152
153 fragments.push(DataFragment {
154 key: MemoryFragmentKey { batch_index },
155 global_start,
156 rows,
157 });
158
159 global_start += rows;
160 }
161
162 Ok(fragments)
163 }
164
165 fn read_fragment_range(
166 &self,
167 key: &Self::Key,
168 local_start: usize,
169 local_len: usize,
170 chunk_size: Option<usize>,
171 ) -> LadduDataResult<Box<dyn Iterator<Item = LadduDataResult<EventBatch>> + Send>> {
172 if matches!(chunk_size, Some(0)) {
173 return Err(LadduDataError::InvalidArgument(
174 "chunk_size must be nonzero",
175 ));
176 }
177
178 let batch = self
179 .batches
180 .get(key.batch_index)
181 .ok_or_else(|| LadduDataError::Source("invalid memory batch index".into()))?
182 .clone();
183
184 let end = local_start
185 .checked_add(local_len)
186 .ok_or(LadduDataError::InvalidArgument(
187 "slice range overflows usize",
188 ))?;
189
190 if end > batch.len() {
191 return Err(LadduDataError::InvalidArgument(
192 "memory fragment range exceeds batch length",
193 ));
194 }
195
196 Ok(Box::new(MemoryRangeIter {
197 batch,
198 pos: local_start,
199 end,
200 chunk_size: chunk_size.unwrap_or(local_len.max(1)),
201 }))
202 }
203}
204
205struct MemoryRangeIter {
206 batch: EventBatch,
207 pos: usize,
208 end: usize,
209 chunk_size: usize,
210}
211
212impl Iterator for MemoryRangeIter {
213 type Item = LadduDataResult<EventBatch>;
214
215 fn next(&mut self) -> Option<Self::Item> {
216 if self.pos >= self.end {
217 return None;
218 }
219
220 let next = (self.pos + self.chunk_size).min(self.end);
221 let batch = self.batch.slice(self.pos, next);
222 self.pos = next;
223
224 Some(Ok(batch))
225 }
226}
227
228#[derive(Clone, Debug, Default)]
230pub struct MemorySink {
231 schema: Option<Arc<Schema>>,
232 batches: Vec<EventBatch>,
233 state: SinkState,
234}
235
236impl MemorySink {
237 pub fn new() -> Self {
239 Self::default()
240 }
241
242 pub fn schema(&self) -> Option<&Arc<Schema>> {
244 self.schema.as_ref()
245 }
246
247 pub fn batches(&self) -> &[EventBatch] {
249 &self.batches
250 }
251
252 pub fn into_batches(self) -> Vec<EventBatch> {
254 self.batches
255 }
256
257 pub fn into_source(self) -> LadduDataResult<MemorySource> {
264 MemorySource::from_batches(self.batches)
265 }
266
267 pub fn into_batch(self) -> LadduDataResult<EventBatch> {
274 EventBatch::concat(&self.batches)
275 }
276
277 pub fn clear(&mut self) {
279 self.schema = None;
280 self.batches.clear();
281 self.state = SinkState::Idle;
282 }
283}
284
285impl EventSink for MemorySink {
286 fn retains_batches(&self) -> bool {
287 true
288 }
289
290 fn begin(&mut self, schema: Arc<Schema>, _plan: WritePlan) -> LadduDataResult<()> {
291 match self.state {
292 SinkState::Idle => {}
293 SinkState::Writing => {
294 return Err(LadduDataError::Sink(
295 "memory sink already initialized".into(),
296 ));
297 }
298 SinkState::Failed => {
299 return Err(LadduDataError::Sink(
300 "memory sink requires abort after failure".into(),
301 ));
302 }
303 }
304
305 self.schema = Some(schema);
306 self.batches.clear();
307 self.state = SinkState::Writing;
308 Ok(())
309 }
310
311 fn write_batch(&mut self, batch: &EventBatch) -> LadduDataResult<()> {
312 if !matches!(self.state, SinkState::Writing) {
313 return Err(LadduDataError::Sink(
314 match self.state {
315 SinkState::Idle => "memory sink not initialized",
316 SinkState::Failed => "memory sink requires abort after failure",
317 SinkState::Writing => unreachable!(),
318 }
319 .into(),
320 ));
321 }
322
323 let schema = self
324 .schema
325 .as_ref()
326 .ok_or_else(|| LadduDataError::Sink("memory sink not initialized".into()))?;
327
328 if schema.as_ref() != batch.schema().as_ref() {
329 return Err(LadduDataError::Sink(
330 "batch schema does not match memory sink schema".into(),
331 ));
332 }
333
334 self.batches.push(batch.clone());
335 Ok(())
336 }
337
338 fn finish(&mut self) -> LadduDataResult<()> {
339 if matches!(self.state, SinkState::Failed) {
340 return Err(LadduDataError::Sink(
341 "memory sink requires abort after failure".into(),
342 ));
343 }
344 self.state = SinkState::Idle;
345 Ok(())
346 }
347
348 fn abort(&mut self) -> LadduDataResult<()> {
349 self.schema = None;
350 self.batches.clear();
351 self.state = SinkState::Idle;
352 Ok(())
353 }
354}
355
356#[cfg(test)]
357mod tests {
358 use laddu_physics::vectors::RealVec4;
359
360 use super::*;
361 use crate::data::EventBatchBuilder;
362
363 fn v(x: f64) -> RealVec4 {
364 RealVec4 {
365 e: x,
366 px: x,
367 py: x,
368 pz: x,
369 }
370 }
371
372 fn schema() -> Arc<Schema> {
373 Arc::new(Schema::new(["p"], ["id"], true).unwrap())
374 }
375
376 fn batch(start: usize, len: usize) -> EventBatch {
377 let schema = schema();
378 let mut builder = EventBatchBuilder::with_capacity(schema, len);
379
380 for i in start..start + len {
381 builder
382 .push_weighted([v(i as f64)], [i as f64], 1.0 + i as f64)
383 .unwrap();
384 }
385
386 builder.finish().unwrap()
387 }
388
389 fn concat_scalars(batches: Vec<EventBatch>) -> Vec<f64> {
390 EventBatch::concat(&batches)
391 .unwrap()
392 .scalar_column(0)
393 .to_vec()
394 }
395
396 #[test]
397 fn memory_source_reports_exact_capabilities_counts_and_weighted_total() {
398 let source = MemorySource::from_batches(vec![batch(0, 2), batch(2, 3)]).unwrap();
399
400 let caps = source.capabilities();
401
402 assert!(caps.exact_len);
403 assert!(caps.exact_weighted_total);
404 assert!(caps.random_access);
405 assert!(caps.deterministic_partitioning);
406 assert!(!caps.streaming);
407
408 assert_eq!(source.num_events().unwrap(), Some(5));
409 assert_eq!(
410 source.weighted_total().unwrap(),
411 Some(1.0 + 2.0 + 3.0 + 4.0 + 5.0)
412 );
413 }
414
415 #[test]
416 fn memory_source_coalesces_by_default_but_respects_chunked_read_plan() {
417 let source = MemorySource::from_batches(vec![batch(0, 2), batch(2, 3)]).unwrap();
418
419 let default_batches: Vec<EventBatch> = source
420 .batches(ReadPlan::default())
421 .unwrap()
422 .map(Result::unwrap)
423 .collect();
424
425 assert_eq!(default_batches.len(), 1);
426 assert_eq!(
427 default_batches[0].scalar_column(0),
428 &[0.0, 1.0, 2.0, 3.0, 4.0]
429 );
430
431 let chunked_batches: Vec<EventBatch> = source
432 .batches(ReadPlan {
433 chunk_size: Some(2),
434 #[cfg(feature = "mpi")]
435 distribution: Default::default(),
436 })
437 .unwrap()
438 .map(Result::unwrap)
439 .collect();
440
441 assert_eq!(
442 chunked_batches
443 .iter()
444 .map(EventBatch::len)
445 .collect::<Vec<_>>(),
446 vec![2, 2, 1]
447 );
448 assert_eq!(
449 concat_scalars(chunked_batches),
450 vec![0.0, 1.0, 2.0, 3.0, 4.0]
451 );
452 }
453
454 #[test]
455 fn memory_source_rejects_empty_or_schema_mismatched_batches() {
456 assert!(matches!(
457 MemorySource::from_batches(vec![]),
458 Err(LadduDataError::InvalidArgument(_))
459 ));
460
461 let first = batch(0, 1);
462
463 let other_schema = Arc::new(Schema::new(["q"], ["id"], true).unwrap());
464 let mut builder = EventBatchBuilder::new(other_schema);
465 builder.push_weighted([v(10.0)], [10.0], 1.0).unwrap();
466 let second = builder.finish().unwrap();
467
468 assert!(matches!(
469 MemorySource::from_batches(vec![first, second]),
470 Err(LadduDataError::Schema(_))
471 ));
472 }
473
474 #[test]
475 fn memory_sink_validates_lifecycle_schema_and_can_be_reused() {
476 let mut sink = MemorySink::new();
477 let first = batch(0, 2);
478
479 assert!(matches!(
480 sink.write_batch(&first),
481 Err(LadduDataError::Sink(_))
482 ));
483
484 sink.begin(Arc::clone(first.schema()), WritePlan::default())
485 .unwrap();
486 sink.write_batch(&first).unwrap();
487 sink.finish().unwrap();
488
489 assert_eq!(sink.batches().len(), 1);
490 assert_eq!(sink.batches()[0].scalar_column(0), &[0.0, 1.0]);
491
492 let mismatched_schema = Arc::new(Schema::new(["other"], ["id"], true).unwrap());
493 let mut builder = EventBatchBuilder::new(mismatched_schema);
494 builder.push_weighted([v(9.0)], [9.0], 9.0).unwrap();
495 let mismatched = builder.finish().unwrap();
496
497 assert!(matches!(
498 sink.write_batch(&mismatched),
499 Err(LadduDataError::Sink(_))
500 ));
501
502 sink.clear();
503 assert!(sink.schema().is_none());
504 assert!(sink.batches().is_empty());
505 }
506
507 #[test]
508 fn memory_sink_into_source_roundtrips_captured_batches() {
509 let mut sink = MemorySink::new();
510 let first = batch(0, 2);
511 let second = batch(2, 2);
512
513 sink.begin(Arc::clone(first.schema()), WritePlan::default())
514 .unwrap();
515 sink.write_batch(&first).unwrap();
516 sink.write_batch(&second).unwrap();
517 sink.finish().unwrap();
518
519 let source = sink.into_source().unwrap();
520 let merged = source.into_batch().unwrap();
521
522 assert_eq!(merged.scalar_column(0), &[0.0, 1.0, 2.0, 3.0]);
523 assert_eq!(merged.weights_column().unwrap(), &[1.0, 2.0, 3.0, 4.0]);
524 }
525
526 #[test]
527 fn memory_sink_abort_discards_partial_batches_and_allows_reuse() {
528 let mut sink = MemorySink::new();
529 let first = batch(0, 2);
530
531 sink.begin(Arc::clone(first.schema()), WritePlan::default())
532 .unwrap();
533 sink.write_batch(&first).unwrap();
534 sink.abort().unwrap();
535 assert!(sink.schema().is_none());
536 assert!(sink.batches().is_empty());
537
538 sink.begin(Arc::clone(first.schema()), WritePlan::default())
539 .unwrap();
540 sink.finish().unwrap();
541 }
542}