1use std::{
17 sync::{
18 Arc,
19 atomic::{AtomicBool, Ordering},
20 },
21 vec::IntoIter,
22};
23
24use ahash::{AHashMap, AHashSet};
25use datafusion::{
26 arrow::record_batch::RecordBatch,
27 error::{DataFusionError, Result},
28 logical_expr::expr::Sort,
29 physical_plan::SendableRecordBatchStream,
30 prelude::*,
31};
32use futures::{Stream, StreamExt};
33use nautilus_common::live::get_runtime;
34use nautilus_core::UnixNanos;
35use nautilus_model::data::{Data, HasTsInit};
36use nautilus_serialization::arrow::{
37 DataStreamingError, DecodeDataFromRecordBatch, EncodeToRecordBatch, EncodingError, WriteStream,
38};
39use object_store::ObjectStore;
40use parking_lot::Mutex;
41use url::Url;
42
43use super::{
44 compare::Compare,
45 kmerge_batch::{EagerStream, ElementBatchIter, KMerge},
46};
47use crate::common::arrow::validate_catalog_schema;
48
49#[derive(Debug, Default)]
50pub struct TsInitComparator;
51
52impl<I> Compare<ElementBatchIter<I, Data>> for TsInitComparator
53where
54 I: Iterator<Item = IntoIter<Data>>,
55{
56 fn compare(
57 &self,
58 l: &ElementBatchIter<I, Data>,
59 r: &ElementBatchIter<I, Data>,
60 ) -> std::cmp::Ordering {
61 l.item.ts_init().cmp(&r.item.ts_init()).reverse()
63 }
64}
65
66#[derive(Debug, thiserror::Error)]
68pub enum QueryError {
69 #[error("Record batch stream error: {0}")]
71 Stream(#[from] DataFusionError),
72 #[error("Record batch decode error: {0}")]
74 Decode(#[from] EncodingError),
75}
76
77#[derive(Default)]
82struct ErrorSlot {
83 failed: AtomicBool,
84 error: Mutex<Option<QueryError>>,
85}
86
87impl ErrorSlot {
88 fn record(&self, error: QueryError) {
89 self.error.lock().get_or_insert(error);
90 self.failed.store(true, Ordering::Release);
91 }
92
93 fn failed(&self) -> bool {
94 self.failed.load(Ordering::Acquire)
95 }
96
97 fn take(&self) -> Option<QueryError> {
98 self.error.lock().take()
99 }
100}
101
102pub struct QueryResult {
107 merge: KMerge<BatchStream, Data, TsInitComparator>,
108 error: Arc<ErrorSlot>,
109}
110
111impl QueryResult {
112 #[must_use]
114 pub fn from_typed_pages<T>(
115 pages: Box<dyn Iterator<Item = anyhow::Result<Vec<T>>> + Send>,
116 ) -> Self
117 where
118 T: Into<Data> + Send + 'static,
119 {
120 let error = Arc::new(ErrorSlot::default());
121 let pages = pages.map(|page| {
122 page.map(|rows| {
123 rows.into_iter()
124 .map(Into::into)
125 .collect::<Vec<Data>>()
126 .into_iter()
127 })
128 .map_err(|e| match e.downcast::<EncodingError>() {
129 Ok(e) => QueryError::Decode(e),
130 Err(e) => QueryError::Stream(DataFusionError::External(e.into())),
131 })
132 });
133 let stream = BatchStream {
134 inner: EagerStream::from_stream_with_runtime(
135 futures::stream::iter(pages),
136 get_runtime().handle().clone(),
137 ),
138 error: Arc::clone(&error),
139 };
140 let mut merge = KMerge::new(TsInitComparator);
141 merge.push_iter(stream);
142 Self { merge, error }
143 }
144
145 pub fn clear(&mut self) {
147 self.merge.clear();
148 }
149}
150
151impl Iterator for QueryResult {
152 type Item = std::result::Result<Data, QueryError>;
154
155 fn next(&mut self) -> Option<Self::Item> {
156 if self.error.failed()
159 && let Some(e) = self.error.take()
160 {
161 self.clear();
162 return Some(Err(e));
163 }
164
165 match self.merge.next() {
166 Some(item) => Some(Ok(item)),
167 None => self.error.take().map(Err),
169 }
170 }
171}
172
173#[cfg_attr(
179 feature = "python",
180 pyo3::pyclass(module = "nautilus_trader.persistence", unsendable)
181)]
182#[cfg_attr(
183 feature = "python",
184 pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.persistence")
185)]
186pub struct DataBackendSession {
187 pub chunk_size: usize,
188 pub runtime: tokio::runtime::Handle,
189 session_ctx: SessionContext,
190 batch_streams: Vec<BatchStream>,
191 error: Arc<ErrorSlot>,
192 registered_tables: AHashSet<String>,
193}
194
195impl DataBackendSession {
196 #[must_use]
198 pub fn new(chunk_size: usize) -> Self {
199 let session_cfg = SessionConfig::new()
200 .set_str("datafusion.optimizer.repartition_file_scans", "false")
201 .set_str("datafusion.optimizer.prefer_existing_sort", "true");
202 let session_ctx = SessionContext::new_with_config(session_cfg);
203 Self {
204 session_ctx,
205 batch_streams: Vec::default(),
206 error: Arc::default(),
207 chunk_size,
208 runtime: get_runtime().handle().clone(),
209 registered_tables: AHashSet::new(),
210 }
211 }
212
213 pub fn register_object_store(&mut self, url: &Url, object_store: Arc<dyn ObjectStore>) {
215 self.session_ctx.register_object_store(url, object_store);
216 }
217
218 pub fn register_object_store_from_uri(
225 &mut self,
226 uri: &str,
227 storage_options: Option<AHashMap<String, String>>,
228 ) -> anyhow::Result<()> {
229 let location =
230 crate::parquet::create_object_store_location_from_path(uri, storage_options)?;
231
232 if let Some(root_url) = location.store_root_url().cloned() {
233 self.register_object_store(&root_url, location.object_store);
234 }
235
236 Ok(())
237 }
238
239 pub fn write_data<T: EncodeToRecordBatch>(
245 data: &[T],
246 metadata: &AHashMap<String, String>,
247 stream: &mut dyn WriteStream,
248 ) -> Result<(), DataStreamingError> {
249 let metadata: std::collections::HashMap<String, String> = metadata
251 .iter()
252 .map(|(k, v)| (k.clone(), v.clone()))
253 .collect();
254 let record_batch = T::encode_batch(&metadata, data)?;
255 stream.write(&record_batch)?;
256 Ok(())
257 }
258
259 pub fn add_file<T>(
277 &mut self,
278 table_name: &str,
279 file_path: &str,
280 sql_query: Option<&str>,
281 custom_type_name: Option<&str>,
282 ) -> Result<()>
283 where
284 T: DecodeDataFromRecordBatch,
285 {
286 let is_new_table = !self.registered_tables.contains(table_name);
288
289 if is_new_table {
290 let parquet_options = ParquetReadOptions::<'_> {
292 skip_metadata: Some(false),
293 file_sort_order: vec![vec![Sort {
294 expr: col("ts_init"),
295 asc: true,
296 nulls_first: false,
297 }]],
298 ..Default::default()
299 };
300 super::block_on(
301 &self.runtime,
302 self.session_ctx
303 .register_parquet(table_name, file_path, parquet_options),
304 )?;
305
306 let table = super::block_on(&self.runtime, self.session_ctx.table(table_name))?;
307 if let Err(e) = validate_catalog_schema(table.schema().as_arrow()) {
308 self.session_ctx.deregister_table(table_name)?;
309 return Err(DataFusionError::External(e.into()));
310 }
311 self.registered_tables.insert(table_name.to_string());
312
313 let default_query = format!("SELECT * FROM {table_name} ORDER BY ts_init");
315 let sql_query = sql_query.unwrap_or(&default_query);
316 let query = super::block_on(&self.runtime, self.session_ctx.sql(sql_query))?;
317 let batch_stream = super::block_on(&self.runtime, query.execute_stream())?;
318 self.add_batch_stream::<T>(batch_stream, custom_type_name.map(String::from));
319 }
320
321 Ok(())
322 }
323
324 pub fn collect_query_batches(
331 &mut self,
332 table_name: &str,
333 file_path: &str,
334 sql_query: Option<&str>,
335 ) -> Result<Vec<RecordBatch>> {
336 if !self.registered_tables.contains(table_name) {
337 let parquet_options = ParquetReadOptions::<'_> {
338 skip_metadata: Some(false),
339 file_sort_order: vec![vec![Sort {
340 expr: col("ts_init"),
341 asc: true,
342 nulls_first: false,
343 }]],
344 ..Default::default()
345 };
346 super::block_on(
347 &self.runtime,
348 self.session_ctx
349 .register_parquet(table_name, file_path, parquet_options),
350 )?;
351
352 let table = super::block_on(&self.runtime, self.session_ctx.table(table_name))?;
353 if let Err(e) = validate_catalog_schema(table.schema().as_arrow()) {
354 self.session_ctx.deregister_table(table_name)?;
355 return Err(DataFusionError::External(e.into()));
356 }
357 self.registered_tables.insert(table_name.to_string());
358 }
359
360 let default_query = format!("SELECT * FROM {table_name} ORDER BY ts_init");
361 let sql_query = sql_query.unwrap_or(&default_query);
362 let query = super::block_on(&self.runtime, self.session_ctx.sql(sql_query))?;
363 let mut batch_stream = super::block_on(&self.runtime, query.execute_stream())?;
364
365 super::block_on(&self.runtime, async {
366 let mut batches = Vec::new();
367 while let Some(batch) = batch_stream.next().await {
368 batches.push(batch?);
369 }
370 Ok::<_, datafusion::error::DataFusionError>(batches)
371 })
372 }
373
374 fn add_batch_stream<T>(
375 &mut self,
376 stream: SendableRecordBatchStream,
377 custom_type_name: Option<String>,
378 ) where
379 T: DecodeDataFromRecordBatch,
380 {
381 self.batch_streams.push(BatchStream {
382 inner: EagerStream::from_stream_with_runtime(
383 decode_batches::<T>(stream, custom_type_name),
384 self.runtime.clone(),
385 ),
386 error: Arc::clone(&self.error),
387 });
388 }
389
390 pub fn get_query_result(&mut self) -> QueryResult {
395 let mut merge: KMerge<_, _, _> = KMerge::new(TsInitComparator);
396
397 self.batch_streams
398 .drain(..)
399 .for_each(|batch_stream| merge.push_iter(batch_stream));
400
401 QueryResult {
402 merge,
403 error: std::mem::take(&mut self.error),
404 }
405 }
406
407 pub fn clear_registered_tables(&mut self) {
412 self.registered_tables.clear();
413 self.batch_streams.clear();
414 self.error = Arc::default();
415
416 let session_cfg = SessionConfig::new()
418 .set_str("datafusion.optimizer.repartition_file_scans", "false")
419 .set_str("datafusion.optimizer.prefer_existing_sort", "true");
420 self.session_ctx = SessionContext::new_with_config(session_cfg);
421 }
422}
423
424type BatchResult = std::result::Result<IntoIter<Data>, QueryError>;
425
426fn decode_batches<T>(
431 stream: SendableRecordBatchStream,
432 custom_type_name: Option<String>,
433) -> impl Stream<Item = BatchResult> + Send + 'static
434where
435 T: DecodeDataFromRecordBatch,
436{
437 futures::stream::unfold(
438 (stream, custom_type_name, false),
439 |(mut stream, custom_type_name, failed)| async move {
440 if failed {
441 return None;
442 }
443
444 let batch = decode_batch::<T>(stream.next().await?, custom_type_name.as_deref());
445 let failed = batch.is_err();
446
447 Some((batch, (stream, custom_type_name, failed)))
448 },
449 )
450}
451
452fn decode_batch<T>(
453 result: std::result::Result<RecordBatch, DataFusionError>,
454 custom_type_name: Option<&str>,
455) -> BatchResult
456where
457 T: DecodeDataFromRecordBatch,
458{
459 let batch = result?;
460 validate_catalog_schema(batch.schema_ref()).map_err(|e| DataFusionError::External(e.into()))?;
461 let mut metadata: std::collections::HashMap<String, String> = batch.schema().metadata().clone();
462
463 if let Some(type_name) = custom_type_name {
464 metadata.insert("type_name".to_string(), type_name.to_string());
465 }
466
467 Ok(T::decode_data_batch(&metadata, batch)?.into_iter())
468}
469
470struct BatchStream {
476 inner: EagerStream<BatchResult>,
477 error: Arc<ErrorSlot>,
478}
479
480impl Iterator for BatchStream {
481 type Item = IntoIter<Data>;
482
483 fn next(&mut self) -> Option<Self::Item> {
484 match self.inner.next()? {
485 Ok(batch) => Some(batch),
486 Err(e) => {
487 self.error.record(e);
488 None
489 }
490 }
491 }
492}
493
494#[must_use]
495pub fn build_query(
496 table: &str,
497 start: Option<UnixNanos>,
498 end: Option<UnixNanos>,
499 where_clause: Option<&str>,
500) -> String {
501 let mut conditions = Vec::new();
502
503 if let Some(clause) = where_clause {
505 conditions.push(clause.to_string());
506 }
507
508 if let Some(start_ts) = start {
510 conditions.push(format!("ts_init >= {start_ts}"));
511 }
512
513 if let Some(end_ts) = end {
515 conditions.push(format!("ts_init <= {end_ts}"));
516 }
517
518 let mut query = format!("SELECT * FROM {table}");
520
521 if !conditions.is_empty() {
523 query.push_str(" WHERE ");
524 query.push_str(&conditions.join(" AND "));
525 }
526
527 query.push_str(" ORDER BY ts_init");
529
530 query
531}
532
533#[cfg_attr(
534 feature = "python",
535 pyo3::pyclass(module = "nautilus_trader.persistence", unsendable)
536)]
537#[cfg_attr(
538 feature = "python",
539 pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.persistence")
540)]
541pub struct DataQueryResult {
542 pub result: QueryResult,
543 pub acc: Vec<Data>,
544 pub size: usize,
545}
546
547impl DataQueryResult {
548 #[must_use]
550 pub const fn new(result: QueryResult, size: usize) -> Self {
551 Self {
552 result,
553 acc: Vec::new(),
554 size,
555 }
556 }
557}
558
559impl Iterator for DataQueryResult {
560 type Item = std::result::Result<Vec<Data>, QueryError>;
562
563 fn next(&mut self) -> Option<Self::Item> {
564 let size = self.size.max(1);
567
568 for _ in 0..size {
569 match self.result.next() {
570 Some(Ok(item)) => self.acc.push(item),
571 Some(Err(e)) => {
572 self.acc.clear();
573 return Some(Err(e));
574 }
575 None => break,
576 }
577 }
578
579 let mut acc: Vec<Data> = Vec::new();
582 std::mem::swap(&mut acc, &mut self.acc);
583 Some(Ok(acc))
584 }
585}
586
587impl Drop for DataQueryResult {
588 fn drop(&mut self) {
589 self.result.clear();
590 }
591}
592
593#[cfg(test)]
594mod tests {
595 use std::{collections::HashMap, sync::atomic::AtomicUsize, task::Poll};
596
597 use datafusion::physical_plan::stream::RecordBatchStreamAdapter;
598 use nautilus_common::live::get_runtime;
599 use nautilus_model::{
600 data::QuoteTick,
601 identifiers::InstrumentId,
602 types::{Price, Quantity},
603 };
604 use nautilus_serialization::arrow::{
605 ArrowSchemaProvider, KEY_INSTRUMENT_ID, KEY_PRICE_PRECISION, KEY_SIZE_PRECISION,
606 };
607 #[cfg(feature = "python")]
608 use pyo3::{Py, Python, exceptions::PyRuntimeError, types::PyAnyMethods};
609 use rstest::rstest;
610
611 use super::*;
612
613 const INSTRUMENT_ID: &str = "EUR/USD.SIM";
614
615 fn quote(ts_init: u64) -> QuoteTick {
616 QuoteTick::new(
617 InstrumentId::from(INSTRUMENT_ID),
618 Price::from("1.0001"),
619 Price::from("1.0002"),
620 Quantity::from("100"),
621 Quantity::from("100"),
622 UnixNanos::from(ts_init),
623 UnixNanos::from(ts_init),
624 )
625 }
626
627 fn quote_metadata() -> HashMap<String, String> {
628 HashMap::from([
629 (KEY_INSTRUMENT_ID.to_string(), INSTRUMENT_ID.to_string()),
630 (KEY_PRICE_PRECISION.to_string(), "4".to_string()),
631 (KEY_SIZE_PRECISION.to_string(), "0".to_string()),
632 ])
633 }
634
635 fn quote_batch(quotes: &[QuoteTick]) -> RecordBatch {
636 QuoteTick::encode_batch("e_metadata(), quotes).expect("failed to encode quotes")
637 }
638
639 fn stream_error() -> DataFusionError {
640 DataFusionError::Execution("injected stream failure".to_string())
641 }
642
643 fn batch_stream(
644 batches: Vec<std::result::Result<RecordBatch, DataFusionError>>,
645 ) -> SendableRecordBatchStream {
646 Box::pin(RecordBatchStreamAdapter::new(
647 Arc::new(QuoteTick::get_schema(Some(quote_metadata()))),
648 futures::stream::iter(batches),
649 ))
650 }
651
652 fn ts_inits(items: &[std::result::Result<Data, QueryError>]) -> Vec<u64> {
653 items
654 .iter()
655 .filter_map(|item| item.as_ref().ok())
656 .map(|data| data.ts_init().as_u64())
657 .collect()
658 }
659
660 #[rstest]
661 fn data_backend_sessions_share_global_runtime() {
662 let first = DataBackendSession::new(10);
663 let second = DataBackendSession::new(10);
664
665 assert_eq!(first.runtime.id(), second.runtime.id());
666 assert_eq!(first.runtime.id(), get_runtime().handle().id());
667 }
668
669 #[rstest]
670 fn query_result_merges_streams_in_order_then_exhausts() {
671 let mut session = DataBackendSession::new(10);
672 session.add_batch_stream::<QuoteTick>(
673 batch_stream(vec![
674 Ok(quote_batch(&[quote(1), quote(3)])),
675 Ok(quote_batch(&[quote(5)])),
676 ]),
677 None,
678 );
679 session.add_batch_stream::<QuoteTick>(
680 batch_stream(vec![Ok(quote_batch(&[quote(2), quote(4)]))]),
681 None,
682 );
683
684 let mut result = session.get_query_result();
685 let items: Vec<_> = result.by_ref().collect();
686
687 assert_eq!(ts_inits(&items), vec![1, 2, 3, 4, 5]);
688 assert_eq!(items.len(), 5);
689 assert!(result.next().is_none());
690 }
691
692 #[rstest]
693 fn query_result_reports_stream_error_after_its_data() {
694 let mut session = DataBackendSession::new(10);
695 session.add_batch_stream::<QuoteTick>(
696 batch_stream(vec![
697 Ok(quote_batch(&[quote(1), quote(2)])),
698 Err(stream_error()),
699 ]),
700 None,
701 );
702
703 let mut result = session.get_query_result();
704 let items: Vec<_> = result.by_ref().collect();
705
706 assert_eq!(ts_inits(&items), vec![1, 2]);
707 assert_eq!(items.len(), 3);
708 assert!(
709 matches!(items[2], Err(QueryError::Stream(_))),
710 "expected a stream error, was {:?}",
711 items[2]
712 );
713 assert!(result.next().is_none());
714 }
715
716 #[rstest]
717 fn query_result_stops_when_one_of_many_streams_fails() {
718 let mut session = DataBackendSession::new(10);
719 session.add_batch_stream::<QuoteTick>(
720 batch_stream(vec![Ok(quote_batch(&[quote(1)])), Err(stream_error())]),
721 None,
722 );
723 session
724 .add_batch_stream::<QuoteTick>(batch_stream(vec![Ok(quote_batch(&[quote(2)]))]), None);
725
726 let mut result = session.get_query_result();
727 let items: Vec<_> = result.by_ref().collect();
728
729 assert_eq!(ts_inits(&items), vec![1]);
731 assert_eq!(items.len(), 2);
732 assert!(
733 matches!(items[1], Err(QueryError::Stream(_))),
734 "expected a stream error, was {:?}",
735 items[1]
736 );
737 assert!(result.next().is_none());
738 }
739
740 #[rstest]
741 fn query_result_reports_decode_error() {
742 let mut session = DataBackendSession::new(10);
743 let batch =
745 QuoteTick::encode_batch(&HashMap::new(), &[quote(1)]).expect("failed to encode quotes");
746 session.add_batch_stream::<QuoteTick>(batch_stream(vec![Ok(batch)]), None);
747
748 let items: Vec<_> = session.get_query_result().collect();
749
750 assert_eq!(items.len(), 1);
751 assert!(
752 matches!(
753 items[0],
754 Err(QueryError::Decode(EncodingError::MissingMetadata(
755 KEY_INSTRUMENT_ID
756 )))
757 ),
758 "expected a decode error, was {:?}",
759 items[0]
760 );
761 }
762
763 #[rstest]
764 fn data_query_result_reports_error_instead_of_an_empty_chunk() {
765 let mut session = DataBackendSession::new(10);
766 session.add_batch_stream::<QuoteTick>(
767 batch_stream(vec![Ok(quote_batch(&[quote(1)])), Err(stream_error())]),
768 None,
769 );
770
771 let mut result = DataQueryResult::new(session.get_query_result(), 10);
772 let chunk = result.next().expect("chunked result must yield an item");
773
774 assert!(
775 matches!(chunk, Err(QueryError::Stream(_))),
776 "expected a stream error, was {chunk:?}"
777 );
778
779 let after = result
780 .next()
781 .expect("chunked result must signal exhaustion")
782 .expect("a failed query must not fail twice");
783
784 assert!(after.is_empty(), "the discarded chunk must not be replayed");
785 }
786
787 #[rstest]
788 fn data_query_result_reports_an_error_with_a_zero_chunk_size() {
789 let mut session = DataBackendSession::new(10);
790 session.add_batch_stream::<QuoteTick>(batch_stream(vec![Err(stream_error())]), None);
791
792 let mut result = DataQueryResult::new(session.get_query_result(), 0);
793 let chunk = result.next().expect("chunked result must yield an item");
794
795 assert!(
796 matches!(chunk, Err(QueryError::Stream(_))),
797 "expected a stream error, was {chunk:?}"
798 );
799 }
800
801 #[rstest]
802 fn data_query_result_ends_with_an_empty_chunk_when_successful() {
803 let mut session = DataBackendSession::new(10);
804 session.add_batch_stream::<QuoteTick>(
805 batch_stream(vec![Ok(quote_batch(&[quote(1), quote(2)]))]),
806 None,
807 );
808
809 let mut result = DataQueryResult::new(session.get_query_result(), 10);
810 let chunk = result
811 .next()
812 .expect("chunked result must yield a chunk")
813 .expect("query must not fail");
814
815 assert_eq!(chunk.len(), 2);
816 assert_eq!(
817 chunk
818 .iter()
819 .map(|data| data.ts_init().as_u64())
820 .collect::<Vec<_>>(),
821 vec![1, 2]
822 );
823
824 let last = result
825 .next()
826 .expect("chunked result must signal exhaustion")
827 .expect("query must not fail");
828
829 assert!(last.is_empty());
830 }
831
832 #[rstest]
833 fn decode_batches_stops_polling_a_failed_stream() {
834 let polls = Arc::new(AtomicUsize::new(0));
835 let counted = Arc::clone(&polls);
836 let mut batches = vec![Ok(quote_batch(&[quote(1)])), Err(stream_error())].into_iter();
837 let inner = futures::stream::poll_fn(move |_| {
838 counted.fetch_add(1, Ordering::SeqCst);
839 Poll::Ready(batches.next())
840 });
841 let stream = Box::pin(RecordBatchStreamAdapter::new(
842 Arc::new(QuoteTick::get_schema(Some(quote_metadata()))),
843 inner,
844 ));
845
846 let decoded = decode_batches::<QuoteTick>(stream, None);
847 let items: Vec<_> = futures::executor::block_on_stream(Box::pin(decoded)).collect();
848
849 assert_eq!(items.len(), 2);
850 assert!(items[0].is_ok());
851 assert!(matches!(items[1], Err(QueryError::Stream(_))));
852 assert_eq!(polls.load(Ordering::SeqCst), 2);
853 }
854
855 #[rstest]
856 fn a_new_query_does_not_inherit_an_earlier_failure() {
857 let mut session = DataBackendSession::new(10);
858 session.add_batch_stream::<QuoteTick>(batch_stream(vec![Err(stream_error())]), None);
859
860 let failed = session.get_query_result();
862
863 session.add_batch_stream::<QuoteTick>(
864 batch_stream(vec![Ok(quote_batch(&[quote(1), quote(2)]))]),
865 None,
866 );
867 let items: Vec<_> = session.get_query_result().collect();
868
869 let failed: Vec<_> = failed.collect();
870
871 assert_eq!(ts_inits(&items), vec![1, 2]);
872 assert_eq!(items.len(), 2);
873 assert_eq!(failed.len(), 1);
874 assert!(
875 matches!(failed[0], Err(QueryError::Stream(_))),
876 "expected a stream error, was {:?}",
877 failed[0]
878 );
879 }
880
881 #[rstest]
882 #[cfg(feature = "python")]
883 fn python_to_list_raises_on_stream_error() {
884 let mut session = DataBackendSession::new(10);
885 session.add_batch_stream::<QuoteTick>(
886 batch_stream(vec![Ok(quote_batch(&[quote(1)])), Err(stream_error())]),
887 None,
888 );
889 let result = DataQueryResult::new(session.get_query_result(), 10);
890
891 Python::initialize();
892 Python::attach(|py| {
893 let result = Py::new(py, result).expect("failed to create the query result");
894 let error = result
895 .bind(py)
896 .call_method0("to_list")
897 .expect_err("to_list must raise when a stream fails");
898
899 assert!(error.is_instance_of::<PyRuntimeError>(py));
900 assert!(
901 error.to_string().contains("Record batch stream error"),
902 "was {error}"
903 );
904 });
905 }
906
907 #[rstest]
908 #[cfg(feature = "python")]
909 fn python_next_raises_on_decode_error() {
910 let mut session = DataBackendSession::new(10);
911 let batch =
913 QuoteTick::encode_batch(&HashMap::new(), &[quote(1)]).expect("failed to encode quotes");
914 session.add_batch_stream::<QuoteTick>(batch_stream(vec![Ok(batch)]), None);
915 let result = DataQueryResult::new(session.get_query_result(), 10);
916
917 Python::initialize();
918 Python::attach(|py| {
919 let result = Py::new(py, result).expect("failed to create the query result");
920 let error = result
921 .bind(py)
922 .call_method0("__next__")
923 .expect_err("__next__ must raise when a batch cannot be decoded");
924
925 assert!(error.is_instance_of::<PyRuntimeError>(py));
926 assert!(
927 error.to_string().contains("Record batch decode error"),
928 "was {error}"
929 );
930 });
931 }
932}