1#![expect(
19 clippy::missing_errors_doc,
20 reason = "query methods forward DataFusion errors"
21)]
22
23use nautilus_model::instruments::NautilusInstrumentType;
24use nautilus_serialization::arrow::{
25 catalog_identifier_from_metadata, instrument::decode_instrument_any_batch,
26 record_batch_with_identifier_column,
27};
28
29use super::{
30 ArrowSchemaProvider, Bar, CustomDataDecoder, Data, DecodeDataFromRecordBatch,
31 DecodeTypedFromRecordBatch, FundingRateUpdate, HasCatalogDataType, HasTsInit, HashMap,
32 INSTRUMENT_PATH_PREFIXES, InstrumentAny, InstrumentClose, NautilusDataType, OptionGreeks,
33 OrderBookDelta, OrderBookDepth, ParquetDataCatalog, Path, QuoteTick, RecordBatch, TradeTick,
34 UnixNanos, build_query, catalog_record_batch_to_display, datafusion,
35 decode_object_store_segment, extract_bar_type_instrument_id, extract_identifier_from_path,
36 extract_sql_safe_filename, filter_instruments_for_request_range, instrument_path_prefix,
37 is_monotonically_increasing_by_init, make_object_store_path, make_sql_safe_identifier,
38 parquet_data_path_prefix, parse_filename_timestamps, query_intersects_filename,
39 read_parquet_from_object_store, read_parquet_schema_from_object_store,
40 session::{MergedPages, TypedPages, decode_typed_pages},
41 urisafe_instrument_id,
42};
43use crate::{
44 catalog::types::{
45 CatalogDataType, parquet_catalog_data_type_path_prefixes,
46 parquet_catalog_data_type_table_stem,
47 },
48 common::arrow::{empty_display_batch_with_identifier, validate_catalog_schema},
49};
50
51impl ParquetDataCatalog {
52 pub fn query_instruments(
95 &self,
96 instrument_ids: Option<&[String]>,
97 ) -> anyhow::Result<Vec<InstrumentAny>> {
98 self.query_instruments_filtered(instrument_ids, None, None)
99 }
100
101 pub fn query_instruments_filtered(
107 &self,
108 instrument_ids: Option<&[String]>,
109 start: Option<UnixNanos>,
110 end: Option<UnixNanos>,
111 ) -> anyhow::Result<Vec<InstrumentAny>> {
112 let instrument_files = self.discover_instrument_files(instrument_ids, end, None)?;
113 self.decode_instrument_files(instrument_files, start, end)
114 }
115
116 pub fn query_instruments_filtered_with_where(
121 &mut self,
122 instrument_ids: Option<&[String]>,
123 start: Option<UnixNanos>,
124 end: Option<UnixNanos>,
125 where_clause: Option<&str>,
126 ) -> anyhow::Result<Vec<InstrumentAny>> {
127 self.query_instruments_filtered_with_where_and_type(
128 instrument_ids,
129 start,
130 end,
131 where_clause,
132 None,
133 )
134 }
135
136 pub fn query_instruments_filtered_with_where_and_type(
141 &mut self,
142 instrument_ids: Option<&[String]>,
143 start: Option<UnixNanos>,
144 end: Option<UnixNanos>,
145 where_clause: Option<&str>,
146 instrument_type: Option<&NautilusInstrumentType>,
147 ) -> anyhow::Result<Vec<InstrumentAny>> {
148 let Some(where_clause) = where_clause else {
149 let instrument_files =
150 self.discover_instrument_files(instrument_ids, end, instrument_type)?;
151 return self.decode_instrument_files(instrument_files, start, end);
152 };
153
154 self.clear_session_tables();
155 self.register_remote_object_store()?;
156
157 let mut all_instruments = Vec::new();
158 let instrument_files =
159 self.discover_instrument_files(instrument_ids, end, instrument_type)?;
160
161 for (index, file_path) in instrument_files.into_iter().enumerate() {
162 let object_path = self.to_object_path_parsed(&file_path)?;
163
164 let (_, builder_schema) = self.execute_async(|| async {
165 read_parquet_from_object_store(self.object_store.clone(), &object_path).await
166 })?;
167
168 validate_catalog_schema(&builder_schema)?;
169 let metadata = builder_schema.metadata().clone();
170 let target_schema = InstrumentAny::get_schema(Some(metadata.clone()));
171
172 let table_name = format!(
173 "instruments_{}_{}",
174 index,
175 extract_sql_safe_filename(&file_path)
176 );
177
178 let query = build_query(&table_name, None, end, Some(where_clause));
180 let resolved_path = self.resolve_path_for_datafusion(&file_path);
181 let batches = self.session.collect_parquet_files_batches(
182 &table_name,
183 vec![resolved_path],
184 Some(&query),
185 )?;
186
187 for batch in batches {
188 let batch = datafusion::cast_record_batch_to_schema(&batch, &target_schema)?;
189 all_instruments.extend(decode_instrument_any_batch(&metadata, &batch)?);
190 }
191 }
192
193 Ok(filter_instruments_for_request_range(
194 all_instruments,
195 start,
196 end,
197 ))
198 }
199
200 fn discover_instrument_files(
203 &self,
204 instrument_ids: Option<&[String]>,
205 end: Option<UnixNanos>,
206 instrument_type: Option<&NautilusInstrumentType>,
207 ) -> anyhow::Result<Vec<String>> {
208 let base_dir = make_object_store_path(&self.base_path, ["data"]);
209 let end_u64 = end.map(|ts| ts.as_u64());
210 let list_result = self.list_objects(&base_dir)?;
211
212 let mut instrument_files = Vec::new();
213
214 for object in list_result {
215 let path_str = object.location.to_string();
216 if !path_str.ends_with(".parquet") {
217 continue;
218 }
219
220 let path_parts: Vec<&str> = path_str.split('/').collect();
221
222 let Some(data_index) = path_parts.iter().position(|part| *part == "data") else {
223 continue;
224 };
225
226 let Some(type_dir) = path_parts.get(data_index + 1) else {
227 continue;
228 };
229
230 let type_dir = decode_object_store_segment(type_dir);
231 if !is_parquet_instrument_type_prefix(&type_dir)
232 || instrument_type.is_some_and(|value| instrument_path_prefix(value) != type_dir)
233 {
234 continue;
235 }
236
237 if path_parts.len() < data_index + 4 {
238 continue;
239 }
240
241 let instrument_id_dir = decode_object_store_segment(path_parts[path_parts.len() - 2]);
242
243 if let Some(ids) = instrument_ids
244 && !ids
245 .iter()
246 .map(|id| urisafe_instrument_id(id))
247 .any(|x| x.as_str() == urisafe_instrument_id(&instrument_id_dir))
248 {
249 continue;
250 }
251
252 let include_file = if path_str.ends_with("/instrument.parquet") {
253 true
254 } else if let Some((file_start, _)) = parse_filename_timestamps(&path_str) {
255 end_u64.is_none_or(|end| file_start <= end)
256 } else {
257 log::warn!(
260 "Including instrument file with unparsable interval filename: {path_str}"
261 );
262 true
263 };
264
265 if include_file {
266 instrument_files.push(path_str);
267 }
268 }
269
270 instrument_files.sort();
271 Ok(instrument_files)
272 }
273
274 fn decode_instrument_files(
275 &self,
276 instrument_files: Vec<String>,
277 start: Option<UnixNanos>,
278 end: Option<UnixNanos>,
279 ) -> anyhow::Result<Vec<InstrumentAny>> {
280 let mut instruments = Vec::new();
281
282 for file_path in instrument_files {
283 let object_path = self.to_object_path_parsed(&file_path)?;
284
285 let (batches, builder_schema) = self.execute_async(|| async {
286 read_parquet_from_object_store(self.object_store.clone(), &object_path).await
287 })?;
288
289 validate_catalog_schema(&builder_schema)?;
290 let metadata = builder_schema.metadata().clone();
291 let target_schema = InstrumentAny::get_schema(Some(metadata.clone()));
292
293 for batch in batches {
294 let batch = datafusion::cast_record_batch_to_schema(&batch, &target_schema)?;
295 instruments.extend(decode_instrument_any_batch(&metadata, &batch)?);
296 }
297 }
298
299 Ok(filter_instruments_for_request_range(
300 instruments,
301 start,
302 end,
303 ))
304 }
305
306 pub fn query<T>(
416 &mut self,
417 identifiers: Option<Vec<String>>,
418 start: Option<UnixNanos>,
419 end: Option<UnixNanos>,
420 where_clause: Option<&str>,
421 files: Option<Vec<String>>,
422 optimize_file_loading: bool,
423 ) -> anyhow::Result<Vec<T>>
424 where
425 T: DecodeTypedFromRecordBatch + HasCatalogDataType + HasTsInit,
426 {
427 self.clear_session_tables();
428
429 self.register_remote_object_store()?;
430
431 let data_type = T::catalog_data_type();
432 let path_prefix = parquet_data_path_prefix(&data_type);
433
434 let files_list = if let Some(files) = files {
435 files
436 } else {
437 self.query_files(
438 &CatalogDataType::Data(data_type.clone()),
439 identifiers,
440 start,
441 end,
442 )?
443 };
444
445 let table_prefix = make_sql_safe_identifier(path_prefix.as_ref());
446 let tables =
447 self.resolve_tables_for_datafusion(&table_prefix, &files_list, optimize_file_loading)?;
448 let mut all_records = Vec::new();
449
450 for table in tables {
451 let query = build_query(&table.name, start, end, where_clause);
452 let batches = self.session.collect_parquet_files_batches(
453 &table.name,
454 vec![table.path],
455 Some(&query),
456 )?;
457
458 all_records.extend(self.convert_record_batches_to_typed::<T>(batches)?);
459 }
460
461 if !is_monotonically_increasing_by_init(&all_records) {
462 all_records.sort_by_key(HasTsInit::ts_init);
463 }
464
465 Ok(all_records)
466 }
467
468 pub(super) fn query_typed_pages<T>(
469 &mut self,
470 identifiers: Option<Vec<String>>,
471 start: Option<UnixNanos>,
472 end: Option<UnixNanos>,
473 where_clause: Option<&str>,
474 files: Option<Vec<String>>,
475 optimize_file_loading: bool,
476 ) -> anyhow::Result<TypedPages<T>>
477 where
478 T: DecodeTypedFromRecordBatch + HasCatalogDataType + HasTsInit + Send + 'static,
479 {
480 self.clear_session_tables();
481 self.register_remote_object_store()?;
482 let data_type = T::catalog_data_type();
483
484 let files = match files {
485 Some(files) => files,
486 None => self.query_files(&CatalogDataType::Data(data_type), identifiers, start, end)?,
487 };
488
489 let tables =
490 self.resolve_tables_for_datafusion("parquet", &files, optimize_file_loading)?;
491 let mut sources = Vec::with_capacity(tables.len());
492 for table in tables {
493 let sql = build_query(&table.name, start, end, where_clause);
494 let stream = self.session.parquet_files_batch_stream(
495 &table.name,
496 vec![table.path],
497 Some(&sql),
498 )?;
499 let pages = decode_typed_pages::<T>(stream);
500 sources.push(
501 Box::new(datafusion::BlockingBatchStream::from_stream_with_runtime(
502 pages,
503 &self.session.runtime,
504 )) as TypedPages<T>,
505 );
506 }
507
508 Ok(Box::new(MergedPages::new(sources, self.batch_size)))
509 }
510
511 pub fn query_record_batches(
517 &mut self,
518 data_type: &CatalogDataType,
519 identifier: Option<String>,
520 start: Option<UnixNanos>,
521 end: Option<UnixNanos>,
522 where_clause: Option<&str>,
523 optimize_file_loading: bool,
524 ) -> anyhow::Result<Vec<RecordBatch>> {
525 self.clear_session_tables();
526 self.register_remote_object_store()?;
527
528 let identifiers = identifier.map(|value| vec![value]);
529 let files_list = self.query_files(data_type, identifiers, start, end)?;
530 let mut record_batches = Vec::new();
531 let table_prefix =
532 make_sql_safe_identifier(&parquet_catalog_data_type_table_stem(data_type));
533
534 let tables =
535 self.resolve_tables_for_datafusion(&table_prefix, &files_list, optimize_file_loading)?;
536
537 for table in tables {
538 let query = build_query(&table.name, start, end, where_clause);
539 record_batches.extend(self.session.collect_parquet_files_batches(
540 &table.name,
541 vec![table.path],
542 Some(&query),
543 )?);
544 }
545
546 Ok(record_batches)
547 }
548
549 pub fn query_display_record_batches(
556 &mut self,
557 data_type: &NautilusDataType,
558 identifiers: Option<Vec<String>>,
559 start: Option<UnixNanos>,
560 end: Option<UnixNanos>,
561 where_clause: Option<&str>,
562 optimize_file_loading: bool,
563 ) -> anyhow::Result<Vec<RecordBatch>> {
564 self.clear_session_tables();
565 self.register_remote_object_store()?;
566
567 let data_path_prefix = parquet_data_path_prefix(data_type);
568 let files_list = self.query_files(
569 &CatalogDataType::Data(data_type.clone()),
570 identifiers,
571 start,
572 end,
573 )?;
574 let mut display_batches = Vec::new();
575 let table_prefix = make_sql_safe_identifier(data_path_prefix.as_ref());
576 let tables =
577 self.resolve_tables_for_datafusion(&table_prefix, &files_list, optimize_file_loading)?;
578
579 for table in tables {
580 let path_identifier = display_identifier(data_type, &table.directory);
581 let query = build_query(&table.name, start, end, where_clause);
582 let batches = self.session.collect_parquet_files_batches(
583 &table.name,
584 vec![table.path],
585 Some(&query),
586 )?;
587
588 for batch in batches {
589 let identifier =
590 display_batch_identifier(data_type, &batch, path_identifier.as_deref());
591 let batch = record_batch_with_identifier_column(batch, identifier.as_deref())?;
592 let metadata = batch.schema().metadata().clone();
593 display_batches.push(catalog_record_batch_to_display(
594 data_type, &metadata, &batch,
595 )?);
596 }
597 }
598
599 if display_batches.is_empty() {
600 display_batches.push(empty_display_batch_with_identifier(data_type)?);
601 }
602
603 Ok(display_batches)
604 }
605
606 pub fn query_identifiers(
608 &mut self,
609 data_type: &CatalogDataType,
610 identifiers: Option<Vec<String>>,
611 start: Option<UnixNanos>,
612 end: Option<UnixNanos>,
613 where_clause: Option<&str>,
614 _optimize_file_loading: bool,
615 ) -> anyhow::Result<Vec<String>> {
616 self.clear_session_tables();
617 self.register_remote_object_store()?;
618
619 let files_list = self.query_files(data_type, identifiers, start, end)?;
620 let table_prefix =
621 make_sql_safe_identifier(&parquet_catalog_data_type_table_stem(data_type));
622 let tables = self.resolve_tables_for_datafusion(&table_prefix, &files_list, true)?;
623 let mut identifiers = Vec::new();
624
625 for table in tables {
626 let identifier = dir_identifier(&table.directory);
627 let query = format!(
628 "{} LIMIT 1",
629 build_query(&table.name, start, end, where_clause)
630 );
631 let batches = self.session.collect_parquet_files_batches(
632 &table.name,
633 vec![table.path],
634 Some(&query),
635 )?;
636
637 if batches.iter().any(|batch| batch.num_rows() != 0) {
638 identifiers.push(decode_object_store_segment(&identifier));
639 }
640 }
641
642 identifiers.sort();
643 identifiers.dedup();
644 Ok(identifiers)
645 }
646
647 fn resolve_tables_for_datafusion(
651 &mut self,
652 table_prefix: &str,
653 files: &[String],
654 optimize_file_loading: bool,
655 ) -> anyhow::Result<Vec<DataFusionTable>> {
656 let table_name = |index: usize| format!("{table_prefix}_{index}");
657 let mut tables = Vec::new();
658
659 if !optimize_file_loading {
660 for file in files {
661 tables.push(self.file_table(table_name(tables.len()), file)?);
662 }
663 return Ok(tables);
664 }
665
666 for directory in parent_directories(files) {
668 let name = table_name(tables.len());
669 let path = self.resolve_directory_for_datafusion(&directory);
670
671 if self
672 .session
673 .try_register_parquet_files_table(&name, vec![path.clone()])?
674 {
675 tables.push(DataFusionTable {
676 name,
677 directory,
678 path,
679 });
680 continue;
681 }
682
683 for file in files
684 .iter()
685 .filter(|file| parent_directory(file).as_ref() == Some(&directory))
686 {
687 tables.push(self.file_table(table_name(tables.len()), file)?);
688 }
689 }
690
691 Ok(tables)
692 }
693
694 fn file_table(&self, name: String, file: &str) -> anyhow::Result<DataFusionTable> {
695 let directory = parent_directory(file)
696 .ok_or_else(|| anyhow::anyhow!("Cannot extract directory from '{file}'"))?;
697 let path = self.resolve_path_for_datafusion(file);
698
699 Ok(DataFusionTable {
700 name,
701 directory,
702 path,
703 })
704 }
705
706 #[expect(clippy::too_many_arguments)]
732 pub fn query_custom_data_dynamic(
733 &mut self,
734 type_name: &str,
735 identifiers: Option<&[String]>,
736 start: Option<UnixNanos>,
737 end: Option<UnixNanos>,
738 where_clause: Option<&str>,
739 files: Option<Vec<String>>,
740 _optimize_file_loading: bool,
741 ) -> anyhow::Result<Vec<Data>> {
742 self.clear_session_tables();
743
744 self.register_remote_object_store()?;
745
746 let files = if let Some(f) = files {
747 f.into_iter()
748 .map(|p| self.to_object_path_parsed(&p).map(|op| op.to_string()))
749 .collect::<anyhow::Result<Vec<_>>>()?
750 } else {
751 self.list_parquet_files_with_criteria(
752 &CatalogDataType::Data(NautilusDataType::Custom {
753 type_name: type_name.to_string(),
754 }),
755 identifiers,
756 start,
757 end,
758 )?
759 };
760
761 if files.is_empty() {
762 return Ok(Vec::new());
763 }
764
765 let mut lookup_metadata = HashMap::new();
769 lookup_metadata.insert("type_name".to_string(), type_name.to_string());
770 let registered_schema = CustomDataDecoder::get_schema(Some(lookup_metadata.clone()));
771 registered_schema.field_with_name("ts_init").map_err(|_| {
772 anyhow::anyhow!(
773 "custom data type '{type_name}' is not registered with an Arrow schema containing ts_init; \
774 call ensure_custom_data_registered::<T>() before querying"
775 )
776 })?;
777
778 let mut all_data = Vec::new();
779
780 for file in files {
781 let object_path = self.to_object_path_parsed(&file)?;
782
783 let mut decode_metadata = self.execute_async(|| async {
784 let schema =
785 read_parquet_schema_from_object_store(self.object_store.clone(), &object_path)
786 .await?;
787 validate_catalog_schema(&schema)?;
788 Ok::<HashMap<String, String>, anyhow::Error>(schema.metadata().clone())
789 })?;
790
791 decode_metadata.extend(lookup_metadata.clone());
792 let identifier = extract_identifier_from_path(&file)
793 .ok_or_else(|| anyhow::anyhow!("Cannot extract identifier from path '{file}'"))?;
794 let safe_type_name = make_sql_safe_identifier(type_name);
795 let safe_sql_identifier = make_sql_safe_identifier(identifier);
796 let safe_filename = extract_sql_safe_filename(&file);
797 let table_name =
798 format!("custom_{safe_type_name}_{safe_sql_identifier}_{safe_filename}");
799 let resolved_path = self.resolve_path_for_datafusion(&file);
800 let sql_query = build_query(&table_name, start, end, where_clause);
801
802 let batches = self.session.collect_parquet_files_batches(
806 &table_name,
807 vec![resolved_path],
808 Some(&sql_query),
809 )?;
810
811 for batch in batches {
812 all_data.extend(CustomDataDecoder::decode_data_batch(
813 &decode_metadata,
814 batch,
815 )?);
816 }
817 }
818
819 all_data.sort_by_key(HasTsInit::ts_init);
820 Ok(all_data)
821 }
822
823 pub fn query_files(
878 &self,
879 data_type: &CatalogDataType,
880 identifiers: Option<Vec<String>>,
881 start: Option<UnixNanos>,
882 end: Option<UnixNanos>,
883 ) -> anyhow::Result<Vec<String>> {
884 let identifiers = identifiers.map(Vec::into_boxed_slice);
886
887 let mut files = Vec::new();
888
889 for prefix in parquet_catalog_data_type_path_prefixes(data_type) {
890 files.extend(self.query_prefix_files(
891 prefix.as_ref(),
892 identifiers.as_deref(),
893 start,
894 end,
895 )?);
896 }
897
898 files.sort();
899 files.dedup();
900
901 Ok(files)
902 }
903
904 fn query_prefix_files(
905 &self,
906 data_cls: &str,
907 identifiers: Option<&[String]>,
908 start: Option<UnixNanos>,
909 end: Option<UnixNanos>,
910 ) -> anyhow::Result<Vec<String>> {
911 let mut files = Vec::new();
912
913 let start_u64 = start.map(|s| s.as_u64());
914 let end_u64 = end.map(|e| e.as_u64());
915
916 let base_dir = self.make_path(data_cls, None)?;
917
918 let list_result = self.list_objects(&base_dir)?;
920
921 let mut file_paths: Vec<String> = list_result
922 .into_iter()
923 .filter_map(|object| {
924 let path_str = object.location.to_string();
925 if path_str.ends_with(".parquet") {
926 Some(path_str)
927 } else {
928 None
929 }
930 })
931 .collect();
932
933 if let Some(identifiers) = identifiers {
934 file_paths =
935 filter_identifier_files(file_paths, identifiers, is_parquet_bar_prefix(data_cls));
936 }
937
938 file_paths.retain(|file_path| query_intersects_filename(file_path, start_u64, end_u64));
940
941 for file_path in file_paths {
942 files.push(self.path_for_query_list(&file_path));
943 }
944
945 Ok(files)
946 }
947
948 pub fn quote_ticks(
950 &mut self,
951 instrument_ids: Option<Vec<String>>,
952 start: Option<UnixNanos>,
953 end: Option<UnixNanos>,
954 ) -> anyhow::Result<Vec<QuoteTick>> {
955 self.query::<QuoteTick>(instrument_ids, start, end, None, None, true)
956 }
957
958 pub fn trade_ticks(
960 &mut self,
961 instrument_ids: Option<Vec<String>>,
962 start: Option<UnixNanos>,
963 end: Option<UnixNanos>,
964 ) -> anyhow::Result<Vec<TradeTick>> {
965 self.query::<TradeTick>(instrument_ids, start, end, None, None, true)
966 }
967
968 pub fn bars(
970 &mut self,
971 instrument_ids: Option<Vec<String>>,
972 start: Option<UnixNanos>,
973 end: Option<UnixNanos>,
974 ) -> anyhow::Result<Vec<Bar>> {
975 self.query::<Bar>(instrument_ids, start, end, None, None, true)
976 }
977
978 pub fn order_book_deltas(
980 &mut self,
981 instrument_ids: Option<Vec<String>>,
982 start: Option<UnixNanos>,
983 end: Option<UnixNanos>,
984 ) -> anyhow::Result<Vec<OrderBookDelta>> {
985 self.query::<OrderBookDelta>(instrument_ids, start, end, None, None, true)
986 }
987
988 pub fn order_book_depths(
990 &mut self,
991 instrument_ids: Option<Vec<String>>,
992 start: Option<UnixNanos>,
993 end: Option<UnixNanos>,
994 ) -> anyhow::Result<Vec<OrderBookDepth>> {
995 self.query::<OrderBookDepth>(instrument_ids, start, end, None, None, true)
996 }
997
998 pub fn funding_rates(
1000 &mut self,
1001 instrument_ids: Option<Vec<String>>,
1002 start: Option<UnixNanos>,
1003 end: Option<UnixNanos>,
1004 ) -> anyhow::Result<Vec<FundingRateUpdate>> {
1005 self.query::<FundingRateUpdate>(instrument_ids, start, end, None, None, true)
1006 }
1007
1008 pub fn instrument_closes(
1010 &mut self,
1011 instrument_ids: Option<Vec<String>>,
1012 start: Option<UnixNanos>,
1013 end: Option<UnixNanos>,
1014 ) -> anyhow::Result<Vec<InstrumentClose>> {
1015 self.query::<InstrumentClose>(instrument_ids, start, end, None, None, true)
1016 }
1017
1018 pub fn option_greeks(
1020 &mut self,
1021 instrument_ids: Option<Vec<String>>,
1022 start: Option<UnixNanos>,
1023 end: Option<UnixNanos>,
1024 ) -> anyhow::Result<Vec<OptionGreeks>> {
1025 self.query::<OptionGreeks>(instrument_ids, start, end, None, None, true)
1026 }
1027
1028 pub fn instruments(
1030 &self,
1031 instrument_ids: Option<&[String]>,
1032 start: Option<UnixNanos>,
1033 end: Option<UnixNanos>,
1034 ) -> anyhow::Result<Vec<InstrumentAny>> {
1035 self.query_instruments_filtered(instrument_ids, start, end)
1036 }
1037
1038 pub fn get_file_list_from_data_cls(
1078 &self,
1079 data_type: &CatalogDataType,
1080 ) -> anyhow::Result<Vec<String>> {
1081 let mut file_paths = Vec::new();
1082
1083 for data_cls in parquet_catalog_data_type_path_prefixes(data_type) {
1084 let directory = self.make_path(data_cls.as_ref(), None)?;
1085 file_paths.extend(self.list_parquet_files(&directory)?);
1086 }
1087
1088 Ok(file_paths)
1089 }
1090
1091 pub fn filter_files(
1141 &self,
1142 data_type: &CatalogDataType,
1143 file_paths: Vec<String>,
1144 identifiers: Option<Vec<String>>,
1145 start: Option<UnixNanos>,
1146 end: Option<UnixNanos>,
1147 ) -> anyhow::Result<Vec<String>> {
1148 let has_bar_prefix = parquet_catalog_data_type_path_prefixes(data_type)
1149 .iter()
1150 .any(|data_cls| is_parquet_bar_prefix(data_cls.as_ref()));
1151 let mut filtered_paths = file_paths;
1152
1153 if let Some(identifiers) = identifiers {
1154 filtered_paths = filter_identifier_files(filtered_paths, &identifiers, has_bar_prefix);
1155 }
1156
1157 let start_u64 = start.map(|s| s.as_u64());
1159 let end_u64 = end.map(|e| e.as_u64());
1160 filtered_paths.retain(|file_path| query_intersects_filename(file_path, start_u64, end_u64));
1161
1162 Ok(filtered_paths)
1163 }
1164}
1165
1166fn is_parquet_instrument_type_prefix(prefix: &str) -> bool {
1167 INSTRUMENT_PATH_PREFIXES.contains(&prefix)
1168}
1169
1170pub(super) fn is_parquet_bar_prefix(data_cls: &str) -> bool {
1171 data_cls == parquet_data_path_prefix(&NautilusDataType::Bar).as_ref()
1172}
1173
1174pub(super) fn filter_identifier_files(
1178 file_paths: Vec<String>,
1179 identifiers: &[String],
1180 bars: bool,
1181) -> Vec<String> {
1182 let directories = file_paths
1183 .iter()
1184 .map(|file_path| identifier_directory(file_path))
1185 .collect::<Vec<_>>();
1186 let mut keep = vec![false; file_paths.len()];
1187
1188 for safe_id in identifiers.iter().map(|id| urisafe_instrument_id(id)) {
1189 let has_exact = directories
1190 .iter()
1191 .flatten()
1192 .any(|directory| *directory == safe_id);
1193 let partial = bars && !has_exact;
1194
1195 for (keep, directory) in keep.iter_mut().zip(&directories) {
1196 let Some(directory) = directory else {
1197 continue;
1198 };
1199
1200 *keep |= if partial {
1201 extract_bar_type_instrument_id(directory) == Some(safe_id.as_str())
1202 } else {
1203 *directory == safe_id
1204 };
1205 }
1206 }
1207
1208 file_paths
1209 .into_iter()
1210 .zip(keep)
1211 .filter_map(|(file_path, keep)| keep.then_some(file_path))
1212 .collect()
1213}
1214
1215fn identifier_directory(file_path: &str) -> Option<String> {
1216 let mut segments = file_path.rsplit('/');
1217 segments.next()?;
1218 segments.next().map(decode_object_store_segment)
1219}
1220
1221struct DataFusionTable {
1224 name: String,
1225 directory: String,
1226 path: String,
1227}
1228
1229fn parent_directories(files: &[String]) -> Vec<String> {
1232 let mut directories: Vec<String> = files
1233 .iter()
1234 .filter_map(|file_uri| parent_directory(file_uri))
1235 .collect();
1236
1237 directories.sort();
1238 directories.dedup();
1239 directories
1240}
1241
1242fn parent_directory(file_uri: &str) -> Option<String> {
1243 Path::new(file_uri)
1244 .parent()
1245 .map(|path| path.to_string_lossy().to_string())
1246}
1247
1248fn dir_identifier(directory: &str) -> String {
1250 directory
1251 .rsplit_once('/')
1252 .map_or(directory, |(_, name)| name)
1253 .to_string()
1254}
1255
1256fn display_identifier(data_type: &NautilusDataType, directory: &str) -> Option<String> {
1257 let identifier = dir_identifier(directory);
1258 let is_unpartitioned_custom = matches!(data_type, NautilusDataType::Custom { .. })
1259 && Path::new(directory)
1260 .ends_with(Path::new("data").join(parquet_data_path_prefix(data_type).as_ref()));
1261
1262 (!is_unpartitioned_custom).then(|| decode_object_store_segment(&identifier))
1263}
1264
1265fn display_batch_identifier(
1266 data_type: &NautilusDataType,
1267 batch: &RecordBatch,
1268 path_identifier: Option<&str>,
1269) -> Option<String> {
1270 if matches!(data_type, NautilusDataType::Custom { .. }) {
1271 path_identifier.map(str::to_string)
1272 } else {
1273 catalog_identifier_from_metadata(batch.schema().metadata())
1274 .or_else(|| path_identifier.map(str::to_string))
1275 }
1276}