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