1#![expect(
19 clippy::missing_errors_doc,
20 clippy::used_underscore_binding,
21 reason = "query methods forward DataFusion errors and underscore fields mirror SQL aliases"
22)]
23
24use nautilus_model::instruments::NautilusInstrumentType;
25use nautilus_serialization::arrow::{
26 catalog_identifier_from_metadata, instrument::decode_instrument_any_batch,
27 record_batch_with_identifier_column,
28};
29
30use super::{
31 ArrowSchemaProvider, Bar, CustomDataDecoder, Data, DecodeDataFromRecordBatch,
32 DecodeTypedFromRecordBatch, FundingRateUpdate, HasCatalogDataType, HasTsInit, HashMap,
33 INSTRUMENT_PATH_PREFIXES, InstrumentAny, InstrumentClose, NautilusDataType, OptionGreeks,
34 OrderBookDelta, OrderBookDepth, ParquetDataCatalog, Path, QuoteTick, RecordBatch, TradeTick,
35 UnixNanos, build_query, catalog_record_batch_to_display, datafusion,
36 decode_object_store_segment, extract_bar_type_instrument_id, extract_identifier_from_path,
37 extract_sql_safe_filename, filter_instruments_for_request_range, instrument_path_prefix,
38 is_monotonically_increasing_by_init, make_object_store_path, make_sql_safe_identifier,
39 parquet_data_path_prefix, parse_filename_timestamps, query_intersects_filename,
40 read_parquet_from_object_store, read_parquet_schema_from_object_store,
41 session::{MergedPages, TypedPages, decode_typed_pages},
42 urisafe_instrument_id,
43};
44use crate::{
45 catalog::types::{
46 CatalogDataType, custom_data_read_prefixes, custom_type_name,
47 parquet_catalog_data_type_path_prefixes, parquet_catalog_data_type_table_stem,
48 },
49 common::arrow::{empty_display_batch_with_identifier, validate_catalog_schema},
50};
51
52impl ParquetDataCatalog {
53 pub fn query<T>(
55 &mut self,
56 identifiers: Option<Vec<String>>,
57 start: Option<UnixNanos>,
58 end: Option<UnixNanos>,
59 where_clause: Option<&str>,
60 files: Option<Vec<String>>,
61 optimize_file_loading: bool,
62 ) -> anyhow::Result<crate::backend::session::QueryResult>
63 where
64 T: DecodeTypedFromRecordBatch
65 + HasCatalogDataType
66 + HasTsInit
67 + Into<Data>
68 + Send
69 + 'static,
70 {
71 self.query_typed_pages::<T>(
72 identifiers,
73 start,
74 end,
75 where_clause,
76 files,
77 optimize_file_loading,
78 )
79 .map(crate::backend::session::QueryResult::from_typed_pages)
80 }
81
82 pub fn query_instruments(
125 &self,
126 instrument_ids: Option<&[String]>,
127 ) -> anyhow::Result<Vec<InstrumentAny>> {
128 self.query_instruments_filtered(instrument_ids, None, None)
129 }
130
131 pub fn query_instruments_filtered(
137 &self,
138 instrument_ids: Option<&[String]>,
139 _start: Option<UnixNanos>,
140 end: Option<UnixNanos>,
141 ) -> anyhow::Result<Vec<InstrumentAny>> {
142 let instrument_files = self.discover_instrument_files(instrument_ids, end, None)?;
143 self.decode_instrument_files(instrument_files, _start, end)
144 }
145
146 pub fn query_instruments_filtered_with_where(
151 &mut self,
152 instrument_ids: Option<&[String]>,
153 start: Option<UnixNanos>,
154 end: Option<UnixNanos>,
155 where_clause: Option<&str>,
156 ) -> anyhow::Result<Vec<InstrumentAny>> {
157 self.query_instruments_filtered_with_where_and_type(
158 instrument_ids,
159 start,
160 end,
161 where_clause,
162 None,
163 )
164 }
165
166 pub fn query_instruments_filtered_with_where_and_type(
167 &mut self,
168 instrument_ids: Option<&[String]>,
169 start: Option<UnixNanos>,
170 end: Option<UnixNanos>,
171 where_clause: Option<&str>,
172 instrument_type: Option<&NautilusInstrumentType>,
173 ) -> anyhow::Result<Vec<InstrumentAny>> {
174 let Some(where_clause) = where_clause else {
175 let instrument_files =
176 self.discover_instrument_files(instrument_ids, end, instrument_type)?;
177 return self.decode_instrument_files(instrument_files, start, end);
178 };
179
180 self.session.clear_registered_tables();
181 self.register_remote_object_store()?;
182
183 let mut all_instruments = Vec::new();
184 let instrument_files =
185 self.discover_instrument_files(instrument_ids, end, instrument_type)?;
186
187 for (index, file_path) in instrument_files.into_iter().enumerate() {
188 let object_path = self.to_object_path_parsed(&file_path)?;
189 let (_, builder_schema) = self.execute_async(|| async {
190 read_parquet_from_object_store(self.object_store.clone(), &object_path).await
191 })?;
192 validate_catalog_schema(&builder_schema)?;
193 let metadata: std::collections::HashMap<String, String> =
194 builder_schema.metadata().clone();
195 let target_schema = InstrumentAny::get_schema(Some(metadata.clone()));
196
197 let table_name = format!(
198 "instruments_{}_{}",
199 index,
200 extract_sql_safe_filename(&file_path)
201 );
202 let query = build_query(&table_name, start, end, Some(where_clause));
203 let resolved_path = self.resolve_path_for_datafusion(&file_path);
204 let batches = self.session.collect_parquet_files_batches(
205 &table_name,
206 vec![resolved_path],
207 Some(&query),
208 )?;
209
210 for batch in batches {
211 let batch = datafusion::cast_record_batch_to_schema(&batch, &target_schema)?;
212 all_instruments.extend(decode_instrument_any_batch(&metadata, &batch)?);
213 }
214 }
215
216 Ok(filter_instruments_for_request_range(
217 all_instruments,
218 start,
219 end,
220 ))
221 }
222
223 fn discover_instrument_files(
226 &self,
227 instrument_ids: Option<&[String]>,
228 end: Option<UnixNanos>,
229 instrument_type: Option<&NautilusInstrumentType>,
230 ) -> anyhow::Result<Vec<String>> {
231 let base_dir = make_object_store_path(&self.base_path, ["data"]);
232 let end_u64 = end.map(|ts| ts.as_u64());
233 let list_result = self.list_objects(&base_dir)?;
234
235 let mut instrument_files = Vec::new();
236
237 for object in list_result {
238 let path_str = object.location.to_string();
239 if !path_str.ends_with(".parquet") {
240 continue;
241 }
242
243 let path_parts: Vec<&str> = path_str.split('/').collect();
244 let Some(data_index) = path_parts.iter().position(|part| *part == "data") else {
245 continue;
246 };
247 let Some(type_dir) = path_parts.get(data_index + 1) else {
248 continue;
249 };
250
251 let type_dir = decode_object_store_segment(type_dir);
252 if !is_parquet_instrument_type_prefix(&type_dir)
253 || instrument_type.is_some_and(|value| instrument_path_prefix(value) != type_dir)
254 {
255 continue;
256 }
257
258 if path_parts.len() < data_index + 4 {
259 continue;
260 }
261
262 let instrument_id_dir = decode_object_store_segment(path_parts[path_parts.len() - 2]);
263
264 if let Some(ids) = instrument_ids
265 && !ids
266 .iter()
267 .map(|id| urisafe_instrument_id(id))
268 .any(|x| x.as_str() == urisafe_instrument_id(&instrument_id_dir))
269 {
270 continue;
271 }
272
273 let include_file = if path_str.ends_with("/instrument.parquet") {
274 true
275 } else if let Some((file_start, _)) = parse_filename_timestamps(&path_str) {
276 end_u64.is_none_or(|end| file_start <= end)
277 } else {
278 log::warn!(
281 "Including instrument file with unparsable interval filename: {path_str}"
282 );
283 true
284 };
285
286 if include_file {
287 instrument_files.push(path_str);
288 }
289 }
290
291 instrument_files.sort();
292 Ok(instrument_files)
293 }
294
295 fn decode_instrument_files(
296 &self,
297 instrument_files: Vec<String>,
298 start: Option<UnixNanos>,
299 end: Option<UnixNanos>,
300 ) -> anyhow::Result<Vec<InstrumentAny>> {
301 let mut instruments = Vec::new();
302
303 for file_path in instrument_files {
304 let object_path = self.to_object_path_parsed(&file_path)?;
305 let (batches, builder_schema) = self.execute_async(|| async {
306 read_parquet_from_object_store(self.object_store.clone(), &object_path).await
307 })?;
308 validate_catalog_schema(&builder_schema)?;
309 let metadata = builder_schema.metadata().clone();
310 let target_schema = InstrumentAny::get_schema(Some(metadata.clone()));
311
312 for batch in batches {
313 let batch = datafusion::cast_record_batch_to_schema(&batch, &target_schema)?;
314 instruments.extend(decode_instrument_any_batch(&metadata, &batch)?);
315 }
316 }
317
318 Ok(filter_instruments_for_request_range(
319 instruments,
320 start,
321 end,
322 ))
323 }
324
325 pub fn query_typed_data<T>(
433 &mut self,
434 identifiers: Option<Vec<String>>,
435 start: Option<UnixNanos>,
436 end: Option<UnixNanos>,
437 where_clause: Option<&str>,
438 files: Option<Vec<String>>,
439 optimize_file_loading: bool,
440 ) -> anyhow::Result<Vec<T>>
441 where
442 T: DecodeTypedFromRecordBatch + HasCatalogDataType + HasTsInit,
443 {
444 self.query_typed::<T>(
445 identifiers,
446 start,
447 end,
448 where_clause,
449 files,
450 optimize_file_loading,
451 )
452 }
453
454 pub(super) fn query_typed_pages<T>(
455 &mut self,
456 identifiers: Option<Vec<String>>,
457 start: Option<UnixNanos>,
458 end: Option<UnixNanos>,
459 where_clause: Option<&str>,
460 files: Option<Vec<String>>,
461 optimize_file_loading: bool,
462 ) -> anyhow::Result<TypedPages<T>>
463 where
464 T: DecodeTypedFromRecordBatch + HasCatalogDataType + HasTsInit + Send + 'static,
465 {
466 self.clear_session_tables();
467 self.register_remote_object_store()?;
468 let data_type = T::catalog_data_type();
469 let files = match files {
470 Some(files) => files,
471 None => self.query_files(&CatalogDataType::Data(data_type), identifiers, start, end)?,
472 };
473 let paths = if optimize_file_loading {
474 parent_directories(&files)
475 .into_iter()
476 .map(|directory| self.resolve_directory_for_datafusion(&directory))
477 .collect::<Vec<_>>()
478 } else {
479 files
480 .iter()
481 .map(|file| self.resolve_path_for_datafusion(file))
482 .collect()
483 };
484 let mut sources = Vec::with_capacity(paths.len());
485 for (index, path) in paths.into_iter().enumerate() {
486 let table = format!("parquet_{index}");
487 let sql = build_query(&table, start, end, where_clause);
488 let stream = self
489 .session
490 .parquet_files_batch_stream(&table, vec![path], Some(&sql))?;
491 let pages = decode_typed_pages::<T>(stream);
492 sources.push(
493 Box::new(datafusion::BlockingBatchStream::from_stream_with_runtime(
494 pages,
495 &self.session.runtime,
496 )) as TypedPages<T>,
497 );
498 }
499 Ok(Box::new(MergedPages::new(sources, self.batch_size)))
500 }
501
502 pub fn query_typed<T>(
504 &mut self,
505 identifiers: Option<Vec<String>>,
506 start: Option<UnixNanos>,
507 end: Option<UnixNanos>,
508 where_clause: Option<&str>,
509 files: Option<Vec<String>>,
510 optimize_file_loading: bool,
511 ) -> anyhow::Result<Vec<T>>
512 where
513 T: DecodeTypedFromRecordBatch + HasCatalogDataType + HasTsInit,
514 {
515 self.clear_session_tables();
516
517 self.register_remote_object_store()?;
518
519 let data_type = T::catalog_data_type();
520 let path_prefix = parquet_data_path_prefix(&data_type);
521
522 let files_list = if let Some(files) = files {
523 files
524 } else {
525 self.query_files(
526 &CatalogDataType::Data(data_type.clone()),
527 identifiers,
528 start,
529 end,
530 )?
531 };
532
533 let mut all_records = Vec::new();
534
535 if optimize_file_loading {
536 for directory in parent_directories(&files_list) {
537 let identifier = dir_identifier(&directory);
538 let safe_sql_identifier = make_sql_safe_identifier(&identifier);
539 let table_name = format!("{}_{}", path_prefix.as_ref(), safe_sql_identifier);
540 let query = build_query(&table_name, start, end, where_clause);
541 let resolved_path = self.resolve_directory_for_datafusion(&directory);
542 let batches = self.session.collect_parquet_files_batches(
543 &table_name,
544 vec![resolved_path],
545 Some(&query),
546 )?;
547
548 all_records.extend(self.convert_record_batches_to_typed::<T>(batches)?);
549 }
550 } else {
551 for file_uri in &files_list {
552 let identifier = extract_identifier_from_path(file_uri).ok_or_else(|| {
553 anyhow::anyhow!("Cannot extract identifier from path '{file_uri}'")
554 })?;
555 let safe_sql_identifier = make_sql_safe_identifier(identifier);
556 let safe_filename = extract_sql_safe_filename(file_uri);
557 let table_name = format!(
558 "{}_{}_{}",
559 path_prefix.as_ref(),
560 safe_sql_identifier,
561 safe_filename
562 );
563 let query = build_query(&table_name, start, end, where_clause);
564 let resolved_path = self.resolve_path_for_datafusion(file_uri);
565 let batches = self.session.collect_parquet_files_batches(
566 &table_name,
567 vec![resolved_path],
568 Some(&query),
569 )?;
570
571 all_records.extend(self.convert_record_batches_to_typed::<T>(batches)?);
572 }
573 }
574
575 if !is_monotonically_increasing_by_init(&all_records) {
576 all_records.sort_by_key(HasTsInit::ts_init);
577 }
578
579 Ok(all_records)
580 }
581
582 pub fn query_record_batches(
588 &mut self,
589 data_type: &CatalogDataType,
590 identifier: Option<String>,
591 start: Option<UnixNanos>,
592 end: Option<UnixNanos>,
593 where_clause: Option<&str>,
594 optimize_file_loading: bool,
595 ) -> anyhow::Result<Vec<RecordBatch>> {
596 self.clear_session_tables();
597 self.register_remote_object_store()?;
598
599 let identifiers = identifier.map(|value| vec![value]);
600 let files_list = self.query_files(data_type, identifiers, start, end)?;
601 let mut record_batches = Vec::new();
602 let table_prefix =
603 make_sql_safe_identifier(&parquet_catalog_data_type_table_stem(data_type));
604
605 if optimize_file_loading {
606 for (index, directory) in parent_directories(&files_list).into_iter().enumerate() {
608 let table_name = format!("{table_prefix}_{index}");
609 let query = build_query(&table_name, start, end, where_clause);
610 let resolved_path = self.resolve_directory_for_datafusion(&directory);
611 record_batches.extend(self.session.collect_parquet_files_batches(
612 &table_name,
613 vec![resolved_path],
614 Some(&query),
615 )?);
616 }
617 } else {
618 for (index, file_uri) in files_list.iter().enumerate() {
619 let table_name = format!("{table_prefix}_{index}");
620 let query = build_query(&table_name, start, end, where_clause);
621 let resolved_path = self.resolve_path_for_datafusion(file_uri);
622 record_batches.extend(self.session.collect_parquet_files_batches(
623 &table_name,
624 vec![resolved_path],
625 Some(&query),
626 )?);
627 }
628 }
629
630 Ok(record_batches)
631 }
632
633 pub fn query_display_record_batches(
640 &mut self,
641 data_type: &NautilusDataType,
642 identifiers: Option<Vec<String>>,
643 start: Option<UnixNanos>,
644 end: Option<UnixNanos>,
645 where_clause: Option<&str>,
646 optimize_file_loading: bool,
647 ) -> anyhow::Result<Vec<RecordBatch>> {
648 self.clear_session_tables();
649 self.register_remote_object_store()?;
650
651 let data_path_prefix = parquet_data_path_prefix(data_type);
652 let files_list = self.query_files(
653 &CatalogDataType::Data(data_type.clone()),
654 identifiers,
655 start,
656 end,
657 )?;
658 let mut display_batches = Vec::new();
659 let table_prefix = make_sql_safe_identifier(data_path_prefix.as_ref());
660
661 if optimize_file_loading {
662 for (index, directory) in parent_directories(&files_list).into_iter().enumerate() {
664 let path_identifier = display_identifier(data_type, &directory);
665 let table_name = format!("{table_prefix}_{index}");
666 let query = build_query(&table_name, start, end, where_clause);
667 let resolved_path = self.resolve_directory_for_datafusion(&directory);
668 let batches = self.session.collect_parquet_files_batches(
669 &table_name,
670 vec![resolved_path],
671 Some(&query),
672 )?;
673
674 for batch in batches {
675 let identifier =
676 display_batch_identifier(data_type, &batch, path_identifier.as_deref());
677 let batch = record_batch_with_identifier_column(batch, identifier.as_deref())?;
678 let metadata = batch.schema().metadata().clone();
679 display_batches.push(catalog_record_batch_to_display(
680 data_type, &metadata, &batch,
681 )?);
682 }
683 }
684 } else {
685 for (index, file_uri) in files_list.iter().enumerate() {
686 let directory = Path::new(file_uri)
687 .parent()
688 .ok_or_else(|| anyhow::anyhow!("Cannot extract directory from '{file_uri}'"))?
689 .to_string_lossy();
690 let path_identifier = display_identifier(data_type, &directory);
691 let table_name = format!("{table_prefix}_{index}");
692 let query = build_query(&table_name, start, end, where_clause);
693 let resolved_path = self.resolve_path_for_datafusion(file_uri);
694 let batches = self.session.collect_parquet_files_batches(
695 &table_name,
696 vec![resolved_path],
697 Some(&query),
698 )?;
699
700 for batch in batches {
701 let identifier =
702 display_batch_identifier(data_type, &batch, path_identifier.as_deref());
703 let batch = record_batch_with_identifier_column(batch, identifier.as_deref())?;
704 let metadata = batch.schema().metadata().clone();
705 display_batches.push(catalog_record_batch_to_display(
706 data_type, &metadata, &batch,
707 )?);
708 }
709 }
710 }
711
712 if display_batches.is_empty() {
713 display_batches.push(empty_display_batch_with_identifier(data_type)?);
714 }
715 Ok(display_batches)
716 }
717
718 pub fn query_identifiers(
720 &mut self,
721 data_type: &CatalogDataType,
722 identifiers: Option<Vec<String>>,
723 start: Option<UnixNanos>,
724 end: Option<UnixNanos>,
725 where_clause: Option<&str>,
726 _optimize_file_loading: bool,
727 ) -> anyhow::Result<Vec<String>> {
728 self.clear_session_tables();
729 self.register_remote_object_store()?;
730
731 let files_list = self.query_files(data_type, identifiers, start, end)?;
732 let table_prefix =
733 make_sql_safe_identifier(&parquet_catalog_data_type_table_stem(data_type));
734 let mut identifiers = Vec::new();
735
736 for (index, directory) in parent_directories(&files_list).into_iter().enumerate() {
737 let identifier = dir_identifier(&directory);
738 let table_name = format!("{table_prefix}_{index}_identifier_check");
739 let query = format!(
740 "{} LIMIT 1",
741 build_query(&table_name, start, end, where_clause)
742 );
743 let resolved_path = self.resolve_directory_for_datafusion(&directory);
744 let batches = self.session.collect_parquet_files_batches(
745 &table_name,
746 vec![resolved_path],
747 Some(&query),
748 )?;
749
750 if batches.iter().any(|batch| batch.num_rows() != 0) {
751 identifiers.push(decode_object_store_segment(&identifier));
752 }
753 }
754
755 identifiers.sort();
756 identifiers.dedup();
757 Ok(identifiers)
758 }
759
760 #[expect(clippy::too_many_arguments)]
786 pub fn query_custom_data_dynamic(
787 &mut self,
788 type_name: &str,
789 identifiers: Option<&[String]>,
790 start: Option<UnixNanos>,
791 end: Option<UnixNanos>,
792 where_clause: Option<&str>,
793 files: Option<Vec<String>>,
794 _optimize_file_loading: bool,
795 ) -> anyhow::Result<Vec<Data>> {
796 self.clear_session_tables();
797
798 self.register_remote_object_store()?;
799
800 let files = if let Some(f) = files {
801 f.into_iter()
802 .map(|p| self.to_object_path(&p).map(|op| op.to_string()))
803 .collect::<anyhow::Result<Vec<_>>>()?
804 } else {
805 self.list_parquet_files_with_criteria(
806 &CatalogDataType::Data(NautilusDataType::Custom {
807 type_name: type_name.to_string(),
808 }),
809 identifiers,
810 start,
811 end,
812 )?
813 };
814
815 if files.is_empty() {
816 return Ok(Vec::new());
817 }
818
819 let mut lookup_metadata = HashMap::new();
823 lookup_metadata.insert("type_name".to_string(), type_name.to_string());
824 let registered_schema = CustomDataDecoder::get_schema(Some(lookup_metadata.clone()));
825 registered_schema.field_with_name("ts_init").map_err(|_| {
826 anyhow::anyhow!(
827 "custom data type '{type_name}' is not registered with an Arrow schema containing ts_init; \
828 call ensure_custom_data_registered::<T>() before querying"
829 )
830 })?;
831
832 let mut all_data = Vec::new();
833
834 for file in files {
835 let object_path = self.to_object_path_parsed(&file)?;
836 let mut decode_metadata = self.execute_async(|| async {
837 let schema =
838 read_parquet_schema_from_object_store(self.object_store.clone(), &object_path)
839 .await?;
840 validate_catalog_schema(&schema)?;
841 Ok::<HashMap<String, String>, anyhow::Error>(schema.metadata().clone())
842 })?;
843 decode_metadata.extend(lookup_metadata.clone());
844 let identifier = extract_identifier_from_path(&file)
845 .ok_or_else(|| anyhow::anyhow!("Cannot extract identifier from path '{file}'"))?;
846 let (data_cls, _) = self.extract_data_cls_and_identifier_from_path(&file)?;
849 let layout_tag = make_sql_safe_identifier(data_cls.as_deref().unwrap_or("custom"));
850 let safe_type_name = make_sql_safe_identifier(type_name);
851 let safe_sql_identifier = make_sql_safe_identifier(identifier);
852 let safe_filename = extract_sql_safe_filename(&file);
853 let table_name = format!(
854 "custom_{safe_type_name}_{layout_tag}_{safe_sql_identifier}_{safe_filename}"
855 );
856 let resolved_path = self.resolve_path_for_datafusion(&file);
857 let sql_query = build_query(&table_name, start, end, where_clause);
858
859 let batches = self.session.collect_parquet_files_batches(
863 &table_name,
864 vec![resolved_path],
865 Some(&sql_query),
866 )?;
867
868 for batch in batches {
869 all_data.extend(CustomDataDecoder::decode_data_batch(
870 &decode_metadata,
871 batch,
872 )?);
873 }
874 }
875 all_data.sort_by_key(HasTsInit::ts_init);
876 Ok(all_data)
877 }
878
879 pub fn query_files(
934 &self,
935 data_type: &CatalogDataType,
936 identifiers: Option<Vec<String>>,
937 start: Option<UnixNanos>,
938 end: Option<UnixNanos>,
939 ) -> anyhow::Result<Vec<String>> {
940 let identifiers = identifiers.map(Vec::into_boxed_slice);
942
943 if let Some(type_name) = custom_type_name(data_type) {
944 let mut files = Vec::new();
945 for prefix in custom_data_read_prefixes(type_name) {
946 files.extend(self.query_prefix_files(
947 prefix.as_ref(),
948 identifiers.as_deref(),
949 start,
950 end,
951 )?);
952 }
953
954 files.sort();
955 files.dedup();
956 return Ok(files);
957 }
958
959 let mut files = Vec::new();
960 for data_cls in parquet_catalog_data_type_path_prefixes(data_type) {
961 files.extend(self.query_prefix_files(
962 data_cls.as_ref(),
963 identifiers.as_deref(),
964 start,
965 end,
966 )?);
967 }
968 files.sort();
969
970 Ok(files)
971 }
972
973 fn query_prefix_files(
974 &self,
975 data_cls: &str,
976 identifiers: Option<&[String]>,
977 start: Option<UnixNanos>,
978 end: Option<UnixNanos>,
979 ) -> anyhow::Result<Vec<String>> {
980 let mut files = Vec::new();
981
982 let start_u64 = start.map(|s| s.as_u64());
983 let end_u64 = end.map(|e| e.as_u64());
984
985 let base_dir = self.make_path(data_cls, None)?;
986
987 let list_result = self.list_objects(&base_dir)?;
989
990 let mut file_paths: Vec<String> = list_result
991 .into_iter()
992 .filter_map(|object| {
993 let path_str = object.location.to_string();
994 if path_str.ends_with(".parquet") {
995 Some(path_str)
996 } else {
997 None
998 }
999 })
1000 .collect();
1001
1002 if let Some(identifiers) = identifiers {
1004 let safe_identifiers: Vec<String> = identifiers
1005 .iter()
1006 .map(|id| urisafe_instrument_id(id))
1007 .collect();
1008
1009 let exact_match_file_paths: Vec<String> = file_paths
1011 .iter()
1012 .filter(|file_path| {
1013 let path_parts: Vec<&str> = file_path.split('/').collect();
1015 if path_parts.len() >= 2 {
1016 let dir_name =
1017 decode_object_store_segment(path_parts[path_parts.len() - 2]);
1018 safe_identifiers.iter().any(|safe_id| safe_id == &dir_name)
1019 } else {
1020 false
1021 }
1022 })
1023 .cloned()
1024 .collect();
1025
1026 if exact_match_file_paths.is_empty() && is_parquet_bar_prefix(data_cls) {
1027 file_paths.retain(|file_path| {
1028 let path_parts: Vec<&str> = file_path.split('/').collect();
1029 if path_parts.len() >= 2 {
1030 let dir_name =
1031 decode_object_store_segment(path_parts[path_parts.len() - 2]);
1032
1033 if let Some(bar_instrument_id) = extract_bar_type_instrument_id(&dir_name) {
1034 safe_identifiers.iter().any(|id| id == bar_instrument_id)
1035 } else {
1036 false
1037 }
1038 } else {
1039 false
1040 }
1041 });
1042 } else {
1043 file_paths = exact_match_file_paths;
1044 }
1045 }
1046
1047 file_paths.retain(|file_path| query_intersects_filename(file_path, start_u64, end_u64));
1049
1050 for file_path in file_paths {
1051 files.push(self.path_for_query_list(&file_path));
1052 }
1053
1054 Ok(files)
1055 }
1056
1057 pub fn quote_ticks(
1058 &mut self,
1059 instrument_ids: Option<Vec<String>>,
1060 start: Option<UnixNanos>,
1061 end: Option<UnixNanos>,
1062 ) -> anyhow::Result<Vec<QuoteTick>> {
1063 self.query_typed_data::<QuoteTick>(instrument_ids, start, end, None, None, true)
1064 }
1065
1066 pub fn trade_ticks(
1068 &mut self,
1069 instrument_ids: Option<Vec<String>>,
1070 start: Option<UnixNanos>,
1071 end: Option<UnixNanos>,
1072 ) -> anyhow::Result<Vec<TradeTick>> {
1073 self.query_typed_data::<TradeTick>(instrument_ids, start, end, None, None, true)
1074 }
1075
1076 pub fn bars(
1078 &mut self,
1079 instrument_ids: Option<Vec<String>>,
1080 start: Option<UnixNanos>,
1081 end: Option<UnixNanos>,
1082 ) -> anyhow::Result<Vec<Bar>> {
1083 self.query_typed_data::<Bar>(instrument_ids, start, end, None, None, true)
1084 }
1085
1086 pub fn order_book_deltas(
1088 &mut self,
1089 instrument_ids: Option<Vec<String>>,
1090 start: Option<UnixNanos>,
1091 end: Option<UnixNanos>,
1092 ) -> anyhow::Result<Vec<OrderBookDelta>> {
1093 self.query_typed_data::<OrderBookDelta>(instrument_ids, start, end, None, None, true)
1094 }
1095
1096 pub fn order_book_depths(
1098 &mut self,
1099 instrument_ids: Option<Vec<String>>,
1100 start: Option<UnixNanos>,
1101 end: Option<UnixNanos>,
1102 ) -> anyhow::Result<Vec<OrderBookDepth>> {
1103 self.query_typed_data::<OrderBookDepth>(instrument_ids, start, end, None, None, true)
1104 }
1105
1106 pub fn funding_rates(
1108 &mut self,
1109 instrument_ids: Option<Vec<String>>,
1110 start: Option<UnixNanos>,
1111 end: Option<UnixNanos>,
1112 ) -> anyhow::Result<Vec<FundingRateUpdate>> {
1113 self.query_typed::<FundingRateUpdate>(instrument_ids, start, end, None, None, true)
1114 }
1115
1116 pub fn instrument_closes(
1118 &mut self,
1119 instrument_ids: Option<Vec<String>>,
1120 start: Option<UnixNanos>,
1121 end: Option<UnixNanos>,
1122 ) -> anyhow::Result<Vec<InstrumentClose>> {
1123 self.query_typed_data::<InstrumentClose>(instrument_ids, start, end, None, None, true)
1124 }
1125
1126 pub fn option_greeks(
1128 &mut self,
1129 instrument_ids: Option<Vec<String>>,
1130 start: Option<UnixNanos>,
1131 end: Option<UnixNanos>,
1132 ) -> anyhow::Result<Vec<OptionGreeks>> {
1133 self.query_typed_data::<OptionGreeks>(instrument_ids, start, end, None, None, true)
1134 }
1135
1136 pub fn instruments(
1138 &self,
1139 instrument_ids: Option<&[String]>,
1140 start: Option<UnixNanos>,
1141 end: Option<UnixNanos>,
1142 ) -> anyhow::Result<Vec<InstrumentAny>> {
1143 self.query_instruments_filtered(instrument_ids, start, end)
1144 }
1145
1146 pub fn get_file_list_from_data_cls(
1186 &self,
1187 data_type: &CatalogDataType,
1188 ) -> anyhow::Result<Vec<String>> {
1189 if let Some(type_name) = custom_type_name(data_type) {
1190 let mut file_paths = Vec::new();
1191 for prefix in custom_data_read_prefixes(type_name) {
1192 file_paths.extend(self.prefix_file_list(prefix.as_ref())?);
1193 }
1194
1195 file_paths.sort();
1196 file_paths.dedup();
1197 return Ok(file_paths);
1198 }
1199
1200 let mut file_paths = Vec::new();
1201 for data_cls in parquet_catalog_data_type_path_prefixes(data_type) {
1202 file_paths.extend(self.prefix_file_list(data_cls.as_ref())?);
1203 }
1204
1205 Ok(file_paths)
1206 }
1207
1208 fn prefix_file_list(&self, data_cls: &str) -> anyhow::Result<Vec<String>> {
1209 let base_dir = self.make_path(data_cls, None)?;
1210
1211 let list_result = self.list_objects(&base_dir)?;
1212
1213 let file_paths: Vec<String> = list_result
1214 .into_iter()
1215 .filter_map(|object| {
1216 let path_str = object.location.to_string();
1217 if path_str.ends_with(".parquet") {
1218 Some(path_str)
1219 } else {
1220 None
1221 }
1222 })
1223 .collect();
1224
1225 Ok(file_paths)
1226 }
1227
1228 pub fn filter_files(
1278 &self,
1279 data_type: &CatalogDataType,
1280 file_paths: Vec<String>,
1281 identifiers: Option<Vec<String>>,
1282 start: Option<UnixNanos>,
1283 end: Option<UnixNanos>,
1284 ) -> anyhow::Result<Vec<String>> {
1285 let has_bar_prefix = parquet_catalog_data_type_path_prefixes(data_type)
1286 .iter()
1287 .any(|data_cls| is_parquet_bar_prefix(data_cls.as_ref()));
1288 let mut filtered_paths = file_paths;
1289
1290 if let Some(identifiers) = identifiers {
1292 let safe_identifiers: Vec<String> = identifiers
1293 .iter()
1294 .map(|id| urisafe_instrument_id(id))
1295 .collect();
1296
1297 let file_safe_identifiers: Vec<String> = filtered_paths
1299 .iter()
1300 .map(|file_path| {
1301 let path_parts: Vec<&str> = file_path.split('/').collect();
1302 if path_parts.len() >= 2 {
1303 decode_object_store_segment(path_parts[path_parts.len() - 2])
1304 } else {
1305 String::new()
1306 }
1307 })
1308 .collect();
1309
1310 let exact_match_file_paths: Vec<String> = filtered_paths
1312 .iter()
1313 .enumerate()
1314 .filter_map(|(i, file_path)| {
1315 let dir_name = &file_safe_identifiers[i];
1316 if safe_identifiers.iter().any(|safe_id| safe_id == dir_name) {
1317 Some(file_path.clone())
1318 } else {
1319 None
1320 }
1321 })
1322 .collect();
1323
1324 if exact_match_file_paths.is_empty() && has_bar_prefix {
1325 filtered_paths.retain(|file_path| {
1327 let path_parts: Vec<&str> = file_path.split('/').collect();
1328 if path_parts.len() >= 2 {
1329 let dir_name =
1330 decode_object_store_segment(path_parts[path_parts.len() - 2]);
1331 safe_identifiers
1332 .iter()
1333 .any(|safe_id| dir_name.starts_with(&format!("{safe_id}-")))
1334 } else {
1335 false
1336 }
1337 });
1338 } else {
1339 filtered_paths = exact_match_file_paths;
1340 }
1341 }
1342
1343 let start_u64 = start.map(|s| s.as_u64());
1345 let end_u64 = end.map(|e| e.as_u64());
1346 filtered_paths.retain(|file_path| query_intersects_filename(file_path, start_u64, end_u64));
1347
1348 Ok(filtered_paths)
1349 }
1350}
1351
1352fn is_parquet_instrument_type_prefix(prefix: &str) -> bool {
1353 INSTRUMENT_PATH_PREFIXES.contains(&prefix)
1354}
1355
1356pub(super) fn is_parquet_bar_prefix(data_cls: &str) -> bool {
1357 data_cls == parquet_data_path_prefix(&NautilusDataType::Bar).as_ref()
1358}
1359
1360fn parent_directories(files: &[String]) -> Vec<String> {
1363 let mut directories: Vec<String> = files
1364 .iter()
1365 .filter_map(|file_uri| {
1366 Path::new(file_uri)
1367 .parent()
1368 .map(|path| path.to_string_lossy().to_string())
1369 })
1370 .collect();
1371 directories.sort();
1372 directories.dedup();
1373 directories
1374}
1375
1376fn dir_identifier(directory: &str) -> String {
1378 directory
1379 .rsplit('/')
1380 .next()
1381 .unwrap_or("unknown")
1382 .to_string()
1383}
1384
1385fn display_identifier(data_type: &NautilusDataType, directory: &str) -> Option<String> {
1386 let identifier = dir_identifier(directory);
1387 let is_unpartitioned_custom = matches!(data_type, NautilusDataType::Custom { .. })
1388 && Path::new(directory)
1389 .ends_with(Path::new("data").join(parquet_data_path_prefix(data_type).as_ref()));
1390
1391 (!is_unpartitioned_custom).then(|| decode_object_store_segment(&identifier))
1392}
1393
1394fn display_batch_identifier(
1395 data_type: &NautilusDataType,
1396 batch: &RecordBatch,
1397 path_identifier: Option<&str>,
1398) -> Option<String> {
1399 if matches!(data_type, NautilusDataType::Custom { .. }) {
1400 path_identifier.map(str::to_string)
1401 } else {
1402 catalog_identifier_from_metadata(batch.schema().metadata())
1403 .or_else(|| path_identifier.map(str::to_string))
1404 }
1405}