1use std::collections::HashMap;
18use std::fmt;
19use std::fmt::{Debug, Formatter};
20use std::num::NonZeroU64;
21use std::sync::atomic::{AtomicBool, Ordering};
22use std::sync::{Arc, Mutex, RwLock};
23
24use base64::prelude::{BASE64_STANDARD, Engine};
25use bytes::Bytes;
26use common_base::readable_size::ReadableSize;
27use common_telemetry::{debug, error, warn};
28use common_time::Timestamp;
29use datatypes::schema::FulltextBackend;
30use partition::expr::PartitionExpr;
31use serde::{Deserialize, Serialize};
32use smallvec::SmallVec;
33use store_api::metadata::ColumnMetadata;
34use store_api::region_request::PathType;
35use store_api::storage::{ColumnId, FileId, IndexVersion, RegionId};
36
37use crate::access_layer::AccessLayerRef;
38use crate::cache::CacheManagerRef;
39use crate::cache::file_cache::{FileType, IndexKey};
40use crate::sst::file_purger::FilePurgerRef;
41use crate::sst::index::fulltext_index::FULLTEXT_BLOOM_VERSION;
42use crate::sst::location;
43use crate::sst::parquet::SstInfo;
44use crate::sst::primary_key::PrimaryKeyRangeMapper;
45
46fn serialize_bytes_option<S>(bytes: &Option<Bytes>, serializer: S) -> Result<S::Ok, S::Error>
48where
49 S: serde::Serializer,
50{
51 match bytes {
52 None => serializer.serialize_none(),
53 Some(b) => serializer.serialize_some(&BASE64_STANDARD.encode(b)),
54 }
55}
56
57fn deserialize_bytes_option<'de, D>(deserializer: D) -> Result<Option<Bytes>, D::Error>
58where
59 D: serde::Deserializer<'de>,
60{
61 let opt: Option<String> = Option::deserialize(deserializer)?;
62 match opt {
63 None => Ok(None),
64 Some(s) => {
65 let decoded = BASE64_STANDARD
66 .decode(&s)
67 .map_err(serde::de::Error::custom)?;
68 Ok(Some(Bytes::from(decoded)))
69 }
70 }
71}
72
73fn serialize_partition_expr<S>(
75 partition_expr: &Option<PartitionExpr>,
76 serializer: S,
77) -> Result<S::Ok, S::Error>
78where
79 S: serde::Serializer,
80{
81 use serde::ser::Error;
82
83 match partition_expr {
84 None => serializer.serialize_none(),
85 Some(expr) => {
86 let json_str = expr.as_json_str().map_err(S::Error::custom)?;
87 serializer.serialize_some(&json_str)
88 }
89 }
90}
91
92fn deserialize_partition_expr<'de, D>(deserializer: D) -> Result<Option<PartitionExpr>, D::Error>
93where
94 D: serde::Deserializer<'de>,
95{
96 use serde::de::Error;
97
98 let opt_json_str: Option<String> = Option::deserialize(deserializer)?;
99 match opt_json_str {
100 None => Ok(None),
101 Some(json_str) => {
102 if json_str.is_empty() {
103 Ok(None)
105 } else {
106 PartitionExpr::from_json_str(&json_str).map_err(D::Error::custom)
108 }
109 }
110 }
111}
112
113pub type Level = u8;
115pub const MAX_LEVEL: Level = 2;
117pub type IndexTypes = SmallVec<[IndexType; 4]>;
119
120#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
124pub struct RegionFileId {
125 region_id: RegionId,
127 file_id: FileId,
129}
130
131impl RegionFileId {
132 pub fn new(region_id: RegionId, file_id: FileId) -> Self {
134 Self { region_id, file_id }
135 }
136
137 pub fn region_id(&self) -> RegionId {
139 self.region_id
140 }
141
142 pub fn file_id(&self) -> FileId {
144 self.file_id
145 }
146}
147
148impl fmt::Display for RegionFileId {
149 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
150 write!(f, "{}/{}", self.region_id, self.file_id)
151 }
152}
153
154#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
156pub struct RegionIndexId {
157 pub file_id: RegionFileId,
158 pub version: IndexVersion,
159}
160
161impl RegionIndexId {
162 pub fn new(file_id: RegionFileId, version: IndexVersion) -> Self {
163 Self { file_id, version }
164 }
165
166 pub fn region_id(&self) -> RegionId {
167 self.file_id.region_id
168 }
169
170 pub fn file_id(&self) -> FileId {
171 self.file_id.file_id
172 }
173}
174
175impl fmt::Display for RegionIndexId {
176 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
177 if self.version == 0 {
178 write!(f, "{}/{}", self.file_id.region_id, self.file_id.file_id)
179 } else {
180 write!(
181 f,
182 "{}/{}.{}",
183 self.file_id.region_id, self.file_id.file_id, self.version
184 )
185 }
186 }
187}
188
189pub type FileTimeRange = (Timestamp, Timestamp);
192
193pub(crate) fn overlaps(l: &FileTimeRange, r: &FileTimeRange) -> bool {
195 let (l, r) = if l.0 <= r.0 { (l, r) } else { (r, l) };
196 let (_, l_end) = l;
197 let (r_start, _) = r;
198
199 r_start <= l_end
200}
201
202#[derive(Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
204#[serde(default)]
205pub struct FileMeta {
206 pub region_id: RegionId,
208 pub file_id: FileId,
210 pub time_range: FileTimeRange,
213 pub level: Level,
215 pub file_size: u64,
217 pub max_row_group_uncompressed_size: u64,
219 pub available_indexes: IndexTypes,
221 pub indexes: Vec<ColumnIndexMetadata>,
231 pub index_file_size: u64,
233 pub index_version: u64,
237 pub num_rows: u64,
243 pub num_row_groups: u64,
249 pub sequence: Option<NonZeroU64>,
257 #[serde(
265 serialize_with = "serialize_partition_expr",
266 deserialize_with = "deserialize_partition_expr"
267 )]
268 pub partition_expr: Option<PartitionExpr>,
269 pub num_series: u64,
273 #[serde(
276 default,
277 skip_serializing_if = "Option::is_none",
278 serialize_with = "serialize_bytes_option",
279 deserialize_with = "deserialize_bytes_option"
280 )]
281 pub primary_key_min: Option<Bytes>,
282 #[serde(
285 default,
286 skip_serializing_if = "Option::is_none",
287 serialize_with = "serialize_bytes_option",
288 deserialize_with = "deserialize_bytes_option"
289 )]
290 pub primary_key_max: Option<Bytes>,
291 #[serde(default, skip_serializing_if = "is_false")]
296 pub preserve_row_sequence: bool,
297}
298
299fn is_false(value: &bool) -> bool {
300 !*value
301}
302
303struct DebugFmt<F: Fn(&mut Formatter<'_>) -> fmt::Result>(F);
306
307impl<F: Fn(&mut Formatter<'_>) -> fmt::Result> std::fmt::Debug for DebugFmt<F> {
308 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
309 (self.0)(f)
310 }
311}
312
313impl Debug for FileMeta {
314 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
315 let mut debug_struct = f.debug_struct("FileMeta");
316 debug_struct
317 .field("region_id", &self.region_id)
318 .field("file_id", &DebugFmt(|f| write!(f, "{} ", self.file_id)))
319 .field(
320 "time_range",
321 &DebugFmt(|f| {
322 write!(
323 f,
324 "({}, {}) ",
325 self.time_range.0.to_iso8601_string(),
326 self.time_range.1.to_iso8601_string()
327 )
328 }),
329 )
330 .field("level", &self.level)
331 .field("file_size", &ReadableSize(self.file_size))
332 .field(
333 "max_row_group_uncompressed_size",
334 &ReadableSize(self.max_row_group_uncompressed_size),
335 );
336 if !self.available_indexes.is_empty() {
337 debug_struct
338 .field("available_indexes", &self.available_indexes)
339 .field("indexes", &self.indexes)
340 .field("index_file_size", &ReadableSize(self.index_file_size));
341 }
342 debug_struct
343 .field("num_rows", &self.num_rows)
344 .field("num_row_groups", &self.num_row_groups)
345 .field(
346 "sequence",
347 &DebugFmt(|f| match self.sequence {
348 None => {
349 write!(f, "None")
350 }
351 Some(seq) => {
352 write!(f, "{}", seq)
353 }
354 }),
355 )
356 .field("partition_expr", &self.partition_expr)
357 .field("num_series", &self.num_series);
358 if self.primary_key_min.is_some() || self.primary_key_max.is_some() {
359 debug_struct
360 .field(
361 "primary_key_min",
362 &self.primary_key_min.as_ref().map(|b| b.len()),
363 )
364 .field(
365 "primary_key_max",
366 &self.primary_key_max.as_ref().map(|b| b.len()),
367 );
368 }
369 debug_struct.finish()
370 }
371}
372
373#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
375pub enum IndexType {
376 InvertedIndex,
378 FulltextIndex,
380 BloomFilterIndex,
382 VectorIndex,
384}
385
386#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
396#[serde(default)]
397pub struct ColumnIndexMetadata {
398 pub column_id: ColumnId,
400 pub created_indexes: IndexTypes,
402 #[serde(skip_serializing_if = "is_zero")]
406 pub fulltext_bloom_version: u8,
407}
408
409fn is_zero(version: &u8) -> bool {
410 *version == 0
411}
412
413impl FileMeta {
414 pub fn primary_key_range(&self) -> Option<(Bytes, Bytes)> {
416 match (&self.primary_key_min, &self.primary_key_max) {
417 (Some(min), Some(max)) => Some((min.clone(), max.clone())),
418 _ => None,
419 }
420 }
421
422 pub fn exists_index(&self) -> bool {
423 !self.available_indexes.is_empty()
424 }
425
426 pub fn index_version(&self) -> Option<IndexVersion> {
427 if self.exists_index() {
428 Some(self.index_version)
429 } else {
430 None
431 }
432 }
433
434 pub fn is_index_up_to_date(&self, other: &FileMeta) -> bool {
436 self.exists_index() && other.exists_index() && self.index_version >= other.index_version
437 }
438
439 pub fn inverted_index_available(&self) -> bool {
441 self.available_indexes.contains(&IndexType::InvertedIndex)
442 }
443
444 pub fn fulltext_index_available(&self) -> bool {
446 self.available_indexes.contains(&IndexType::FulltextIndex)
447 }
448
449 pub fn bloom_filter_index_available(&self) -> bool {
451 self.available_indexes
452 .contains(&IndexType::BloomFilterIndex)
453 }
454
455 pub fn index_file_size(&self) -> u64 {
456 self.index_file_size
457 }
458
459 pub fn is_index_consistent_with_region(&self, metadata: &[ColumnMetadata]) -> bool {
461 let id_to_indexes = self
462 .indexes
463 .iter()
464 .map(|index| (index.column_id, index))
465 .collect::<std::collections::HashMap<_, _>>();
466 for column in metadata {
467 if !column.column_schema.is_indexed() {
468 continue;
469 }
470 if let Some(index) = id_to_indexes.get(&column.column_id) {
471 let indexes = &index.created_indexes;
472 if column.column_schema.is_inverted_indexed()
473 && !indexes.contains(&IndexType::InvertedIndex)
474 {
475 return false;
476 }
477 if column.column_schema.is_fulltext_indexed()
478 && !indexes.contains(&IndexType::FulltextIndex)
479 {
480 return false;
481 }
482 if column
483 .column_schema
484 .fulltext_options()
485 .ok()
486 .flatten()
487 .is_some_and(|options| {
488 options.enable && options.backend == FulltextBackend::Bloom
489 })
490 && index.fulltext_bloom_version < FULLTEXT_BLOOM_VERSION
491 {
492 debug!(
493 "Fulltext bloom index is outdated, region_id: {}, file_id: {}, column_id: {}, version: {}, current: {}",
494 self.region_id,
495 self.file_id,
496 column.column_id,
497 index.fulltext_bloom_version,
498 FULLTEXT_BLOOM_VERSION
499 );
500 return false;
501 }
502 if column.column_schema.is_skipping_indexed()
503 && !indexes.contains(&IndexType::BloomFilterIndex)
504 {
505 return false;
506 }
507 } else {
508 return false;
509 }
510 }
511 true
512 }
513
514 pub fn file_id(&self) -> RegionFileId {
516 RegionFileId::new(self.region_id, self.file_id)
517 }
518
519 pub fn index_id(&self) -> RegionIndexId {
521 RegionIndexId::new(self.file_id(), self.index_version)
522 }
523}
524
525#[derive(Clone)]
527pub struct FileHandle {
528 inner: Arc<FileHandleInner>,
529}
530
531impl fmt::Debug for FileHandle {
532 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
533 f.debug_struct("FileHandle")
534 .field("meta", self.meta_ref())
535 .field("compacting", &self.compacting())
536 .field("deleted", &self.inner.deleted.load(Ordering::Relaxed))
537 .finish()
538 }
539}
540
541impl FileHandle {
542 pub fn new(meta: FileMeta, file_purger: FilePurgerRef) -> FileHandle {
544 let pk_range = meta.primary_key_range();
545 FileHandle {
546 inner: Arc::new(FileHandleInner::new(meta, file_purger, pk_range)),
547 }
548 }
549
550 #[cfg(test)]
551 pub fn new_with_primary_key_range(
552 meta: FileMeta,
553 file_purger: FilePurgerRef,
554 primary_key_range: Option<(Bytes, Bytes)>,
555 ) -> FileHandle {
556 FileHandle {
557 inner: Arc::new(FileHandleInner::new(meta, file_purger, primary_key_range)),
558 }
559 }
560
561 pub fn region_id(&self) -> RegionId {
563 self.inner.meta.region_id
564 }
565
566 pub(crate) fn is_effective_target_sequence_trusted(&self, target_region_id: RegionId) -> bool {
571 if self.region_id() != target_region_id {
572 self.meta_ref().sequence.is_some()
573 } else {
574 self.meta_ref().preserve_row_sequence
575 }
576 }
577
578 pub fn file_id(&self) -> RegionFileId {
580 RegionFileId::new(self.inner.meta.region_id, self.inner.meta.file_id)
581 }
582
583 pub fn index_id(&self) -> RegionIndexId {
585 RegionIndexId::new(self.file_id(), self.inner.meta.index_version)
586 }
587
588 pub fn file_path(&self, table_dir: &str, path_type: PathType) -> String {
590 location::sst_file_path(table_dir, self.file_id(), path_type)
591 }
592
593 pub fn time_range(&self) -> FileTimeRange {
595 self.inner.meta.time_range
596 }
597
598 pub fn mark_deleted(&self) {
600 self.inner.deleted.store(true, Ordering::Relaxed);
601 }
602
603 pub fn compacting(&self) -> bool {
604 self.inner.compacting.load(Ordering::Relaxed)
605 }
606
607 pub fn set_compacting(&self, compacting: bool) {
608 self.inner.compacting.store(compacting, Ordering::Relaxed);
609 }
610
611 pub fn try_set_compacting(&self) -> bool {
613 self.inner
614 .compacting
615 .compare_exchange(false, true, Ordering::Relaxed, Ordering::Relaxed)
616 .is_ok()
617 }
618
619 pub fn index_outdated(&self) -> bool {
620 self.inner.index_outdated.load(Ordering::Relaxed)
621 }
622
623 pub fn set_index_outdated(&self, index_outdated: bool) {
624 self.inner
625 .index_outdated
626 .store(index_outdated, Ordering::Relaxed);
627 }
628
629 pub fn meta_ref(&self) -> &FileMeta {
631 &self.inner.meta
632 }
633
634 pub fn file_purger(&self) -> FilePurgerRef {
635 self.inner.file_purger.clone()
636 }
637
638 pub fn size(&self) -> u64 {
639 self.inner.meta.file_size
640 }
641
642 pub fn index_size(&self) -> u64 {
643 self.inner.meta.index_file_size
644 }
645
646 pub fn num_rows(&self) -> usize {
647 self.inner.meta.num_rows as usize
648 }
649
650 pub fn level(&self) -> Level {
651 self.inner.meta.level
652 }
653
654 pub fn is_deleted(&self) -> bool {
655 self.inner.deleted.load(Ordering::Relaxed)
656 }
657
658 pub(crate) fn primary_key_range(
661 &self,
662 mapper: &PrimaryKeyRangeMapper,
663 ) -> Option<(Bytes, Bytes)> {
664 debug_assert_eq!(self.region_id().table_id(), mapper.region_id().table_id());
665 if let Some(range) = self
666 .inner
667 .primary_key_range
668 .read()
669 .unwrap()
670 .aligned(mapper.schema_version())
671 {
672 return range.clone();
673 }
674 let aligned = self.inner.primary_key_range.write().unwrap().align(mapper);
676 match aligned {
677 Ok(range) => range,
678 Err(err) => {
679 warn!(err; "Invalid SST primary key range; using unknown bounds, region: {}, file: {}, schema version: {}",
680 self.region_id(), self.file_id(), mapper.schema_version());
681 None
682 }
683 }
684 }
685
686 pub fn raw_primary_key_range(&self) -> Option<(Bytes, Bytes)> {
688 self.inner.primary_key_range.read().unwrap().raw().cloned()
689 }
690
691 pub(crate) fn set_primary_key_range(&self, primary_key_range: (Bytes, Bytes)) {
692 let mut range = self.inner.primary_key_range.write().unwrap();
695 if matches!(*range, PrimaryKeyRange::Missing) {
696 *range = PrimaryKeyRange::Raw(primary_key_range);
697 }
698 }
699}
700
701type PrimaryKeyBounds = (Bytes, Bytes);
702
703enum PrimaryKeyRange {
706 Missing,
707 Raw(PrimaryKeyBounds),
708 Aligned {
709 raw: PrimaryKeyBounds,
710 schema_version: u64,
711 bounds: Option<PrimaryKeyBounds>,
712 },
713}
714
715impl PrimaryKeyRange {
716 fn raw(&self) -> Option<&PrimaryKeyBounds> {
717 match self {
718 Self::Missing => None,
719 Self::Raw(raw) | Self::Aligned { raw, .. } => Some(raw),
720 }
721 }
722
723 fn aligned(&self, target_version: u64) -> Option<&Option<PrimaryKeyBounds>> {
724 match self {
725 Self::Aligned {
726 schema_version,
727 bounds,
728 ..
729 } if *schema_version == target_version => Some(bounds),
730 _ => None,
731 }
732 }
733
734 fn align(
735 &mut self,
736 mapper: &PrimaryKeyRangeMapper,
737 ) -> crate::error::Result<Option<PrimaryKeyBounds>> {
738 if let Some(bounds) = self.aligned(mapper.schema_version()) {
739 return Ok(bounds.clone());
740 }
741 let Some(raw) = self.raw().cloned() else {
742 return Ok(None);
743 };
744 let aligned = mapper.map(raw.clone());
745 *self = Self::Aligned {
747 raw,
748 schema_version: mapper.schema_version(),
749 bounds: aligned.as_ref().ok().cloned().flatten(),
750 };
751 aligned
752 }
753}
754
755struct FileHandleInner {
759 meta: FileMeta,
760 compacting: AtomicBool,
761 deleted: AtomicBool,
762 index_outdated: AtomicBool,
763 primary_key_range: RwLock<PrimaryKeyRange>,
764 file_purger: FilePurgerRef,
765}
766
767impl Drop for FileHandleInner {
768 fn drop(&mut self) {
769 self.file_purger.remove_file(
770 self.meta.clone(),
771 self.deleted.load(Ordering::Acquire),
772 self.index_outdated.load(Ordering::Acquire),
773 );
774 }
775}
776
777impl FileHandleInner {
778 fn new(
780 meta: FileMeta,
781 file_purger: FilePurgerRef,
782 primary_key_range: Option<(Bytes, Bytes)>,
783 ) -> FileHandleInner {
784 file_purger.new_file(&meta);
785 FileHandleInner {
786 meta,
787 compacting: AtomicBool::new(false),
788 deleted: AtomicBool::new(false),
789 index_outdated: AtomicBool::new(false),
790 primary_key_range: RwLock::new(
791 primary_key_range.map_or(PrimaryKeyRange::Missing, PrimaryKeyRange::Raw),
792 ),
793 file_purger,
794 }
795 }
796}
797
798pub async fn delete_files(
805 region_id: RegionId,
806 file_ids: &[(FileId, u64)],
807 delete_index: bool,
808 access_layer: &AccessLayerRef,
809 cache_manager: &Option<CacheManagerRef>,
810) -> crate::error::Result<()> {
811 if let Some(cache) = &cache_manager {
813 for (file_id, _) in file_ids {
814 cache.remove_parquet_meta_data(RegionFileId::new(region_id, *file_id));
815 }
816 }
817 let mut attempted_files = Vec::with_capacity(file_ids.len());
818 let mut index_ids = Vec::new();
819
820 for (file_id, index_version) in file_ids {
821 let region_file_id = RegionFileId::new(region_id, *file_id);
822 attempted_files.push(*file_id);
823 index_ids.extend(
824 (0..=*index_version).map(|version| RegionIndexId::new(region_file_id, version)),
825 );
826 }
827
828 access_layer
829 .delete_ssts(region_id, &attempted_files)
830 .await?;
831 access_layer.delete_indexes(&index_ids).await?;
832
833 debug!(
834 "Attempted to delete {} files for region {}: {:?}",
835 attempted_files.len(),
836 region_id,
837 attempted_files
838 );
839
840 for (file_id, index_version) in file_ids {
841 purge_index_cache_stager(
842 region_id,
843 delete_index,
844 access_layer,
845 cache_manager,
846 *file_id,
847 *index_version,
848 )
849 .await;
850 }
851 Ok(())
852}
853
854#[derive(Clone)]
859pub(crate) struct UncommittedSsts {
860 region_id: RegionId,
861 files: Arc<Mutex<HashMap<FileId, (u64, bool)>>>,
862 access_layer: AccessLayerRef,
863 cache_manager: Option<CacheManagerRef>,
864}
865
866impl UncommittedSsts {
867 pub(crate) fn new(
868 region_id: RegionId,
869 access_layer: AccessLayerRef,
870 cache_manager: Option<CacheManagerRef>,
871 ) -> Self {
872 Self {
873 region_id,
874 files: Arc::new(Mutex::new(HashMap::new())),
875 access_layer,
876 cache_manager,
877 }
878 }
879
880 pub(crate) fn track(&self, ssts: &[SstInfo]) {
882 let mut files = self.files.lock().unwrap();
883 for sst in ssts {
884 files.insert(
885 sst.file_id,
886 (sst.index_metadata.version, sst.index_metadata.file_size > 0),
887 );
888 }
889 }
890
891 pub(crate) fn disarm_cleanup(&self) {
896 self.files.lock().unwrap().clear();
897 }
898
899 #[cfg(test)]
900 pub(crate) fn num_tracked_files(&self) -> usize {
901 self.files.lock().unwrap().len()
902 }
903
904 pub(crate) async fn cleanup(&self) {
906 if let Err(err) = self.try_cleanup().await {
907 error!(err; "Failed to clean uncommitted SSTs for region {}", self.region_id);
908 }
909 }
910
911 async fn try_cleanup(&self) -> crate::error::Result<()> {
912 let files = std::mem::take(&mut *self.files.lock().unwrap());
913 if files.is_empty() {
914 return Ok(());
915 }
916
917 let delete_index = files.values().any(|(_, exists_index)| *exists_index);
918 let file_ids = files
919 .into_iter()
920 .map(|(file_id, (index_version, _))| (file_id, index_version))
921 .collect::<Vec<_>>();
922 delete_files(
923 self.region_id,
924 &file_ids,
925 delete_index,
926 &self.access_layer,
927 &self.cache_manager,
928 )
929 .await
930 }
931
932 #[cfg(test)]
933 pub(crate) async fn cleanup_for_test(&self) -> crate::error::Result<()> {
934 self.try_cleanup().await
935 }
936}
937
938pub async fn delete_index(
939 region_index_id: RegionIndexId,
940 access_layer: &AccessLayerRef,
941 cache_manager: &Option<CacheManagerRef>,
942) -> crate::error::Result<()> {
943 delete_index_and_purge(region_index_id, access_layer, cache_manager).await?;
944
945 Ok(())
946}
947
948pub async fn delete_indexes(
949 index_ids: &[RegionIndexId],
950 access_layer: &AccessLayerRef,
951 cache_manager: &Option<CacheManagerRef>,
952) -> crate::error::Result<()> {
953 if index_ids.is_empty() {
954 return Ok(());
955 }
956
957 if let Err(e) = access_layer.delete_indexes(index_ids).await {
958 error!(e; "Failed to batch delete index files");
959
960 for index_id in index_ids {
961 delete_index_and_purge(*index_id, access_layer, cache_manager).await?;
962 }
963
964 return Ok(());
965 }
966
967 purge_indexes(index_ids, access_layer, cache_manager).await;
968
969 Ok(())
970}
971
972async fn delete_index_and_purge(
973 index_id: RegionIndexId,
974 access_layer: &AccessLayerRef,
975 cache_manager: &Option<CacheManagerRef>,
976) -> crate::error::Result<()> {
977 access_layer.delete_index(index_id).await?;
978 purge_index_cache_stager(
979 index_id.region_id(),
980 true,
981 access_layer,
982 cache_manager,
983 index_id.file_id(),
984 index_id.version,
985 )
986 .await;
987 Ok(())
988}
989
990async fn purge_indexes(
991 index_ids: &[RegionIndexId],
992 access_layer: &AccessLayerRef,
993 cache_manager: &Option<CacheManagerRef>,
994) {
995 for index_id in index_ids {
996 purge_index_cache_stager(
997 index_id.region_id(),
998 true,
999 access_layer,
1000 cache_manager,
1001 index_id.file_id(),
1002 index_id.version,
1003 )
1004 .await;
1005 }
1006}
1007
1008async fn purge_index_cache_stager(
1009 region_id: RegionId,
1010 delete_index: bool,
1011 access_layer: &AccessLayerRef,
1012 cache_manager: &Option<CacheManagerRef>,
1013 file_id: FileId,
1014 index_version: u64,
1015) {
1016 if let Some(write_cache) = cache_manager.as_ref().and_then(|cache| cache.write_cache()) {
1017 if delete_index {
1019 write_cache
1020 .remove(IndexKey::new(
1021 region_id,
1022 file_id,
1023 FileType::Puffin(index_version),
1024 ))
1025 .await;
1026 }
1027
1028 write_cache
1030 .remove(IndexKey::new(region_id, file_id, FileType::Parquet))
1031 .await;
1032 }
1033
1034 if let Err(e) = access_layer
1036 .puffin_manager_factory()
1037 .purge_stager(RegionIndexId::new(
1038 RegionFileId::new(region_id, file_id),
1039 index_version,
1040 ))
1041 .await
1042 {
1043 error!(e; "Failed to purge stager with index file, file_id: {}, index_version: {}, region: {}",
1044 file_id, index_version, region_id);
1045 }
1046}
1047
1048#[cfg(test)]
1049mod tests {
1050 use std::str::FromStr;
1051
1052 use datatypes::prelude::ConcreteDataType;
1053 use datatypes::schema::{
1054 ColumnSchema, FulltextAnalyzer, FulltextBackend, FulltextOptions, SkippingIndexOptions,
1055 };
1056 use datatypes::value::Value;
1057 use partition::expr::{PartitionExpr, col};
1058
1059 use super::*;
1060
1061 fn create_file_meta(file_id: FileId, level: Level) -> FileMeta {
1062 FileMeta {
1063 region_id: 0.into(),
1064 file_id,
1065 time_range: FileTimeRange::default(),
1066 level,
1067 file_size: 0,
1068 max_row_group_uncompressed_size: 0,
1069 available_indexes: SmallVec::from_iter([IndexType::InvertedIndex]),
1070 indexes: vec![ColumnIndexMetadata {
1071 column_id: 0,
1072 created_indexes: SmallVec::from_iter([IndexType::InvertedIndex]),
1073 ..Default::default()
1074 }],
1075 index_file_size: 0,
1076 index_version: 0,
1077 num_rows: 0,
1078 num_row_groups: 0,
1079 sequence: None,
1080 partition_expr: None,
1081 num_series: 0,
1082 ..Default::default()
1083 }
1084 }
1085
1086 #[test]
1087 fn test_deserialize_file_meta() {
1088 let file_meta = create_file_meta(FileId::random(), 0);
1089 let serialized_file_meta = serde_json::to_string(&file_meta).unwrap();
1090 let deserialized_file_meta = serde_json::from_str(&serialized_file_meta);
1091 assert_eq!(file_meta, deserialized_file_meta.unwrap());
1092 }
1093
1094 #[test]
1095 fn test_deserialize_from_string() {
1096 let json_file_meta = "{\"region_id\":0,\"file_id\":\"bc5896ec-e4d8-4017-a80d-f2de73188d55\",\
1097 \"time_range\":[{\"value\":0,\"unit\":\"Millisecond\"},{\"value\":0,\"unit\":\"Millisecond\"}],\
1098 \"available_indexes\":[\"InvertedIndex\"],\"indexes\":[{\"column_id\": 0, \"created_indexes\": [\"InvertedIndex\"]}],\"level\":0}";
1099 let file_meta = create_file_meta(
1100 FileId::from_str("bc5896ec-e4d8-4017-a80d-f2de73188d55").unwrap(),
1101 0,
1102 );
1103 let deserialized_file_meta: FileMeta = serde_json::from_str(json_file_meta).unwrap();
1104 assert_eq!(file_meta, deserialized_file_meta);
1105 }
1106
1107 #[test]
1108 fn test_deserialize_legacy_vector_index() {
1109 let json = r#"{
1110 "region_id": 0,
1111 "file_id": "bc5896ec-e4d8-4017-a80d-f2de73188d55",
1112 "time_range": [{"value":0,"unit":"Millisecond"},{"value":0,"unit":"Millisecond"}],
1113 "available_indexes": ["VectorIndex", "InvertedIndex"],
1114 "indexes": [{"column_id": 1, "created_indexes": ["VectorIndex"]}],
1115 "level": 0
1116 }"#;
1117 let meta: FileMeta = serde_json::from_str(json).unwrap();
1118 assert!(meta.inverted_index_available());
1119 assert!(!meta.fulltext_index_available());
1120 assert!(!meta.bloom_filter_index_available());
1121 let encoded = serde_json::to_value(&meta).unwrap();
1122 assert_eq!(
1123 encoded["available_indexes"],
1124 serde_json::json!(["VectorIndex", "InvertedIndex"])
1125 );
1126 assert_eq!(
1127 encoded["indexes"][0]["created_indexes"],
1128 serde_json::json!(["VectorIndex"])
1129 );
1130 }
1131
1132 #[test]
1133 fn test_file_meta_with_partition_expr() {
1134 let file_id = FileId::random();
1135 let partition_expr = PartitionExpr::new(
1136 col("a"),
1137 partition::expr::RestrictedOp::GtEq,
1138 Value::UInt32(10).into(),
1139 );
1140
1141 let file_meta_with_partition = FileMeta {
1142 region_id: 0.into(),
1143 file_id,
1144 time_range: FileTimeRange::default(),
1145 level: 0,
1146 file_size: 0,
1147 max_row_group_uncompressed_size: 0,
1148 available_indexes: SmallVec::from_iter([IndexType::InvertedIndex]),
1149 indexes: vec![ColumnIndexMetadata {
1150 column_id: 0,
1151 created_indexes: SmallVec::from_iter([IndexType::InvertedIndex]),
1152 ..Default::default()
1153 }],
1154 index_file_size: 0,
1155 index_version: 0,
1156 num_rows: 0,
1157 num_row_groups: 0,
1158 sequence: None,
1159 partition_expr: Some(partition_expr.clone()),
1160 num_series: 0,
1161 ..Default::default()
1162 };
1163
1164 let serialized = serde_json::to_string(&file_meta_with_partition).unwrap();
1166 let deserialized: FileMeta = serde_json::from_str(&serialized).unwrap();
1167 assert_eq!(file_meta_with_partition, deserialized);
1168
1169 let serialized_value: serde_json::Value = serde_json::from_str(&serialized).unwrap();
1171 assert!(serialized_value["partition_expr"].as_str().is_some());
1172 let partition_expr_json = serialized_value["partition_expr"].as_str().unwrap();
1173 assert!(partition_expr_json.contains("\"Column\":\"a\""));
1174 assert!(partition_expr_json.contains("\"op\":\"GtEq\""));
1175
1176 let file_meta_none = FileMeta {
1178 partition_expr: None,
1179 ..file_meta_with_partition.clone()
1180 };
1181 let serialized_none = serde_json::to_string(&file_meta_none).unwrap();
1182 let deserialized_none: FileMeta = serde_json::from_str(&serialized_none).unwrap();
1183 assert_eq!(file_meta_none, deserialized_none);
1184 }
1185
1186 #[test]
1187 fn test_file_meta_partition_expr_backward_compatibility() {
1188 let json_with_partition_expr = r#"{
1190 "region_id": 0,
1191 "file_id": "bc5896ec-e4d8-4017-a80d-f2de73188d55",
1192 "time_range": [
1193 {"value": 0, "unit": "Millisecond"},
1194 {"value": 0, "unit": "Millisecond"}
1195 ],
1196 "level": 0,
1197 "file_size": 0,
1198 "available_indexes": ["InvertedIndex"],
1199 "index_file_size": 0,
1200 "num_rows": 0,
1201 "num_row_groups": 0,
1202 "sequence": null,
1203 "partition_expr": "{\"Expr\":{\"lhs\":{\"Column\":\"a\"},\"op\":\"GtEq\",\"rhs\":{\"Value\":{\"UInt32\":10}}}}"
1204 }"#;
1205
1206 let file_meta: FileMeta = serde_json::from_str(json_with_partition_expr).unwrap();
1207 assert!(file_meta.partition_expr.is_some());
1208 let expr = file_meta.partition_expr.unwrap();
1209 assert_eq!(format!("{}", expr), "a >= 10");
1210
1211 let json_with_empty_expr = r#"{
1213 "region_id": 0,
1214 "file_id": "bc5896ec-e4d8-4017-a80d-f2de73188d55",
1215 "time_range": [
1216 {"value": 0, "unit": "Millisecond"},
1217 {"value": 0, "unit": "Millisecond"}
1218 ],
1219 "level": 0,
1220 "file_size": 0,
1221 "available_indexes": [],
1222 "index_file_size": 0,
1223 "num_rows": 0,
1224 "num_row_groups": 0,
1225 "sequence": null,
1226 "partition_expr": ""
1227 }"#;
1228
1229 let file_meta_empty: FileMeta = serde_json::from_str(json_with_empty_expr).unwrap();
1230 assert!(file_meta_empty.partition_expr.is_none());
1231
1232 let json_with_null_expr = r#"{
1234 "region_id": 0,
1235 "file_id": "bc5896ec-e4d8-4017-a80d-f2de73188d55",
1236 "time_range": [
1237 {"value": 0, "unit": "Millisecond"},
1238 {"value": 0, "unit": "Millisecond"}
1239 ],
1240 "level": 0,
1241 "file_size": 0,
1242 "available_indexes": [],
1243 "index_file_size": 0,
1244 "num_rows": 0,
1245 "num_row_groups": 0,
1246 "sequence": null,
1247 "partition_expr": null
1248 }"#;
1249
1250 let file_meta_null: FileMeta = serde_json::from_str(json_with_null_expr).unwrap();
1251 assert!(file_meta_null.partition_expr.is_none());
1252
1253 let json_with_empty_expr = r#"{
1255 "region_id": 0,
1256 "file_id": "bc5896ec-e4d8-4017-a80d-f2de73188d55",
1257 "time_range": [
1258 {"value": 0, "unit": "Millisecond"},
1259 {"value": 0, "unit": "Millisecond"}
1260 ],
1261 "level": 0,
1262 "file_size": 0,
1263 "available_indexes": [],
1264 "index_file_size": 0,
1265 "num_rows": 0,
1266 "num_row_groups": 0,
1267 "sequence": null
1268 }"#;
1269
1270 let file_meta_empty: FileMeta = serde_json::from_str(json_with_empty_expr).unwrap();
1271 assert!(file_meta_empty.partition_expr.is_none());
1272 }
1273
1274 #[test]
1275 fn test_file_meta_indexes_backward_compatibility() {
1276 let json_old_file_meta = r#"{
1278 "region_id": 0,
1279 "file_id": "bc5896ec-e4d8-4017-a80d-f2de73188d55",
1280 "time_range": [
1281 {"value": 0, "unit": "Millisecond"},
1282 {"value": 0, "unit": "Millisecond"}
1283 ],
1284 "available_indexes": ["InvertedIndex"],
1285 "level": 0,
1286 "file_size": 0,
1287 "index_file_size": 0,
1288 "num_rows": 0,
1289 "num_row_groups": 0
1290 }"#;
1291
1292 let deserialized_file_meta: FileMeta = serde_json::from_str(json_old_file_meta).unwrap();
1293
1294 assert_eq!(deserialized_file_meta.indexes, vec![]);
1296
1297 let expected_indexes: IndexTypes = SmallVec::from_iter([IndexType::InvertedIndex]);
1298 assert_eq!(deserialized_file_meta.available_indexes, expected_indexes);
1299
1300 assert_eq!(
1301 deserialized_file_meta.file_id,
1302 FileId::from_str("bc5896ec-e4d8-4017-a80d-f2de73188d55").unwrap()
1303 );
1304 assert!(!deserialized_file_meta.preserve_row_sequence);
1305 }
1306
1307 #[test]
1308 fn test_file_meta_preserve_row_sequence_serde() {
1309 let file_meta = FileMeta {
1310 preserve_row_sequence: true,
1311 ..Default::default()
1312 };
1313
1314 let serialized = serde_json::to_string(&file_meta).unwrap();
1315 let value: serde_json::Value = serde_json::from_str(&serialized).unwrap();
1316 assert_eq!(value["preserve_row_sequence"], true);
1317
1318 let deserialized: FileMeta = serde_json::from_str(&serialized).unwrap();
1319 assert_eq!(file_meta, deserialized);
1320
1321 let file_meta_false = FileMeta {
1322 preserve_row_sequence: false,
1323 ..file_meta.clone()
1324 };
1325 let serialized_false = serde_json::to_string(&file_meta_false).unwrap();
1326 let value_false: serde_json::Value = serde_json::from_str(&serialized_false).unwrap();
1327 assert!(value_false.get("preserve_row_sequence").is_none());
1328 }
1329 #[test]
1330 fn test_is_index_consistent_with_region() {
1331 fn new_column_meta(
1332 id: ColumnId,
1333 name: &str,
1334 inverted: bool,
1335 fulltext: bool,
1336 skipping: bool,
1337 ) -> ColumnMetadata {
1338 let mut column_schema =
1339 ColumnSchema::new(name, ConcreteDataType::string_datatype(), true);
1340 if inverted {
1341 column_schema = column_schema.with_inverted_index(true);
1342 }
1343 if fulltext {
1344 column_schema = column_schema
1345 .with_fulltext_options(FulltextOptions::new_unchecked(
1346 true,
1347 FulltextAnalyzer::English,
1348 false,
1349 FulltextBackend::Bloom,
1350 1000,
1351 0.01,
1352 ))
1353 .unwrap();
1354 }
1355 if skipping {
1356 column_schema = column_schema
1357 .with_skipping_options(SkippingIndexOptions::new_unchecked(
1358 1024,
1359 0.01,
1360 datatypes::schema::SkippingIndexType::BloomFilter,
1361 ))
1362 .unwrap();
1363 }
1364
1365 ColumnMetadata {
1366 column_schema,
1367 semantic_type: api::v1::SemanticType::Tag,
1368 column_id: id,
1369 }
1370 }
1371
1372 let mut file_meta = FileMeta {
1374 indexes: vec![ColumnIndexMetadata {
1375 column_id: 1,
1376 created_indexes: SmallVec::from_iter([IndexType::InvertedIndex]),
1377 ..Default::default()
1378 }],
1379 ..Default::default()
1380 };
1381 let region_meta = vec![new_column_meta(1, "tag1", true, false, false)];
1382 assert!(file_meta.is_index_consistent_with_region(®ion_meta));
1383
1384 file_meta.indexes = vec![ColumnIndexMetadata {
1386 column_id: 1,
1387 created_indexes: SmallVec::from_iter([
1388 IndexType::InvertedIndex,
1389 IndexType::BloomFilterIndex,
1390 ]),
1391 ..Default::default()
1392 }];
1393 let region_meta = vec![new_column_meta(1, "tag1", true, false, false)];
1394 assert!(file_meta.is_index_consistent_with_region(®ion_meta));
1395
1396 file_meta.indexes = vec![ColumnIndexMetadata {
1398 column_id: 1,
1399 created_indexes: SmallVec::from_iter([IndexType::InvertedIndex]),
1400 ..Default::default()
1401 }];
1402 let region_meta = vec![new_column_meta(1, "tag1", true, true, false)]; assert!(!file_meta.is_index_consistent_with_region(®ion_meta));
1404
1405 file_meta.indexes = vec![ColumnIndexMetadata {
1407 column_id: 2, created_indexes: SmallVec::from_iter([IndexType::InvertedIndex]),
1409 ..Default::default()
1410 }];
1411 let region_meta = vec![new_column_meta(1, "tag1", true, false, false)]; assert!(!file_meta.is_index_consistent_with_region(®ion_meta));
1413
1414 file_meta.indexes = vec![ColumnIndexMetadata {
1416 column_id: 1,
1417 created_indexes: SmallVec::from_iter([IndexType::InvertedIndex]),
1418 ..Default::default()
1419 }];
1420 let region_meta = vec![new_column_meta(1, "tag1", false, false, false)]; assert!(file_meta.is_index_consistent_with_region(®ion_meta));
1422
1423 file_meta.indexes = vec![];
1425 let region_meta = vec![new_column_meta(1, "tag1", true, false, false)];
1426 assert!(!file_meta.is_index_consistent_with_region(®ion_meta));
1427
1428 file_meta.indexes = vec![
1430 ColumnIndexMetadata {
1431 column_id: 1,
1432 created_indexes: SmallVec::from_iter([IndexType::InvertedIndex]),
1433 ..Default::default()
1434 },
1435 ColumnIndexMetadata {
1436 column_id: 2, created_indexes: SmallVec::from_iter([IndexType::FulltextIndex]),
1438 ..Default::default()
1439 },
1440 ];
1441 let region_meta = vec![
1442 new_column_meta(1, "tag1", true, false, false),
1443 new_column_meta(2, "tag2", false, true, true), ];
1445 assert!(!file_meta.is_index_consistent_with_region(®ion_meta));
1446 }
1447
1448 #[test]
1449 fn test_index_consistency_requires_current_fulltext_bloom_version() {
1450 let fulltext_column = |backend| {
1451 let column_schema = ColumnSchema::new("msg", ConcreteDataType::string_datatype(), true)
1452 .with_fulltext_options(FulltextOptions::new_unchecked(
1453 true,
1454 FulltextAnalyzer::English,
1455 false,
1456 backend,
1457 1000,
1458 0.01,
1459 ))
1460 .unwrap();
1461 vec![ColumnMetadata {
1462 column_schema,
1463 semantic_type: api::v1::SemanticType::Field,
1464 column_id: 1,
1465 }]
1466 };
1467 let file_meta = |fulltext_bloom_version| FileMeta {
1468 indexes: vec![ColumnIndexMetadata {
1469 column_id: 1,
1470 created_indexes: SmallVec::from_iter([IndexType::FulltextIndex]),
1471 fulltext_bloom_version,
1472 }],
1473 ..Default::default()
1474 };
1475
1476 let bloom = fulltext_column(FulltextBackend::Bloom);
1477 assert!(!file_meta(0).is_index_consistent_with_region(&bloom));
1479 assert!(file_meta(FULLTEXT_BLOOM_VERSION).is_index_consistent_with_region(&bloom));
1480 let tantivy = fulltext_column(FulltextBackend::Tantivy);
1482 assert!(file_meta(0).is_index_consistent_with_region(&tantivy));
1483 }
1484}