nautilus_persistence/backend/
session.rs1use std::{sync::Arc, vec::IntoIter};
17
18use ahash::{AHashMap, AHashSet};
19use datafusion::{
20 arrow::record_batch::RecordBatch, error::Result, logical_expr::expr::Sort,
21 physical_plan::SendableRecordBatchStream, prelude::*,
22};
23use futures::StreamExt;
24use nautilus_core::{UnixNanos, ffi::cvec::CVec};
25use nautilus_model::data::{Data, HasTsInit};
26use nautilus_serialization::arrow::{
27 DataStreamingError, DecodeDataFromRecordBatch, EncodeToRecordBatch, WriteStream,
28};
29use object_store::ObjectStore;
30use url::Url;
31
32use super::{
33 compare::Compare,
34 kmerge_batch::{EagerStream, ElementBatchIter, KMerge},
35};
36
37#[derive(Debug, Default)]
38pub struct TsInitComparator;
39
40impl<I> Compare<ElementBatchIter<I, Data>> for TsInitComparator
41where
42 I: Iterator<Item = IntoIter<Data>>,
43{
44 fn compare(
45 &self,
46 l: &ElementBatchIter<I, Data>,
47 r: &ElementBatchIter<I, Data>,
48 ) -> std::cmp::Ordering {
49 l.item.ts_init().cmp(&r.item.ts_init()).reverse()
51 }
52}
53
54pub type QueryResult = KMerge<EagerStream<std::vec::IntoIter<Data>>, Data, TsInitComparator>;
55
56#[cfg_attr(
62 feature = "python",
63 pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.persistence", unsendable)
64)]
65#[cfg_attr(
66 feature = "python",
67 pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.persistence")
68)]
69pub struct DataBackendSession {
70 pub chunk_size: usize,
71 pub runtime: Arc<tokio::runtime::Runtime>,
72 session_ctx: SessionContext,
73 batch_streams: Vec<EagerStream<IntoIter<Data>>>,
74 registered_tables: AHashSet<String>,
75}
76
77impl DataBackendSession {
78 #[must_use]
84 pub fn new(chunk_size: usize) -> Self {
85 let runtime = tokio::runtime::Builder::new_multi_thread()
86 .enable_all()
87 .build()
88 .unwrap();
89 let session_cfg = SessionConfig::new()
90 .set_str("datafusion.optimizer.repartition_file_scans", "false")
91 .set_str("datafusion.optimizer.prefer_existing_sort", "true");
92 let session_ctx = SessionContext::new_with_config(session_cfg);
93 Self {
94 session_ctx,
95 batch_streams: Vec::default(),
96 chunk_size,
97 runtime: Arc::new(runtime),
98 registered_tables: AHashSet::new(),
99 }
100 }
101
102 pub fn register_object_store(&mut self, url: &Url, object_store: Arc<dyn ObjectStore>) {
104 self.session_ctx.register_object_store(url, object_store);
105 }
106
107 pub fn register_object_store_from_uri(
114 &mut self,
115 uri: &str,
116 storage_options: Option<AHashMap<String, String>>,
117 ) -> anyhow::Result<()> {
118 let location =
119 crate::parquet::create_object_store_location_from_path(uri, storage_options)?;
120
121 if let Some(root_url) = location.store_root_url().cloned() {
122 self.register_object_store(&root_url, location.object_store);
123 }
124
125 Ok(())
126 }
127
128 pub fn write_data<T: EncodeToRecordBatch>(
134 data: &[T],
135 metadata: &AHashMap<String, String>,
136 stream: &mut dyn WriteStream,
137 ) -> Result<(), DataStreamingError> {
138 let metadata: std::collections::HashMap<String, String> = metadata
140 .iter()
141 .map(|(k, v)| (k.clone(), v.clone()))
142 .collect();
143 let record_batch = T::encode_batch(&metadata, data)?;
144 stream.write(&record_batch)?;
145 Ok(())
146 }
147
148 pub fn add_file<T>(
166 &mut self,
167 table_name: &str,
168 file_path: &str,
169 sql_query: Option<&str>,
170 custom_type_name: Option<&str>,
171 ) -> Result<()>
172 where
173 T: DecodeDataFromRecordBatch,
174 {
175 let is_new_table = !self.registered_tables.contains(table_name);
177
178 if is_new_table {
179 let parquet_options = ParquetReadOptions::<'_> {
181 skip_metadata: Some(false),
182 file_sort_order: vec![vec![Sort {
183 expr: col("ts_init"),
184 asc: true,
185 nulls_first: false,
186 }]],
187 ..Default::default()
188 };
189 self.runtime.block_on(self.session_ctx.register_parquet(
190 table_name,
191 file_path,
192 parquet_options,
193 ))?;
194
195 self.registered_tables.insert(table_name.to_string());
196
197 let default_query = format!("SELECT * FROM {table_name} ORDER BY ts_init");
199 let sql_query = sql_query.unwrap_or(&default_query);
200 let query = self.runtime.block_on(self.session_ctx.sql(sql_query))?;
201 let batch_stream = self.runtime.block_on(query.execute_stream())?;
202 self.add_batch_stream::<T>(batch_stream, custom_type_name.map(String::from));
203 }
204
205 Ok(())
206 }
207
208 pub fn collect_query_batches(
215 &mut self,
216 table_name: &str,
217 file_path: &str,
218 sql_query: Option<&str>,
219 ) -> Result<Vec<RecordBatch>> {
220 if !self.registered_tables.contains(table_name) {
221 let parquet_options = ParquetReadOptions::<'_> {
222 skip_metadata: Some(false),
223 file_sort_order: vec![vec![Sort {
224 expr: col("ts_init"),
225 asc: true,
226 nulls_first: false,
227 }]],
228 ..Default::default()
229 };
230 self.runtime.block_on(self.session_ctx.register_parquet(
231 table_name,
232 file_path,
233 parquet_options,
234 ))?;
235
236 self.registered_tables.insert(table_name.to_string());
237 }
238
239 let default_query = format!("SELECT * FROM {table_name} ORDER BY ts_init");
240 let sql_query = sql_query.unwrap_or(&default_query);
241 let query = self.runtime.block_on(self.session_ctx.sql(sql_query))?;
242 let mut batch_stream = self.runtime.block_on(query.execute_stream())?;
243
244 self.runtime.block_on(async {
245 let mut batches = Vec::new();
246 while let Some(batch) = batch_stream.next().await {
247 batches.push(batch?);
248 }
249 Ok::<_, datafusion::error::DataFusionError>(batches)
250 })
251 }
252
253 fn add_batch_stream<T>(
254 &mut self,
255 stream: SendableRecordBatchStream,
256 custom_type_name: Option<String>,
257 ) where
258 T: DecodeDataFromRecordBatch,
259 {
260 let transform = stream.map(move |result| match result {
261 Ok(batch) => {
262 let mut metadata: std::collections::HashMap<String, String> =
263 batch.schema().metadata().clone();
264
265 if let Some(ref tn) = custom_type_name {
266 metadata.insert("type_name".to_string(), tn.clone());
267 }
268 T::decode_data_batch(&metadata, batch).unwrap().into_iter()
269 }
270 Err(e) => panic!("Error getting next batch from RecordBatchStream: {e}"),
271 });
272
273 self.batch_streams
274 .push(EagerStream::from_stream_with_runtime(
275 transform,
276 self.runtime.clone(),
277 ));
278 }
279
280 pub fn get_query_result(&mut self) -> QueryResult {
285 let mut kmerge: KMerge<_, _, _> = KMerge::new(TsInitComparator);
286
287 self.batch_streams
288 .drain(..)
289 .for_each(|eager_stream| kmerge.push_iter(eager_stream));
290
291 kmerge
292 }
293
294 pub fn clear_registered_tables(&mut self) {
299 self.registered_tables.clear();
300 self.batch_streams.clear();
301
302 let session_cfg = SessionConfig::new()
304 .set_str("datafusion.optimizer.repartition_file_scans", "false")
305 .set_str("datafusion.optimizer.prefer_existing_sort", "true");
306 self.session_ctx = SessionContext::new_with_config(session_cfg);
307 }
308}
309
310#[must_use]
311pub fn build_query(
312 table: &str,
313 start: Option<UnixNanos>,
314 end: Option<UnixNanos>,
315 where_clause: Option<&str>,
316) -> String {
317 let mut conditions = Vec::new();
318
319 if let Some(clause) = where_clause {
321 conditions.push(clause.to_string());
322 }
323
324 if let Some(start_ts) = start {
326 conditions.push(format!("ts_init >= {start_ts}"));
327 }
328
329 if let Some(end_ts) = end {
331 conditions.push(format!("ts_init <= {end_ts}"));
332 }
333
334 let mut query = format!("SELECT * FROM {table}");
336
337 if !conditions.is_empty() {
339 query.push_str(" WHERE ");
340 query.push_str(&conditions.join(" AND "));
341 }
342
343 query.push_str(" ORDER BY ts_init");
345
346 query
347}
348
349#[cfg_attr(
350 feature = "python",
351 pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.persistence", unsendable)
352)]
353#[cfg_attr(
354 feature = "python",
355 pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.persistence")
356)]
357pub struct DataQueryResult {
358 pub chunk: Option<CVec>,
359 pub result: QueryResult,
360 pub acc: Vec<Data>,
361 pub size: usize,
362}
363
364impl DataQueryResult {
365 #[must_use]
367 pub const fn new(result: QueryResult, size: usize) -> Self {
368 Self {
369 chunk: None,
370 result,
371 acc: Vec::new(),
372 size,
373 }
374 }
375
376 pub fn set_chunk(&mut self, data: Vec<Data>) -> CVec {
380 self.drop_chunk();
381
382 let chunk: CVec = data.into();
383 self.chunk = Some(chunk);
384 chunk
385 }
386
387 pub fn drop_chunk(&mut self) {
396 if let Some(CVec { ptr, len, cap }) = self.chunk.take() {
397 assert!(
398 len <= cap,
399 "drop_chunk: len ({len}) > cap ({cap}) - memory corruption or wrong chunk type"
400 );
401 assert!(
402 len == 0 || !ptr.is_null(),
403 "drop_chunk: null ptr with non-zero len ({len}) - memory corruption"
404 );
405
406 let data: Vec<Data> = unsafe { Vec::from_raw_parts(ptr.cast::<Data>(), len, cap) };
409 drop(data);
410 }
411 }
412}
413
414impl Iterator for DataQueryResult {
415 type Item = Vec<Data>;
416
417 fn next(&mut self) -> Option<Self::Item> {
418 for _ in 0..self.size {
419 match self.result.next() {
420 Some(item) => self.acc.push(item),
421 None => break,
422 }
423 }
424
425 let mut acc: Vec<Data> = Vec::new();
428 std::mem::swap(&mut acc, &mut self.acc);
429 Some(acc)
430 }
431}
432
433impl Drop for DataQueryResult {
434 fn drop(&mut self) {
435 self.drop_chunk();
436 self.result.clear();
437 }
438}