Skip to main content

mito2/sst/
file.rs

1// Copyright 2023 Greptime Team
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! Structures to describe metadata of files.
16
17use 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};
28use common_time::Timestamp;
29use partition::expr::PartitionExpr;
30use serde::{Deserialize, Serialize};
31use smallvec::SmallVec;
32use store_api::metadata::ColumnMetadata;
33use store_api::region_request::PathType;
34use store_api::storage::{ColumnId, FileId, IndexVersion, RegionId};
35
36use crate::access_layer::AccessLayerRef;
37use crate::cache::CacheManagerRef;
38use crate::cache::file_cache::{FileType, IndexKey};
39use crate::sst::file_purger::FilePurgerRef;
40use crate::sst::location;
41use crate::sst::parquet::SstInfo;
42
43/// Custom serde functions for Bytes fields serialized as base64 strings.
44fn serialize_bytes_option<S>(bytes: &Option<Bytes>, serializer: S) -> Result<S::Ok, S::Error>
45where
46    S: serde::Serializer,
47{
48    match bytes {
49        None => serializer.serialize_none(),
50        Some(b) => serializer.serialize_some(&BASE64_STANDARD.encode(b)),
51    }
52}
53
54fn deserialize_bytes_option<'de, D>(deserializer: D) -> Result<Option<Bytes>, D::Error>
55where
56    D: serde::Deserializer<'de>,
57{
58    let opt: Option<String> = Option::deserialize(deserializer)?;
59    match opt {
60        None => Ok(None),
61        Some(s) => {
62            let decoded = BASE64_STANDARD
63                .decode(&s)
64                .map_err(serde::de::Error::custom)?;
65            Ok(Some(Bytes::from(decoded)))
66        }
67    }
68}
69
70/// Custom serde functions for partition_expr field in FileMeta
71fn serialize_partition_expr<S>(
72    partition_expr: &Option<PartitionExpr>,
73    serializer: S,
74) -> Result<S::Ok, S::Error>
75where
76    S: serde::Serializer,
77{
78    use serde::ser::Error;
79
80    match partition_expr {
81        None => serializer.serialize_none(),
82        Some(expr) => {
83            let json_str = expr.as_json_str().map_err(S::Error::custom)?;
84            serializer.serialize_some(&json_str)
85        }
86    }
87}
88
89fn deserialize_partition_expr<'de, D>(deserializer: D) -> Result<Option<PartitionExpr>, D::Error>
90where
91    D: serde::Deserializer<'de>,
92{
93    use serde::de::Error;
94
95    let opt_json_str: Option<String> = Option::deserialize(deserializer)?;
96    match opt_json_str {
97        None => Ok(None),
98        Some(json_str) => {
99            if json_str.is_empty() {
100                // Empty string represents explicit "single-region/no-partition" designation
101                Ok(None)
102            } else {
103                // Parse the JSON string to PartitionExpr
104                PartitionExpr::from_json_str(&json_str).map_err(D::Error::custom)
105            }
106        }
107    }
108}
109
110/// Type to store SST level.
111pub type Level = u8;
112/// Maximum level of SSTs.
113pub const MAX_LEVEL: Level = 2;
114/// Type to store index types for a column.
115pub type IndexTypes = SmallVec<[IndexType; 4]>;
116
117/// Cross-region file id.
118///
119/// It contains a region id and a file id. The string representation is `{region_id}/{file_id}`.
120#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
121pub struct RegionFileId {
122    /// The region that creates the file.
123    region_id: RegionId,
124    /// The id of the file.
125    file_id: FileId,
126}
127
128impl RegionFileId {
129    /// Creates a new [RegionFileId] from `region_id` and `file_id`.
130    pub fn new(region_id: RegionId, file_id: FileId) -> Self {
131        Self { region_id, file_id }
132    }
133
134    /// Gets the region id.
135    pub fn region_id(&self) -> RegionId {
136        self.region_id
137    }
138
139    /// Gets the file id.
140    pub fn file_id(&self) -> FileId {
141        self.file_id
142    }
143}
144
145impl fmt::Display for RegionFileId {
146    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
147        write!(f, "{}/{}", self.region_id, self.file_id)
148    }
149}
150
151/// Unique identifier for an index file, combining the SST file ID and the index version.
152#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
153pub struct RegionIndexId {
154    pub file_id: RegionFileId,
155    pub version: IndexVersion,
156}
157
158impl RegionIndexId {
159    pub fn new(file_id: RegionFileId, version: IndexVersion) -> Self {
160        Self { file_id, version }
161    }
162
163    pub fn region_id(&self) -> RegionId {
164        self.file_id.region_id
165    }
166
167    pub fn file_id(&self) -> FileId {
168        self.file_id.file_id
169    }
170}
171
172impl fmt::Display for RegionIndexId {
173    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
174        if self.version == 0 {
175            write!(f, "{}/{}", self.file_id.region_id, self.file_id.file_id)
176        } else {
177            write!(
178                f,
179                "{}/{}.{}",
180                self.file_id.region_id, self.file_id.file_id, self.version
181            )
182        }
183    }
184}
185
186/// Time range (min and max timestamps) of a SST file.
187/// Both min and max are inclusive.
188pub type FileTimeRange = (Timestamp, Timestamp);
189
190/// Checks if two inclusive timestamp ranges overlap with each other.
191pub(crate) fn overlaps(l: &FileTimeRange, r: &FileTimeRange) -> bool {
192    let (l, r) = if l.0 <= r.0 { (l, r) } else { (r, l) };
193    let (_, l_end) = l;
194    let (r_start, _) = r;
195
196    r_start <= l_end
197}
198
199/// Metadata of a SST file.
200#[derive(Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
201#[serde(default)]
202pub struct FileMeta {
203    /// Region that created the file. The region id may not be the id of the current region.
204    pub region_id: RegionId,
205    /// Compared to normal file names, FileId ignore the extension
206    pub file_id: FileId,
207    /// Timestamp range of file. The timestamps have the same time unit as the
208    /// data in the SST.
209    pub time_range: FileTimeRange,
210    /// SST level of the file.
211    pub level: Level,
212    /// Size of the file.
213    pub file_size: u64,
214    /// Maximum uncompressed row group size of the file. 0 means unknown.
215    pub max_row_group_uncompressed_size: u64,
216    /// Available indexes of the file.
217    pub available_indexes: IndexTypes,
218    /// Created indexes of the file for each column.
219    ///
220    /// This is essentially a more granular, column-level version of `available_indexes`,
221    /// primarily used for manual index building in the asynchronous index construction mode.
222    ///
223    /// For backward compatibility, older `FileMeta` versions might only contain `available_indexes`.
224    /// In such cases, we cannot deduce specific column index information from `available_indexes` alone.
225    /// Therefore, defaulting this `indexes` field to an empty list during deserialization is a
226    /// reasonable and necessary step to ensure column information consistency.
227    pub indexes: Vec<ColumnIndexMetadata>,
228    /// Size of the index file.
229    pub index_file_size: u64,
230    /// Version of the index file.
231    /// Used to generate the index file name: "{file_id}.{index_version}.puffin".
232    /// Default is 0 (which maps to "{file_id}.puffin" for compatibility).
233    pub index_version: u64,
234    /// Number of rows in the file.
235    ///
236    /// For historical reasons, this field might be missing in old files. Thus
237    /// the default value `0` doesn't means the file doesn't contains any rows,
238    /// but instead means the number of rows is unknown.
239    pub num_rows: u64,
240    /// Number of row groups in the file.
241    ///
242    /// For historical reasons, this field might be missing in old files. Thus
243    /// the default value `0` doesn't means the file doesn't contains any rows,
244    /// but instead means the number of rows is unknown.
245    pub num_row_groups: u64,
246    /// Sequence in this file.
247    ///
248    /// This sequence is the only sequence in this file. And it's retrieved from the max
249    /// sequence of the rows on generating this file.
250    pub sequence: Option<NonZeroU64>,
251    /// Partition expression from the region metadata when the file is created.
252    ///
253    /// This is stored as a PartitionExpr object in memory for convenience,
254    /// but serialized as JSON string for manifest compatibility.
255    /// Compatibility behavior:
256    /// - None: no partition expr was set when the file was created (legacy files).
257    /// - Some(expr): partition expression from region metadata.
258    #[serde(
259        serialize_with = "serialize_partition_expr",
260        deserialize_with = "deserialize_partition_expr"
261    )]
262    pub partition_expr: Option<PartitionExpr>,
263    /// Number of series in the file.
264    ///
265    /// The number is 0 if the series number is not available.
266    pub num_series: u64,
267    /// Minimum primary key value in the file, encoded as bytes.
268    /// `None` if the primary key range is not available (e.g., legacy files).
269    #[serde(
270        default,
271        skip_serializing_if = "Option::is_none",
272        serialize_with = "serialize_bytes_option",
273        deserialize_with = "deserialize_bytes_option"
274    )]
275    pub primary_key_min: Option<Bytes>,
276    /// Maximum primary key value in the file, encoded as bytes.
277    /// `None` if the primary key range is not available (e.g., legacy files).
278    #[serde(
279        default,
280        skip_serializing_if = "Option::is_none",
281        serialize_with = "serialize_bytes_option",
282        deserialize_with = "deserialize_bytes_option"
283    )]
284    pub primary_key_max: Option<Bytes>,
285}
286
287impl Debug for FileMeta {
288    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
289        let mut debug_struct = f.debug_struct("FileMeta");
290        debug_struct
291            .field("region_id", &self.region_id)
292            .field_with("file_id", |f| write!(f, "{} ", self.file_id))
293            .field_with("time_range", |f| {
294                write!(
295                    f,
296                    "({}, {}) ",
297                    self.time_range.0.to_iso8601_string(),
298                    self.time_range.1.to_iso8601_string()
299                )
300            })
301            .field("level", &self.level)
302            .field("file_size", &ReadableSize(self.file_size))
303            .field(
304                "max_row_group_uncompressed_size",
305                &ReadableSize(self.max_row_group_uncompressed_size),
306            );
307        if !self.available_indexes.is_empty() {
308            debug_struct
309                .field("available_indexes", &self.available_indexes)
310                .field("indexes", &self.indexes)
311                .field("index_file_size", &ReadableSize(self.index_file_size));
312        }
313        debug_struct
314            .field("num_rows", &self.num_rows)
315            .field("num_row_groups", &self.num_row_groups)
316            .field_with("sequence", |f| match self.sequence {
317                None => {
318                    write!(f, "None")
319                }
320                Some(seq) => {
321                    write!(f, "{}", seq)
322                }
323            })
324            .field("partition_expr", &self.partition_expr)
325            .field("num_series", &self.num_series);
326        if self.primary_key_min.is_some() || self.primary_key_max.is_some() {
327            debug_struct
328                .field(
329                    "primary_key_min",
330                    &self.primary_key_min.as_ref().map(|b| b.len()),
331                )
332                .field(
333                    "primary_key_max",
334                    &self.primary_key_max.as_ref().map(|b| b.len()),
335                );
336        }
337        debug_struct.finish()
338    }
339}
340
341/// Type of index.
342#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
343pub enum IndexType {
344    /// Inverted index.
345    InvertedIndex,
346    /// Full-text index.
347    FulltextIndex,
348    /// Bloom Filter index
349    BloomFilterIndex,
350    /// Vector index (HNSW).
351    #[cfg(feature = "vector_index")]
352    VectorIndex,
353}
354
355/// Metadata of indexes created for a specific column in an SST file.
356///
357/// This structure tracks which index types have been successfully created for a column.
358/// It provides more granular, column-level index information compared to the file-level
359/// `available_indexes` field in [`FileMeta`].
360///
361/// This is primarily used for:
362/// - Manual index building in asynchronous index construction mode
363/// - Verifying index consistency between files and region metadata
364#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
365#[serde(default)]
366pub struct ColumnIndexMetadata {
367    /// The column ID this index metadata applies to.
368    pub column_id: ColumnId,
369    /// List of index types that have been successfully created for this column.
370    pub created_indexes: IndexTypes,
371}
372
373impl FileMeta {
374    /// Returns the primary key range if both min and max are present.
375    pub fn primary_key_range(&self) -> Option<(Bytes, Bytes)> {
376        match (&self.primary_key_min, &self.primary_key_max) {
377            (Some(min), Some(max)) => Some((min.clone(), max.clone())),
378            _ => None,
379        }
380    }
381
382    pub fn exists_index(&self) -> bool {
383        !self.available_indexes.is_empty()
384    }
385
386    pub fn index_version(&self) -> Option<IndexVersion> {
387        if self.exists_index() {
388            Some(self.index_version)
389        } else {
390            None
391        }
392    }
393
394    /// Whether the index file is up-to-date comparing to another file meta.
395    pub fn is_index_up_to_date(&self, other: &FileMeta) -> bool {
396        self.exists_index() && other.exists_index() && self.index_version >= other.index_version
397    }
398
399    /// Returns true if the file has an inverted index
400    pub fn inverted_index_available(&self) -> bool {
401        self.available_indexes.contains(&IndexType::InvertedIndex)
402    }
403
404    /// Returns true if the file has a fulltext index
405    pub fn fulltext_index_available(&self) -> bool {
406        self.available_indexes.contains(&IndexType::FulltextIndex)
407    }
408
409    /// Returns true if the file has a bloom filter index.
410    pub fn bloom_filter_index_available(&self) -> bool {
411        self.available_indexes
412            .contains(&IndexType::BloomFilterIndex)
413    }
414
415    /// Returns true if the file has a vector index.
416    #[cfg(feature = "vector_index")]
417    pub fn vector_index_available(&self) -> bool {
418        self.available_indexes.contains(&IndexType::VectorIndex)
419    }
420
421    pub fn index_file_size(&self) -> u64 {
422        self.index_file_size
423    }
424
425    /// Check whether the file index is consistent with the given region metadata.
426    pub fn is_index_consistent_with_region(&self, metadata: &[ColumnMetadata]) -> bool {
427        let id_to_indexes = self
428            .indexes
429            .iter()
430            .map(|index| (index.column_id, index.created_indexes.clone()))
431            .collect::<std::collections::HashMap<_, _>>();
432        for column in metadata {
433            if !column.column_schema.is_indexed() {
434                continue;
435            }
436            if let Some(indexes) = id_to_indexes.get(&column.column_id) {
437                if column.column_schema.is_inverted_indexed()
438                    && !indexes.contains(&IndexType::InvertedIndex)
439                {
440                    return false;
441                }
442                if column.column_schema.is_fulltext_indexed()
443                    && !indexes.contains(&IndexType::FulltextIndex)
444                {
445                    return false;
446                }
447                if column.column_schema.is_skipping_indexed()
448                    && !indexes.contains(&IndexType::BloomFilterIndex)
449                {
450                    return false;
451                }
452            } else {
453                return false;
454            }
455        }
456        true
457    }
458
459    /// Returns the cross-region file id.
460    pub fn file_id(&self) -> RegionFileId {
461        RegionFileId::new(self.region_id, self.file_id)
462    }
463
464    /// Returns the RegionIndexId for this file.
465    pub fn index_id(&self) -> RegionIndexId {
466        RegionIndexId::new(self.file_id(), self.index_version)
467    }
468}
469
470/// Handle to a SST file.
471#[derive(Clone)]
472pub struct FileHandle {
473    inner: Arc<FileHandleInner>,
474}
475
476impl fmt::Debug for FileHandle {
477    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
478        f.debug_struct("FileHandle")
479            .field("meta", self.meta_ref())
480            .field("compacting", &self.compacting())
481            .field("deleted", &self.inner.deleted.load(Ordering::Relaxed))
482            .finish()
483    }
484}
485
486impl FileHandle {
487    pub fn new(meta: FileMeta, file_purger: FilePurgerRef) -> FileHandle {
488        let pk_range = meta.primary_key_range();
489        FileHandle {
490            inner: Arc::new(FileHandleInner::new(meta, file_purger, pk_range)),
491        }
492    }
493
494    #[cfg(test)]
495    pub fn new_with_primary_key_range(
496        meta: FileMeta,
497        file_purger: FilePurgerRef,
498        primary_key_range: Option<(Bytes, Bytes)>,
499    ) -> FileHandle {
500        FileHandle {
501            inner: Arc::new(FileHandleInner::new(meta, file_purger, primary_key_range)),
502        }
503    }
504
505    /// Returns the region id of the file.
506    pub fn region_id(&self) -> RegionId {
507        self.inner.meta.region_id
508    }
509
510    /// Returns the cross-region file id.
511    pub fn file_id(&self) -> RegionFileId {
512        RegionFileId::new(self.inner.meta.region_id, self.inner.meta.file_id)
513    }
514
515    /// Returns the RegionIndexId for this file.
516    pub fn index_id(&self) -> RegionIndexId {
517        RegionIndexId::new(self.file_id(), self.inner.meta.index_version)
518    }
519
520    /// Returns the complete file path of the file.
521    pub fn file_path(&self, table_dir: &str, path_type: PathType) -> String {
522        location::sst_file_path(table_dir, self.file_id(), path_type)
523    }
524
525    /// Returns the time range of the file.
526    pub fn time_range(&self) -> FileTimeRange {
527        self.inner.meta.time_range
528    }
529
530    /// Mark the file as deleted and will delete it on drop asynchronously
531    pub fn mark_deleted(&self) {
532        self.inner.deleted.store(true, Ordering::Relaxed);
533    }
534
535    pub fn compacting(&self) -> bool {
536        self.inner.compacting.load(Ordering::Relaxed)
537    }
538
539    pub fn set_compacting(&self, compacting: bool) {
540        self.inner.compacting.store(compacting, Ordering::Relaxed);
541    }
542
543    /// Atomically marks this file as compacting if it is currently available.
544    pub fn try_set_compacting(&self) -> bool {
545        self.inner
546            .compacting
547            .compare_exchange(false, true, Ordering::Relaxed, Ordering::Relaxed)
548            .is_ok()
549    }
550
551    pub fn index_outdated(&self) -> bool {
552        self.inner.index_outdated.load(Ordering::Relaxed)
553    }
554
555    pub fn set_index_outdated(&self, index_outdated: bool) {
556        self.inner
557            .index_outdated
558            .store(index_outdated, Ordering::Relaxed);
559    }
560
561    /// Returns a reference to the [FileMeta].
562    pub fn meta_ref(&self) -> &FileMeta {
563        &self.inner.meta
564    }
565
566    pub fn file_purger(&self) -> FilePurgerRef {
567        self.inner.file_purger.clone()
568    }
569
570    pub fn size(&self) -> u64 {
571        self.inner.meta.file_size
572    }
573
574    pub fn index_size(&self) -> u64 {
575        self.inner.meta.index_file_size
576    }
577
578    pub fn num_rows(&self) -> usize {
579        self.inner.meta.num_rows as usize
580    }
581
582    pub fn level(&self) -> Level {
583        self.inner.meta.level
584    }
585
586    pub fn is_deleted(&self) -> bool {
587        self.inner.deleted.load(Ordering::Relaxed)
588    }
589
590    pub fn primary_key_range(&self) -> Option<(Bytes, Bytes)> {
591        self.inner.primary_key_range.read().unwrap().clone()
592    }
593
594    pub(crate) fn set_primary_key_range(&self, primary_key_range: (Bytes, Bytes)) {
595        *self.inner.primary_key_range.write().unwrap() = Some(primary_key_range);
596    }
597}
598
599/// Inner data of [FileHandle].
600///
601/// Contains meta of the file, and other mutable info like whether the file is compacting.
602struct FileHandleInner {
603    meta: FileMeta,
604    compacting: AtomicBool,
605    deleted: AtomicBool,
606    index_outdated: AtomicBool,
607    primary_key_range: RwLock<Option<(Bytes, Bytes)>>,
608    file_purger: FilePurgerRef,
609}
610
611impl Drop for FileHandleInner {
612    fn drop(&mut self) {
613        self.file_purger.remove_file(
614            self.meta.clone(),
615            self.deleted.load(Ordering::Acquire),
616            self.index_outdated.load(Ordering::Acquire),
617        );
618    }
619}
620
621impl FileHandleInner {
622    /// There should only be one `FileHandleInner` for each file on a datanode
623    fn new(
624        meta: FileMeta,
625        file_purger: FilePurgerRef,
626        primary_key_range: Option<(Bytes, Bytes)>,
627    ) -> FileHandleInner {
628        file_purger.new_file(&meta);
629        FileHandleInner {
630            meta,
631            compacting: AtomicBool::new(false),
632            deleted: AtomicBool::new(false),
633            index_outdated: AtomicBool::new(false),
634            primary_key_range: RwLock::new(primary_key_range),
635            file_purger,
636        }
637    }
638}
639
640/// Delete files for a region.
641/// - `region_id`: Region id.
642/// - `file_ids`: List of (file id, index version) tuples to delete.
643/// - `delete_index`: Whether to delete the index file from the cache.
644/// - `access_layer`: Access layer to delete files.
645/// - `cache_manager`: Cache manager to remove files from cache.
646pub async fn delete_files(
647    region_id: RegionId,
648    file_ids: &[(FileId, u64)],
649    delete_index: bool,
650    access_layer: &AccessLayerRef,
651    cache_manager: &Option<CacheManagerRef>,
652) -> crate::error::Result<()> {
653    // Remove meta of the file from cache.
654    if let Some(cache) = &cache_manager {
655        for (file_id, _) in file_ids {
656            cache.remove_parquet_meta_data(RegionFileId::new(region_id, *file_id));
657        }
658    }
659    let mut attempted_files = Vec::with_capacity(file_ids.len());
660    let mut index_ids = Vec::new();
661
662    for (file_id, index_version) in file_ids {
663        let region_file_id = RegionFileId::new(region_id, *file_id);
664        attempted_files.push(*file_id);
665        index_ids.extend(
666            (0..=*index_version).map(|version| RegionIndexId::new(region_file_id, version)),
667        );
668    }
669
670    access_layer
671        .delete_ssts(region_id, &attempted_files)
672        .await?;
673    access_layer.delete_indexes(&index_ids).await?;
674
675    debug!(
676        "Attempted to delete {} files for region {}: {:?}",
677        attempted_files.len(),
678        region_id,
679        attempted_files
680    );
681
682    for (file_id, index_version) in file_ids {
683        purge_index_cache_stager(
684            region_id,
685            delete_index,
686            access_layer,
687            cache_manager,
688            *file_id,
689            *index_version,
690        )
691        .await;
692    }
693    Ok(())
694}
695
696/// Tracks finalized SSTs until their manifest edit is committed.
697///
698/// Flush and local compaction jobs use this to remove files that were written successfully but
699/// abandoned by cancellation or a later failure.
700#[derive(Clone)]
701pub(crate) struct UncommittedSsts {
702    region_id: RegionId,
703    files: Arc<Mutex<HashMap<FileId, (u64, bool)>>>,
704    access_layer: AccessLayerRef,
705    cache_manager: Option<CacheManagerRef>,
706}
707
708impl UncommittedSsts {
709    pub(crate) fn new(
710        region_id: RegionId,
711        access_layer: AccessLayerRef,
712        cache_manager: Option<CacheManagerRef>,
713    ) -> Self {
714        Self {
715            region_id,
716            files: Arc::new(Mutex::new(HashMap::new())),
717            access_layer,
718            cache_manager,
719        }
720    }
721
722    /// Tracks newly finalized SSTs before they become visible in the manifest.
723    pub(crate) fn track(&self, ssts: &[SstInfo]) {
724        let mut files = self.files.lock().unwrap();
725        for sst in ssts {
726            files.insert(
727                sst.file_id,
728                (sst.index_metadata.version, sst.index_metadata.file_size > 0),
729            );
730        }
731    }
732
733    /// Disarms cleanup after the manifest edit has committed or may have committed.
734    ///
735    /// A manifest update error does not guarantee that the edit was not persisted. Once an edit
736    /// may become visible, its SSTs must be retained to avoid deleting referenced files.
737    pub(crate) fn disarm_cleanup(&self) {
738        self.files.lock().unwrap().clear();
739    }
740
741    #[cfg(test)]
742    pub(crate) fn num_tracked_files(&self) -> usize {
743        self.files.lock().unwrap().len()
744    }
745
746    /// Removes all finalized SSTs still owned by this job.
747    pub(crate) async fn cleanup(&self) {
748        if let Err(err) = self.try_cleanup().await {
749            error!(err; "Failed to clean uncommitted SSTs for region {}", self.region_id);
750        }
751    }
752
753    async fn try_cleanup(&self) -> crate::error::Result<()> {
754        let files = std::mem::take(&mut *self.files.lock().unwrap());
755        if files.is_empty() {
756            return Ok(());
757        }
758
759        let delete_index = files.values().any(|(_, exists_index)| *exists_index);
760        let file_ids = files
761            .into_iter()
762            .map(|(file_id, (index_version, _))| (file_id, index_version))
763            .collect::<Vec<_>>();
764        delete_files(
765            self.region_id,
766            &file_ids,
767            delete_index,
768            &self.access_layer,
769            &self.cache_manager,
770        )
771        .await
772    }
773
774    #[cfg(test)]
775    pub(crate) async fn cleanup_for_test(&self) -> crate::error::Result<()> {
776        self.try_cleanup().await
777    }
778}
779
780pub async fn delete_index(
781    region_index_id: RegionIndexId,
782    access_layer: &AccessLayerRef,
783    cache_manager: &Option<CacheManagerRef>,
784) -> crate::error::Result<()> {
785    delete_index_and_purge(region_index_id, access_layer, cache_manager).await?;
786
787    Ok(())
788}
789
790pub async fn delete_indexes(
791    index_ids: &[RegionIndexId],
792    access_layer: &AccessLayerRef,
793    cache_manager: &Option<CacheManagerRef>,
794) -> crate::error::Result<()> {
795    if index_ids.is_empty() {
796        return Ok(());
797    }
798
799    if let Err(e) = access_layer.delete_indexes(index_ids).await {
800        error!(e; "Failed to batch delete index files");
801
802        for index_id in index_ids {
803            delete_index_and_purge(*index_id, access_layer, cache_manager).await?;
804        }
805
806        return Ok(());
807    }
808
809    purge_indexes(index_ids, access_layer, cache_manager).await;
810
811    Ok(())
812}
813
814async fn delete_index_and_purge(
815    index_id: RegionIndexId,
816    access_layer: &AccessLayerRef,
817    cache_manager: &Option<CacheManagerRef>,
818) -> crate::error::Result<()> {
819    access_layer.delete_index(index_id).await?;
820    purge_index_cache_stager(
821        index_id.region_id(),
822        true,
823        access_layer,
824        cache_manager,
825        index_id.file_id(),
826        index_id.version,
827    )
828    .await;
829    Ok(())
830}
831
832async fn purge_indexes(
833    index_ids: &[RegionIndexId],
834    access_layer: &AccessLayerRef,
835    cache_manager: &Option<CacheManagerRef>,
836) {
837    for index_id in index_ids {
838        purge_index_cache_stager(
839            index_id.region_id(),
840            true,
841            access_layer,
842            cache_manager,
843            index_id.file_id(),
844            index_id.version,
845        )
846        .await;
847    }
848}
849
850async fn purge_index_cache_stager(
851    region_id: RegionId,
852    delete_index: bool,
853    access_layer: &AccessLayerRef,
854    cache_manager: &Option<CacheManagerRef>,
855    file_id: FileId,
856    index_version: u64,
857) {
858    if let Some(write_cache) = cache_manager.as_ref().and_then(|cache| cache.write_cache()) {
859        // Removes index file from the cache.
860        if delete_index {
861            write_cache
862                .remove(IndexKey::new(
863                    region_id,
864                    file_id,
865                    FileType::Puffin(index_version),
866                ))
867                .await;
868        }
869
870        // Remove the SST file from the cache.
871        write_cache
872            .remove(IndexKey::new(region_id, file_id, FileType::Parquet))
873            .await;
874    }
875
876    // Purges index content in the stager.
877    if let Err(e) = access_layer
878        .puffin_manager_factory()
879        .purge_stager(RegionIndexId::new(
880            RegionFileId::new(region_id, file_id),
881            index_version,
882        ))
883        .await
884    {
885        error!(e; "Failed to purge stager with index file, file_id: {}, index_version: {}, region: {}",
886                file_id, index_version, region_id);
887    }
888}
889
890#[cfg(test)]
891mod tests {
892    use std::str::FromStr;
893
894    use datatypes::prelude::ConcreteDataType;
895    use datatypes::schema::{
896        ColumnSchema, FulltextAnalyzer, FulltextBackend, FulltextOptions, SkippingIndexOptions,
897    };
898    use datatypes::value::Value;
899    use partition::expr::{PartitionExpr, col};
900
901    use super::*;
902
903    fn create_file_meta(file_id: FileId, level: Level) -> FileMeta {
904        FileMeta {
905            region_id: 0.into(),
906            file_id,
907            time_range: FileTimeRange::default(),
908            level,
909            file_size: 0,
910            max_row_group_uncompressed_size: 0,
911            available_indexes: SmallVec::from_iter([IndexType::InvertedIndex]),
912            indexes: vec![ColumnIndexMetadata {
913                column_id: 0,
914                created_indexes: SmallVec::from_iter([IndexType::InvertedIndex]),
915            }],
916            index_file_size: 0,
917            index_version: 0,
918            num_rows: 0,
919            num_row_groups: 0,
920            sequence: None,
921            partition_expr: None,
922            num_series: 0,
923            ..Default::default()
924        }
925    }
926
927    #[test]
928    fn test_deserialize_file_meta() {
929        let file_meta = create_file_meta(FileId::random(), 0);
930        let serialized_file_meta = serde_json::to_string(&file_meta).unwrap();
931        let deserialized_file_meta = serde_json::from_str(&serialized_file_meta);
932        assert_eq!(file_meta, deserialized_file_meta.unwrap());
933    }
934
935    #[test]
936    fn test_deserialize_from_string() {
937        let json_file_meta = "{\"region_id\":0,\"file_id\":\"bc5896ec-e4d8-4017-a80d-f2de73188d55\",\
938        \"time_range\":[{\"value\":0,\"unit\":\"Millisecond\"},{\"value\":0,\"unit\":\"Millisecond\"}],\
939        \"available_indexes\":[\"InvertedIndex\"],\"indexes\":[{\"column_id\": 0, \"created_indexes\": [\"InvertedIndex\"]}],\"level\":0}";
940        let file_meta = create_file_meta(
941            FileId::from_str("bc5896ec-e4d8-4017-a80d-f2de73188d55").unwrap(),
942            0,
943        );
944        let deserialized_file_meta: FileMeta = serde_json::from_str(json_file_meta).unwrap();
945        assert_eq!(file_meta, deserialized_file_meta);
946    }
947
948    #[test]
949    fn test_file_meta_with_partition_expr() {
950        let file_id = FileId::random();
951        let partition_expr = PartitionExpr::new(
952            col("a"),
953            partition::expr::RestrictedOp::GtEq,
954            Value::UInt32(10).into(),
955        );
956
957        let file_meta_with_partition = FileMeta {
958            region_id: 0.into(),
959            file_id,
960            time_range: FileTimeRange::default(),
961            level: 0,
962            file_size: 0,
963            max_row_group_uncompressed_size: 0,
964            available_indexes: SmallVec::from_iter([IndexType::InvertedIndex]),
965            indexes: vec![ColumnIndexMetadata {
966                column_id: 0,
967                created_indexes: SmallVec::from_iter([IndexType::InvertedIndex]),
968            }],
969            index_file_size: 0,
970            index_version: 0,
971            num_rows: 0,
972            num_row_groups: 0,
973            sequence: None,
974            partition_expr: Some(partition_expr.clone()),
975            num_series: 0,
976            ..Default::default()
977        };
978
979        // Test serialization/deserialization
980        let serialized = serde_json::to_string(&file_meta_with_partition).unwrap();
981        let deserialized: FileMeta = serde_json::from_str(&serialized).unwrap();
982        assert_eq!(file_meta_with_partition, deserialized);
983
984        // Verify the serialized JSON contains the expected partition expression string
985        let serialized_value: serde_json::Value = serde_json::from_str(&serialized).unwrap();
986        assert!(serialized_value["partition_expr"].as_str().is_some());
987        let partition_expr_json = serialized_value["partition_expr"].as_str().unwrap();
988        assert!(partition_expr_json.contains("\"Column\":\"a\""));
989        assert!(partition_expr_json.contains("\"op\":\"GtEq\""));
990
991        // Test with None (legacy files)
992        let file_meta_none = FileMeta {
993            partition_expr: None,
994            ..file_meta_with_partition.clone()
995        };
996        let serialized_none = serde_json::to_string(&file_meta_none).unwrap();
997        let deserialized_none: FileMeta = serde_json::from_str(&serialized_none).unwrap();
998        assert_eq!(file_meta_none, deserialized_none);
999    }
1000
1001    #[test]
1002    fn test_file_meta_partition_expr_backward_compatibility() {
1003        // Test that we can deserialize old JSON format with partition_expr as string
1004        let json_with_partition_expr = r#"{
1005            "region_id": 0,
1006            "file_id": "bc5896ec-e4d8-4017-a80d-f2de73188d55",
1007            "time_range": [
1008                {"value": 0, "unit": "Millisecond"},
1009                {"value": 0, "unit": "Millisecond"}
1010            ],
1011            "level": 0,
1012            "file_size": 0,
1013            "available_indexes": ["InvertedIndex"],
1014            "index_file_size": 0,
1015            "num_rows": 0,
1016            "num_row_groups": 0,
1017            "sequence": null,
1018            "partition_expr": "{\"Expr\":{\"lhs\":{\"Column\":\"a\"},\"op\":\"GtEq\",\"rhs\":{\"Value\":{\"UInt32\":10}}}}"
1019        }"#;
1020
1021        let file_meta: FileMeta = serde_json::from_str(json_with_partition_expr).unwrap();
1022        assert!(file_meta.partition_expr.is_some());
1023        let expr = file_meta.partition_expr.unwrap();
1024        assert_eq!(format!("{}", expr), "a >= 10");
1025
1026        // Test empty partition expression string
1027        let json_with_empty_expr = r#"{
1028            "region_id": 0,
1029            "file_id": "bc5896ec-e4d8-4017-a80d-f2de73188d55",
1030            "time_range": [
1031                {"value": 0, "unit": "Millisecond"},
1032                {"value": 0, "unit": "Millisecond"}
1033            ],
1034            "level": 0,
1035            "file_size": 0,
1036            "available_indexes": [],
1037            "index_file_size": 0,
1038            "num_rows": 0,
1039            "num_row_groups": 0,
1040            "sequence": null,
1041            "partition_expr": ""
1042        }"#;
1043
1044        let file_meta_empty: FileMeta = serde_json::from_str(json_with_empty_expr).unwrap();
1045        assert!(file_meta_empty.partition_expr.is_none());
1046
1047        // Test null partition expression
1048        let json_with_null_expr = r#"{
1049            "region_id": 0,
1050            "file_id": "bc5896ec-e4d8-4017-a80d-f2de73188d55",
1051            "time_range": [
1052                {"value": 0, "unit": "Millisecond"},
1053                {"value": 0, "unit": "Millisecond"}
1054            ],
1055            "level": 0,
1056            "file_size": 0,
1057            "available_indexes": [],
1058            "index_file_size": 0,
1059            "num_rows": 0,
1060            "num_row_groups": 0,
1061            "sequence": null,
1062            "partition_expr": null
1063        }"#;
1064
1065        let file_meta_null: FileMeta = serde_json::from_str(json_with_null_expr).unwrap();
1066        assert!(file_meta_null.partition_expr.is_none());
1067
1068        // Test partition expression doesn't exist
1069        let json_with_empty_expr = r#"{
1070            "region_id": 0,
1071            "file_id": "bc5896ec-e4d8-4017-a80d-f2de73188d55",
1072            "time_range": [
1073                {"value": 0, "unit": "Millisecond"},
1074                {"value": 0, "unit": "Millisecond"}
1075            ],
1076            "level": 0,
1077            "file_size": 0,
1078            "available_indexes": [],
1079            "index_file_size": 0,
1080            "num_rows": 0,
1081            "num_row_groups": 0,
1082            "sequence": null
1083        }"#;
1084
1085        let file_meta_empty: FileMeta = serde_json::from_str(json_with_empty_expr).unwrap();
1086        assert!(file_meta_empty.partition_expr.is_none());
1087    }
1088
1089    #[test]
1090    fn test_file_meta_indexes_backward_compatibility() {
1091        // Old FileMeta format without the 'indexes' field
1092        let json_old_file_meta = r#"{
1093            "region_id": 0,
1094            "file_id": "bc5896ec-e4d8-4017-a80d-f2de73188d55",
1095            "time_range": [
1096                {"value": 0, "unit": "Millisecond"},
1097                {"value": 0, "unit": "Millisecond"}
1098            ],
1099            "available_indexes": ["InvertedIndex"],
1100            "level": 0,
1101            "file_size": 0,
1102            "index_file_size": 0,
1103            "num_rows": 0,
1104            "num_row_groups": 0
1105        }"#;
1106
1107        let deserialized_file_meta: FileMeta = serde_json::from_str(json_old_file_meta).unwrap();
1108
1109        // Verify backward compatibility: indexes field should default to empty vec
1110        assert_eq!(deserialized_file_meta.indexes, vec![]);
1111
1112        let expected_indexes: IndexTypes = SmallVec::from_iter([IndexType::InvertedIndex]);
1113        assert_eq!(deserialized_file_meta.available_indexes, expected_indexes);
1114
1115        assert_eq!(
1116            deserialized_file_meta.file_id,
1117            FileId::from_str("bc5896ec-e4d8-4017-a80d-f2de73188d55").unwrap()
1118        );
1119    }
1120    #[test]
1121    fn test_is_index_consistent_with_region() {
1122        fn new_column_meta(
1123            id: ColumnId,
1124            name: &str,
1125            inverted: bool,
1126            fulltext: bool,
1127            skipping: bool,
1128        ) -> ColumnMetadata {
1129            let mut column_schema =
1130                ColumnSchema::new(name, ConcreteDataType::string_datatype(), true);
1131            if inverted {
1132                column_schema = column_schema.with_inverted_index(true);
1133            }
1134            if fulltext {
1135                column_schema = column_schema
1136                    .with_fulltext_options(FulltextOptions::new_unchecked(
1137                        true,
1138                        FulltextAnalyzer::English,
1139                        false,
1140                        FulltextBackend::Bloom,
1141                        1000,
1142                        0.01,
1143                    ))
1144                    .unwrap();
1145            }
1146            if skipping {
1147                column_schema = column_schema
1148                    .with_skipping_options(SkippingIndexOptions::new_unchecked(
1149                        1024,
1150                        0.01,
1151                        datatypes::schema::SkippingIndexType::BloomFilter,
1152                    ))
1153                    .unwrap();
1154            }
1155
1156            ColumnMetadata {
1157                column_schema,
1158                semantic_type: api::v1::SemanticType::Tag,
1159                column_id: id,
1160            }
1161        }
1162
1163        // Case 1: Perfect match. File has exactly the required indexes.
1164        let mut file_meta = FileMeta {
1165            indexes: vec![ColumnIndexMetadata {
1166                column_id: 1,
1167                created_indexes: SmallVec::from_iter([IndexType::InvertedIndex]),
1168            }],
1169            ..Default::default()
1170        };
1171        let region_meta = vec![new_column_meta(1, "tag1", true, false, false)];
1172        assert!(file_meta.is_index_consistent_with_region(&region_meta));
1173
1174        // Case 2: Superset match. File has more indexes than required.
1175        file_meta.indexes = vec![ColumnIndexMetadata {
1176            column_id: 1,
1177            created_indexes: SmallVec::from_iter([
1178                IndexType::InvertedIndex,
1179                IndexType::BloomFilterIndex,
1180            ]),
1181        }];
1182        let region_meta = vec![new_column_meta(1, "tag1", true, false, false)];
1183        assert!(file_meta.is_index_consistent_with_region(&region_meta));
1184
1185        // Case 3: Missing index type. File has the column but lacks the required index type.
1186        file_meta.indexes = vec![ColumnIndexMetadata {
1187            column_id: 1,
1188            created_indexes: SmallVec::from_iter([IndexType::InvertedIndex]),
1189        }];
1190        let region_meta = vec![new_column_meta(1, "tag1", true, true, false)]; // Requires fulltext too
1191        assert!(!file_meta.is_index_consistent_with_region(&region_meta));
1192
1193        // Case 4: Missing column. Region requires an index on a column not in the file's index list.
1194        file_meta.indexes = vec![ColumnIndexMetadata {
1195            column_id: 2, // File only has index for column 2
1196            created_indexes: SmallVec::from_iter([IndexType::InvertedIndex]),
1197        }];
1198        let region_meta = vec![new_column_meta(1, "tag1", true, false, false)]; // Requires index on column 1
1199        assert!(!file_meta.is_index_consistent_with_region(&region_meta));
1200
1201        // Case 5: No indexes required by region. Should always be consistent.
1202        file_meta.indexes = vec![ColumnIndexMetadata {
1203            column_id: 1,
1204            created_indexes: SmallVec::from_iter([IndexType::InvertedIndex]),
1205        }];
1206        let region_meta = vec![new_column_meta(1, "tag1", false, false, false)]; // No index required
1207        assert!(file_meta.is_index_consistent_with_region(&region_meta));
1208
1209        // Case 6: Empty file indexes. Region requires an index.
1210        file_meta.indexes = vec![];
1211        let region_meta = vec![new_column_meta(1, "tag1", true, false, false)];
1212        assert!(!file_meta.is_index_consistent_with_region(&region_meta));
1213
1214        // Case 7: Multiple columns, one is inconsistent.
1215        file_meta.indexes = vec![
1216            ColumnIndexMetadata {
1217                column_id: 1,
1218                created_indexes: SmallVec::from_iter([IndexType::InvertedIndex]),
1219            },
1220            ColumnIndexMetadata {
1221                column_id: 2, // Column 2 is missing the required BloomFilterIndex
1222                created_indexes: SmallVec::from_iter([IndexType::FulltextIndex]),
1223            },
1224        ];
1225        let region_meta = vec![
1226            new_column_meta(1, "tag1", true, false, false),
1227            new_column_meta(2, "tag2", false, true, true), // Requires Fulltext and BloomFilter
1228        ];
1229        assert!(!file_meta.is_index_consistent_with_region(&region_meta));
1230    }
1231}